"""本地 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)