fdbw commited on
Commit
207cfe1
·
verified ·
1 Parent(s): b93c194

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +241 -62
app.py CHANGED
@@ -1,99 +1,278 @@
1
- import os
2
  import gradio as gr
 
 
3
  import requests
 
 
 
 
 
4
 
5
- API_KEY = os.environ.get("SILICONFLOW_API_KEY", "")
 
 
 
6
 
7
  ASR_URL = "https://api.siliconflow.cn/v1/audio/transcriptions"
8
  CHAT_URL = "https://api.siliconflow.cn/v1/chat/completions"
9
  ASR_MODEL = "FunAudioLLM/SenseVoiceSmall"
10
  MT_MODEL = "tencent/Hunyuan-MT-7B"
11
 
12
- LANG_MAP = {
13
- "英语 → 中文": {"src": "English", "tgt": "Chinese", "src_tag": "EN", "tgt_tag": "中"},
14
- "汉语 → 英文": {"src": "Chinese", "tgt": "English", "src_tag": "中", "tgt_tag": "EN"},
15
- }
16
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- def transcribe(audio_path):
 
 
 
19
  with open(audio_path, "rb") as f:
20
- files = {"file": (os.path.basename(audio_path), f, "audio/wav")}
21
  data = {"model": ASR_MODEL}
22
- headers = {"Authorization": "Bearer " + API_KEY}
23
- r = requests.post(ASR_URL, headers=headers, data=data, files=files, timeout=60)
24
- r.raise_for_status()
25
- text = r.json().get("text", "").strip()
26
- text = text.replace("<|", "").replace("|>", "")
27
- return text
28
-
 
 
 
 
29
 
30
- def translate(text, src, tgt):
31
- headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
32
  payload = {
33
  "model": MT_MODEL,
34
  "messages": [
35
- {
36
- "role": "system",
37
- "content": f"You are a professional translator. Translate the user text from {src} to {tgt}. "
38
- f"Output ONLY the translated text, no explanations, no quotes, no pinyin.",
39
- },
40
- {"role": "user", "content": text},
41
  ],
42
  "temperature": 0.3,
43
- "max_tokens": 512,
44
  }
45
- r = requests.post(CHAT_URL, headers=headers, json=payload, timeout=60)
46
- r.raise_for_status()
47
- return r.json()["choices"][0]["message"]["content"].strip()
 
 
 
 
 
48
 
 
 
 
 
 
 
 
 
 
49
 
50
- def process(audio_path, mode_label, history):
51
- history = history or []
 
 
 
 
 
 
 
 
 
52
 
53
- if not API_KEY:
54
- history.append({"role": "assistant", "content": "⚠️ 服务器未配置 SILICONFLOW_API_KEY"})
55
- return history, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- if audio_path is None:
58
- return history, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- cfg = LANG_MAP[mode_label]
 
 
 
61
 
62
- try:
63
- text = transcribe(audio_path)
64
- if not text:
65
- return history, None
66
- trans = translate(text, cfg["src"], cfg["tgt"])
67
- except Exception as e:
68
- history.append({"role": "assistant", "content": f"⚠️ 出错了:{e}"})
69
- return history, None
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- history.append({"role": "user", "content": f"[{cfg['src_tag']}] {text}"})
72
- history.append({"role": "assistant", "content": f"[{cfg['tgt_tag']}] {trans}"})
73
- return history, None
 
 
 
 
 
74
 
 
 
 
 
 
 
 
 
75
 
76
- with gr.Blocks(title="传声筒") as demo:
77
- gr.Markdown("## 🎙️ 传声筒 · 实时语音传译\n点麦克风开始录音,说一句完整的话,再点一次停止,自动识别+翻译。")
 
 
 
 
 
78
 
79
- mode = gr.Radio(
80
- choices=list(LANG_MAP.keys()),
81
- value="英语 → 中文",
82
- label="翻译方向",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  )
84
 
85
- audio_in = gr.Audio(
86
- sources=["microphone"],
87
- type="filepath",
88
- label="点击录音,再点一次停止",
89
  )
90
 
91
- chatbot = gr.Chatbot(label="传译记录", height=420, type="messages")
 
 
 
92
 
93
- audio_in.stop_recording(
94
- fn=process,
95
- inputs=[audio_in, mode, chatbot],
96
- outputs=[chatbot, audio_in],
 
 
 
97
  )
98
 
99
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import os
3
+ import tempfile
4
  import requests
5
+ import json
6
+ import time
7
+ import numpy as np
8
+ import re
9
+ from pathlib import Path
10
 
11
+ # ========== 配置 ==========
12
+ API_KEY = os.environ.get("SILICONFLOW_API_KEY")
13
+ if not API_KEY:
14
+ raise ValueError("请在 Space 的 Settings -> Secrets 中设置 SILICONFLOW_API_KEY")
15
 
16
  ASR_URL = "https://api.siliconflow.cn/v1/audio/transcriptions"
17
  CHAT_URL = "https://api.siliconflow.cn/v1/chat/completions"
18
  ASR_MODEL = "FunAudioLLM/SenseVoiceSmall"
19
  MT_MODEL = "tencent/Hunyuan-MT-7B"
20
 
21
+ # VAD 参数
22
+ SILENCE_THRESHOLD = 0.015
23
+ SILENCE_DURATION = 0.8
24
+ MIN_SPEECH_DURATION = 0.3
25
 
26
+ # ========== 全局状态 ==========
27
+ class AppState:
28
+ def __init__(self):
29
+ self.mode = "en-zh"
30
+ self.history = []
31
+ self.is_speaking = False
32
+ self.audio_buffer = []
33
+ self.silence_start = None
34
+ self.speech_start = None
35
+ self.last_audio_time = 0
36
 
37
+ state = AppState()
38
+
39
+ # ========== API 调用 ==========
40
+ def transcribe_audio(audio_path):
41
  with open(audio_path, "rb") as f:
42
+ files = {"file": f}
43
  data = {"model": ASR_MODEL}
44
+ headers = {"Authorization": f"Bearer {API_KEY}"}
45
+ try:
46
+ resp = requests.post(ASR_URL, files=files, data=data, headers=headers, timeout=30)
47
+ resp.raise_for_status()
48
+ result = resp.json()
49
+ text = result.get("text", "").strip()
50
+ text = re.sub(r"<\|[^|]*\|>", "", text).strip()
51
+ return text if text else None
52
+ except Exception as e:
53
+ print(f"ASR 错误: {e}")
54
+ return None
55
 
56
+ def translate_text(text, src_lang, tgt_lang):
57
+ system_prompt = f"Translate from {src_lang} to {tgt_lang}. Output ONLY translation, no extra text."
58
  payload = {
59
  "model": MT_MODEL,
60
  "messages": [
61
+ {"role": "system", "content": system_prompt},
62
+ {"role": "user", "content": text}
 
 
 
 
63
  ],
64
  "temperature": 0.3,
65
+ "max_tokens": 512
66
  }
67
+ headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
68
+ try:
69
+ resp = requests.post(CHAT_URL, json=payload, headers=headers, timeout=30)
70
+ resp.raise_for_status()
71
+ return resp.json()["choices"][0]["message"]["content"].strip()
72
+ except Exception as e:
73
+ print(f"翻译错误: {e}")
74
+ return None
75
 
76
+ # ========== 音频处理 ==========
77
+ def process_audio_chunk(audio_data, sr):
78
+ """处理音频块,返回 (是否触发翻译, 原文, 译文)"""
79
+ if len(audio_data.shape) > 1:
80
+ audio_data = audio_data.mean(axis=1)
81
+
82
+ rms = np.sqrt(np.mean(audio_data**2))
83
+ is_speaking = rms > SILENCE_THRESHOLD
84
+ now = time.time()
85
 
86
+ if is_speaking:
87
+ if not state.is_speaking:
88
+ state.is_speaking = True
89
+ state.speech_start = now
90
+ state.audio_buffer = [audio_data]
91
+ state.silence_start = None
92
+ else:
93
+ state.audio_buffer.append(audio_data)
94
+ state.silence_start = None
95
+ state.last_audio_time = now
96
+ return None, None, None
97
 
98
+ else:
99
+ if state.is_speaking:
100
+ if state.silence_start is None:
101
+ state.silence_start = now
102
+ elif now - state.silence_start > SILENCE_DURATION:
103
+ duration = now - state.speech_start
104
+ if duration >= MIN_SPEECH_DURATION and len(state.audio_buffer) > 0:
105
+ full_audio = np.concatenate(state.audio_buffer)
106
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
107
+ tmp_path = tmp.name
108
+ import soundfile as sf
109
+ sf.write(tmp_path, full_audio, sr)
110
+
111
+ src_text = transcribe_audio(tmp_path)
112
+ os.unlink(tmp_path)
113
+
114
+ if src_text:
115
+ if state.mode == "en-zh":
116
+ src_lang, tgt_lang = "English", "Chinese"
117
+ else:
118
+ src_lang, tgt_lang = "Chinese", "English"
119
+ translated = translate_text(src_text, src_lang, tgt_lang)
120
+ if translated:
121
+ state.history.append((src_text, translated))
122
+ state.is_speaking = False
123
+ state.audio_buffer = []
124
+ state.silence_start = None
125
+ return True, src_text, translated
126
+
127
+ state.is_speaking = False
128
+ state.audio_buffer = []
129
+ state.silence_start = None
130
+
131
+ return None, None, None
132
 
133
+ # ========== Gradio 回调 ==========
134
+ def on_audio_change(audio):
135
+ if audio is None:
136
+ return gr.update(), gr.update(), gr.update(), gr.update()
137
+
138
+ sr, audio_data = audio
139
+ if audio_data.dtype != np.float32:
140
+ audio_data = audio_data.astype(np.float32) / 32768.0
141
+
142
+ triggered, src, tgt = process_audio_chunk(audio_data, sr)
143
+
144
+ # 构建历史记录 HTML
145
+ history_html = ""
146
+ if state.history:
147
+ for s, t in reversed(state.history):
148
+ tag_src = "EN" if state.mode == "en-zh" else "中"
149
+ tag_tgt = "中" if state.mode == "en-zh" else "EN"
150
+ history_html += f"""
151
+ <div style='background:#1C1915;border-left:3px solid #FFB000;border-radius:4px 10px 10px 4px;padding:12px 14px;margin-bottom:10px;'>
152
+ <div style='font-family:monospace;font-size:14px;color:#9A9488;margin-bottom:4px;'>
153
+ <span style='color:#8A6112;margin-right:6px;'>{tag_src}</span>{s}
154
+ </div>
155
+ <div style='font-size:16px;font-weight:600;color:#5FA8AC;'>
156
+ <span style='font-family:monospace;font-weight:400;font-size:12px;color:#2E5254;margin-right:6px;'>{tag_tgt}</span>{t}
157
+ </div>
158
+ </div>
159
+ """
160
+ else:
161
+ history_html = "<div style='color:#9A9488;text-align:center;padding:30px;'>还没有记录,开口试试看</div>"
162
+
163
+ status = "🔴 正在说话..." if state.is_speaking else "⚪ 待命"
164
+
165
+ if triggered and src and tgt:
166
+ return gr.update(value=f"✅ {src} → {tgt}"), gr.update(value=history_html), gr.update(value=status), gr.update()
167
+ else:
168
+ return gr.update(), gr.update(value=history_html), gr.update(value=status), gr.update()
169
 
170
+ def change_mode(mode):
171
+ state.mode = mode
172
+ state.history = [] # 切换模式时清空历史
173
+ return gr.update(value="")
174
 
175
+ def get_history():
176
+ history_html = ""
177
+ if state.history:
178
+ for s, t in reversed(state.history):
179
+ tag_src = "EN" if state.mode == "en-zh" else "中"
180
+ tag_tgt = "中" if state.mode == "en-zh" else "EN"
181
+ history_html += f"""
182
+ <div style='background:#1C1915;border-left:3px solid #FFB000;border-radius:4px 10px 10px 4px;padding:12px 14px;margin-bottom:10px;'>
183
+ <div style='font-family:monospace;font-size:14px;color:#9A9488;margin-bottom:4px;'>
184
+ <span style='color:#8A6112;margin-right:6px;'>{tag_src}</span>{s}
185
+ </div>
186
+ <div style='font-size:16px;font-weight:600;color:#5FA8AC;'>
187
+ <span style='font-family:monospace;font-weight:400;font-size:12px;color:#2E5254;margin-right:6px;'>{tag_tgt}</span>{t}
188
+ </div>
189
+ </div>
190
+ """
191
+ else:
192
+ history_html = "<div style='color:#9A9488;text-align:center;padding:30px;'>还没有记录,开口试试看</div>"
193
+ return gr.update(value=history_html)
194
 
195
+ # ========== 构建界面 ==========
196
+ css = """
197
+ .gradio-container { max-width: 640px; margin: 0 auto; }
198
+ .header { font-family: 'Manrope', sans-serif; text-align: center; margin-bottom: 20px; }
199
+ .header h1 { font-size: 32px; font-weight: 800; letter-spacing: -0.01em; margin: 0; }
200
+ .header .sub { color: #9A9488; font-size: 14px; }
201
+ .history-box { background: #14120F; border: 1px solid #33302A; border-radius: 14px; padding: 16px; min-height: 150px; max-height: 350px; overflow-y: auto; }
202
+ """
203
 
204
+ with gr.Blocks(css=css, title="传声筒 · 实时语音传译") as demo:
205
+ gr.HTML("""
206
+ <div class="header">
207
+ <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:0.18em;color:#8A6112;text-transform:uppercase;">REAL-TIME VOICE RELAY · 硅基流动版</div>
208
+ <h1>传声筒</h1>
209
+ <div class="sub">开口即译 — 说一句,出一句</div>
210
+ </div>
211
+ """)
212
 
213
+ with gr.Row():
214
+ mode_radio = gr.Radio(
215
+ choices=[("🇬🇧 英语 → 中文 🇨🇳", "en-zh"), ("🇨🇳 中文 → 英语 🇬🇧", "zh-en")],
216
+ value="en-zh",
217
+ label="翻译方向",
218
+ interactive=True
219
+ )
220
 
221
+ with gr.Row():
222
+ status_display = gr.Textbox(value="⚪ 待命", label="状态", interactive=False)
223
+
224
+ with gr.Row():
225
+ audio_input = gr.Audio(
226
+ sources=["microphone"],
227
+ type="numpy",
228
+ label="🎙️ 点击麦克风开始说话",
229
+ streaming=True
230
+ )
231
+
232
+ result_display = gr.Textbox(label="实时识别结果", interactive=False)
233
+
234
+ with gr.Column():
235
+ gr.Markdown("### 📜 传译记录")
236
+ history_display = gr.HTML(value="<div style='color:#9A9488;text-align:center;padding:30px;'>还没有记录,开口试试看</div>", elem_classes="history-box")
237
+
238
+ with gr.Row():
239
+ refresh_btn = gr.Button("🔄 刷新历史")
240
+ clear_btn = gr.Button("🗑️ 清空历史")
241
+
242
+ # 事件绑定
243
+ audio_input.change(
244
+ fn=on_audio_change,
245
+ inputs=audio_input,
246
+ outputs=[result_display, history_display, status_display, audio_input]
247
  )
248
 
249
+ mode_radio.change(
250
+ fn=change_mode,
251
+ inputs=mode_radio,
252
+ outputs=result_display
253
  )
254
 
255
+ refresh_btn.click(
256
+ fn=get_history,
257
+ outputs=history_display
258
+ )
259
 
260
+ def clear_history():
261
+ state.history = []
262
+ return gr.update(value="<div style='color:#9A9488;text-align:center;padding:30px;'>还没有记录,开口试试看</div>")
263
+
264
+ clear_btn.click(
265
+ fn=clear_history,
266
+ outputs=history_display
267
  )
268
 
269
+ gr.Markdown("""
270
+ ---
271
+ **提示**:
272
+ - 点击麦克风开始录音,说完自动识别翻译
273
+ - 使用硅基流动 API(SenseVoiceSmall + Hunyuan-MT-7B)
274
+ - API Key 从环境变量 `SILICONFLOW_API_KEY` 读取
275
+ """)
276
+
277
+ if __name__ == "__main__":
278
+ demo.launch(debug=True)