# Плагин Transcriber — транскрибация аудио в текст ## Обзор VidConf использует **Strategy pattern** для подключаемых реализаций транскрибации речи. Это позволяет менять провайдеров (faster-whisper на CPU, OpenAI Whisper, Vosk и т.д.) через конфигурацию без изменения ядра. ## Контракт Transcriber ### Интерфейс Все реализации наследуют абстрактный класс `Transcriber` из `backend/core/plugins/transcriber.py`: ```python from abc import ABC, abstractmethod from dataclasses import dataclass from typing import ClassVar @dataclass(frozen=True, slots=True) class Segment: """Один транскрибированный сегмент трека.""" start: float # Начало в секундах от старта файла end: float # Конец в секундах от старта файла text: str # Распознанный текст class Transcriber(ABC): """Интерфейс Strategy для реализаций преобразования речи в текст.""" provider: ClassVar[str] # Уникальный идентификатор провайдера @abstractmethod def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]: """Транскрибировать аудиофайл в список сегментов.""" ... ``` ### Сигнатура метода transcribe | Параметр | Тип | Описание | |----------|-----|---------| | `audio_path` | `str` | Абсолютный путь к аудиофайлу на диске | | `language` | `str` | Код языка (ISO 639-1), дефолт `"ru"` | | **Возврат** | `list[Segment]` | Упорядоченный список сегментов, отсортированный по `start` | **Требования:** - Если файл не найден → исключение `FileNotFoundError` - Сегменты должны быть отсортированы по `start` в возрастающем порядке - Дублирующиеся/перекрывающиеся сегменты допустимы (алгоритм фраз их обработает) ## Реализация по умолчанию: FasterWhisperCPU ### Описание `FasterWhisperCPU` — оптимизированная реализация faster-whisper (CTranslate2-бэкэнд) для транскрибации на CPU с int8-квантизацией. Она встроена в проект как реализация по умолчанию. **Файл:** `backend/core/plugins/faster_whisper.py` ### Параметры конструктора ```python @register_transcriber class FasterWhisperCPU(Transcriber): provider: ClassVar[str] = "faster_whisper_cpu" def __init__( self, model: str | None = None, language: str = "ru", download_root: str | None = None, ) -> None: """Инициализировать транскрибер. Args: model: Имя модели Whisper (small/base/medium/etc); дефолт 'small' (оптимум для CPU). language: Код языка (ISO 639-1), дефолт 'ru'. download_root: Папка для кэша загруженных моделей; дефолт ~/.cache/huggingface/hub. """ ``` | Параметр | Тип | Дефолт | Описание | |----------|-----|--------|---------| | `model` | `str` | `"small"` | Модель Whisper: `tiny`, `base`, `small`, `medium`, `large` | | `language` | `str` | `"ru"` | Язык распознавания (ISO 639-1) | | `download_root` | `str` | `~/.cache/` | Папка для кэша моделей | ### Особенности #### VAD (Voice Activity Detection) FasterWhisperCPU включает встроенный **Silero VAD** для автоматического разбиения аудио на речевые сегменты и подавления молчания. Это улучшает качество транскрибации и снижает галлюцинации Whisper на тишине/шуме. - **Параметр:** `vad_filter=True` - **Порог молчания:** `vad_min_silence_duration_ms=500` (0.5 сек) #### Отбрасывание коротких сегментов Whisper часто халлюцинирует на очень коротких сегментах (шум, клики, тишина). FasterWhisperCPU автоматически отбрасывает сегменты, короче **0.3 секунды**: ```python MIN_SEGMENT_DURATION_S = 0.3 # Фильтрация в transcribe() 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 ] ``` #### Ленивый импорт Зависимость `faster-whisper` импортируется лениво — только при первом вызове `transcribe()`. Это позволяет API-процессу, который не использует транскрибацию, избежать загрузки тяжёлой библиотеки в память. ```python def _get_model(self) -> "WhisperModel": if FasterWhisperCPU._model is None: from faster_whisper import WhisperModel # Импорт здесь, не в начале файла FasterWhisperCPU._model = WhisperModel(...) return FasterWhisperCPU._model ``` #### Синглтон модели на процесс Модель Whisper создаётся один раз при первом вызове `transcribe()` и переиспользуется всеми последующими вызовами в пределах одного процесса воркера. Это снижает нагрузку на память и CPU (загрузка модели — дорогая операция). > **Примечание:** Celery-воркер транскрибации запускается с флагом `--pool=solo --concurrency=1`, поэтому гонок за синглтоном не бывает. ### Конфигурация В `config/plugins.yaml`: ```yaml transcriber: enabled: true provider: "faster_whisper_cpu" model: "small" language: "ru" options: download_root: "/models/whisper" ``` Переменные окружения (если нужны): ```bash # Опционально: переопределить папку кэша моделей export HF_HOME=/models ``` ### Производительность - **Модель small:** ~8 минут аудио за 1 минуту на современном CPU (зависит от CPU) - **Память:** ~1.5 ГБ на процесс (с моделью и бэкэндом) - **Формат входа:** WAV, MP3, OGG, FLAC, M4A и др. (поддерживает ffmpeg-compatible форматы) ## Добавление нового Transcriber ### Пошаговая инструкция #### Шаг 1: Создать файл реализации Создайте новый файл в `backend/core/plugins/`, например `my_transcriber.py`: ```python """Плагин Transcriber на основе MyService.""" from typing import ClassVar from core.plugins.factory import register_transcriber from core.plugins.transcriber import Segment, Transcriber @register_transcriber class MyTranscriber(Transcriber): """Транскрибер, использующий MyService API.""" provider: ClassVar[str] = "my_service" def __init__( self, model: str | None = None, language: str = "ru", api_key: str | None = None, **options, ) -> None: """Инициализировать транскрибер. Args: model: Имя модели (опционально). language: Язык распознавания. api_key: API-ключ для сервиса. **options: Дополнительные параметры из config/plugins.yaml. """ self.model = model or "default-model" self.language = language self.api_key = api_key or os.getenv("MY_SERVICE_API_KEY") self.options = options def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]: """Транскрибировать аудиофайл через MyService API. Args: audio_path: Путь к аудиофайлу на диске. language: Язык распознавания (может переопределить конструктор). Returns: Упорядоченный список сегментов, отсортированный по start. """ # 1. Прочитать аудиофайл with open(audio_path, "rb") as f: audio_data = f.read() # 2. Отправить на API (пример) response = requests.post( "https://api.myservice.com/transcribe", files={"audio": audio_data}, json={"language": language, "model": self.model}, headers={"Authorization": f"Bearer {self.api_key}"}, ) response.raise_for_status() # 3. Распарсить ответ в list[Segment] result = response.json() segments = [ Segment( start=float(item["start"]), end=float(item["end"]), text=item["text"], ) for item in result.get("segments", []) ] # 4. Отсортировать по start (важно!) segments.sort(key=lambda s: s.start) return segments ``` **Важные правила:** - Класс **должен** наследовать `Transcriber` - Класс **должен** иметь декоратор `@register_transcriber` (регистрирует в реестре) - Класс-переменная `provider: ClassVar[str]` **должна быть** уникальным идентификатором - Конструктор **должен** принимать `model`, `language` и `**options` - Метод `transcribe()` **должен** возвращать сегменты, отсортированные по `start` #### Шаг 2: Импортировать в `__init__.py` Добавьте импорт в `backend/core/plugins/__init__.py`: ```python """Пакет плагинов Transcriber/Summarizer.""" from core.plugins import faster_whisper as faster_whisper # noqa: F401 from core.plugins import my_transcriber as my_transcriber # noqa: F401 # ДОБАВИТЬ from core.plugins import null as null # noqa: F401 ``` > **Примечание:** `noqa: F401` подавляет предупреждение о неиспользуемом импорте — он нужен для побочного эффекта (регистрация). Используйте `as` для явности. #### Шаг 3: Обновить конфигурацию Отредактируйте `config/plugins.yaml`: ```yaml transcriber: enabled: true provider: "my_service" # Совпадает с MyTranscriber.provider model: "default-model" language: "ru" options: api_key: "${MY_SERVICE_API_KEY}" # Переменная окружения # Дополнительные параметры, специфичные для вашего сервиса timeout_seconds: 300 retry_count: 3 ``` #### Шаг 4: Добавить переменные окружения Отредактируйте `.env` (или `.env.example`): ```bash # MyService API MY_SERVICE_API_KEY=sk-... ``` #### Шаг 5: Написать тесты Добавьте тесты в `backend/tests/test_plugins_factory.py` или новый `test_my_transcriber.py`: ```python import pytest from core.plugins.factory import create_transcriber from core.plugins.config import TranscriberConfig def test_my_transcriber_registered(): """Проверить регистрацию плагина.""" cfg = TranscriberConfig(provider="my_service", model="test") transcriber = create_transcriber(cfg) assert transcriber.provider == "my_service" assert transcriber.model == "test" def test_my_transcriber_transcribe(tmp_path): """Тест транскрибации с моком API.""" # Создать тестовый WAV-файл audio_file = tmp_path / "test.wav" # ... создать корректный WAV-файл ... cfg = TranscriberConfig(provider="my_service", model="test") transcriber = create_transcriber(cfg) # Вызвать transcribe segments = transcriber.transcribe(str(audio_file), language="ru") # Проверить результат assert len(segments) > 0 assert all(s.start < s.end for s in segments) assert segments == sorted(segments, key=lambda s: s.start) ``` #### Шаг 6: Протестировать локально ```bash cd backend # Убедиться, что конфиг загружается uv run python -c " from core.plugins.factory import _TRANSCRIBERS print('Зарегистрированные транскрибаторы:', list(_TRANSCRIBERS.keys())) " # Запустить тесты uv run pytest tests/test_my_transcriber.py -v # Запустить пайплайн с новым провайдером (интеграционный тест) CELERY_ALWAYS_EAGER=True uv run pytest tests/test_build_phrases.py -v ``` ### Отключение транскрибации Если нужно полностью отключить модуль транскрибации, установите в `config/plugins.yaml`: ```yaml transcriber: enabled: false provider: "null" model: null ``` Поведение: - Запись аудиотреков продолжает работать (egress в активном состоянии) - При `room_finished` `run_pipeline()` завершится до инстанцирования плагина - `pipeline_status` сеанса останется `"recording"` (не перейдёт в `"transcribing"`) - Лог-сообщение: "transcriber отключён (enabled=false) — сеанс ... пропущен" ## Архитектурные решения ### Идемпотентный пайплайн пост-обработки Пайплайн пост-обработки — идемпотентная машина состояний (см. `backend/models/session.py`): ``` recording → transcribing → summarizing → notified / failed ``` **run_pipeline()** диспетчер: 1. Проверяет текущий `pipeline_status` сеанса 2. Продолжает работу с последнего успешного шага 3. Отслеживает статус каждого трека (`session_audio_tracks.status`) 4. Коммитит результаты транскрибации в `session_audio_tracks.segments` (JSONB) после каждого трека Это позволяет восстановиться после сбоя воркера без потери данных или дублирования. ### Атрибуция к участнику сеанса (ADR-002) Фразы и аудиотреки атрибутированы не к `users`, а к `conference_participants` (участник сеанса). Это позволяет работать с гостями без `user_id`: ```python # Получить спикера фразы participant = session.query(ConferenceParticipant).get(phrase.participant_id) user = participant.user # Может быть None для гостей guest = participant.guest # Может быть None для пользователей ``` ## Ссылки - **ADR-002:** `docs/architecture/adr/002-phrase-attribution-session-participant.md` - **Контракты плагинов:** `docs/plugins/contracts.md` - **Модели:** `backend/models/audio_track.py`, `backend/models/phrase.py` - **Пайплайн:** `workers/tasks/pipeline.py` - **Реконструкция фраз:** `workers/transcription/phrases.py` - **Тесты:** `backend/tests/test_plugins_factory.py`, `backend/tests/test_build_phrases.py`