声音 × 文字CS4313 · ASR 与 TTS 入门

Capstone Project · 压轴项目

你说中文,它用“你的声音”说外语

教程里学过的环节,在这里各就各位。这是真实项目 simple_X_Translator 的完整拆解:按住按钮说一句中文,两三秒后,它用你自己的音色说出英语、日语或法语。

👂 X-ASR · 听懂你 🌐 HY-MT · 换种语言 🗣️ X-Voice · 用你的音色说 ⏱ 一次往返 · 通常 2–4 秒

Pipeline · 全链路

一次翻译的完整旅程

点「走一遍」,看一句中文从声音变成文字、换一种语言、再变回声音;每个节点可点开看细节。

你的录音 · 约 3 秒

asr_text

translated_text
text = 译文 ✓ ref = 你的录音 🎙️
POST /synthesize
像你在说英语 · 示意波形

Services · 三个 AI 服务

拆开看:三个各司其职的服务

编排服务只做调度,把三个独立服务串成一条流水线。

SERVICE 01 · 语音识别

X-ASR

WebSocket 流式 1 秒识别窗

先发 start,把 16kHz 的 PCM 切成小块逐块发送,最后发 end,服务端返回最终文本。它按 1 秒为一窗出结果——这个设定,正是下面「补静音」细节的来历。

① → {"type": "start"} ② → PCM 块 × N (16kHz int16) ③ → {"type": "end"} ④ ← final: "帮我把窗户打开"
SERVICE 02 · 机器翻译

HY-MT

chat-completions hunyuan_mt prompt 即契约

一个 OpenAI 接口兼容的翻译模型,没有专用协议:把识别出的中文放进 messages,靠 system prompt 约定行为。prompt 写得越死,输出越干净。

system: 你是实时语音翻译,把用户的话 翻成目标语言;只输出译文, 不解释、不加任何多余内容。 user: 帮我把窗户打开 ← "Could you open the window?"
SERVICE 03 · 语音合成

X-Voice

HTTP /synthesize 克隆音色 需参考音频

给它两样东西:要说的文本 + 一段参考音频。它把参考音频里的音色“穿”到文本上——参考给谁的声音,说出来就像谁。这个项目递过去的参考,就是你刚才的录音。

POST /synthesize { "text": 译文, "audio_paths": [你的录音], "language": "en" } ← 48kHz WAV

Engineering · 工程细节

两个不起眼、却决定成败的细节

真实项目和课堂例子之间,差的往往就是这几行代码。

细节一结尾补 1 秒静音

最后 0.4 秒的字,去哪了?X-ASR 攒够 1 秒音频才出一次结果。你说了 3.4 秒:前 3 秒被完整识别,最后 0.4 秒凑不满一窗——直接被丢掉。解法很简单:发送前在 PCM 末尾拼上 1 秒全零采样。真正的句尾,就这样被“推”进了完整窗口。

被吃掉的不是随机的字,恰恰是句尾——而中文的关键信息常在句尾:“帮我把窗户打开”少了最后一个字,意思就悬在半空。
# pipeline.py · 细节一:末尾补 1 秒静音
tail = int(sample_rate * 1.0) * 2          # 1 秒 × 16bit(2 字节/采样)
pcm_bytes = pcm_bytes + b"\x00" * tail     # 把最后几个字“推”进完整识别窗口

细节二你的录音,就是音色参考

凭什么合成的外语,听起来像“你”在说?X-Voice 是克隆音色的 TTS,每次请求必须带一段参考音频。不带,它就不知道“用谁的声音说”。项目把你这次的录音直接复用为参考——一份录音,两个用途。译文听起来就像你在说外语。

这正是教程第 5 幕说的:文字丢掉了音色,TTS 必须把它补回来。补什么音色?这台翻译机的答案是——补你的。
# pipeline.py · 细节二:用本次录音做音色参考
payload = {
    "text": translated_text,          # 翻译后的外语文本
    "audio_paths": [clone_audio_path],# ← 你刚才的录音,成为“你的音色”
    "language": target_lang,
}

Live Demo · 在线试驾

连上后端,真的翻一句

这台翻译机的编排后端(xtranslate)和三个 AI 服务都跑在课程 GPU 服务器上,浏览器通过 WebSocket 流式协议直连编排服务。状态灯全绿时可直接试用;否则看录播演示。

服务状态 … 编排服务 … X-ASR … HY-MT … X-Voice

按住说、松开发送;也可以点一下开始、再点一下结束。录音最长 10 秒。后端地址留空即自动直连(线上走本站通道,本机调试走 http://localhost:7654 隧道),填了自定义地址则只发往你填的后端。浏览器录音要求页面运行在 localhost 或 HTTPS 下。

不知道说什么?试试:“帮我把窗户打开”,或者“这道题真的只有一种解法吗”。

录播演示:一次真实往返长什么样

🎬 录播示例 · 非实时 无需后端 · 数据为示例
STEP 1 · 录音 → 识别

asr_text = "帮我把窗户打开"

X-ASR 流式返回识别文本;句尾那个“开”字,靠补静音才保住。

STEP 2 · 翻译(en)

system: 只输出译文,不解释
translated_text =
"Could you open the window?"

HY-MT 走 chat 接口一问一答;prompt 管住它只输出译文。

STEP 3 · 合成(你的音色)

<binary> ← 48kHz PCM16 语音流

合成语音以 WebSocket 二进制帧流式返回,拼起来就是一段 48kHz 音频。想听“自己的”真声,请建隧道试驾。

# 一次往返的 WebSocket 消息流(录播示例,字段与真实协议一致)
connect ws://localhost:7654/ws
→ {"action":"session_config", "target_lang":"en", "sample_rate":16000}
→ {"event":"start"}  {"event":"config_audio"}  {"event":"turn_started"}
→ <binary> 16kHz PCM16 × N 帧(你的录音)      → {"event":"stop"}
← {"action":"asr_result", "data":{"type":"partial", "text":"帮我把窗户"}}
← {"action":"asr_result", "data":{"type":"final", "text":"帮我把窗户打开"}}
← {"action":"translation_ready", "data":{"translated_text":"Could you open the window?"}}
← <binary> 48kHz PCM16 × M 帧(合成语音,你的音色)

DIY · 自己跑起来

在自己的电脑上跑起来

服务已在课程 GPU 服务器常驻;本机只需一条 SSH 隧道,把 7654 端口映射过来。

# 课程 GPU 服务器上已在 tmux 里常驻三件套 + 编排服务:
#   HY-MT(翻译,:8000)· X-Voice(合成,:11998)· SenseVoice(识别,进程内)
#   xtranslate 编排服务 → http://0.0.0.0:7654(WebSocket 入口 /ws)

# 本机只需一条 SSH 隧道,把 7654 映射到 localhost:
ssh -L 7654:localhost:7654 -p 2222 <账号>@ssh.zw1.paratera.com
curl http://localhost:7654/healthz   # → {"status":"ok"} ——翻译机活了
三个 AI 服务(X-ASR / HY-MT / X-Voice)的地址在编排服务的 config.json 里配置;浏览器与编排服务之间走 WebSocket(/ws)流式协议。隧道建好后,上面试驾区的后端地址留空(默认 http://localhost:7654)即可。
一台翻译机 = ASR(压缩:声音 → 文字) + MT(转换:中文 → 外语) + TTS(补全:文字 → 你的音色)。回头看教程第 5 幕的“镜子”比喻——现在,你应该能讲给别人听了。想知道每一环再往下一层的原理,去进阶篇:12 个专题,挑感兴趣的读。