Первоначальная версия VidConf
This commit is contained in:
321
docs/plugins/contracts.md
Normal file
321
docs/plugins/contracts.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Архитектура плагинов
|
||||
|
||||
## Обзор
|
||||
|
||||
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`
|
||||
Reference in New Issue
Block a user