GPT-Live 这类语音接入,对话和实时字幕走的往往是同一条音频链路,但时间目标不一样:对话要尽快给出能被打断的回应,字幕要按语句节奏稳定吐字。把采样、分段、生成窗口和丢字补偿拆成两组配置分开维护,再在每轮只改一个变量做回归,通常比硬凑一套配置更容易两边都达标。具体阈值需要结合上行带宽、模型首字耗时和播放端缓冲确认,不建议直接照搬别人的参数。
同一条链路里,对话优先“快且可打断”,字幕优先“稳且能连上语义”。建议把采样与分段、生成窗口、丢字补偿拆成两组配置分开维护,每轮只动一个变量,用同一段音频回归。冲突点主要在分段长度、静音判定阈值和是否保留被打断片段上;边界是这些数值要按你的端到端延迟和播放缓冲实测调整,整段音频丢失时补偿无法凭空还原内容。
把对话和字幕对时间的要求各自写成一列
先把两边的诉求拆开写清楚,再谈参数,不然很容易出现“改了对话、字幕断句,改了字幕、对话变慢”的循环。冲突不是抽象的,它具体落在分段长度、静音切分阈值、生成窗口下限、是否保留被打断内容这几个参数上。
| 维度 | 对话场景 | 字幕场景 |
|---|---|---|
| 可接受等待 | 用户停顿后希望立刻续接,等待不宜长 | 允许按句或按短语对齐,晚到一点可以接受 |
| 可丢弃内容 | 可丢重复、语气词,以及被打断后的剩余未播内容 | 丢的是整句或整段,需要靠补偿把语义接回来 |
| 是否需要回放 | 一般不需要回放,插话即覆盖旧内容 | 需要,字幕要能回溯上一句,保证上下文连续 |
| 主要冲突参数 | 分段长度、最短生成窗口、打断灵敏度 | 分段长度、静音切分阈值、补偿窗口 |
对照表的作用是:当有人说“慢”或“丢字”时,能立刻定位到是哪一列的要求被压缩了。对话列被压的是等待时间,字幕列被压的是断句完整性,两者用的却是同一组分段参数,所以才会互相拖累。
固定采样与分段参数,每轮只改一个变量
先列一份需要固定的清单:采样率、声道数、音频帧长、编码格式、VAD 实现与静音时长、单段最大时长、最小分段时长、生成窗口下限、丢字补偿开关。其中采样率、声道、帧长、编码、VAD 实现这几项建议整轮不动,它们是基线,动了之后前后两轮就没有可比性。
# 通用接入骨架,字段名按你所用组件替换
audio:
sample_rate: 16000 # 采集与识别两端必须一致,先固定
channels: 1
frame_ms: 20 # 先固定
vad:
silence_ms: 500 # 本轮唯一变量:对话调小,字幕调大
segment:
max_ms: 8000 # 先固定
min_ms: 1000
generate:
min_window_ms: 0 # 对话用,下一节再动
subtitle:
compensate: false # 字幕用,改这一项时其余全部锁定
单变量对照的做法是:准备一段包含停顿、插话和连续长句的音频,只改一个值跑一遍,只记录这个值对应的表现——首响是否更早、断句落在哪、有没有连续两段为空。同一轮里不要同时改 silence_ms 和 min_window_ms,否则首响变快到底是哪一项带来的,无法归因。
给字幕场景写一段丢字补偿的伪代码
字幕丢字通常出现在两种时刻:上游返回空文本,或者两段之间的时间戳空隙突然变大。补偿的目的不是重新生成整句,而是把接缝补上,让文字仍然连得上语义。
# 通用骨架,函数名与数据结构按你的实现替换
# 触发条件:当前段文本为空,或时间戳空隙超过 GAP_MS
gap = cur.start_ts - prev.end_ts
if is_empty(cur.text) or gap > GAP_MS:
if not ends_with_punct(prev.text):
merged = prev.text + cur.text # 同一句被切断,直接拼接
else:
merged = prev.text + SEP + cur.text # 跨句,补一个分隔符
emit(merged) # 只补文本接缝,不动时间戳
mark_compensated(cur.segment_id) # 打点,便于事后核对
GAP_MS 和拼接时保留的尾部、头部字数,需要按实际语速和显示宽度调;SEP 建议用统一的标点或空格,避免字幕左右跳动。风险边界要写清楚:如果上游整段音频缺失,补偿没有素材可用,这时只应标记一次丢字事件并让字幕显示占位,不要凭空造句子填满。
给对话场景设置可打断的最短生成窗口
用户插话被整段忽略,通常是因为打断事件来得比第一段生成还早,系统把这次插话当成了噪声丢掉。给对话配一个最短生成窗口,窗口内不响应打断,窗口之后再放开打断,就能保住一句话的开头。
dialog:
min_window_ms: 300 # 生成长度不足此值时,不响应打断
interrupt: true
interrupt_min_speech_ms: 200 # 插话需持续这么久才认定为打断
flush_on_interrupt: true # 打断后丢弃尚未播出的内容
验证是否生效,靠日志而不是感觉。建议在打断路径上打点:生成开始时间、已生成字符数、打断事件时间、被丢弃的内容长度。测试时先用固定音频让回答开始生成,在生成不足 min_window_ms 时插入一句“等一下”,看日志里是否没有触发丢弃、开头是否完整保留;再把插话点移到窗口之后,确认能正常打断且不会重复播放上一句。如果两次表现一致,说明窗口没有真正生效,需要检查打断判定是不是在更早的位置就被短促噪声触发了。
用同一段音频在两种配置下各跑一遍
分开配置之后,必须用同一段音频回归,否则两组参数只是纸面上分开,实际效果无从比较。音频内容建议包含:短促停顿、一次中途插话、一段连续长句、一次明显的环境噪声。
| 轮次 | 配置组 | 唯一变量 | 首响相对表现 | 断句位置 | 是否出现空段 | 打断是否生效 |
|---|---|---|---|---|---|---|
| 1 | 对话组 | min_window_ms | 相对上一轮更早/更晚 | 不关注 | 不关注 | 保留开头 / 吞字 |
| 2 | 字幕组 | silence_ms | 不关注 | 落在句内还是句末 | 有 / 无 | 不关注 |
判读要点分两边看:字幕组看断句是否落在语义边界、有没有连续两段缺失、补偿打点是否被触发;对话组看首响是否在停顿后立刻续接、插话后有没有重复或吞掉开头。同一组配置建议至少跑两遍,表现一致再切到另一组,避免把偶发抖动当成配置改动带来的效果。哪一边达标、哪一边还没达标,直接记在对应行,不要合并成一列,否则下一轮改参数时又会分不清是哪一列的要求被牺牲了。