一、问题背景
最近在排查中文语音识别的流式输出问题时,遇到一个比较影响体验的现象:模型整体识别效果正常,但在少数情况下,输出文本会出现连续重复,例如:
原始输出:我们今天讨论一下方案 the the the
期望输出:我们今天讨论一下方案
类似的问题不一定只出现在英文单词中,中文也可能出现"我们我们我们""这个这个这个"等重复内容。由于问题不是每次都能复现,单纯听几段录音很难确定原因。
刚开始排查时,容易怀疑是模型推理异常,甚至考虑重新训练模型。但从工程角度看,重复输出并不一定意味着模型本身出了问题。流式识别涉及音频分块、增量解码、缓存管理和文本拼接,其中任意一个环节处理不当,都可能产生重复文本。
因此,这次修复的思路是:先定位重复产生的阶段,再针对具体原因做最小范围的修改,而不是直接对最终文本进行全局去重。
二、先区分三类重复问题
在动代码之前,需要先判断重复文本属于哪一种情况。
| 问题类型 | 典型表现 | 优先排查位置 |
|---|---|---|
| 模型解码重复 | 同一段音频的单次解码结果已经重复 | 解码策略、模型输出 |
| 分块拼接重复 | 单个分块正常,拼接后出现重复 | 分块边界、文本合并 |
| 流式状态重复 | 每次增量更新都追加完整结果 | 状态管理、前端渲染 |
例如,识别器连续返回以下结果:
第1次:今天讨论方案
第2次:今天讨论方案的细节
第3次:今天讨论方案的细节已经确定
如果程序把每次返回值都直接追加到历史文本中,最终可能得到:
今天讨论方案今天讨论方案的细节今天讨论方案的细节已经确定
这不是模型重复识别,而是应用层错误地处理了增量结果。
另一种情况是,模型采用滑动窗口处理音频,相邻两个分块存在重叠区域。前一个分块识别出了"明天安排会议",后一个分块又识别出了"安排会议",如果直接拼接,也会产生重复。
还有一种情况是,重复内容已经存在于同一次解码结果中。这时才需要继续检查模型解码、缓存和搜索策略。
三、为什么流式输出容易出现重复
流式 ASR(Automatic Speech Recognition,自动语音识别)的特点,是不必等整段音频结束,就可以持续返回当前识别结果。
但中间结果并不一定是最终结果。随着新音频到来,模型可能修正之前的判断。相关研究也指出,增量识别的稳定性与识别准确率、输出延迟之间存在一定的权衡。
工程中比较常见的原因有以下几种。
1. 将完整中间结果误当成新增文本
有些识别接口每次返回的是当前完整假设,而不是本次新增的字符。如果消费端直接追加,就会重复历史内容。
2. 分块之间存在音频重叠
为了减少切分处的漏字,系统通常会保留一部分上下文。相邻分块可能识别到同一段语音,需要根据重叠区域处理重复。
3. 增量解码状态没有正确维护
如果缓存、解码器状态或历史假设管理不一致,可能使相同内容在多个输出阶段重复出现。
4. 文本后处理过于简单
直接使用字符串替换、全局去重或者删除连续相同的词,虽然可能暂时消除现象,却容易误删用户真实说出的重复内容。
因此,不能仅凭出现了 the the the 就认定是模型幻觉,更不能直接通过删除重复词来掩盖所有问题。
四、定点修复:先明确结果语义
假设当前识别服务通过回调返回中间结果,接口约定为:
partial:当前尚未确认的完整假设,可以被后续结果修正。delta:本次新增的文本片段。final:当前语音段的最终识别结果。
需要注意,以上是应用层约定,并非所有 ASR 引擎都使用相同字段名称。
如果上游返回的是完整假设,就应该替换当前临时文本,而不是追加:
ruby
class StreamTextState:
def __init__(self):
self.partial_text = ""
self.final_text = ""
def update_partial(self, text: str):
# 中间结果是完整假设,应覆盖而不是追加
self.partial_text = text
def commit_final(self, text: str):
self.final_text += text
self.partial_text = ""
如果接口明确返回的是增量文本,则应按照增量协议追加,不能直接套用上述逻辑。
这一步看似简单,却是流式重复问题中最值得优先检查的地方。
五、针对连续重复词增加轻量检查
如果日志已经证明,问题出现在单次模型输出或文本拼接后的局部重复,可以增加一个范围明确的后处理函数。
下面的示例针对空格分隔的英文词,检测连续三次及以上出现的相同词,并保留一次。它适合用作排查工具或受控场景下的兜底处理,不应直接视为通用 ASR 修复方案。
python
import re
def remove_repeated_english_words(text: str) -> str:
"""
清理连续出现三次及以上的相同英文词。
只处理明确匹配的英文单词,不做全局文本去重。
"""
pattern = re.compile(
r"\b([A-Za-z]+)(?:\s+\1){2,}\b",
re.IGNORECASE
)
return pattern.sub(lambda m: m.group(1), text)
if __name__ == "__main__":
samples = [
"Please open the the the document",
"We need to review the report",
"The team said no no no to the proposal",
]
for sample in samples:
print(remove_repeated_english_words(sample))
预期输出:
perl
Please open the document
We need to review the report
The team said no no no to the proposal
这里特意只清理连续三次及以上的重复,而不直接删除所有相邻重复词。因为"no no no"可能是说话人有意强调,不能仅凭文本相同就判定为错误。
不过,这个示例仍有局限:它无法判断重复是否真的来自模型错误。如果用户确实连续说了三次同一个词,也可能被误处理。因此,生产环境应结合时间戳、置信度、分块边界或上游异常信号决定是否触发修复。
对于中文,不能简单套用英文的空格分词规则。中文需要结合分词结果、字符边界和上下文进行处理,避免把"研究研究生培养方案"等不同结构误判为重复。
六、如果问题来自分块拼接,应该怎么修
对于带时间戳的分块输出,更可靠的方式是检查两个片段是否对应同一段音频,而不是只比较字符串。
例如:
python
def should_remove_overlap(
previous_end: float,
current_start: float,
max_gap: float = 0.5
) -> bool:
"""
示例:判断两个片段在时间上是否可能存在重叠。
时间单位为秒,具体阈值应依据实际分块策略调整。
"""
return current_start <= previous_end + max_gap
previous = {
"text": "明天安排会议",
"start": 1.0,
"end": 3.0,
}
current = {
"text": "安排会议并确认议程",
"start": 2.5,
"end": 4.5,
}
if should_remove_overlap(previous["end"], current["start"]):
print("检测到潜在时间重叠,需要进一步对齐文本")
这里仅判断是否存在潜在重叠,并没有直接删除文本。真正的去重还需要比较重叠区间对应的词、字符或 Token,并确认它们指向同一段音频。
尤其要注意,两个不同的词可能具有相同的子词前缀。仅凭 Token 相同就删除内容,可能导致漏字。因此,如果引擎提供绝对时间戳,应优先利用时间信息限制去重范围。
如果接口没有时间戳,可以先采用受限的尾部文本匹配作为临时方案,但必须通过实际录音验证,不能对整段历史文本执行无限范围匹配。
七、如何验证修复是否有效
修复完成后,建议准备三类测试数据。
- 异常样本:收集已出现重复的录音,验证修复是否消除目标问题。
- 正常重复样本:包含"再说一遍""不不不"等真实重复表达,检查是否出现误删。
- 分块边界样本:在短停顿、快速语速、句子切换以及中英文混说等情况下检查拼接效果。
可以先用下面的简单测试验证英文后处理逻辑:
java
def test_remove_repeated_words():
assert remove_repeated_english_words(
"open the the the file"
) == "open the file"
assert remove_repeated_english_words(
"open the file"
) == "open the file"
assert remove_repeated_english_words(
"no no no"
) == "no no no"
if __name__ == "__main__":
test_remove_repeated_words()
print("基础回归测试通过")
实际项目还应统计修复前后的重复率、误删率、最终文本字错率以及端到端延迟。若是流式系统,还需要确认修复没有导致中间文本闪烁、已确认文本被反复改写或句尾内容丢失。
可以将每次输出的原始文本、分块编号、时间戳、最终文本和修复动作记录在日志中。涉及真实会议内容时,应避免在普通日志中直接保存完整敏感转写文本。
八、总结
这次问题的关键不在于找到一个能够删除重复词的正则表达式,而在于先弄清楚重复发生在哪个环节。
如果完整中间结果被当作增量追加,就修正状态管理;如果相邻分块重复识别同一段音频,就结合时间戳和重叠区域处理;如果单次解码本身已经出现异常,再检查解码器状态、缓存和模型输出。
在类似熙瑾会悟这样的离线会议转写应用中,实时文本的可读性直接影响用户对识别质量的感受。但修复不能以牺牲真实语义为代价。先定位、再缩小修改范围,最后用正常重复样本和异常样本共同回归,比简单地对输出文本做全局去重更稳妥。
流式识别的工程优化往往不是一次性改动,而是通过日志、边界测试和持续回归逐步减少异常。只有确保识别内容没有被误删,才能真正改善用户体验。