中文流式识别偶发重复词的定位与修复实践

一、问题背景

最近在排查中文语音识别的流式输出问题时,遇到一个比较影响体验的现象:模型整体识别效果正常,但在少数情况下,输出文本会出现连续重复,例如:

复制代码
原始输出:我们今天讨论一下方案 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 相同就删除内容,可能导致漏字。因此,如果引擎提供绝对时间戳,应优先利用时间信息限制去重范围。

如果接口没有时间戳,可以先采用受限的尾部文本匹配作为临时方案,但必须通过实际录音验证,不能对整段历史文本执行无限范围匹配。

七、如何验证修复是否有效

修复完成后,建议准备三类测试数据。

  1. 异常样本:收集已出现重复的录音,验证修复是否消除目标问题。
  2. 正常重复样本:包含"再说一遍""不不不"等真实重复表达,检查是否出现误删。
  3. 分块边界样本:在短停顿、快速语速、句子切换以及中英文混说等情况下检查拼接效果。

可以先用下面的简单测试验证英文后处理逻辑:

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("基础回归测试通过")

实际项目还应统计修复前后的重复率、误删率、最终文本字错率以及端到端延迟。若是流式系统,还需要确认修复没有导致中间文本闪烁、已确认文本被反复改写或句尾内容丢失。

可以将每次输出的原始文本、分块编号、时间戳、最终文本和修复动作记录在日志中。涉及真实会议内容时,应避免在普通日志中直接保存完整敏感转写文本。

八、总结

这次问题的关键不在于找到一个能够删除重复词的正则表达式,而在于先弄清楚重复发生在哪个环节。

如果完整中间结果被当作增量追加,就修正状态管理;如果相邻分块重复识别同一段音频,就结合时间戳和重叠区域处理;如果单次解码本身已经出现异常,再检查解码器状态、缓存和模型输出。

在类似熙瑾会悟这样的离线会议转写应用中,实时文本的可读性直接影响用户对识别质量的感受。但修复不能以牺牲真实语义为代价。先定位、再缩小修改范围,最后用正常重复样本和异常样本共同回归,比简单地对输出文本做全局去重更稳妥。

流式识别的工程优化往往不是一次性改动,而是通过日志、边界测试和持续回归逐步减少异常。只有确保识别内容没有被误删,才能真正改善用户体验。

相关推荐
小蒜学长2 小时前
基于Java的公司采购系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·公司采购系统
小坏讲微服务2 小时前
Spring Boot 4 新特性全解析:从上手到生产实战
java·spring boot·后端·架构·springboot4
huaweichenai4 小时前
spring boot 实现file文件上传
java·spring boot·后端
Zelman4 小时前
测试层级与测试类型
后端·面试·测试
韩振方5 小时前
容器已经能运行了,为什么 Kubernetes 还要用 Pod?
后端
会编程的吕洞宾5 小时前
AgentScope Java 实战:给 AI Agent 加上权限管控
后端
Thneonl7 小时前
全集群钟差 300 毫秒会发生什么:证书悄悄过期,日志倒流
运维·后端
金銀銅鐵7 小时前
[Java] 借助GUI展示class文件的版本号
后端·python·ai编程