Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

321
docs/plugins/contracts.md Normal file
View 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 ГБ / ~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`