Skip to content

断句、收尾与逐句时延 ​

流式识别的时延不能只看请求总耗时。用户真正感知的是:最后一个有效语音帧之后,多久看到稳定文字,多久收到最终句子,多久确认会话已经结束。断句和收尾的状态设计,通常比单次模型推理耗时更容易造成尾部延迟和漏字。

先统一时间口径 ​

一条句子的时间线可以写成:

text
真实末音
  -> VAD 判定结束
  -> 静音窗口完成或显式提交
  -> 音频段送入 ASR
  -> Preview 产生
  -> Final 定稿
  -> terminal 发送

至少记录以下时间点:

时间点含义用于发现的问题
audio_end输入中最后一个有效语音帧的时间测量起点是否真实
vad_endVAD 确认语音区间结束尾部静音阈值和模型误判
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 走同一条收尾路径,只在触发来源上做区分。这样既能减少重复逻辑,也能保证“正常停止”和“输入结束”使用相同的缓存归属规则。

静音预计算的边界 ​

把静音窗口提前计入调度可以降低等待,但它必须建立在清晰的音频边界上:

  1. 先确认静音帧确实属于当前句子,而不是下一句的开头。
  2. 预计算不能绕过尾帧提交和 in-flight 任务等待。
  3. 任何提前发送的 Preview 都不能被误当成 Final。
  4. 参数变化要绑定到具体采样率、帧长和处理模式,不能只记录一个“静音 1.5 秒”。

如果发现时延下降但漏字增加,优先检查尾帧归属和提交顺序,而不是继续缩短静音阈值。

状态机检查清单 ​

排查停止或尾字问题时,逐项回答:

  • 最后一帧 PCM 是否到达会话处理器?
  • VAD 是否将最后一段标记为可提交?
  • 提交任务是否带有唯一 segment_id?
  • Final 是否只由该段的唯一完成路径发送?
  • Final 后是否仍允许角色或文本覆盖?
  • terminal 是否等待所有 in-flight 任务结束?
  • 客户端是否把 terminal 和正常关闭分别处理?

这些问题比单看一条最终文本更能定位收尾缺陷。

小结 ​

逐句时延的起点是“真实末音”,终点要区分“句子 Final”和“会话 terminal”。把尾部缓存、提交任务和最终事件纳入一套状态机,才能同时降低等待和避免漏字、重复、提前结束。

别急,先让缓存热一下。