from __future__ import annotations import logging from typing import Optional import numpy as np logger = logging.getLogger(__name__) class ASRService: def __init__(self) -> None: self._model = None self._post = None self._ready = False self._attempted = False self._init_error: Optional[str] = None self._device: str = "uninitialized" def _ensure_loaded(self) -> None: if self._ready or self._attempted: return self._attempted = True from funasr import AutoModel from funasr.utils.postprocess_utils import rich_transcription_postprocess errors: list[str] = [] for device in ("cuda:0", "cpu"): try: self._model = AutoModel( model="FunAudioLLM/SenseVoiceSmall", device=device, hub="hf", ) self._post = rich_transcription_postprocess self._ready = True self._init_error = None self._device = device if device != "cuda:0": logger.warning("ASR running in degraded mode on %s", device) return except Exception as exc: # pragma: no cover errors.append(f"{device}: {exc}") self._init_error = " | ".join(errors) self._device = "unavailable" logger.warning("ASR unavailable: %s", self._init_error) async def transcribe(self, audio_16k: np.ndarray) -> str: self._ensure_loaded() if self._ready and self._model is not None and self._post is not None: res = self._model.generate( input=audio_16k, cache={}, language="zh", use_itn=True, ) return self._post(res[0]["text"]) # When ASR is unavailable, avoid emitting fake user text that would trigger a bogus LLM reply. return "" @property def health(self) -> dict: return {"ready": self._ready, "attempted": self._attempted, "device": self._device, "error": self._init_error}