R2T2 这类实时语音识别接入自有应用时,重复字、丢字和断句错位往往不是模型识别不准,而是两个工程环节各自留下的痕迹:音频被切成块时边界切在哪,以及中间结果到达界面时是追加还是覆盖。判断顺序建议先定位错位落点,再分别查分块重叠和拼接策略,不要一上来就调模型参数或换引擎。
先把每个错字标到时间轴和块序号上:丢字多出现在块首或块尾,说明切分切断了音节或尾音;重复字多出现在中间结果刷新处,说明拼接把同一段文本写了两次。适用场景是自有应用直接消费识别流;操作动作是打开分块日志与中间结果日志;验证方式是拿同一段音频反复比对块序号与错字位置;边界是若整句语义都乱,而不是个别字错位,应回头确认采样率、声道和编码是否在链路上被改动过。
在输出里定位重复字和丢字分别出现在什么位置
不要只看最终文本。把每次回调的中间结果、每块的起止时间、块序号都落盘,再拿最终文本逐字回标。做法是给每块生成一个带序号的中间文件,例如 chunk_0007_2310ms_3090ms.txt,内容是该块当前返回的文本。这样重复字能查到它是在第几次回调里第二次出现的,丢字能查到它上一块的尾部和下一块的首部各是什么。
需要保留的中间输出包括:每块原始音频或其校验值、每块起止时间、每次中间结果回调的完整文本与是否标记为终态。基线建议先用同一段音频跑一次“只要最终结果、丢弃中间结果”,确认最终结果本身是对的,再打开中间结果,看错位是不是只在流式刷新阶段出现。
[chunk] index=7 start=2310ms end=3090ms audio_ms=780
[asr] seq=14 text="今天天气不错" final=false
[asr] seq=15 text="今天天气不错我们" final=false
[asr] seq=16 text="今天天气不错,我们" final=true
如果最终文本也丢字,且丢的字总落在 index 的边界附近,先按下一节查分块;如果最终文本多字,且多出的字在 seq 递增时首次出现的位置很规律,先查拼接。
检查块与块之间有没有留重叠音频
无重叠切分最省算力,但块边界正好切在音节或词的中间时,前一块听不到后半段,后一块听不到前半段,两边都可能把那个字丢掉或识别成别的字。有重叠切分让边界附近的声音在两块里各出现一次,丢字会少,但代价是同一段文本可能被两块都识别出来,拼接没做去重就变成重复字。
- 无重叠对照:把重叠设为 0,跑同一段音频,重点看相邻两块的尾部和首部是否出现半个字或音近字。
- 有重叠对照:给一个较小的重叠,跑同一段音频,重点看边界词是否在最终文本里出现两次。
重叠长度没有通用值,通常从几百毫秒量级起步,但必须结合识别引擎的最小输入长度、音频帧长和你的静音切分策略确认。调整方式是每次只改一个量,用同一段含边界难词的音频对比:先找到能消除丢字的最小重叠,再检查这个重叠下重复是否可被拼接逻辑吃掉。
# 可替换的配置示例,参数名按实际实现改
chunk_size_ms = 2000
chunk_overlap_ms = 300 # 先试 0 与某个非零值做对照
结果拼接时用追加还是覆盖刷新
追加是每次回调把新文本直接接到已显示文本后面,实现最简单,但很多实时识别会把“当前整段”而不是“本块新增”发回来,追加就会把已有内容再写一遍。覆盖刷新是每次回调用一个完整字符串替换当前显示内容,界面不会重复,但需要知道哪一段已经定稿。
比较稳妥的写法是维护一个已定稿前缀和一个可变尾部:中间结果到达时,只覆盖可变尾部;收到终态标记时,把可变尾部替换成定稿文本,并把定稿部分并入前缀,再推进前缀指针。这样既可追加,又不会重复。
prefix = "" # 已定稿文本
tail = "" # 当前块可变的中间结果
def on_result(text, is_final):
global prefix, tail
if is_final:
prefix = prefix + text # 或按对齐规则合并
tail = ""
else:
tail = text
render(prefix + tail)
风险边界是:如果引擎的终态文本包含了前缀内容,上面的 prefix + text 会重复,需要改成按最长公共前后缀对齐后再合并,或者直接用终态文本覆盖整段。
用一段带停顿和口头语的音频压测这条链路
只用朗读干净文本的测试音频,分块对齐的问题很难暴露。素材里应有:超过一处的长停顿、明显的口头语、语速切换、以及数字或专有名词。停顿容易触发静音切分,口头语容易被中间结果反复改写,数字和专名容易在块边界被切开,这三类情况能较集中地把链路问题提前暴露出来。
- 需要记录的异常现象:同一词在最终文本出现两次;块序号连续但中间某块输出为空;停顿处被拆成两个短句;专名被拆到两块各识别一半;中间结果出现后又被整段回退。
- 重复次数安排:同一素材至少完整跑多轮,确认现象可复现而不是偶发;每改一个分块或拼接参数,回到同一素材重跑并对比,避免换素材导致结论不可比。
压测通过的标准不是完全没有错字,而是同一类错位在重复运行时表现一致,且能被归到某一块或某一次回调上。
接入验证:把每块的输入时长和输出字数打出来
最快的自查手段是加两条日志:每条音频块记一行,每次识别结果记一行。字段名以实际实现为准,含义对齐即可。
chunk_index, chunk_start_ms, chunk_end_ms, audio_ms, overlap_ms, bytes
seq, chunk_index, text_len, is_final, text_preview
判断方法:如果每块的 audio_ms 都接近预期,每块 text_len 也没有异常为 0,而最终文本仍然重复,问题大概率在拼接;如果某些块的 text_len 明显偏小或为 0,而丢字正好落在这些块的时间范围内,问题大概率在分块或静音切分;如果 chunk_start_ms 出现回退或跳跃,先查音频入队逻辑,而不是识别模型。
这套字段足够支撑前四节的排查,不需要额外埋点。先在开发环境用同一段素材把两类日志跑通,再决定要不要动重叠长度和拼接策略。