背景:实时语音故障往往发生在模型之前
实时语音应用常被简化成”麦克风音频送给模型,再把文字交给 LLM”。真正上线后,故障通常不只来自识别模型:浏览器送来 48 kHz 立体声,服务端按 16 kHz 单声道解析;网络重连导致音频片段重复;VAD 过早结束一句话;前端把尚未稳定的 Partial Transcript 当成最终文本;不同语音段的完成事件乱序到达,最终出现字幕倒序、内容重复或工具误调用。
因此,语音输入管线应被视为一个具有明确协议和状态机的生产系统。核心不是追求单一的最低延迟,而是在以下四个目标之间建立可测量的平衡:
- 边界正确:不漏掉句首、轻声和尾音,也不过度吞入长时间噪声。
- 状态一致:增量文本可以修改,最终文本只能按唯一语音段提交一次。
- 格式确定:采样率、声道数、编码、字节序和时间基准均可追踪。
- 延迟可解释:能区分采集、网络、VAD、识别首字和最终确认各阶段耗时。
核心原理一:先建立音频契约,再谈模型效果
音频契约必须版本化
每个音频会话至少应记录以下字段:
{
"audio_contract_version": "audio-v3",
"codec": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1,
"frame_duration_ms": 20,
"sequence_origin": 0,
"clock": "monotonic",
"vad_profile": "call-center-noisy-v2",
"transcription_profile": "realtime-balanced-v4"
}
这些字段不是普通日志,而是重放条件。没有它们,离线回放时无法判断质量变化究竟来自模型、重采样器、VAD 参数还是客户端编码升级。
Whisper 官方实现将音频重采样为 16 kHz 单声道 PCM,并以 30 秒为标准块处理;代码中的音频帧步长为 10 ms,时间戳 Token 粒度为 20 ms。这说明格式转换和时间基准是模型输入的一部分,而不是无关紧要的外围处理。但 16 kHz 不能被泛化为所有模型的固定要求,应由具体模型适配器声明能力。
建议把格式转换放在一个位置
客户端、边缘网关和识别服务同时做重采样,会产生难以追踪的重复处理。较稳妥的做法是:
- 客户端只负责采集、轻量封包和递增序号;
- 音频网关负责解码、声道归一化、重采样和幅度检查;
- 模型适配器只接收已经符合声明格式的帧;
- 原始音频是否保留,由隐私策略和故障采样策略决定。
音频网关应为每一帧附加 session_id、stream_epoch、sequence_no、capture_ts 和 duration_ms。发生重连时递增 stream_epoch,避免新旧连接使用相同序号造成误去重。
核心原理二:VAD 是可调的延迟—完整性决策器
OpenAI Realtime 文档区分两类转折检测:Server VAD 根据静音切分,Semantic VAD 根据话语是否语义完整判断用户是否说完。Server VAD 常见参数包括激活阈值、句首预留时长和静音结束时长;Semantic VAD 则通过 eagerness 控制更快切段还是给用户更多停顿时间。
这意味着 VAD 不是简单的”有声音 / 无声音”开关,而是在持续回答三个问题:
- 这段信号是不是语音?
- 语音从哪里开始,句首要向前回补多少?
- 当前停顿是句内犹豫,还是一次真正的发言结束?
不要只调一个静音时长
生产 VAD 至少要按场景分 Profile:
| 场景 | 特点 | VAD 策略建议 |
|---|---|---|
| 近讲麦克风 | 噪声低 | 可以更积极结束 |
| 电话窄带音频 | 压缩失真、双讲 | 处理压缩失真和双讲 |
| 会议室远场 | 多人和回声 | 提高句首回补 |
| 车载或户外 | 持续噪声 | 提高抗噪要求 |
| 长思考问答 | 长停顿 | 允许更长句内停顿 |
NVIDIA Riva 同时提供流式中间结果、神经 VAD、End-of-Utterance 和 Two-Pass EOU。Two-Pass 思路很有价值:先产生可供下游预处理的早期转写,再等待更可靠的最终端点确认。它揭示了实时系统的关键设计——早结果和最终结果应当是两个不同状态,而不是同一个文本字段不断覆盖。
VAD 的上线指标
除了识别错误率,还应记录:
| 指标 | 含义 |
|---|---|
speech_start_detection_ms | 真实句首到检测句首的延迟 |
front_end_clipping_ms | 被截掉的句首长度 |
end_of_utterance_wait_ms | 真实句尾到最终提交的等待 |
false_turn_rate | 噪声被提交为语音段的比例 |
split_utterance_rate | 一句话被错误拆成多段的比例 |
merged_utterance_rate | 多句话被错误合并的比例 |
empty_final_rate | 最终事件为空的比例 |
只看平均端点延迟,会掩盖 P95/P99 下的长等待和特定口音、噪声场景中的截断。
核心原理三:Partial 和 Final 必须通过状态机对账
实时转写通常同时返回增量文本和最终文本。OpenAI Realtime Transcription 文档明确区分 delta 与 completed 事件,并提示不同语音段的完成事件不保证按顺序到达,需要使用 item_id 进行匹配。
正确的数据模型不是一个可变字符串,而是语音段状态机:
CAPTURING -> SPEECH_STARTED -> PARTIAL_ACTIVE -> AUDIO_COMMITTED
-> FINAL_RECEIVED -> DOWNSTREAM_COMMITTED
异常分支:
-> CANCELLED -> TIMED_OUT -> SUPERSEDED -> REPLAY_REQUIRED
Partial 只用于展示和预计算
Partial Transcript 可以用于:
- 实时字幕;
- 提前做语言识别、意图候选和实体预热;
- 预取可能需要的工具或知识;
- 估计后续文本 Token 预算。
但它不应直接触发不可逆动作,例如提交订单、发送消息、修改数据库或确认身份。因为后续音频可能修正前面的词,数字、否定词和专有名词尤其容易变化。
Final 也不能”到达即执行”
最终事件应先通过以下门禁:
session_id+stream_epoch+item_id是否唯一;- 是否已经处理过相同完成事件;
- 音频序号区间是否连续,是否存在缺帧;
- Final 是否对应当前有效会话,而不是重连前的旧流;
- 是否需要等待更早的 item 完成,以恢复逻辑顺序;
- 是否满足业务侧最小置信度或关键实体复核规则。
一个简化的处理示例如下:
type SegmentState = {
itemId: string;
epoch: number;
partial: string;
final?: string;
status: "partial" | "final" | "committed" | "cancelled";
};
function onTranscriptEvent(event: TranscriptEvent) {
const key = `${event.sessionId}:${event.epoch}:${event.itemId}`;
const state = segmentStore.getOrCreate(key);
if (event.type === "delta" && state.status === "partial") {
state.partial += event.delta;
publishEphemeralCaption(key, state.partial);
return;
}
if (event.type === "completed") {
if (state.status === "committed" || state.status === "cancelled") return;
state.final = event.transcript;
state.status = "final";
finalReorderBuffer.offer(event.logicalOrder, state);
}
}
其中 publishEphemeralCaption 与最终业务提交必须使用不同通道。前者允许覆盖,后者要求幂等。
工程落地:把实时语音拆成六个可替换模块
1. Capture Adapter
负责浏览器、移动端、SIP 或设备音频接入。统一输出帧序号、采集时间和格式声明,不负责猜测模型格式。
2. Audio Normalizer
完成解码、声道转换、重采样、幅度归一化和异常帧检查。应暴露重采样耗时、削波率、静音比例和输入电平分布。
3. Turn Detector
封装 Server VAD、Semantic VAD 或本地神经 VAD。配置必须按 vad_profile 版本发布,不允许在线直接覆盖无审计参数。
4. Streaming Transcriber
负责向供应商或自建 ASR 发送有序音频块,接收 Partial、Final、时间戳、置信度和说话人标签。对于模型不支持的可选字段,要明确降级,而不是返回伪造默认值。
5. Transcript Reconciler
负责乱序恢复、Partial 替换、Final 幂等、重连 Epoch 隔离和时间戳归一化。它是语音管线中最容易被低估的模块。
6. Downstream Gate
把最终文本交给 LLM 或业务系统前,执行关键实体规则、会话状态检查和可逆 / 不可逆动作分级。语音识别结果只是输入证据,不应绕过业务授权。
时间戳与重放:别只保存一段文本
长音频采用滑动窗口或缓冲分段时,容易发生时间戳漂移、重复和幻觉。WhisperX 通过 VAD Cut & Merge 与强制音素对齐生成更精确的词级时间戳,其研究也指出长音频缓冲或滑窗方式容易出现漂移、重复和时间戳不准。
生产回放包建议包含:
{
"trace_id": "voice-01J...",
"audio_contract_version": "audio-v3",
"vad_profile": "call-center-noisy-v2",
"model_profile": "realtime-balanced-v4",
"audio_sha256": "...",
"frame_ranges": [[0, 181], [182, 346]],
"turn_events": [
{"type": "speech_started", "at_ms": 420},
{"type": "speech_stopped", "at_ms": 3180}
],
"partial_events": [],
"final_events": [],
"client_clock_offset_ms": 0,
"server_received_at": "2026-07-12T23:01:57Z"
}
对隐私敏感业务,可以只保存经过授权的抽样音频、不可逆音频指纹和完整事件元数据。没有真实音频时,至少保留帧范围、格式指纹、事件时间线和最终文本,便于定位协议错误。
评测方法:同时测字词、边界和交互延迟
仅用 WER 或 CER 评估实时语音不够。建议建立四层指标:
识别层
- WER/CER;
- 数字、日期、金额、邮箱、产品名等关键实体准确率;
- 空转写、截断转写和重复转写比例。
边界层
- 句首截断与句尾拖延;
- 错误拆句和错误合句;
- 噪声误触发;
- 打断和双讲场景下的恢复。
状态层
- Partial 被 Final 修改的字符比例;
- Final 乱序率;
- 重复完成事件率;
- 重连后旧 Epoch 污染率;
- 时间戳回退或重叠比例。
体验层
- 首个 Partial 延迟;
- 最终确认延迟;
- 用户说完到 LLM 开始回答的延迟;
- 字幕抖动次数;
- 用户因系统抢话而重复表达的比例。
OpenAI 当前实时转写文档也建议使用真实麦克风、电话音频、口音、背景噪声、代码切换、领域词汇和长会话进行测试,并分别跟踪空文本、截断和延迟,而不是只看 WER。
适用场景
该架构适合:
- 实时客服与电话机器人;
- 会议字幕、纪要和说话人归属;
- 车载、穿戴设备与语音助手;
- 语音驱动的 Agent 或工具调用;
- 医疗、保险、金融等需要关键实体复核的语音录入;
- 多供应商 ASR 切换与自建模型灰度。
离线播客转写若没有实时交互要求,可以简化 Partial 状态机,但仍应保留音频契约、分段和时间戳回放。
常见误区
误区一:VAD 越快,体验越好
过快切段会截断句内停顿并增加文本碎片。端点等待应按场景调优,不能只追求最小值。
误区二:Partial 只是 Final 的前缀
增量结果可能重写前文,不能假设只追加字符。UI 和存储都要支持替换。
误区三:完成事件天然有序
网络、并行识别和服务端调度都可能导致不同语音段的完成事件乱序。必须按稳定标识和逻辑序号对账。
误区四:所有音频统一 16 kHz 就够了
采样率只是契约的一部分。声道、编码、字节序、帧长、重连和时间基准同样影响结果。
误区五:真实噪声可以用白噪声代替
电话压缩、回声、双讲、远场混响、键盘声和移动网络丢包具有不同结构。合成白噪声只能覆盖有限场景。
上线检查清单
- 每种客户端都上报明确的编码、采样率、声道和帧长。
- 音频契约、VAD Profile、模型 Profile 均有不可变版本号。
- 重连时递增 Stream Epoch,并隔离旧连接事件。
- Partial 与 Final 使用不同存储语义和下游权限。
- Final 按 item_id 幂等,并支持乱序缓冲。
- 关键实体有独立准确率和人工复核策略。
- 测试集包含真实设备、电话、噪声、口音、代码切换和长会话。
- 分别监控首个 Partial、最终确认和下游响应延迟。
- 对 VAD 配置变更执行影子流量、回放和小比例灰度。
- 保留可审计的事件时间线,并按隐私策略限制原始音频留存。
- 模型不支持时间戳、置信度或说话人标签时,有明确降级路径。
- 出现空转写、持续噪声、乱序或超时风暴时,可以快速切换到安全 Profile。
更多 FAQ
VAD 应放在客户端还是服务端?
两者都可以,但只能有一个组件负责最终语音段提交。客户端 VAD 适合降低带宽和交互延迟,服务端 VAD 更便于统一治理。常见方案是客户端提供提示性事件,服务端根据统一 Profile 做最终裁决。
什么时候可以让 Partial 提前触发 LLM?
只适合可撤销的预计算,例如意图候选、检索预取或模型预热。任何不可逆工具调用都应等待 Final 和业务门禁。
如何判断一次 VAD 调整是否真正变好?
同时比较端点等待、句首截断、错误拆句、噪声误触发、Final 质量和用户打断率。只降低延迟但增加截断,不算改善。
参考资料
- OpenAI, Voice activity detection (VAD): https://developers.openai.com/api/docs/guides/realtime-vad
- OpenAI, Realtime transcription: https://developers.openai.com/api/docs/guides/realtime-transcription
- OpenAI Whisper, audio.py: https://github.com/openai/whisper/blob/main/whisper/audio.py
- Radford et al., Robust Speech Recognition via Large-Scale Weak Supervision: https://arxiv.org/abs/2212.04356
- Bain et al., WhisperX: Time-Accurate Speech Transcription of Long-Form Audio: https://arxiv.org/abs/2303.00747
- NVIDIA Riva, ASR Overview: https://docs.nvidia.com/deeplearning/riva/user-guide/docs/asr/asr-overview.html