Первоначальная версия VidConf
This commit is contained in:
389
docs/plugins/transcriber.md
Normal file
389
docs/plugins/transcriber.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Плагин Transcriber — транскрибация аудио в текст
|
||||
|
||||
## Обзор
|
||||
|
||||
VidConf использует **Strategy pattern** для подключаемых реализаций транскрибации речи. Это позволяет менять провайдеров (faster-whisper на CPU, OpenAI Whisper, Vosk и т.д.) через конфигурацию без изменения ядра.
|
||||
|
||||
## Контракт Transcriber
|
||||
|
||||
### Интерфейс
|
||||
|
||||
Все реализации наследуют абстрактный класс `Transcriber` из `backend/core/plugins/transcriber.py`:
|
||||
|
||||
```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` | Код языка (ISO 639-1), дефолт `"ru"` |
|
||||
| **Возврат** | `list[Segment]` | Упорядоченный список сегментов, отсортированный по `start` |
|
||||
|
||||
**Требования:**
|
||||
- Если файл не найден → исключение `FileNotFoundError`
|
||||
- Сегменты должны быть отсортированы по `start` в возрастающем порядке
|
||||
- Дублирующиеся/перекрывающиеся сегменты допустимы (алгоритм фраз их обработает)
|
||||
|
||||
## Реализация по умолчанию: FasterWhisperCPU
|
||||
|
||||
### Описание
|
||||
|
||||
`FasterWhisperCPU` — оптимизированная реализация faster-whisper (CTranslate2-бэкэнд) для транскрибации на CPU с int8-квантизацией. Она встроена в проект как реализация по умолчанию.
|
||||
|
||||
**Файл:** `backend/core/plugins/faster_whisper.py`
|
||||
|
||||
### Параметры конструктора
|
||||
|
||||
```python
|
||||
@register_transcriber
|
||||
class FasterWhisperCPU(Transcriber):
|
||||
provider: ClassVar[str] = "faster_whisper_cpu"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str | None = None,
|
||||
language: str = "ru",
|
||||
download_root: str | None = None,
|
||||
) -> None:
|
||||
"""Инициализировать транскрибер.
|
||||
|
||||
Args:
|
||||
model: Имя модели Whisper (small/base/medium/etc);
|
||||
дефолт 'small' (оптимум для CPU).
|
||||
language: Код языка (ISO 639-1), дефолт 'ru'.
|
||||
download_root: Папка для кэша загруженных моделей;
|
||||
дефолт ~/.cache/huggingface/hub.
|
||||
"""
|
||||
```
|
||||
|
||||
| Параметр | Тип | Дефолт | Описание |
|
||||
|----------|-----|--------|---------|
|
||||
| `model` | `str` | `"small"` | Модель Whisper: `tiny`, `base`, `small`, `medium`, `large` |
|
||||
| `language` | `str` | `"ru"` | Язык распознавания (ISO 639-1) |
|
||||
| `download_root` | `str` | `~/.cache/` | Папка для кэша моделей |
|
||||
|
||||
### Особенности
|
||||
|
||||
#### VAD (Voice Activity Detection)
|
||||
|
||||
FasterWhisperCPU включает встроенный **Silero VAD** для автоматического разбиения аудио на речевые сегменты и подавления молчания. Это улучшает качество транскрибации и снижает галлюцинации Whisper на тишине/шуме.
|
||||
|
||||
- **Параметр:** `vad_filter=True`
|
||||
- **Порог молчания:** `vad_min_silence_duration_ms=500` (0.5 сек)
|
||||
|
||||
#### Отбрасывание коротких сегментов
|
||||
|
||||
Whisper часто халлюцинирует на очень коротких сегментах (шум, клики, тишина). FasterWhisperCPU автоматически отбрасывает сегменты, короче **0.3 секунды**:
|
||||
|
||||
```python
|
||||
MIN_SEGMENT_DURATION_S = 0.3
|
||||
|
||||
# Фильтрация в transcribe()
|
||||
return [
|
||||
Segment(start=segment.start, end=segment.end, text=segment.text)
|
||||
for segment in raw_segments
|
||||
if (segment.end - segment.start) >= MIN_SEGMENT_DURATION_S
|
||||
]
|
||||
```
|
||||
|
||||
#### Ленивый импорт
|
||||
|
||||
Зависимость `faster-whisper` импортируется лениво — только при первом вызове `transcribe()`. Это позволяет API-процессу, который не использует транскрибацию, избежать загрузки тяжёлой библиотеки в память.
|
||||
|
||||
```python
|
||||
def _get_model(self) -> "WhisperModel":
|
||||
if FasterWhisperCPU._model is None:
|
||||
from faster_whisper import WhisperModel # Импорт здесь, не в начале файла
|
||||
FasterWhisperCPU._model = WhisperModel(...)
|
||||
return FasterWhisperCPU._model
|
||||
```
|
||||
|
||||
#### Синглтон модели на процесс
|
||||
|
||||
Модель Whisper создаётся один раз при первом вызове `transcribe()` и переиспользуется всеми последующими вызовами в пределах одного процесса воркера. Это снижает нагрузку на память и CPU (загрузка модели — дорогая операция).
|
||||
|
||||
> **Примечание:** Celery-воркер транскрибации запускается с флагом `--pool=solo --concurrency=1`, поэтому гонок за синглтоном не бывает.
|
||||
|
||||
### Конфигурация
|
||||
|
||||
В `config/plugins.yaml`:
|
||||
|
||||
```yaml
|
||||
transcriber:
|
||||
enabled: true
|
||||
provider: "faster_whisper_cpu"
|
||||
model: "small"
|
||||
language: "ru"
|
||||
options:
|
||||
download_root: "/models/whisper"
|
||||
```
|
||||
|
||||
Переменные окружения (если нужны):
|
||||
|
||||
```bash
|
||||
# Опционально: переопределить папку кэша моделей
|
||||
export HF_HOME=/models
|
||||
```
|
||||
|
||||
### Производительность
|
||||
|
||||
- **Модель small:** ~8 минут аудио за 1 минуту на современном CPU (зависит от CPU)
|
||||
- **Память:** ~1.5 ГБ на процесс (с моделью и бэкэндом)
|
||||
- **Формат входа:** WAV, MP3, OGG, FLAC, M4A и др. (поддерживает ffmpeg-compatible форматы)
|
||||
|
||||
## Добавление нового Transcriber
|
||||
|
||||
### Пошаговая инструкция
|
||||
|
||||
#### Шаг 1: Создать файл реализации
|
||||
|
||||
Создайте новый файл в `backend/core/plugins/`, например `my_transcriber.py`:
|
||||
|
||||
```python
|
||||
"""Плагин Transcriber на основе MyService."""
|
||||
|
||||
from typing import ClassVar
|
||||
from core.plugins.factory import register_transcriber
|
||||
from core.plugins.transcriber import Segment, Transcriber
|
||||
|
||||
@register_transcriber
|
||||
class MyTranscriber(Transcriber):
|
||||
"""Транскрибер, использующий MyService API."""
|
||||
|
||||
provider: ClassVar[str] = "my_service"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str | None = None,
|
||||
language: str = "ru",
|
||||
api_key: str | None = None,
|
||||
**options,
|
||||
) -> None:
|
||||
"""Инициализировать транскрибер.
|
||||
|
||||
Args:
|
||||
model: Имя модели (опционально).
|
||||
language: Язык распознавания.
|
||||
api_key: API-ключ для сервиса.
|
||||
**options: Дополнительные параметры из config/plugins.yaml.
|
||||
"""
|
||||
self.model = model or "default-model"
|
||||
self.language = language
|
||||
self.api_key = api_key or os.getenv("MY_SERVICE_API_KEY")
|
||||
self.options = options
|
||||
|
||||
def transcribe(self, audio_path: str, language: str = "ru") -> list[Segment]:
|
||||
"""Транскрибировать аудиофайл через MyService API.
|
||||
|
||||
Args:
|
||||
audio_path: Путь к аудиофайлу на диске.
|
||||
language: Язык распознавания (может переопределить конструктор).
|
||||
|
||||
Returns:
|
||||
Упорядоченный список сегментов, отсортированный по start.
|
||||
"""
|
||||
# 1. Прочитать аудиофайл
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_data = f.read()
|
||||
|
||||
# 2. Отправить на API (пример)
|
||||
response = requests.post(
|
||||
"https://api.myservice.com/transcribe",
|
||||
files={"audio": audio_data},
|
||||
json={"language": language, "model": self.model},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# 3. Распарсить ответ в list[Segment]
|
||||
result = response.json()
|
||||
segments = [
|
||||
Segment(
|
||||
start=float(item["start"]),
|
||||
end=float(item["end"]),
|
||||
text=item["text"],
|
||||
)
|
||||
for item in result.get("segments", [])
|
||||
]
|
||||
|
||||
# 4. Отсортировать по start (важно!)
|
||||
segments.sort(key=lambda s: s.start)
|
||||
|
||||
return segments
|
||||
```
|
||||
|
||||
**Важные правила:**
|
||||
- Класс **должен** наследовать `Transcriber`
|
||||
- Класс **должен** иметь декоратор `@register_transcriber` (регистрирует в реестре)
|
||||
- Класс-переменная `provider: ClassVar[str]` **должна быть** уникальным идентификатором
|
||||
- Конструктор **должен** принимать `model`, `language` и `**options`
|
||||
- Метод `transcribe()` **должен** возвращать сегменты, отсортированные по `start`
|
||||
|
||||
#### Шаг 2: Импортировать в `__init__.py`
|
||||
|
||||
Добавьте импорт в `backend/core/plugins/__init__.py`:
|
||||
|
||||
```python
|
||||
"""Пакет плагинов Transcriber/Summarizer."""
|
||||
|
||||
from core.plugins import faster_whisper as faster_whisper # noqa: F401
|
||||
from core.plugins import my_transcriber as my_transcriber # noqa: F401 # ДОБАВИТЬ
|
||||
from core.plugins import null as null # noqa: F401
|
||||
```
|
||||
|
||||
> **Примечание:** `noqa: F401` подавляет предупреждение о неиспользуемом импорте — он нужен для побочного эффекта (регистрация). Используйте `as` для явности.
|
||||
|
||||
#### Шаг 3: Обновить конфигурацию
|
||||
|
||||
Отредактируйте `config/plugins.yaml`:
|
||||
|
||||
```yaml
|
||||
transcriber:
|
||||
enabled: true
|
||||
provider: "my_service" # Совпадает с MyTranscriber.provider
|
||||
model: "default-model"
|
||||
language: "ru"
|
||||
options:
|
||||
api_key: "${MY_SERVICE_API_KEY}" # Переменная окружения
|
||||
# Дополнительные параметры, специфичные для вашего сервиса
|
||||
timeout_seconds: 300
|
||||
retry_count: 3
|
||||
```
|
||||
|
||||
#### Шаг 4: Добавить переменные окружения
|
||||
|
||||
Отредактируйте `.env` (или `.env.example`):
|
||||
|
||||
```bash
|
||||
# MyService API
|
||||
MY_SERVICE_API_KEY=sk-...
|
||||
```
|
||||
|
||||
#### Шаг 5: Написать тесты
|
||||
|
||||
Добавьте тесты в `backend/tests/test_plugins_factory.py` или новый `test_my_transcriber.py`:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from core.plugins.factory import create_transcriber
|
||||
from core.plugins.config import TranscriberConfig
|
||||
|
||||
def test_my_transcriber_registered():
|
||||
"""Проверить регистрацию плагина."""
|
||||
cfg = TranscriberConfig(provider="my_service", model="test")
|
||||
transcriber = create_transcriber(cfg)
|
||||
assert transcriber.provider == "my_service"
|
||||
assert transcriber.model == "test"
|
||||
|
||||
def test_my_transcriber_transcribe(tmp_path):
|
||||
"""Тест транскрибации с моком API."""
|
||||
# Создать тестовый WAV-файл
|
||||
audio_file = tmp_path / "test.wav"
|
||||
# ... создать корректный WAV-файл ...
|
||||
|
||||
cfg = TranscriberConfig(provider="my_service", model="test")
|
||||
transcriber = create_transcriber(cfg)
|
||||
|
||||
# Вызвать transcribe
|
||||
segments = transcriber.transcribe(str(audio_file), language="ru")
|
||||
|
||||
# Проверить результат
|
||||
assert len(segments) > 0
|
||||
assert all(s.start < s.end for s in segments)
|
||||
assert segments == sorted(segments, key=lambda s: s.start)
|
||||
```
|
||||
|
||||
#### Шаг 6: Протестировать локально
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Убедиться, что конфиг загружается
|
||||
uv run python -c "
|
||||
from core.plugins.factory import _TRANSCRIBERS
|
||||
print('Зарегистрированные транскрибаторы:', list(_TRANSCRIBERS.keys()))
|
||||
"
|
||||
|
||||
# Запустить тесты
|
||||
uv run pytest tests/test_my_transcriber.py -v
|
||||
|
||||
# Запустить пайплайн с новым провайдером (интеграционный тест)
|
||||
CELERY_ALWAYS_EAGER=True uv run pytest tests/test_build_phrases.py -v
|
||||
```
|
||||
|
||||
### Отключение транскрибации
|
||||
|
||||
Если нужно полностью отключить модуль транскрибации, установите в `config/plugins.yaml`:
|
||||
|
||||
```yaml
|
||||
transcriber:
|
||||
enabled: false
|
||||
provider: "null"
|
||||
model: null
|
||||
```
|
||||
|
||||
Поведение:
|
||||
- Запись аудиотреков продолжает работать (egress в активном состоянии)
|
||||
- При `room_finished` `run_pipeline()` завершится до инстанцирования плагина
|
||||
- `pipeline_status` сеанса останется `"recording"` (не перейдёт в `"transcribing"`)
|
||||
- Лог-сообщение: "transcriber отключён (enabled=false) — сеанс ... пропущен"
|
||||
|
||||
## Архитектурные решения
|
||||
|
||||
### Идемпотентный пайплайн пост-обработки
|
||||
|
||||
Пайплайн пост-обработки — идемпотентная машина состояний (см. `backend/models/session.py`):
|
||||
|
||||
```
|
||||
recording → transcribing → summarizing → notified / failed
|
||||
```
|
||||
|
||||
**run_pipeline()** диспетчер:
|
||||
1. Проверяет текущий `pipeline_status` сеанса
|
||||
2. Продолжает работу с последнего успешного шага
|
||||
3. Отслеживает статус каждого трека (`session_audio_tracks.status`)
|
||||
4. Коммитит результаты транскрибации в `session_audio_tracks.segments` (JSONB) после каждого трека
|
||||
|
||||
Это позволяет восстановиться после сбоя воркера без потери данных или дублирования.
|
||||
|
||||
### Атрибуция к участнику сеанса (ADR-002)
|
||||
|
||||
Фразы и аудиотреки атрибутированы не к `users`, а к `conference_participants` (участник сеанса). Это позволяет работать с гостями без `user_id`:
|
||||
|
||||
```python
|
||||
# Получить спикера фразы
|
||||
participant = session.query(ConferenceParticipant).get(phrase.participant_id)
|
||||
user = participant.user # Может быть None для гостей
|
||||
guest = participant.guest # Может быть None для пользователей
|
||||
```
|
||||
|
||||
## Ссылки
|
||||
|
||||
- **ADR-002:** `docs/architecture/adr/002-phrase-attribution-session-participant.md`
|
||||
- **Контракты плагинов:** `docs/plugins/contracts.md`
|
||||
- **Модели:** `backend/models/audio_track.py`, `backend/models/phrase.py`
|
||||
- **Пайплайн:** `workers/tasks/pipeline.py`
|
||||
- **Реконструкция фраз:** `workers/transcription/phrases.py`
|
||||
- **Тесты:** `backend/tests/test_plugins_factory.py`, `backend/tests/test_build_phrases.py`
|
||||
Reference in New Issue
Block a user