25acf9db6e
横向对比云端 gummy 与本地开源模型(faster-whisper/SenseVoice/Paraformer), 重点覆盖中英混说,产出准确率(CER/WER/MER)/速度(延迟/RTF)/资源(cpu/mem/模型大小) 对比报告。公共集(ASCEND/AISHELL/LibriSpeech)统一走 HF 适配器 + 自定义 JSONL manifest。 gummy 引擎对照 server/internal/asr/gummy.go 协议移植。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
2.8 KiB
Python
99 lines
2.8 KiB
Python
"""准确率指标:CER(字级)/ WER(词级)/ MER(中英混合)。
|
|
|
|
分词策略:
|
|
- CER : 去空格后逐字符(中文每字、英文每字母各一 token)。
|
|
- WER : 按空白切词(主要对英文有意义)。
|
|
- MER : 中文按【字】、英文/数字按【词】,混合 token 序列算编辑距离——这是中英混说的 headline 指标。
|
|
|
|
聚合:错误率必须在语料级聚合(累加 S/D/I/N 后再求率),不能对每条样本的率求平均,
|
|
否则短句权重被放大、结果失真。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
import jiwer
|
|
|
|
from .text_norm import normalize
|
|
|
|
_CJK = re.compile(r"[一-鿿㐀-䶿]")
|
|
|
|
|
|
@dataclass
|
|
class Counts:
|
|
"""一组编辑距离统计。N = 参考长度 = S + D + H。"""
|
|
|
|
S: int = 0 # 替换
|
|
D: int = 0 # 删除
|
|
I: int = 0 # 插入
|
|
N: int = 0 # 参考 token 数
|
|
|
|
def __add__(self, o: "Counts") -> "Counts":
|
|
return Counts(self.S + o.S, self.D + o.D, self.I + o.I, self.N + o.N)
|
|
|
|
@property
|
|
def rate(self) -> float:
|
|
if self.N == 0:
|
|
return 1.0 if (self.S + self.D + self.I) > 0 else 0.0
|
|
return (self.S + self.D + self.I) / self.N
|
|
|
|
|
|
def char_tokens(text: str) -> list[str]:
|
|
return [c for c in text if not c.isspace()]
|
|
|
|
|
|
def word_tokens(text: str) -> list[str]:
|
|
return text.split()
|
|
|
|
|
|
def mixed_tokens(text: str) -> list[str]:
|
|
"""中文按字、其余按词。"""
|
|
tokens: list[str] = []
|
|
buf = ""
|
|
for ch in text:
|
|
if _CJK.match(ch):
|
|
if buf.strip():
|
|
tokens.extend(buf.split())
|
|
buf = ""
|
|
tokens.append(ch)
|
|
else:
|
|
buf += ch
|
|
if buf.strip():
|
|
tokens.extend(buf.split())
|
|
return tokens
|
|
|
|
|
|
def _counts(ref_tokens: list[str], hyp_tokens: list[str]) -> Counts:
|
|
if not ref_tokens:
|
|
return Counts(S=0, D=0, I=len(hyp_tokens), N=0)
|
|
out = jiwer.process_words(" ".join(ref_tokens), " ".join(hyp_tokens))
|
|
return Counts(
|
|
S=out.substitutions,
|
|
D=out.deletions,
|
|
I=out.insertions,
|
|
N=out.substitutions + out.deletions + out.hits,
|
|
)
|
|
|
|
|
|
def score_sample(ref_text: str, hyp_text: str) -> dict[str, Counts]:
|
|
"""对一条样本算 CER/WER/MER 的原始计数(供语料级聚合)。"""
|
|
ref = normalize(ref_text)
|
|
hyp = normalize(hyp_text)
|
|
return {
|
|
"cer": _counts(char_tokens(ref), char_tokens(hyp)),
|
|
"wer": _counts(word_tokens(ref), word_tokens(hyp)),
|
|
"mer": _counts(mixed_tokens(ref), mixed_tokens(hyp)),
|
|
}
|
|
|
|
|
|
def empty_counts() -> dict[str, Counts]:
|
|
return {"cer": Counts(), "wer": Counts(), "mer": Counts()}
|
|
|
|
|
|
def add_counts(acc: dict[str, Counts], one: dict[str, Counts]) -> dict[str, Counts]:
|
|
for k in acc:
|
|
acc[k] = acc[k] + one[k]
|
|
return acc
|