13 KiB
Архитектура плагинов
Обзор
VidConf использует Strategy pattern + Factory pattern для подключения реализаций AI-моделей. Это позволяет:
- Добавлять новые провайдеры без изменения ядра приложения
- Менять реализации через конфигурацию без изменения кода
- Тестировать с no-op реализациями (NullTranscriber, NullSummarizer)
Контракты (ABC)
Transcriber
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
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
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
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 реализует:
_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 содержит заглушки для тестирования и отключения:
@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
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:
@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:
from core.plugins.my_transcriber import my_transcriber # noqa: F401
Шаг 3: Обновить config/plugins.yaml:
transcriber:
provider: "my_provider"
model: "my-model"
options:
key: "value"
Шаг 4: Добавить переменные окружения в .env (если нужны)
Шаг 5: Написать тесты и запустить:
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 (скачивание автоматизировано инсталлятором
install.sh) - Документация: docs/plugins/summarizer.md, docs/architecture/adr/004-ai-tier-matrix.md
- Файл:
backend/core/plugins/qwen_local.py
Тестирование и отладка
Локальное тестирование с null-провайдерами
Отредактируйте config/plugins.yaml:
transcriber:
enabled: true
provider: "null"
summarizer:
enabled: true
provider: "null"
Приложение будет работать без AI-моделей, возвращая пустые результаты.
Проверка регистрации плагинов
cd backend
uv run python -c "
from core.plugins.factory import _TRANSCRIBERS, _SUMMARIZERS
print('Транскрибаторы:', list(_TRANSCRIBERS.keys()))
print('Суммаризаторы:', list(_SUMMARIZERS.keys()))
"
Интеграционный тест пайплайна
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