LabHub
开始
学习 学习路径 课程

语音 AI 智能体 — 会听、会查、会说的流水线

从部分结果到最终结果 — 用 WER 衡量流式 ASR 与 Whisper

在 LabHub 中继续学习

目标

亲手制作 WER 计算器,用同样的语句比较流式 ASR 和离线 ASR,测量准确度和“话说完之后到出最终结果”的延迟。确认电话频段、采样率误报和噪声会如何改变 WER。

为什么重要

语音助手开始回答的时刻,取决于“最终结果什么时候出来”。流式是在说话的同时转写,所以最终结果马上就出来;离线则要等话说完才开始。准确度数字(WER)会随规范化、汇总方式和评估数据的不同而大幅变化,所以必须亲手制作计算器,才能相信这些数字。评分器会用隐藏用例测试你的 wer.py,并把同一个模型重新运行一遍,与你的结果核对。

步骤

  1. 创建 /root/voice/asr/wer.py,让 wer(ref, hyp) 返回 {S, D, I, N, wer}(规范化为小写、去除标点、保留撇号)。
  2. 把 /opt/lab/fixtures/voice/librispeech/ls01~ls08.wav 按每次 100 ms 输入流式 ASR,把部分结果、最终结果和最终延迟写入 /root/voice/asr/stream.jsonl。
  3. 把最终结果的语料库级 WER 写入 /root/voice/asr/wer_stream.json。
  4. 用 Whisper tiny.en 转写同样的 8 条语句,生成 /root/voice/asr/whisper.jsonl 和 /root/voice/asr/wer_whisper.json。
  5. 把两种方式“话说完之后到出最终结果”的中位数写入 /root/voice/asr/latency.json。
  6. 把 8 kHz 电话录音(phone_8k.wav)的 WER 与 16 kHz 原始录音(ls04)比较,并测量把 8 kHz 谎报为 16000 时的 WER,写入 /root/voice/asr/phone.json。
  7. 把混入 SNR 为 20 · 10 · 0 dB 的白噪声时的语料库级 WER 写入 /root/voice/asr/noise.json。
  8. 汇总测量结果,把会选择哪种配置写入 /root/voice/asr/report.json。

参考

制作 WER 计算器

在 /root/voice/asr/wer.py 中创建 wer(ref, hyp)。把两段文字转成小写,去掉撇号以外的标点,然后求以单词为单位的最小编辑距离,并返回 {"S": 치환, "D": 삭제, "I": 삽입, "N": 정답 단어 수, "wer": (S+D+I)/N}(占位符依次为替换数、删除数、插入数、正确答案的单词数;如果正确答案为空,则 wer 为 0)。评分器会用 8 个隐藏用例进行测试。

把莱文斯坦距离从字符改成单词。填完表 d[i][j] 之后,从右下角往回追溯,数出各是哪种编辑。“don't”的撇号是单词的一部分,所以必须保留。

每次输入 100 ms,并记录部分结果

把 /opt/lab/fixtures/voice/librispeech/refs.tsv 中的 ls01 到 ls08 按每次 1600 个采样点(100 ms)输入流式 ASR,每当结果改变就收集 {"audio_s": 지금까지 넣은 소리 길이, "text": 부분 결과}(占位符依次为到目前为止输入的声音长度、部分结果),并把从输入最后一个声音的那一刻起,到出现最终结果为止的 ms 测为 final_ms,以每行一条(id、partials、final、final_ms)写入 /root/voice/asr/stream.jsonl。

必须在最后一个片段之后加入 0.66 秒静音并调用 input_finished(),最后一个词才会出来。final_ms 是这一收尾所花的时间——也就是话说完之后用户要等待的那一份。

语料库级 WER

用你的 wer.py 把 8 条语句的错误数和单词数分别全部相加,求出语料库级 WER,并以 S、D、I、N、wer 写入 /root/voice/asr/wer_stream.json。

不是逐文件 WER 的平均值。为了让较长的语句得到更重的权重,要用错误之和 ÷ 单词数之和来计算。

与 Whisper tiny.en 比较

用 voicekit.models.whisper() 一次性转写同样的 8 条语句,生成 /root/voice/asr/whisper.jsonl(id、text、compute_ms、audio_s),并把语料库级 WER 写入 /root/voice/asr/wer_whisper.json。

离线识别器的用法是 s = rec.create_stream(); s.accept_waveform(sr, x); rec.decode_stream(s); s.result.text。Whisper 会附带标点和大小写,所以你的规范化要在这里发挥作用。

话说完之后到出最终结果

把 stream.jsonl 的 final_ms 和 whisper.jsonl 的 compute_ms 各自的中位数(最近秩:排序后第 ⌈0.5·n⌉ 个值),以 stream_final_ms_p50、whisper_ms_p50 写入 /root/voice/asr/latency.json。

Whisper 要等话说完才开始,所以整个 compute_ms 都是用户的等待时间。流式在说话期间已经完成了大部分,所以只需要等 final_ms。

8 kHz 电话频段——以及谎报采样率时

用流式 ASR 转写与 ls04(16 kHz)同一句话的 /opt/lab/fixtures/voice/librispeech/phone_8k.wav(8 kHz),把相对于 ls04 正确答案的 WER 作为 wer_16k、wer_8k,把以 accept_waveform(16000, …) 输入 8 kHz 采样点的结果的 WER 作为 wer_8k_as_16k,写入 /root/voice/asr/phone.json。

accept_waveform 的第一个参数是声音的采样率。如实告知 8000,sherpa-onnx 会在内部转换成 16 kHz。如果谎称 16000,8,000 个采样点会被读成 0.5 秒,声音就快了一倍。

噪声越强

对 ls01 到 ls08(按排序顺序的第 k 个文件),把 np.random.default_rng(100 + k).standard_normal(n) 噪声以 SNR 20 · 10 · 0 dB(功率比)混入,用流式 ASR(100 ms 片段)转写,并把语料库级 WER 以 {"20": …, "10": …, "0": …} 写入 /root/voice/asr/noise.json。

缩放比例与模块 1 相同:sqrt(P_信号 / 10^(SNR/10) / P_噪声)。如果对每个文件固定种子,评分器就能重新生成同样的噪声来核对。

该选哪一个

在 /root/voice/asr/report.json 中,把 wer_stream、wer_whisper、stream_final_ms_p50、whisper_ms_p50、wer_8k、wer_0db 从前面步骤的文件中搬过来,并在 choice 中从 stream、whisper、two-pass 里写下一个。

要同时看延迟、准确度和数据的相似程度。也别忘了,这份数据(LibriSpeech)与流式模型的训练数据是相同的。