Files
vidconf/docs/plugins/contracts.md

322 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура плагинов
## Обзор
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 ГБ / ~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](../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`