一、整体位置:TTS 在对话链路哪一段?

可以把对话想成一条流水线:

用户说话 → ASR 转文字 → LLM 想回复 → **TTS 把回复变成声音** → RTC 推给用户听

本文关注的是 LLM 输出之后、用户听到声音之前/之后的两个处理阶段:
- **前处理(pre_tts_process)**:LLM 流式文字 → 整理、分句 → 送给 TTS 合成
- **后处理(post_tts_process)**:TTS 合成结果 → 转格式、裁静音、通知下游

flowchart LR LLM["llm<br/>流式输出文字"] --> Main["main 扩展"] subgraph Pre["🔧 前处理 pre_tts_process"] Parse["分句/过滤"] Meta["metadata 热更新"] Send["送 tts_text_input"] end subgraph TTS["🎙️ tts 扩展"] Synth["语音合成"] end subgraph Post["🔧 后处理 post_tts_process"] Text["文字结果转换"] Audio["音频转发/裁静音"] End["结束通知"] end Main --> Pre --> TTS --> Post Post --> RTC["agora_audio3a → rtc<br/>推给用户"] Post --> Avatar["avatar 数字人"] Post --> CTX["context 字幕/状态"] style Pre fill:#3498DB,color:#fff style Post fill:#E67E22,color:#fff style TTS fill:#27AE60,color:#fff

模块 默认开关
pre_tts_process enabled: true
post_tts_process enabled: true
tts_leading_silence_trim enabled: false
tts 扩展本身 —

三、前处理(pre_tts_process)

前处理的核心任务:把 LLM 一块一块吐出来的文字,整理成 TTS 能吃的格式,并决定哪些内容要念、哪些只显示不念。

3.1 输入从哪里来?

sequenceDiagram participant LLM as llm 扩展 participant Main as main participant Pre as pre_tts_process participant TTS as tts 扩展 participant CTX as context LLM->>Main: llm_text_chunk(流式文字块) Main->>Pre: LLMTextChunkEvent alt 普通文字块 Pre->>Pre: 分句 + 过滤 + 合并 Pre->>TTS: tts_text_input Pre->>CTX: text_data(字幕/展示) else metadata 块(content_type=json) Pre->>TTS: update_configs(热更新音色等) Note over Pre,TTS: 阻塞等待 TTS 返回,本块不合成 else 音频模态(payload.audio) Pre->>CTX: text_data(只发字幕,不送 TTS) end Pre->>TTS: 最终 tts_text_input(text_input_end=true)

3.2 三种输入类型

3.3 分句与合并(batch / fast_start)

LLM 是流式输出的,可能一次只给几个字。前处理要把零散文字拼成合适的「句子包」再送给 TTS。

配置 来源 默认值 作用
batch tts.batch true 是否按标点分句;false 则整块直接送
fast_start tts.fast_start false 第一句不等合并,尽快送 TTS(降低首包延迟)
chunk_limit 代码内置 64 合并时每个包最多多少字符
flowchart TD Chunk["收到 llm_text_chunk"] --> Batch{"batch=true?"} Batch -->|否| Whole["整块文字直接送 TTS"] Batch -->|是| Parse["按标点分句<br/>。!?, 等"] Parse --> Fast{"fast_start 且是第一句?"} Fast -->|是| Individual["每句单独送 TTS<br/>(最快出声)"] Fast -->|否| Merge["按 chunk_limit 合并<br/>(减少 TTS 请求次数)"] Individual --> TTS["送 tts_text_input"] Merge --> TTS Whole --> TTS

有些内容只需要显示在字幕里,不需要念出来。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 返回 → 后续文字用新配置合成。

⚠️ 注意:这个过程会阻塞同一条文字流,保证配置更新完才继续合成。

sequenceDiagram participant LLM as LLM 流式输出 participant Pre as pre_tts_process participant TTS as tts 扩展 LLM->>Pre: metadata chunk(含 tts_params) Pre->>TTS: Cmd update_configs(同步等待) TTS->>TTS: merge 配置 → pending 或立即生效 TTS-->>Pre: OK / Error Note over Pre: 本 chunk 不送合成 LLM->>Pre: 后续普通文字 chunk Pre->>TTS: tts_text_input(使用新配置)

场景 前处理行为
正常结束(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 前处理关键配置

四、后处理(post_tts_process)

后处理的核心任务:接收 TTS 扩展吐出的结果(文字、音频、开始/结束信号),翻译成下游模块能理解的格式,并做音频优化。

4.1 输入事件一览

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 没输出内容) 内部状态机
sequenceDiagram participant TTS as tts 扩展 participant Post as post_tts_process participant A3A as agora_audio3a_upstream participant RTC as rtc participant Avatar as avatar participant CTX as context TTS->>Post: tts_text_result Post->>CTX: text_data(带逐字时间戳) TTS->>Post: pcm_frame(音频流) alt 开启 leading_silence_trim Post->>Post: 裁掉开头静音 end Post->>A3A: 转发音频 A3A->>RTC: 推给用户 TTS->>Post: tts_audio_end Post->>Avatar: on_tts_end(含时长信息) Post->>Post: 发布 tts_output_ended 事件

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 的静音样本。

flowchart TD Frame["收到 TTS pcm_frame"] --> Enabled{"trim.enabled?"} Enabled -->|否| Forward["直接转发到 agora_audio3a_upstream"] Enabled -->|是| First{"本轮第一包 audible?"} First -->|已转发过| Forward First -->|还没转发过| Scan["扫描音频样本振幅"] Scan --> Silent{"全部低于阈值?"} Silent -->|是| Drop["丢弃(累积 trim 时长)"] Silent -->|否| Trim["裁掉开头静音部分"] Trim --> Forward Forward --> RTC["→ RTC 推流"] style Trim fill:#E74C3C,color:#fff style Forward fill:#27AE60,color:#fff
配置 默认值 说明
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