# Архитектура плагинов ## Обзор VidConf использует **Strategy pattern + Factory pattern** для подключения реализаций AI-моделей. Это позволяет: - Добавлять новые провайдеры без изменения ядра приложения - Менять реализации через конфигурацию без изменения кода - Тестировать с no-op реализациями (NullTranscriber, NullSummarizer) ## Контракты (ABC) ### Transcriber ```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 = "ru") -> list[Segment]` - Читает аудиофайл с диска по абсолютному пути - Возвращает упорядоченный список сегментов, отсортированный по `start` - Дефолтный язык — русский (`language="ru"`) ### Summarizer ```python from abc import ABC, abstractmethod from typing import ClassVar class Summarizer(ABC): """Интерфейс Strategy для реализаций суммаризации текста.""" provider: ClassVar[str] @abstractmethod def summarize(self, transcript: str) -> str: """Создать резюме полного текста транскрипта.""" ... ``` **Сигнатура:** - `summarize(transcript: str) -> str` - Принимает полный текст транскрипта (или чанк) - Возвращает суммаризованный текст на русском языке ## Конфигурационные модели (Pydantic) ### TranscriberConfig ```python class TranscriberConfig(BaseModel): enabled: bool = True provider: str = Field(default="null", min_length=1) model: str | None = None language: str = "ru" options: dict[str, Any] = Field(default_factory=dict) ``` - `enabled`: включена ли транскрибация (при `false` пайплайн остаётся в статусе `recording`) - `provider`: имя зарегистрированного провайдера (строка в кавычках в YAML) - `model`: опциональное имя модели (передаётся в конструктор провайдера) - `language`: язык распознавания (ISO 639-1, дефолт `"ru"`) - `options`: дополнительные параметры, специфичные для конкретного провайдера (прокидываются как `**kwargs`) ### SummarizerConfig ```python class SummarizerConfig(BaseModel): enabled: bool = True provider: str = Field(default="null", min_length=1) model: str | None = None chunk_minutes: int = 20 options: dict[str, Any] = Field(default_factory=dict) ``` - `enabled`: включена ли суммаризация - `provider`: имя зарегистрированного провайдера - `model`: опциональное имя модели - `chunk_minutes`: размер чанка для map-reduce (минуты, дефолт 20) - `options`: дополнительные параметры провайдера ## Реестр и фабрика Модуль `backend/core/plugins/factory.py` реализует: ```python _TRANSCRIBERS: dict[str, type[Transcriber]] = {} _SUMMARIZERS: dict[str, type[Summarizer]] = {} def register_transcriber[T: type[Transcriber]](cls: T) -> T: """Зарегистрировать подкласс Transcriber под его ключом provider.""" _TRANSCRIBERS[cls.provider] = cls return cls def register_summarizer[S: type[Summarizer]](cls: S) -> S: """Зарегистрировать подкласс Summarizer под его ключом provider.""" _SUMMARIZERS[cls.provider] = cls return cls def create_transcriber(cfg: TranscriberConfig) -> Transcriber: """Инстанцировать Transcriber, зарегистрированный для cfg.provider. Raises: UnknownProviderError: если провайдер не найден в реестре. """ cls = _TRANSCRIBERS[cfg.provider] return cls(model=cfg.model, language=cfg.language, **cfg.options) def create_summarizer(cfg: SummarizerConfig) -> Summarizer: """Инстанцировать Summarizer, зарегистрированный для cfg.provider. Raises: UnknownProviderError: если провайдер не найден в реестре. """ cls = _SUMMARIZERS[cfg.provider] return cls(model=cfg.model, chunk_minutes=cfg.chunk_minutes, **cfg.options) ``` **Исключения:** - `PluginError`: базовое исключение при сбое реестра/фабрики плагинов - `UnknownProviderError`: запрошенный `provider` не зарегистрирован ## Null-реализации (no-op) Модуль `backend/core/plugins/null.py` содержит заглушки для тестирования и отключения: ```python @register_transcriber class NullTranscriber(Transcriber): """Заглушка транскрибера — всегда возвращает пустой список сегментов.""" provider: ClassVar[str] = "null" def __init__(self, model: str | None = None, language: str = "ru", **options): self.model = model self.language = language self.options = options def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]: """Возвращает пустой список (no-op).""" return [] @register_summarizer class NullSummarizer(Summarizer): """Заглушка суммаризатора — всегда возвращает пустую строку.""" provider: ClassVar[str] = "null" def __init__(self, model: str | None = None, **options): self.model = model self.options = options def summarize(self, transcript: str) -> str: """Возвращает пустую строку (no-op).""" return "" ``` **Использование:** - Тестирование пайплайна без реальной обработки - Отключение транскрибации/суммаризации через `enabled: false` в `config/plugins.yaml` - Локальная разработка когда AI-модели не установлены ## Конфигурационный файл **Путь:** `config/plugins.yaml` ```yaml transcriber: enabled: true provider: "faster_whisper_cpu" # Обязательно в кавычках model: "small" language: "ru" options: download_root: /models/whisper summarizer: enabled: true provider: "null" model: null chunk_minutes: 20 options: {} chat: enabled: true ``` **Правила YAML:** - `provider` — всегда строка в кавычках, совпадает с `Provider.provider` класса - `model` — может быть `null` (передаётся как `None` в конструктор) - `options` — YAML-словарь (рекурсивно прокидывается как `**kwargs`) - Переменные окружения поддерживаются как `"${VARIABLE_NAME}"` в значениях строк ## Пошаговая инструкция: добавление нового Transcriber Подробная пошаговая инструкция находится в **`docs/plugins/transcriber.md`**. Краткое резюме: **Шаг 1:** Создать файл `backend/core/plugins/my_transcriber.py`: ```python @register_transcriber class MyTranscriber(Transcriber): provider: ClassVar[str] = "my_provider" def __init__(self, model: str | None = None, language: str = "ru", **options): ... def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]: # Реализация ... ``` **Шаг 2:** Импортировать в `backend/core/plugins/__init__.py`: ```python from core.plugins.my_transcriber import my_transcriber # noqa: F401 ``` **Шаг 3:** Обновить `config/plugins.yaml`: ```yaml transcriber: provider: "my_provider" model: "my-model" options: key: "value" ``` **Шаг 4:** Добавить переменные окружения в `.env` (если нужны) **Шаг 5:** Написать тесты и запустить: ```bash cd backend uv run pytest tests/test_my_transcriber.py -v ``` ## Реализованные плагины ### FasterWhisperCPU (Transcriber) - **Провайдер:** `"faster_whisper_cpu"` - **Параметры:** `model="small"` (дефолт), `language="ru"`, `options.download_root="/models/whisper"` - **VAD:** Silero (встроен, `vad_filter=True`) - **Отфильтровка:** сегменты < 0.3 сек отбрасываются - **Ленивый импорт:** `faster_whisper` грузится только при первом вызове - **Файл:** `backend/core/plugins/faster_whisper.py` ### QwenLocal (Summarizer) - **Провайдер:** `"qwen_local"` - **Модели:** Qwen3.5-4B/9B/35B-A3B (GGUF, Q4_K_M; размеры ~2,8 ГБ / ~6,2 ГБ / ~20–22 ГБ) - **min:** Qwen3.5-4B (4B параметров, CPU llama.cpp) - **medium:** Qwen3.5-9B (9B параметров, CPU/GPU llama.cpp опционально) - **max:** Qwen3.5-35B-A3B (MoE ~3B активных, GPU llama.cpp обязателен) - **Сервер:** llama.cpp (OpenAI-совместимый API на `/v1/chat/completions`) - **Параметры:** выбираются эффективной конфигурацией по `ai_level` (backend/services/instance_settings.py::load_effective_config), перекрывают config/plugins.yaml; per-tier max_tokens_map/max_tokens_reduce, temperature=0.2 - **Map-reduce:** чанки по целевому времени (20 минут) с per-tier лимитом токенов, иерархический reduce при переполнении бюджета - **Надёжность:** retry + circuit breaker в HTTP-клиенте (3 попытки, breaker после 5 ошибок), Celery-retry задачи с нарастающей паузой, идемпотентные guard'ы - **Язык:** русский (промпты в `workers/summarizer/prompts/summary_map_ru.txt` и `summary_reduce_ru.txt`, утверждены и едины для всех уровней) - **Токенизатор:** per-модель (загружается лениво), фолбэк-эвристика `len(text)//3` если отсутствует - **Установка моделей:** [docs/deploy/llm-setup.md](../deploy/llm-setup.md) (скачивание автоматизировано инсталлятором `install.sh`) - **Документация:** [docs/plugins/summarizer.md](./summarizer.md), [docs/architecture/adr/004-ai-tier-matrix.md](../architecture/adr/004-ai-tier-matrix.md) - **Файл:** `backend/core/plugins/qwen_local.py` ## Тестирование и отладка ### Локальное тестирование с null-провайдерами Отредактируйте `config/plugins.yaml`: ```yaml transcriber: enabled: true provider: "null" summarizer: enabled: true provider: "null" ``` Приложение будет работать без AI-моделей, возвращая пустые результаты. ### Проверка регистрации плагинов ```bash cd backend uv run python -c " from core.plugins.factory import _TRANSCRIBERS, _SUMMARIZERS print('Транскрибаторы:', list(_TRANSCRIBERS.keys())) print('Суммаризаторы:', list(_SUMMARIZERS.keys())) " ``` ### Интеграционный тест пайплайна ```bash cd backend CELERY_ALWAYS_EAGER=True uv run pytest tests/test_build_phrases.py -v ``` ## Ссылки - **Детальное руководство:** `docs/plugins/transcriber.md` - **Пайплайн:** `workers/tasks/pipeline.py` - **Конфиг фабрики:** `backend/core/plugins/factory.py` - **Модели БД:** `backend/models/audio_track.py`, `backend/models/phrase.py`