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>
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""本地 FunASR 引擎:SenseVoice-Small(多语,中英混说强)与 Paraformer-zh(纯中 SOTA)。
|
||
|
||
依赖:pip install '.[funasr]'
|
||
两者都走 funasr.AutoModel,离线整段识别。SenseVoice 输出带 <|zh|><|EMO|> 等富标签,需剥离。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
import time
|
||
|
||
from ..audio import duration_sec
|
||
from ..metrics.resource import dir_size_mb
|
||
from .base import Engine, Transcript
|
||
|
||
_TAG = re.compile(r"<\|[^|]*\|>")
|
||
|
||
|
||
def _strip_tags(text: str) -> str:
|
||
return _TAG.sub("", text).strip()
|
||
|
||
|
||
class _FunASRBase(Engine):
|
||
kind = "offline"
|
||
is_local = True
|
||
repo: str = ""
|
||
|
||
def __init__(self, name: str, device: str = "cpu", model_dir: str | None = None):
|
||
self.name = name
|
||
self.device = device
|
||
self.model_dir = model_dir
|
||
self.model = None
|
||
self._weights_path: str | None = None
|
||
|
||
def load(self) -> None:
|
||
from funasr import AutoModel
|
||
|
||
kwargs = {"model": self.repo, "disable_update": True, "device": self.device}
|
||
if self.model_dir:
|
||
kwargs["cache_dir"] = self.model_dir
|
||
self.model = AutoModel(**kwargs)
|
||
self._resolve_weights_path()
|
||
|
||
def _resolve_weights_path(self) -> None:
|
||
# funasr 默认从 modelscope 下载;尝试常见属性与缓存目录(best-effort)
|
||
for attr in ("model_path", "model_pth", "kwargs"):
|
||
val = getattr(self.model, attr, None)
|
||
if isinstance(val, str) and os.path.exists(val):
|
||
self._weights_path = val
|
||
return
|
||
if isinstance(val, dict) and isinstance(val.get("model_path"), str):
|
||
self._weights_path = val["model_path"]
|
||
return
|
||
self._weights_path = self.model_dir
|
||
|
||
def transcribe(self, audio_path: str) -> Transcript:
|
||
if self.model is None:
|
||
self.load()
|
||
t0 = time.monotonic()
|
||
res = self.model.generate(input=audio_path)
|
||
proc = time.monotonic() - t0
|
||
text = ""
|
||
if res and isinstance(res, list) and res[0].get("text"):
|
||
text = _strip_tags(res[0]["text"])
|
||
return Transcript(text=text, audio_sec=duration_sec(audio_path), proc_sec=proc)
|
||
|
||
def model_size_mb(self) -> float | None:
|
||
size = dir_size_mb(self._weights_path) if self._weights_path else 0.0
|
||
return size or None
|
||
|
||
def unload(self) -> None:
|
||
self.model = None
|
||
|
||
|
||
class SenseVoiceEngine(_FunASRBase):
|
||
repo = "iic/SenseVoiceSmall"
|
||
|
||
def __init__(self, name: str = "sensevoice", device: str = "cpu", model_dir: str | None = None):
|
||
super().__init__(name=name, device=device, model_dir=model_dir)
|
||
|
||
|
||
class ParaformerEngine(_FunASRBase):
|
||
repo = "paraformer-zh"
|
||
|
||
def __init__(self, name: str = "paraformer-zh", device: str = "cpu", model_dir: str | None = None):
|
||
super().__init__(name=name, device=device, model_dir=model_dir)
|