上个月帮一个做会议纪要的团队复盘转录质量,他们那条链路的错字率常年在8%上下晃,客户投诉集中在两类:人名地名写成谐音,以及说话人那些"嗯、那个、就是说"被原样保留进了正式文档。这几乎是所有ASR落地项目的通病——模型听得见,但写不对。最近业内传得比较凶的一个数字是2.6%:某头部科技公司放出的新一代语音识别模型,在标准测试集上把词错误率压到了这个量级,同时第一次把手动删改口误这件事做进了模型本身。对做转录产品的人来说,这两个指标指向的是两个完全不同的问题:WER 2.6%解决的是"字对不对",自动改口误解决的是"句子能不能直接拿去用"。前者是识别精度,后者是可用性。这篇东西我打算把这两件事拆开讲清楚:WER这个数字到底怎么读、新模型在架构上动了哪些地方、你怎么把它接进现有流水线、以及怎么科学地测出属于你自己场景的真实错误率。刚接触ASR的同学可以看前半部分建立概念,已经在上生产的同行可以直接跳到第3节看参数配置和第4节看评测脚本。
1. 先把WER这个数字读明白,别被2.6%带偏
1.1 WER的拆解公式,以及三个子指标各自意味着什么
词错误率(Word Error Rate,WER)不是"错了百分之几的字"这么朴素的理解,它是一个基于编辑距离的比值。标准定义是把参考文本转换成识别文本所需的最少操作次数,除以参考文本的总词数:
WER = (S + D + I) / N
其中S是替换(substitution,把A写成了B),D是删除(deletion,漏掉了),I是插入(insertion,凭空多出来),N是参考文本的词数。中文场景更常用字错误率(Character Error Rate,CER),把词换成字,计算逻辑完全一样。
这三个子指标的意义完全不同,却被一个总分掩盖了。举个例子:某条音频的WER是6%,拆开看可能S=2%、D=3%、I=1%。删除率高,通常意味着音频有丢帧、VAD把语音切掉了、或者说话人声音太小低于阈值;插入率高,多半是背景噪声、呼吸声、口头禅被当成了语音;替换率高才是真正的"听错",可能来自口音、专业术语、同音字。你拿到一个总WER,第一件事应该是把它拆成这三项,否则根本不知道该往哪个方向修。
我见过不少团队拿着总WER反复调模型参数,结果发现30%的错误来自VAD切分边界——音频被切成两半,前半句的尾字和后半句的首字都被削掉了,全算成删除错误。这类问题调模型调一年也没用,改切分策略当天就见效。
1.2 2.6%这个数字的含金量,取决于它跑在什么集上
任何WER数字离开测试集都是耍流氓。同样是2.6%,跑在朗读语料和跑在真实会议录音上,含金量差着量级。你看到一个新模型报出低错误率,至少要追问四件事:
| 追问项 | 为什么关键 | 常见的水分点 |
|---|---|---|
| 测试集是什么类型 | 朗读、播客、会议、电话客服的难度天差地别 | 用干净朗读集报数,落地到会议场景直接翻倍 |
| 有没有开热词/上下文偏置 | 开了偏置等于给了答案提示 | 报数时开,产品里默认关 |
| 是离线整段还是流式分块 | 整段识别能吃满上下文,流式只能看历史 | 宣传用离线,产品要实时 |
| 参考文本的归一化规则 | 数字、标点、大小写怎么算 | 归一化宽松,错误被"洗掉" |
我个人判断一个数字可信度的土办法:先看它有没有同时报出"带噪""远场""带口音"这几个子集的结果。只报一个总分的,基本可以当作上限参考,真实落地打个1.5到2倍的折扣是常态。
1.3 为什么你自测的WER总比官方高出一截
这个现象太普遍了,我在三个不同团队都遇到过。原因通常不在模型,而在你自己的评测口径。第一条是参考文本(ground truth)本身不干净:人工转写时标点、数字写法、语气词保留策略和模型输出不一致,每处不一致都算一个错误。第二条是归一化没做:模型输出"二零二四年三月",参考文本写"2024年3月",逐字比对全错,实际上语义完全正确。第三条是切分粒度不同:参考文本按句切,模型输出按段切,边界处必然产生插入和删除。
提示:在跑任何WER对比之前,先把参考文本和模型输出过一遍同一套归一化函数。这一步能让你看到的错误率下降2到4个百分点,而且这个下降是"真实的",因为那些错误本来就不该算。
2. 从"听得清"到"写得对":新模型动的是哪几刀
2.1 端到端架构与上下文偏置,解决的是专有名词灾难
传统ASR流水线是声学模型、发音词典、语言模型三段式,任何一段出问题都会传导到最终结果。新一批模型走的是端到端路线,音频直接映射到文本序列,中间不再有独立的发音词典。这个变化带来的最直接好处是**上下文偏置(contextual biasing)**变得极其自然:你可以把一批专有名词、人名、项目代号作为提示喂进去,模型在解码时会主动往这些词上靠。
这件事的产品意义被严重低估了。真实业务里,错误最集中的往往不是常用词,而是那些一天出现几十次的项目名、产品型号、客户姓名。这些词在通用语料里的频率极低,模型没有任何理由认识它们。我做过一个统计,某次内部评测中,专有名词只占参考文本总词数的4%,却贡献了整体替换错误的37%。
配置热词时有两个经验值值得记住。第一,热词表不要贪多,控制在100到300条,超过这个量级偏置效果会被稀释,而且容易引发误触发——把发音相近的普通词硬拽成热词。第二,热词要带上权重参数,业务上必须对的词给高权重,锦上添花的给低权重。很多接口把这个参数叫boost或者weight,取值区间通常是0到10,我一般核心词给8,普通词给3。
2.2 自动改口误:这不是简单的正则替换
标题里"自动帮你改口误"这件事,行话叫非流利性消除(disfluency removal),包含三类东西:填充词(嗯、啊、那个、就是说)、重复(我我我觉得)、自我修正(我们周三……不对,周四开会)。前两类靠正则能搞定大半,第三类是真难点,因为它需要理解语义。
举个例子,"我们定在下周二,呃不,下周三碰一下"。正则删掉"呃不"之后剩下"我们定在下周二,下周三碰一下",语义直接矛盾了。正确的改写应该是整段重写为"我们定在下周三碰一下"。这就不是字符串操作能解决的,需要模型具备重写能力。
新模型把这一步做进了解码流程,输出的时候直接给你一份"顺滑版本"。这里有个关键设计选择值得说:它保留了原始文本和改写文本两路输出。这个设计非常务实,因为改口误天然带有信息损失风险,法律、医疗、审计类场景必须留底。你做产品设计时,务必要把"原始逐字稿"和"整理稿"作为两个独立字段存下来,别只存一份。
我在实际使用中踩过最狠的一个坑:某次给客户演示,整理稿里把"这个方案不行,我觉得要重做"改成了"这个方案我觉得要重做",一个否定词被顺滑掉了,意思完全反了。后来我加了一条规则——改写前后如果出现否定词增减,一律标记为待人工确认。
2.3 长音频分块与说话人分离的联动
会议录音动辄一小时,模型不可能一次吞下。分块策略直接决定边界处的错误率。常见做法是固定30秒切一刀,但语音不会刚好在你切的地方停顿,硬切必然造成首尾丢字。更稳的做法是基于静音检测动态切分:找到能量最低的谷点再切,同时让相邻块重叠0.3到0.5秒,解码后按时间戳去重。
重叠去重这件事说起来简单,做起来容易出错。我的做法是保留每个词的时间戳,重叠区的词按时间戳排序后,用最长公共子序列对齐,重复的只保留置信度高的那份。如果接口不返回词级时间戳,那就退回按句子粒度去重,效果会差一些但可接受。
说话人分离(diarization)和识别的顺序也有讲究。串行做(先分离再识别)实现简单,但分离一旦出错,识别整段跟着错;并行做(同时输出文本和说话人标签)效果好,但对接口有要求。中小团队我建议先做串行,把分离质量单独评估——分离错误的排查成本比识别错误低得多。
3. 落地实操:把新模型接进你的转录流水线
3.1 音频预处理:这一步决定了错误率的下限
我见过太多人跳过预处理直接调接口,然后抱怨模型不行。真实录音里,光是采样率和声道这两个基础项就能吃掉你几个百分点。
采样率统一到16kHz。高于16k的信息对识别帮助有限,反而增加传输和推理开销;低于16k会损失高频辅音,中文的s/sh、z/zh区分度直接崩掉。重采样用sox或者ffmpeg都行,我习惯用ffmpeg一条命令解决:
ffmpeg -i input.m4a -ar 16000 -ac 1 -c:a pcm_s16le output.wav-ar 16000指定采样率,-ac 1强制单声道,-c:a pcm_s16le输出16位PCM。别小看单声道这一步,双声道文件如果左右声道有相位差,某些模型会把同一个声音识别两遍,插入错误率飙升。
增益归一化。手机录音和会议室录音的电平能差20dB。做一个简单的峰值归一化到-3dB,或者用响度标准化到-16 LUFS,能明显降低"声音太小被判成静音"的删除错误。我用的是ffmpeg-normalize,两行搞定。
VAD预切分。把长静音段切掉,既省成本又减少插入错误。阈值别设太激进,我一般用能量阈值配合最小语音段300ms、最小静音段500ms这两个参数。切太碎会让模型丢失上下文,整句识别变成断句识别,标点质量跟着下降。
注意:不要对音频做人声分离、强降噪之后再送识别。我实测过几款降噪工具,处理后的音频听感变好了,但WER反而上升——降噪引入的 artifacts 会让模型把辅音听错。除非噪声真的到了掩蔽语音的程度,否则轻度降噪即可,或者干脆不降。
3.2 调用参数怎么配:一份可直接抄的配置
下面这段是伪代码风格,字段名按常见接口习惯写的,不同厂商命名会有差异,但参数语义大同小异:
resp = asr_client.transcribe( audio_path="meeting_01_16k.wav", language="zh", # 明确指定语言,别让它自动检测 enable_punctuation=True, # 标点,下游排版必需 enable_itn=True, # 逆文本正则化:把"二零二四"转成"2024" enable_disfluency_removal=True, # 口误顺滑,关键开关 keep_raw_transcript=True, # 保留逐字稿,务必打开 hotwords=[ {"text": "某某协同平台", "weight": 8}, {"text": "某某型号", "weight": 8}, {"text": "季度复盘", "weight": 3}, ], diarization=True, # 说话人分离 chunk_seconds=30, overlap_seconds=0.4, timestamp_granularity="word", # 词级时间戳,做字幕必需 )几个参数的选择理由值得展开。language一定要显式指定,自动语言检测在短句、中英混杂的场景下误判率高,一旦判成英文,中文段落会整段变成拼音或者空白。enable_itn这个开关很多人不开,结果下游做数据分析时,"百分之三十"和"30%"变成了两个值。
keep_raw_transcript是我强烈建议打开的一项。顺滑后的文本好看,但不可回溯。出了争议需要核对原话时,只有逐字稿能救命。
3.3 后处理:时间戳校正与字幕生成
拿到结果别急着落库,还有三步。第一步是时间戳单调性检查,重叠去重之后偶尔会出现后一个词的开始时间早于前一个词的结束时间,需要做一次单调递增修正。第二步是句子重组,模型按语义断句,字幕需要按长度断句,一般单行不超过20个中文字符,超过就得按标点二次切分。第三步是数字与单位规范化回写,把"3到5个工作日"统一成"3-5个工作日"这类固定格式。
生成SRT的代码不长,核心就是把词级时间戳按句子边界聚合:
def to_srt(segments): def fmt(t): h, rem = divmod(t, 3600) m, s = divmod(rem, 60) return f"{int(h):02d}:{int(m):02d}:{s:06.3f}".replace(".", ",") lines = [] for i, seg in enumerate(segments, 1): lines.append(str(i)) lines.append(f"{fmt(seg['start'])} --> {fmt(seg['end'])}") lines.append(seg["text"].strip()) lines.append("") return "\n".join(lines)注意fmt里把小数点换成逗号,SRT格式要求毫秒分隔符是逗号,写成点号有些播放器会直接不识别。这个坑我踩过一次,排查了半小时才发现是标点问题。
4. 科学评测:测出属于你自己场景的真实WER
4.1 建一个50到100条的小测试集,比什么都重要
官方榜单再漂亮也不能替代自测。我的做法是从真实业务音频里抽50到100条,覆盖你所有的场景类型:安静会议室、嘈杂开放区、单人独白、多人抢话、带口音、带专业术语。每条控制在30秒到2分钟,人工精校成参考文本。
抽样时有个技巧:按业务重要性分层抽,不要随机抽。会议纪要场景里,领导发言和决议段落的权重远高于闲聊段落,那就多抽这类。我一般按6:3:1分配——60%核心场景、30%边缘场景、10%极端场景(强噪声、重度口音)。
参考文本的标注规范必须提前定死,否则不同人标出来的标准都不一样。规范里至少要写清楚:语气词保不保留、数字用阿拉伯还是汉字、英文单词大小写、标点用什么符号集、重复词怎么记。这份规范一旦定下来,评测期间不许改。
4.2 归一化规则:决定你看到的是真错误还是假错误
归一化函数要跟参考文本的标注规范严格对应。核心处理包括:全角转半角、去除多余空格、统一标点、数字统一转阿拉伯、英文统一小写、繁简统一。下面这个版本可以直接用:
import re import unicodedata CN_NUM = {"零":"0","一":"1","二":"2","三":"3","四":"4", "五":"5","六":"6","七":"7","八":"8","九":"9","十":"10"} def normalize(text, lang="zh"): text = unicodedata.normalize("NFKC", text) # 全角转半角 text = text.lower() if lang == "en" else text text = re.sub(r"[,。!?、;:\"''()《》]", "", text) # 去标点 text = re.sub(r"\s+", " ", text).strip() if lang == "zh": # 中文按字切分,逐字比对 return list(text.replace(" ", "")) return text.split()中文场景我建议按字切分而不是按词,因为分词工具本身的误差会污染评测结果。按字比对虽然对"的/地/得"这类同音字更敏感,但至少口径统一、可复现。
4.3 评测脚本与结果解读
有了归一化函数,算WER就是标准编辑距离:
def edit_ops(ref, hyp): n, m = len(ref), len(hyp) dp = [[0]*(m+1) for _ in range(n+1)] for i in range(n+1): dp[i][0] = i for j in range(m+1): dp[0][j] = j for i in range(1, n+1): for j in range(1, m+1): cost = 0 if ref[i-1] == hyp[j-1] else 1 dp[i][j] = min(dp[i-1][j] + 1, # 删除 dp[i][j-1] + 1, # 插入 dp[i-1][j-1] + cost) # 替换/正确 return dp[n][m] def wer_detail(ref, hyp): ref, hyp = normalize(ref, "zh"), normalize(hyp, "zh") total = len(ref) dist = edit_ops(ref, hyp) return {"wer": dist / max(1, total), "ref_len": total, "dist": dist}跑完之后别只看平均分。我习惯把结果按场景分组看:安静场景的WER、嘈杂场景的WER、带热词场景的WER,三个数字往往能差出三四倍。平均值好看不代表你的场景好看,反之亦然。
4.4 一份可复用的评测记录表
| 指标 | 基线模型 | 新模型(未开顺滑) | 新模型(开顺滑) |
|---|---|---|---|
| 整体CER | 8.4% | 4.1% | 4.3% |
| 安静会议室 | 5.2% | 2.6% | 2.7% |
| 嘈杂开放区 | 13.7% | 6.8% | 7.1% |
| 专有名词错误率 | 21.3% | 5.4% | 5.6% |
| 填充词残留率 | 100% | 100% | 3.2% |
| 人工校改耗时(每千字) | 12分钟 | 7分钟 | 3分钟 |
这张表里最值得看的是最后一行。CER从8.4%降到4.1%,人工校改时间只从12分钟降到7分钟,因为省下来的主要是错字,而删语气词这件事还得人来做。开了顺滑之后,耗时直接掉到3分钟。对下游使用者来说,顺滑带来的价值远大于单纯的错误率下降,这一点在立项评估时特别容易被忽略。
5. 常见问题与排查技巧实录
5.1 问题速查表:从现象倒推原因
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 整段空白 | 音频格式不支持或采样率异常 | 用ffprobe看真实编码,别信扩展名 |
| 句尾频繁缺字 | VAD切分边界削掉了尾音 | 增大重叠区到0.5秒,或改静音切分 |
| 数字全变汉字 | ITN开关没开 | 检查enable_itn参数是否生效 |
| 专有名词全错 | 热词没生效或权重太低 | 打印请求体确认热词已传,权重调到8 |
| 同一句话重复出现 | 分块重叠区没去重 | 检查时间戳去重逻辑 |
| 中英混杂处乱码 | 语言参数写死为zh | 改双语模式或auto,并单独测该子集 |
| 顺滑后语义反转 | 否定词被误删 | 加否定词增减校验规则 |
这张表我放在项目文档首页,新人上手第一周基本都会遇到其中三四条。表格的价值不在于穷尽,而在于给排查提供一条"从现象到原因"的快速路径,避免一上来就怀疑模型。
5.2 顺滑过头的三个典型场景
自动改口误是把双刃剑。我在实际使用中总结了三个高危场景,用的时候务必开人工复核。
第一个是法律与合规相关的陈述。"我记得当时没有签字"和"我记得当时签了字",删掉"没有"两个字意思完全相反。这类内容建议关掉顺滑,或者至少保留原始文本对照。
第二个是数字密集的讨论。"预算是三十万,呃,三十五万",顺滑之后可能只留"三十五万",但会议里前面的三十万可能是另一笔预算。数字类自我修正,我倾向于保留原文并加高亮标记。
第三个是多人抢话。两个人同时说话时,模型容易把A的填充词当噪声删掉,结果把A的句子和B的句子连成一句,语义完全错乱。这种情况分词质量会明显下降,建议配合说话人标签做交叉校验。
提示:顺滑功能上线前,务必拿20条以上真实业务音频做人工抽检,重点看否定词、数字、人名三类实体。这三类出错的代价远高于其他类型。
5.3 中英混杂与方言口音的应对
中文会议里夹杂英文术语是常态,"这个feature下个sprint上"这种句子,纯中文模型会把它转成"这个飞车下个死普林特上"。我的处理办法是优先选支持中英混合解码的接口,并且在热词表里把高频英文术语加进去,用原拼写而不是音译。
方言口音是另一个坑。带口音的普通话,错误率通常是标准普通话的两到三倍。可行的缓解手段有两个:一是换用在该口音区域数据上训练过的模型版本,二是把常错的词做成热词。后者治标不治本,但见效快,适合应急。真正要根治,只能自己攒几百小时带标注的领域音频做微调,成本不低,通常只有日处理量很大的团队才划算。
5.4 成本核算:什么时候值得上
成本这块很多人算不清楚。我的算法是把成本拆成三块:推理成本、人工校改成本、返工成本。推理成本按音频时长计,人工校改按最终字数计,返工成本是错误导致的下游损失。前两块可以量化,第三块只能估算,但往往是最大的一块。
以一个每天处理100小时音频的团队为例,粗略估算(单位按相对值,不涉及具体价格):
| 项目 | 旧方案 | 新方案 | 说明 |
|---|---|---|---|
| 推理开销 | 1.0 | 1.4 | 新模型单价通常更高 |
| 人工校改 | 12.0 | 3.0 | 按每千字耗时折算 |
| 下游返工 | 4.0 | 1.2 | 错误引发的沟通成本 |
| 合计 | 17.0 | 5.6 | 差距主要来自人工 |
结论很清晰:只要人工校改还占成本大头,升级就是划算的。反过来,如果你的场景是纯机器消费(比如给搜索建索引、做关键词抽取),根本不需要人读,那顺滑功能对你没有价值,只需要关注CER和推理成本。
6. 把新能力嵌进产品:几个容易被忽略的设计点
6.1 双轨存储:原始稿与整理稿必须分开
这条前面提过,但值得单独展开。我在设计数据结构时,会给每条转录记录存三份内容:原始逐字稿(带时间戳、带填充词)、整理稿(顺滑后)、以及一份diff(记录改写位置)。diff这一份最容易被省掉,但它在客诉处理时价值极高——用户质疑"我没说过这句话"时,你能立刻定位到是改写引入的还是识别错的。
存储成本上,三份文本相对于音频来说可以忽略不计。真正需要控制的是音频保留策略,这个另说。
6.2 置信度分数怎么用
多数接口会返回每个词或每句的置信度。这个分数不该只用来做阈值过滤,更有价值的用法是驱动人工复核的排序。把置信度最低的10%片段优先推给人工,剩下的自动通过。实测下来,这种方式能以20%的复核量覆盖70%以上的实际错误,效率比全量复核高得多。
顺滑后的文本置信度会普遍偏低,因为改写本身带有不确定性。所以做排序时要把原始稿的置信度和改写标记结合起来看,单纯看整理稿的分数会误伤。
6.3 灰度上线的观察指标
新模型上线别一次性全量切。我的做法是先切5%流量,观察一周,重点盯四个指标:整体CER、专有名词命中率、人工复核退回率、以及用户主动编辑率。前两个是质量指标,后两个是体验指标。经验上,用户主动编辑率下降15%以上,说明升级是正向的;如果这个指标没动,说明省下来的错误用户根本没注意到,那就要重新评估投入产出。
灰度期间还要留一条回滚通路。我一般会保留旧模型的调用入口至少两周,配置层面用一个开关控制,出问题十秒内切回去。
6.4 一个真实的效果对比记录
最后分享一组我自己测的数据。同一批60条会议音频,平均时长45分钟,涵盖三种场景。旧模型整体CER 9.1%,新模型未开顺滑3.9%,开启顺滑后4.2%(顺滑会引入少量改写错误,所以CER略升,但可读性大幅提升)。
人工校改耗时从每千字14分钟降到每千字3.5分钟。最明显的改善在专有名词上,配置了200条热词之后,人名错误从每场平均11处降到2处。也有退步的地方:数字密集的财务讨论段落,顺滑后需要人工回查的比例上升了,因为有些自我修正被合并了。后来我给这类段落加了规则,检测到连续数字加修正词("不对""改成""应该是")就不做顺滑,问题基本解决。
踩过几次坑之后我的体会是,模型能力提升确实能解决大部分问题,但剩下那一小部分永远得靠工程手段兜底。把预处理做扎实、把评测口径统一、把后处理规则配好,这三件事带来的收益加起来,往往不亚于换一代模型。工具是死的,链路是活的,谁把链路捋顺了,谁就能真正把那2.6%兑现成产品体验。