Files
vidconf/docs/plugins/contracts.md
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

13 KiB
Raw Blame History

Архитектура плагинов

Обзор

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 ГБ / ~2022 ГБ)
    • 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