Spaces:
Runtime error
Runtime error
Download app.py from fdbw/fzd3: direct link, hf CLI and curl.
- Browser
- Download file 8.87 kB
-
https://huggingface.co/spaces/fdbw/fzd3/resolve/main/app.py
- Command line
-
hf download hf://spaces/fdbw/fzd3/app.py
-
curl -L -o app.py https://huggingface.co/spaces/fdbw/fzd3/resolve/main/app.py
8.87 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| AB 双语语音陪练 —— 单机 Gradio 应用 | |
| 交互设计(对应我们前面确认的方案): | |
| A 说话(A语言语音) | |
| -> livetranslate: A原文(A语言文字) / A译文(B语言文字) / A译音(B语言语音,自动播放) | |
| -> 机器人吃 A译文 -> 生成 B原文(B语言文字) | |
| -> B原文 TTS 合成 -> 自动播放(外语,主线,可 🔁 重复播放,只缓存最近一句) | |
| -> B原文 文本翻译 -> B译文(A语言文字),屏幕显示,不自动发声 | |
| -> 🌐 按钮:首次点击才合成 B译文 语音并播放,同一轮内再点直接用缓存 | |
| 屏幕四行:A原文 / A译文 / B原文 / B译文 | |
| 扬声器两路:A译音(自动播) / B原文语音(自动播,可重播)/ B译文语音(按需,可重播) | |
| 运行前: | |
| 1. pip install -r requirements.txt | |
| 2. export DASHSCOPE_API_KEY="sk-xxxx" | |
| 3. python app.py | |
| """ | |
| import tempfile | |
| import os | |
| import gradio as gr | |
| from config import LANGUAGE_NAME | |
| from bailian_client import ( | |
| LiveTranslateClient, | |
| chat_reply, | |
| translate_text, | |
| synthesize_speech, | |
| ) | |
| def _bytes_to_tempfile(audio_bytes: bytes, suffix: str = ".mp3") -> str: | |
| """ | |
| 把音频二进制写到一个新的临时文件里,返回路径。 | |
| 每次都用新文件名,是为了让 Gradio 的 Audio 组件在"内容没变但要求重播"时 | |
| 也能触发 autoplay(同一个文件路径浏览器可能不会重新播放)。 | |
| """ | |
| fd, path = tempfile.mkstemp(suffix=suffix) | |
| with os.fdopen(fd, "wb") as f: | |
| f.write(audio_bytes) | |
| return path | |
| def on_send(audio_filepath, a_lang, b_lang, history, b_audio_cache, b_translated_audio_cache): | |
| """ | |
| 主流程:A 发一句话 -> 走完整个 pipeline -> 更新四行文字 + 两路自动播放音频。 | |
| history: [{"role": "user"/"assistant", "content": "..."}] 都是 B 语言文本 | |
| b_audio_cache: 最近一句 B原文 的语音 bytes(供 🔁 重播用) | |
| b_translated_audio_cache: 最近一句 B译文 的语音 bytes(供 🌐 重播用,可能为 None 表示还没点过) | |
| """ | |
| if not audio_filepath: | |
| raise gr.Error("先录一句话再发送") | |
| # 1. A -> B 方向:实时翻译 | |
| lt_client = LiveTranslateClient(source_lang=a_lang, target_lang=b_lang) | |
| lt_result = lt_client.translate_utterance(audio_filepath) | |
| a_original = lt_result["source_text"] # A原文 | |
| a_translated = lt_result["target_text"] # A译文(B语言) | |
| a_audio_bytes = lt_result["target_audio"] # A译音(B语言语音,wav bytes) | |
| if not a_translated: | |
| raise gr.Error("没有识别到有效的语音内容,请重新录音") | |
| # 2. 聊天机器人:A译文 -> B原文 | |
| b_original = chat_reply(history, a_translated, target_lang=b_lang) | |
| new_history = history + [ | |
| {"role": "user", "content": a_translated}, | |
| {"role": "assistant", "content": b_original}, | |
| ] | |
| # 3. B原文 -> TTS(外语,自动播放,同时作为 🔁 重播的缓存) | |
| b_audio_bytes = synthesize_speech(b_original, lang=b_lang) | |
| # 4. B原文 -> 文本翻译 -> B译文(母语文字,只显示,不自动发声) | |
| b_translated = translate_text(b_original, source_lang=b_lang, target_lang=a_lang) | |
| # 新一轮开始,B译文语音缓存清空,等用户点 🌐 再现合成 | |
| new_b_translated_audio_cache = None | |
| a_audio_path = _bytes_to_tempfile(a_audio_bytes, suffix=".wav") if a_audio_bytes else None | |
| b_audio_path = _bytes_to_tempfile(b_audio_bytes, suffix=".mp3") | |
| return ( | |
| a_original, | |
| a_translated, | |
| b_original, | |
| b_translated, | |
| a_audio_path, # A译音,自动播放 | |
| b_audio_path, # B原文语音,自动播放 | |
| new_history, | |
| b_audio_bytes, # 存进 state,供 🔁 使用 | |
| new_b_translated_audio_cache, # 存进 state,供 🌐 使用(None=还没生成过) | |
| None, # 清空 🌐 播放框,避免残留上一轮的音频 | |
| ) | |
| def on_replay_foreign(b_audio_cache): | |
| """🔁 重播 B原文(外语)——直接用缓存,不重新调用 TTS。""" | |
| if not b_audio_cache: | |
| raise gr.Error("还没有可重播的内容,先发一句话") | |
| return _bytes_to_tempfile(b_audio_cache, suffix=".mp3") | |
| def on_replay_native(b_original_text, a_lang, b_lang, b_translated_audio_cache): | |
| """ | |
| 🌐 播放/重播 B译文(母语)。 | |
| 第一次点击:调用 TTS 合成并缓存;之后再点:直接用缓存,不重复计费。 | |
| """ | |
| if not b_original_text: | |
| raise gr.Error("还没有可播放的内容,先发一句话") | |
| if b_translated_audio_cache is None: | |
| b_translated_text = translate_text(b_original_text, source_lang=b_lang, target_lang=a_lang) | |
| audio_bytes = synthesize_speech(b_translated_text, lang=a_lang) | |
| else: | |
| audio_bytes = b_translated_audio_cache | |
| path = _bytes_to_tempfile(audio_bytes, suffix=".mp3") | |
| return path, audio_bytes # 第二个值写回 state 缓存 | |
| def build_app(): | |
| lang_choices = list(LANGUAGE_NAME.items()) # [(code, name), ...] | |
| lang_labels = [f"{name} ({code})" for code, name in lang_choices] | |
| code_by_label = {f"{name} ({code})": code for code, name in lang_choices} | |
| with gr.Blocks(title="AB 双语语音陪练") as demo: | |
| gr.Markdown("## AB 双语语音陪练\n对着麦克风说母语,机器人用你正在学的语言陪你聊天。") | |
| with gr.Row(): | |
| a_lang_dd = gr.Dropdown( | |
| lang_labels, value=lang_labels[0], label="A:你的母语" | |
| ) | |
| b_lang_dd = gr.Dropdown( | |
| lang_labels, value=lang_labels[1], label="B:机器人 / 练习目标语言" | |
| ) | |
| history_state = gr.State([]) | |
| b_audio_cache_state = gr.State(None) # B原文语音缓存(🔁 用) | |
| b_translated_audio_cache_state = gr.State(None) # B译文语音缓存(🌐 用) | |
| mic = gr.Audio(sources=["microphone"], type="filepath", label="按住说话(A)") | |
| send_btn = gr.Button("发送 🎤", variant="primary") | |
| with gr.Row(): | |
| with gr.Column(): | |
| a_original_box = gr.Textbox(label="A原文", interactive=False) | |
| a_translated_box = gr.Textbox(label="A译文", interactive=False) | |
| with gr.Column(): | |
| b_original_box = gr.Textbox(label="B原文", interactive=False) | |
| b_translated_box = gr.Textbox(label="B译文", interactive=False) | |
| with gr.Row(): | |
| replay_foreign_btn = gr.Button("🔁 重播机器人外语发音") | |
| replay_native_btn = gr.Button("🌐 听母语翻译") | |
| a_audio_player = gr.Audio(label="A译音(自动播放)", autoplay=True, visible=True) | |
| b_audio_player = gr.Audio(label="B原文语音 / 🔁重播", autoplay=True, visible=True) | |
| b_translated_audio_player = gr.Audio(label="B译文语音 / 🌐", autoplay=True, visible=True) | |
| def _send_wrapper(audio_filepath, a_label, b_label, history, b_cache, b_tr_cache): | |
| a_lang = code_by_label[a_label] | |
| b_lang = code_by_label[b_label] | |
| return on_send(audio_filepath, a_lang, b_lang, history, b_cache, b_tr_cache) | |
| send_btn.click( | |
| fn=_send_wrapper, | |
| inputs=[mic, a_lang_dd, b_lang_dd, history_state, b_audio_cache_state, b_translated_audio_cache_state], | |
| outputs=[ | |
| a_original_box, | |
| a_translated_box, | |
| b_original_box, | |
| b_translated_box, | |
| a_audio_player, | |
| b_audio_player, | |
| history_state, | |
| b_audio_cache_state, | |
| b_translated_audio_cache_state, | |
| b_translated_audio_player, | |
| ], | |
| ) | |
| replay_foreign_btn.click( | |
| fn=on_replay_foreign, | |
| inputs=[b_audio_cache_state], | |
| outputs=[b_audio_player], | |
| ) | |
| def _replay_native_wrapper(b_original_text, a_label, b_label, b_tr_cache): | |
| a_lang = code_by_label[a_label] | |
| b_lang = code_by_label[b_label] | |
| return on_replay_native(b_original_text, a_lang, b_lang, b_tr_cache) | |
| replay_native_btn.click( | |
| fn=_replay_native_wrapper, | |
| inputs=[b_original_box, a_lang_dd, b_lang_dd, b_translated_audio_cache_state], | |
| outputs=[b_translated_audio_player, b_translated_audio_cache_state], | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| demo = build_app() | |
| demo.queue().launch() |