一、整体位置:TTS 在对话链路哪一段?
可以把对话想成一条流水线:
用户说话 → ASR 转文字 → LLM 想回复 → **TTS 把回复变成声音** → RTC 推给用户听
本文关注的是 LLM 输出之后、用户听到声音之前/之后的两个处理阶段:
- **前处理(pre_tts_process)**:LLM 流式文字 → 整理、分句 → 送给 TTS 合成
- **后处理(post_tts_process)**:TTS 合成结果 → 转格式、裁静音、通知下游
| 模块 | 默认开关 |
|---|---|
| pre_tts_process | enabled: true |
| post_tts_process | enabled: true |
| tts_leading_silence_trim | enabled: false |
| tts 扩展本身 | — |
三、前处理(pre_tts_process)
前处理的核心任务:把 LLM 一块一块吐出来的文字,整理成 TTS 能吃的格式,并决定哪些内容要念、哪些只显示不念。
3.1 输入从哪里来?
3.2 三种输入类型
| 配置 | 来源 | 默认值 | 作用 |
|---|---|---|---|
| batch | tts.batch | true | 是否按标点分句;false 则整块直接送 |
| fast_start | tts.fast_start | false | 第一句不等合并,尽快送 TTS(降低首包延迟) |
| chunk_limit | 代码内置 | 64 | 合并时每个包最多多少字符 |
有些内容只需要显示在字幕里,不需要念出来。skip_patterns 控制哪些内容「静音跳过」。
| ID | 过滤内容 | 处理方式 |
|---|---|---|
| 1 | ()中文圆括号 | 只发 text_data,不送 TTS |
| 2 | 【】中文方括号 | 只发 text_data,不送 TTS |
| 3 | ( ) 英文圆括号 | 只发 text_data,不送 TTS |
| 4 | [ ] 英文方括号 | 只发 text_data,不送 TTS |
| 5 | { } 花括号 | 只发 text_data,不送 TTS |
| 6 | Emoji 表情 | 只发 text_data,不送 TTS |
3.5 TTS 运行时热更新(update_configs)
LLM 可以在流式输出中夹带 metadata,动态修改 TTS 配置(音色、采样率等),无需重启 Agent。
流程:metadata 块 → 解析 tts_params → 发 update_configs 命令给 tts 扩展 → 等 TTS 返回 → 后续文字用新配置合成。
⚠️ 注意:这个过程会阻塞同一条文字流,保证配置更新完才继续合成。
| 场景 | 前处理行为 |
|---|---|
| 正常结束(is_final=true) | 发最终 tts_text_input(text_input_end=true),上报 llm_latency 指标 |
| 被打断(is_interrupted=true) | 发 INTERRUPTED 结束信号,清空待处理状态 |
| 空轮次(final 但无内容) | 走 post_tts_process 的空轮次结束通道 |
| flush(用户插话等) | 清空所有 turn 上下文,取消 metadata 任务 |
3.7 前处理关键配置
| TTS 输出事件 | 后处理做什么 | 发给谁 |
|---|---|---|
| tts_text_result | 转成 AssistantTranscription → text_data | context(字幕/逐字时间戳) |
| tts_audio_start | 记录开始时间(用于延迟统计) | 内部状态 |
| tts_audio_end | 发布轮次结束 + 发 on_tts_end | avatar、内部状态机 |
| pcm_frame(音频帧) | 转发或裁静音后转发 | agora_audio3a_upstream → rtc |
| empty_tts_output_ended | 空轮次结束(LLM 没输出内容) | 内部状态机 |
4.2 文字结果转换(enable_words)
TTS 合成时会返回逐字时间戳(哪个字什么时候开始、持续多久)。后处理把这些信息包装成 AssistantTranscription,发给 context 用于:
- 实时字幕滚动
- 客户端展示 Agent 说了什么
- 和 ASR 用户字幕对称展示
如果 enable_words=false,后处理跳过 tts_text_result 转换(前处理会自己发 text_data)。
4.3 音频转发与开头静音裁剪
很多 TTS 厂商合成的第一包音频前面有一段静音,用户听起来会有「顿一下」的感觉。
tts_leading_silence_trim 模块会在每轮第一次转发音频前,检测并裁掉低于 amplitude_threshold 的静音样本。
| 配置 | 默认值 | 说明 |
|---|---|---|
| main.tts_leading_silence_trim.enabled | false | 是否裁 TTS 开头静音 |
| main.tts_leading_silence_trim.amplitude_threshold | 300 | 振幅阈值,低于此值视为静音 |
4.4 轮次结束通知
TTS 播完一轮后,后处理要通知下游「我说完了」:
| 结束原因 | 触发条件 | 后处理行为 |
|---|---|---|
| request_end | 正常播完 | 发 on_tts_end + drain 命令(排空音频缓冲) |
| interrupted | 用户打断 | 发 on_tts_end,不发 drain |
| 空轮次 | LLM 无输出 | 由 pre 触发 empty_tts_output_ended,post 统一处理 |
on_tts_end 携带的信息包括:
- audio_duration:实际播放时长(毫秒)
- turn_id:轮次编号
- first_packet_trimmed_duration_ms:首包裁掉的静音时长
- total_trimmed_duration_ms:总共裁掉的静音时长
- first_packet_forward_after_tts_start_ms:首包转发相对 tts_audio_start 的延迟
4.5 后处理关键配置
| 配置路径 | 默认值 | 说明 |
|---|---|---|
| main.post_tts_process.enabled | true | 是否启用后处理侧效果(字幕转换、on_tts_end 等) |
| main.post_tts_process.enable_words | true | 是否把 tts_text_result 转成 text_data |
| main.tts_leading_silence_trim.enabled | false | 是否裁 TTS 开头静音 |
| main.tts_leading_silence_trim.amplitude_threshold | 300 | 静音判定振幅阈值 |
⚠️重要:即使 post_tts_process.enabled=false,TTS 音频帧仍会转发给用户(公共契约)。关闭的只是字幕转换、on_tts_end 等副作用,不是禁音。
flowchart TB
subgraph 输入
LLM["llm 流式输出<br/>llm_text_chunk"]
end
subgraph 前处理["pre_tts_process"]
direction TB
P1["识别 chunk 类型"]
P2["分句 + skip_patterns 过滤"]
P3["batch/fast_start 合并"]
P4["metadata → update_configs"]
P5["发 tts_text_input"]
P1 --> P2 --> P3 --> P5
P1 --> P4
end
subgraph TTS合成["tts 扩展"]
T1["接收 tts_text_input"]
T2["合成语音"]
T3["输出 tts_text_result<br/>pcm_frame<br/>tts_audio_start/end"]
T1 --> T2 --> T3
end
subgraph 后处理["post_tts_process"]
direction TB
Q1["tts_text_result → text_data"]
Q2["pcm_frame → trim → 转发"]
Q3["tts_audio_end → on_tts_end"]
end
subgraph 下游
CTX["context 字幕"]
RTC["rtc 推流"]
AV["avatar 数字人"]
end
LLM --> 前处理 --> TTS合成 --> 后处理
Q1 --> CTX
Q2 --> RTC
Q3 --> AV
style 前处理 fill:#3498DB,color:#fff
style 后处理 fill:#E67E22,color:#fff
style TTS合成 fill:#27AE60,color:#fff六、关键指标(前处理上报)
| 指标 | 含义 | 什么时候记录 |
|---|---|---|
| TTFS | Time To First Sentence,首句送达 TTS 耗时 | 第一个 tts_text_input 发出时 |
| LLM FTFS | LLM 首 token 到首句送 TTS 的耗时 | 首句发出时 |
| llm_latency | ttfb + ttfs 综合延迟 | 轮次 final 时上报 event_bus |
| filler_words | 垫词是否成功送达 TTS | direct_tts 垫词 chunk 处理时 |
七、常见问题 FAQ
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Agent 开口慢 | fast_start 未开启或 batch 合并太多 | 设 tts.fast_start=true |
| 括号里的内容被念出来了 | skip_patterns 未配置 | 加对应 ID 到 tts.skip_patterns |
| 换音色不生效 | TTS 扩展不支持 update_configs | 确认 tts.addon 是否支持热更新 |
| 字幕和语音不同步 | enable_words 配置不一致 | 检查 pre/post 的 enable_words |
| 开头有顿一下 | TTS 首包带静音 | 开启 tts_leading_silence_trim |
| 打断后还在播 | flush 链路问题 | 检查 pre_tts flush + TTS flush 顺序 |
| 空回复卡住 | 空轮次未正确结束 | 检查 empty_tts_output_ended 链路 |
八、术语速查
| 术语 | 含义 |
|---|---|
| tts_text_input | 前处理发给 TTS 的文字输入 |
| tts_text_result | TTS 返回的合成文字结果(含逐字时间戳) |
| pcm_frame | TTS 返回的原始音频帧 |
| text_data | 发给 context 的字幕/转录数据 |
| on_tts_end | 发给 avatar 的「播完了」通知 |
| update_configs | 运行时热更新 TTS 配置的命令 |
| direct_tts | 跳过正常分句、直接送 TTS 的特殊 chunk |
| text_input_end | 标记一轮文字输入结束的标志 |
📎 图模板:templates_v2/convoai_v1_soseos.tmpl.json(main.pre_tts_process / post_tts_process 配置)
📎 前处理实现:ten_packages/extension/convoai_v2_main_python/src/pre_tts_process/
📎 后处理实现:ten_packages/extension/convoai_v2_main_python/src/post_tts_process/
📎 TTS 热更新规范:docs/specs/TTS_RUNTIME_CONFIG_SPEC.md
📎 默认配置:defaults.json → tts.* / parameters.main.tts_leading_silence_trim
Comments 0
Sign in to comment and like
No comments yet.