"""Плагины `Transcriber` на основе faster-whisper: CPU (`min`) и GPU (`medium`/`max`). Оба плагина используют встроенный в faster-whisper Silero VAD (`vad_filter=True`) и дополнительно отбрасывают сегменты короче `MIN_SEGMENT_DURATION_S` — типичные галлюцинации Whisper на тишине/шуме (ТЗ §1.4). Общая логика (ленивая загрузка модели-синглтона процесса, вызов `transcribe` с VAD, фильтрация коротких сегментов) вынесена в `_FasterWhisperBase`; CPU/GPU-варианты отличаются только параметрами устройства/квантизации (ADR-004, `docs/architecture/adr/004-ai-tier-matrix.md`). """ from typing import TYPE_CHECKING, ClassVar from core.plugins.factory import register_transcriber from core.plugins.transcriber import Segment, Transcriber if TYPE_CHECKING: # Импорт только для проверки типов: рантайм-импорт — ленивый, см. `_get_model`, # чтобы API-процесс, где транскрибация не используется, не тянул тяжёлую # зависимость (ctranslate2 и т.п.) в память. from faster_whisper import WhisperModel MIN_SEGMENT_DURATION_S = 0.3 """Минимальная длительность сегмента (сек); короче — отбрасывается как вероятная галлюцинация Whisper на тишине/шуме.""" VAD_MIN_SILENCE_DURATION_MS = 500 """Порог Silero VAD (мс) для разбиения на речевые куски внутри трека.""" class _FasterWhisperBase(Transcriber): """Общая логика плагинов faster-whisper: синглтон модели процесса + VAD-транскрибация. Модель-синглтон принадлежит конкретному подклассу (`FasterWhisperCPU`, `FasterWhisperGPU`), а не общему базовому классу: присваивание `cls._model = ...` в `_get_model` всегда происходит через `type(self)`, поэтому у каждого подкласса — свой атрибут класса, и CPU/GPU-плагины не делят один кэшированный инстанс модели, даже если оба сконфигурированы в одном процессе. """ MIN_SEGMENT_S: ClassVar[float] = MIN_SEGMENT_DURATION_S _model: "ClassVar[WhisperModel | None]" = None # Задаются наследниками в `__init__` (device — фиксированно классом, # compute_type — либо фиксированно, либо конструкторская опция). device: str compute_type: str def __init__( self, model: str, language: str = "ru", download_root: str | None = None, ) -> None: self.model_name = model self.language = language self.download_root = download_root def _get_model(self) -> "WhisperModel": """Лениво создать (или переиспользовать) синглтон `WhisperModel` конкретного подкласса.""" cls = type(self) if cls._model is None: from faster_whisper import WhisperModel # ленивый импорт тяжёлой зависимости cls._model = WhisperModel( self.model_name, device=self.device, compute_type=self.compute_type, download_root=self.download_root, ) return cls._model def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]: """Транскрибировать аудиофайл трека, отбросив короткие сегменты-галлюцинации. VAD (Silero, встроен в faster-whisper) включён с порогом тишины `VAD_MIN_SILENCE_DURATION_MS`; дополнительно отбрасываются сегменты короче `MIN_SEGMENT_DURATION_S`. """ model = self._get_model() raw_segments, _info = model.transcribe( audio_path, language=language, vad_filter=True, vad_parameters={"min_silence_duration_ms": VAD_MIN_SILENCE_DURATION_MS}, ) return [ Segment(start=segment.start, end=segment.end, text=segment.text) for segment in raw_segments if (segment.end - segment.start) >= MIN_SEGMENT_DURATION_S ] @register_transcriber class FasterWhisperCPU(_FasterWhisperBase): """Транскрибер faster-whisper (CTranslate2) на CPU с int8-квантизацией (уровень `min`). Модель — синглтон на процесс: создаётся лениво при первом вызове `transcribe` и переиспользуется всеми последующими вызовами в рамках одного процесса воркера (процесс запускается в Celery-очереди `transcription` с `--pool=solo --concurrency=1`, поэтому гонок за атрибут класса не возникает). """ provider: ClassVar[str] = "faster_whisper_cpu" _model: "ClassVar[WhisperModel | None]" = None def __init__( self, model: str | None = None, language: str = "ru", download_root: str | None = None, ) -> None: super().__init__(model=model or "small", language=language, download_root=download_root) self.device = "cpu" self.compute_type = "int8" @register_transcriber class FasterWhisperGPU(_FasterWhisperBase): """Транскрибер faster-whisper на GPU (CUDA, уровни `medium`/`max`, ADR-004). `compute_type` — конструкторская опция (дефолт `float16`, как в матрице ADR-004); для экономии VRAM конфиг уровня может задать `int8_float16` (options плагина в `TIERS`/`plugins.yaml`). """ provider: ClassVar[str] = "faster_whisper_gpu" _model: "ClassVar[WhisperModel | None]" = None def __init__( self, model: str | None = None, language: str = "ru", download_root: str | None = None, compute_type: str = "float16", ) -> None: super().__init__(model=model or "medium", language=language, download_root=download_root) self.device = "cuda" self.compute_type = compute_type