Appearance
断句、收尾与逐句时延
流式识别的时延不能只看请求总耗时。用户真正感知的是:最后一个有效语音帧之后,多久看到稳定文字,多久收到最终句子,多久确认会话已经结束。断句和收尾的状态设计,通常比单次模型推理耗时更容易造成尾部延迟和漏字。
先统一时间口径
一条句子的时间线可以写成:
text
真实末音
-> VAD 判定结束
-> 静音窗口完成或显式提交
-> 音频段送入 ASR
-> Preview 产生
-> Final 定稿
-> terminal 发送至少记录以下时间点:
| 时间点 | 含义 | 用于发现的问题 |
|---|---|---|
audio_end | 输入中最后一个有效语音帧的时间 | 测量起点是否真实 |
vad_end | VAD 确认语音区间结束 | 尾部静音阈值和模型误判 |
submit | 该段音频进入识别队列 | 排队和调度延迟 |
preview | 首个可见中间结果 | 首字延迟 |
final | 句子完成且不再修改 | 逐句稳定时延 |
terminal | 会话生命周期结束 | 停止和 EOF 收尾 |
推荐同时报告两项指标:audio_end -> final 的逐句时延,以及 stop/EOF -> terminal 的停止时延。两者混在一起,会掩盖“文字已经完成但会话还未结束”的状态问题。
预览与最终结果分别计时
Preview 可以直接发布已有模型假设,不等待最终身份授权,因此可能修订或包含非目标内容。Final 使用独立的过滤和提交规则。预览加速的验收需要同时断言:首次新文字更早出现、原有 Final 对象保持一致、纯非目标输入仍无正式文字、terminal 后不残留临时文字。
统计预览节奏时,只计算产生新文本的模型假设;重复事件和从 Final 派生的显示事件不能充当新的模型输出。否则增加事件发送频率就会被误报为识别速度提升。
用边界对照定位尾字丢失
弱音或清辅音尾部可能在 VAD 判停后被裁掉。一轮连续流回放中,尾部保留 40 ms 时末字没有进入模型;将终点向后移动 0.5 秒恢复末字,将起点提前 1 秒却不能恢复,根因指向结束边界而非前导上下文。
降低 VAD 阈值虽能找回尾音,却合并了多句;将尾部保留扩到 800 ms 又在该测试中触发生成循环。当前生产默认 vad_keep_tail_ms 为 400 ms,是该链路回归后的取舍,不能作为所有音频的最佳值。验证同时检查尾字、句间合并、重复生成、时延与角色归属。
必须回放完整连续流,再对照 Final worker 实际收到的 PCM 起止样本。把出错句子切成独立音频会重置 VAD 状态;滑动预览窗口也不等于 Final 输入,二者都可能掩盖边界错误。
前端参数与有效策略
服务端最大段长默认值不能代表会话实际值。前端曾显式发送 endpoint_max_utterance_ms=8000,使服务端的 20000 ms 默认不生效。修复应让场景配置成为 Web 与 Android 的共同来源,再核对真实 start 帧中的值;当前场景使用 20000 ms 上限。仅检查生成的静态文件不够,已打开的浏览器仍可能运行旧 JavaScript,必须刷新后重新建立会话。
长静音收尾策略应检查波形门控是否实际启用。存在登记 ID 但门控关闭的目标会话,仍需要正常的长静音结束上限;按“是否登记”分支会使这类会话一直等待。回归组合包括普通模式、目标且门控关闭、目标且门控开启,并分别发送静音、最大段长和 EOF。
低推理耗时为什么仍有数秒等待
一种实际现象是模型几百毫秒完成,但短停顿未达到确认门槛,音频仍留在未提交区间。直到最大段长触发,处理器才回看早先的停顿并提交。此时瓶颈发生在模型调用之前,继续优化解码无法消除等待。
排查时画出停顿候选、确认时刻、提交时刻和执行时刻。不要将文件尾、VAD 推定尾部和人工末音视为同一个起点。输出时间区间指向文件中间的句子,也不能用整段文件结束时间计算该句时延。
缩短静音阈值、限制旧停顿回溯或组合更早的辅助 VAD,都曾在局部样本减少等待,却在其他样本新增切点、切开词组、改变角色或漏字。可复用的是“先分析未提交区间,再对照切分结果”的方法;任何历史阈值都需要在当前链路重新验收。
预计算结果的所有权
静音确认期可以提前准备 ASR 或角色结果,确认后复用。但已入队任务与会话准备器必须分开管理:队列接受任务时移交结果所有权,后续 prepare 的取消不能让已接受任务失效。EOF 应等待任务的完成路径,而不是把所有准备结果统一清空。
text
prepare 拥有可取消候选
-> 校验 PCM / 配置 / 角色计划
-> 队列接受:移交给 immutable job
-> job 独立完成并发布
队列拒绝:所有权仍留在 prepare,由其释放验证至少覆盖“已接受后立刻 EOF”“准备期间续说”“队列满”“新准备取消旧准备”和“角色计划变化”。只测普通句子无法触发这些竞争条件。
尾部缓存必须有唯一归属
静音或停止到来时,处理器经常同时持有三类数据:已提交段、正在等待的尾部帧、尚未完成的识别任务。若多个分支都能清理或提交尾部缓存,就会出现:
- 末尾几个字没有进入任何段。
- 同一段被提交两次,产生重复句子。
- Final 已发送,但尾部异步任务又写回旧句子。
- terminal 提前发送,客户端把后续事件视为无效。
实践中应给缓存定义单一所有者,并为每次提交生成唯一段标识:
text
收音中 -> 追加到 current
VAD 结束 -> current 转为 pending,创建 segment_id
提交识别 -> pending 只读,进入 in_flight
Final -> in_flight 转为 finalized
terminal -> 仅在 current/pending/in_flight 全部清空后发送停止和 EOF 走同一条收尾路径,只在触发来源上做区分。这样既能减少重复逻辑,也能保证“正常停止”和“输入结束”使用相同的缓存归属规则。
静音预计算的边界
把静音窗口提前计入调度可以降低等待,但它必须建立在清晰的音频边界上:
- 先确认静音帧确实属于当前句子,而不是下一句的开头。
- 预计算不能绕过尾帧提交和 in-flight 任务等待。
- 任何提前发送的 Preview 都不能被误当成 Final。
- 参数变化要绑定到具体采样率、帧长和处理模式,不能只记录一个“静音 1.5 秒”。
如果发现时延下降但漏字增加,优先检查尾帧归属和提交顺序,而不是继续缩短静音阈值。
状态机检查清单
排查停止或尾字问题时,逐项回答:
- 最后一帧 PCM 是否到达会话处理器?
- VAD 是否将最后一段标记为可提交?
- 提交任务是否带有唯一
segment_id? - Final 是否只由该段的唯一完成路径发送?
- Final 后是否仍允许角色或文本覆盖?
- terminal 是否等待所有 in-flight 任务结束?
- 客户端是否把 terminal 和正常关闭分别处理?
这些问题比单看一条最终文本更能定位收尾缺陷。
小结
逐句时延的起点是“真实末音”,终点要区分“句子 Final”和“会话 terminal”。把尾部缓存、提交任务和最终事件纳入一套状态机,才能同时降低等待和避免漏字、重复、提前结束。
