Files
local-transcriber/docs/superpowers/specs/2026-04-18-parakeet-backend-design.md
T
ddadmin cb6c1d7728 docs(specs): дизайн-спек Parakeet backend
- Зачем:
  - зафиксировать дизайн экспериментального бэкенда до имплементации; гипотеза — Parakeet TDT v3 быстрее/качественнее Whisper на Intel/AMD встройках.
- Что:
  - добавлен docs/superpowers/specs/2026-04-18-parakeet-backend-design.md после 3 раундов adversarial-review.
  - зафиксированы: onnx-asr+Silero VAD как runtime, CPU EP в MVP, --device parakeet, int8/fp32 compute_type, no-fallback политика, long-audio gate с blocker-thresholds (peak RSS ≤4GB, первый status ≤10с, interval ≤30с, offline после первого запуска).
- Проверка:
  - cat docs/superpowers/specs/2026-04-18-parakeet-backend-design.md.
2026-04-18 20:26:56 +03:00

278 lines
22 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.
# Parakeet backend — design spec
**Дата:** 2026-04-18
**Статус:** Готов к планированию реализации
**Ветка реализации:** `feature/parakeet-backend` (в отдельном git worktree)
## Контекст и гипотеза
`local-transcriber` — CLI для локальной транскрипции аудио/видео (Whisper). Архитектура pluggable backends (ADR-003): `FasterWhisperBackend` (CTranslate2, CPU/CUDA), `OpenVINOBackend` (Intel CPU/iGPU/NPU). Дефолтный язык — `ru`. Целевая аудитория — ноутбуки с Intel и AMD встройками; NVIDIA — меньшинство.
**Гипотеза.** NVIDIA Parakeet TDT 0.6B v3 (через ONNX Runtime на CPU) быстрее и/или качественнее Whisper-бэкендов на Intel/AMD встройках. Проверяется прогоном на реальных русских файлах с сравнением wall-clock и качества против OpenVINO-Whisper medium и faster-whisper CPU medium.
## Выбор технологии (зафиксирован)
- **Модель:** `nvidia/parakeet-tdt-0.6b-v3` — multilingual, 25 языков включая русский.
- **Runtime:** ONNX Runtime через Python-пакет `onnx-asr` ([`istupakov/onnx-asr`](https://github.com/istupakov/onnx-asr)). Отклонены: NeMo toolkit (~1-2GB зависимостей), OpenVINO Parakeet (AMD iGPU не поддерживается, Intel — экспериментальный порт без сообщества).
- **Execution Provider в MVP:** CPU. DirectML (Windows AMD/Intel iGPU), ORT-OpenVINO EP, `onnxruntime-gpu` (CUDA) — отдельные фазы после валидации гипотезы.
- **VAD:** Silero VAD через `asr.with_vad()` — обязательно (без VAD лимит 20-30 сек на файл; все реальные файлы длиннее).
- **Quantization:** `int8` по умолчанию (~670MB диск, ~2GB RAM при inference), `fp32` — опциональное значение `--compute-type float32`.
## Архитектура
### Инвариант
`TranscribeFileResult`, `TranscribeResult`, `Segment`, `format_transcript()`**не меняем**. Регрессионный риск для существующих Whisper/OpenVINO-путей минимизирован.
Parakeet интегрируется через две минимальные точки:
1. `result.language = "multi"` внутри `ParakeetBackend.transcribe` — соответствует поведению Parakeet v3.
2. Атрибут backend-инстанса `effective_model_name` — аналог существующих `actual_compute_type`, `actual_ov_device`.
CLI после `_transcribe_file` читает `getattr(tfr.backend, "effective_model_name", defaults["model"])` для rich-вывода и `format_transcript(..., model_name=...)`. Rich-строка `Модель: ...` **переносится** после `_transcribe_file` (точка, где уже доступен `tfr.backend`). До транскрипции выводится только `Файл: ...` и сообщения `on_status` от загрузки модели.
**Шапка транскрипта для Parakeet:** `result.language = "multi"` + `language_mode = "detected"` (CLI форсит `"detected"` при `--device parakeet*`). Формат — `**Язык**: multi (detected)`.
### Контракт `--language`
- `result.language = "multi"` всегда.
- CLI при `--device parakeet*` форсит `language_mode = "detected"` (независимо от того, задавал ли пользователь `--language`).
- **Warning только при явном CLI-флаге `--language`.** Если `language` пришёл из `.transcriber.toml` или `HARDCODED_DEFAULTS` (без явного CLI) — warning подавлен. Источник явности: `cli_values["language"] is not None` (не `"language" in config`).
- Текст warning: `Parakeet игнорирует --language; язык определяется автоматически`.
### Контракт `--compute-type`
| CLI `--compute-type` | `onnx_asr.load_model(quantization=...)` | Примечание |
|----------------------|------------------------------------------|------------|
| `int8` | `"int8"` | Дефолт |
| `float32` | `None` | Без quantization |
| другое | — | `ValueError("Parakeet поддерживает только int8 или float32")` |
### Контракт VAD — runtime-зависимость, без prefetch
`ensure_model_available` отвечает только за Parakeet-модель. `create_model` вызывает `onnx_asr.load_vad(model="silero")`, который при первом запуске скачивает Silero VAD (~15MB).
Одна точка правды — `onnx_asr.load_vad`. Двойной prefetch (наш `snapshot_download` + внутренний `onnx-asr`) без точного знания repo/layout создавал несостыковку: `ensure_model_available` рапортует «готово», но `create_model` всё равно тянет из сети.
Документировано в ADR-005 и README: **первый запуск `--device parakeet` требует интернет для Silero VAD; повторные — оффлайн** (проверяется в long-audio gate).
### Контракт fallback — **НЕ fallback'ить Parakeet на Whisper CPU**
В `load_model` и `_transcribe_file` при `actual_device.startswith("parakeet")` — пропускаем fallback-ветку, исключение пробрасывается наружу.
Обоснование: Parakeet — другое семейство моделей с другим качеством и семантикой языка (`multi` vs `ru`). Молчаливая подмена на Whisper medium ломает интерпретацию транскрипта. Пользователь видит понятную ошибку (`Ошибка загрузки Silero VAD — проверьте интернет` / `Parakeet inference crashed — см. --verbose`) и делает осознанный выбор.
### `detect_device` (utils.py)
`--device auto` **не переключается на Parakeet.** Порядок остаётся: CUDA → OpenVINO GPU → OpenVINO CPU → CPU. Parakeet — только через явный `--device parakeet` / `--device parakeet-cpu`.
### `config.py`
```python
_VALID_DEVICES = {
"auto", "cpu", "cuda",
"openvino", "openvino-gpu", "openvino-cpu",
"parakeet", "parakeet-cpu",
}
DEVICE_DEFAULTS["parakeet"] = {"model": "parakeet-tdt-0.6b-v3", "compute_type": "int8"}
DEVICE_DEFAULTS["parakeet-cpu"] = {"model": "parakeet-tdt-0.6b-v3", "compute_type": "int8"}
```
**Config-conflict на `model`.** Если в глобальном `.transcriber.toml` стоит `model = "medium"` и пользователь запускает `--device parakeet` без `--model`, `apply_device_defaults` возьмёт `"medium"` из конфига → `ParakeetBackend.ensure_model_available` кидает **понятный `ValueError`**:
> `Parakeet поддерживает только 'parakeet-tdt-0.6b-v3' или алиас 'parakeet'. Получено: 'medium'. Передайте --model parakeet или уберите 'model' из .transcriber.toml.`
Принципиально **не делаем** скрытого override в `apply_device_defaults` — это ломает инвариант «CLI > config > defaults». Пользователь получает ясную ошибку и делает явный фикс.
Алиас `"parakeet"``"parakeet-tdt-0.6b-v3"` допустим в `ensure_model_available` (UX-удобство).
### Новый бэкенд — `backends/parakeet.py`
```python
class ParakeetBackend:
SUPPORTED_MODEL_NAMES = {"parakeet-tdt-0.6b-v3", "parakeet"}
SUPPORTED_COMPUTE_TYPES = {"int8", "float32"}
def __init__(self, compute_type_explicit: bool = True):
self._compute_type_explicit = compute_type_explicit
self.actual_compute_type: str | None = None
self.effective_model_name: str = "parakeet-tdt-0.6b-v3"
def ensure_model_available(self, model_name, compute_type, on_status) -> str:
# 1. model_name ∈ SUPPORTED_MODEL_NAMES иначе ValueError (текст выше)
# 2. compute_type ∈ SUPPORTED_COMPUTE_TYPES иначе ValueError
# 3. snapshot_download("nvidia/parakeet-tdt-0.6b-v3") → локальный путь
# 4. Silero VAD здесь НЕ трогаем (runtime-зависимость)
# 5. self.actual_compute_type = compute_type
# 6. Возвращаем путь к каталогу
def create_model(self, model_path, device, compute_type, cpu_threads=0) -> Any:
# 1. quantization = "int8" if compute_type == "int8" else None
# 2. asr = onnx_asr.load_model("nemo-parakeet-tdt-0.6b-v3", path=model_path, quantization=quantization)
# 3. vad = onnx_asr.load_vad(model="silero") # может качать VAD при первом запуске
# 4. asr_with_vad = asr.with_vad(vad=vad)
# 5. cpu_threads → sess_options через providers_options
# 6. Возвращаем asr_with_vad
def transcribe(self, model, file_path, language, on_segment, on_status) -> TranscribeResult:
# 1. Если language задан явно (см. «Контракт --language») — warnings.warn
# 2. result = model.recognize(str(file_path))
# 3. Поддержка iterator И list (детектим на лету)
# 4. Для каждого SegmentResult → Segment(start, end, text); on_segment; on_status c heartbeat
# 5. Возвращаем TranscribeResult(segments, language="multi", language_probability=0.0, duration, device_used="")
```
### Регистрация в `backends/__init__.py`
```python
if device in ("parakeet", "parakeet-cpu"):
try:
from .parakeet import ParakeetBackend
except ImportError:
raise ValueError(
"Parakeet бэкенд недоступен. Установите: pip install onnx-asr[cpu,hub]"
) from None
return ParakeetBackend(compute_type_explicit=compute_type_explicit)
```
### `transcriber.py` — no-fallback для Parakeet
```python
def _is_parakeet_error(exc: BaseException) -> bool:
if isinstance(exc, RuntimeError):
return True
mod = type(exc).__module__ or ""
return "onnxruntime" in mod or "onnx_asr" in mod
def _is_backend_error(exc, device):
if device.startswith("parakeet"):
return _is_parakeet_error(exc)
# ... existing branches
```
В `load_model` и `_transcribe_file` добавляется ранний выход из fallback-ветки при `actual_device.startswith("parakeet")` — исключение пробрасывается (как будто `strict_device=True`).
### `cli.py` — точечные изменения
1. **Rich-строка `Модель: ...` переносится после `_transcribe_file`.** Использует `getattr(tfr.backend, "effective_model_name", defaults["model"])`.
2. **`format_transcript(..., model_name=...)`** получает `getattr(tfr.backend, "effective_model_name", defaults["model"])`.
3. **`language_mode`** при Parakeet: `if actual_device.startswith("parakeet"): language_mode = "detected"`. Иначе — существующая логика.
4. **Warning на явный `--language`** при Parakeet: проверка `cli_values["language"] is not None` (не `"language" in config`). Срабатывает в `_run_single` / `_run_batch` до `_transcribe_file` — пользователь видит сразу.
5. **`_format_device_info`**: `if device_used == "parakeet-cpu": return "Parakeet (CPU via ONNX Runtime)"`.
Для Whisper-бэкендов код не меняется.
### Progress UX — heartbeat если блокирующий вызов
Если `asr_with_vad.recognize()` возвращает iterator — итерируем с вызовами `on_status` per segment. Если блокирующий — обёртка в поток с heartbeat-таймером (паттерн `openvino.py::_generate_with_progress`). Тип возврата проверяется в long-audio gate (см. ниже); при блокирующем вызове heartbeat — **обязателен до merge**.
## Зависимости
```toml
# pyproject.toml
dependencies = [
# ... existing
"onnx-asr[cpu,hub]>=0.7", # минимальная версия уточнить после uv lock
]
```
`uv lock` обязателен до любой имплементации — проверить, что `onnxruntime` не конфликтует с тем, что тянут `faster-whisper` / `openvino-genai`. Platform markers не нужны: `onnx-asr` — pure Python; `onnxruntime` CPU-wheels покрывают Linux/macOS/Windows × x86_64/arm64.
## Тесты
### `tests/test_parakeet_backend.py` — юнит с mock `onnx_asr`
1. `test_ensure_model_available_accepts_alias``model_name="parakeet"` → скачивает `nvidia/parakeet-tdt-0.6b-v3`.
2. `test_ensure_model_available_rejects_unknown_model``model_name="medium"``ValueError` с текстом-подсказкой.
3. `test_ensure_model_available_rejects_unknown_compute_type``compute_type="float16"``ValueError`.
4. `test_create_model_maps_compute_type``int8``quantization="int8"`; `float32``quantization=None`.
5. `test_transcribe_produces_segments` — mock `recognize` возвращает 3 `SegmentResult` → 3 `Segment`.
6. `test_transcribe_warns_on_explicit_cli_language` — явный `--language``warnings.warn`.
7. `test_transcribe_does_not_warn_on_config_language``language` из конфига → warning НЕ срабатывает.
8. `test_transcribe_calls_on_segment` — callback на каждый сегмент.
9. `test_transcribe_returns_multi_language``result.language == "multi"`, `language_probability == 0.0`.
10. `test_backend_sets_effective_model_name` — атрибут проставлен.
11. `test_transcribe_accepts_list_and_iterator_results` — оба типа возврата обрабатываются.
### `tests/test_transcriber.py`
12. `test_load_model_parakeet_no_fallback` — при ошибке Parakeet исключение пробрасывается (без fallback на CPU Whisper).
### `tests/test_cli.py` — regression + новые
13. `test_cli_whisper_transcript_header_unchanged``--device cpu --language ru` → шапка содержит `**Язык**: ru (forced)` (регрессия отсутствует).
14. `test_cli_whisper_auto_language_header_unchanged``--device cpu --language auto` → шапка отражает detected-режим.
15. `test_cli_parakeet_transcript_header``--device parakeet` → шапка `**Язык**: multi (detected)` и `**Модель**: parakeet-tdt-0.6b-v3`.
16. `test_cli_parakeet_model_row_shows_effective` — config `model="medium"` + `--device parakeet --model parakeet` → rich-вывод и `format_transcript` получают `parakeet-tdt-0.6b-v3`.
17. `test_cli_errors_on_config_model_mismatch_without_override` — config `model="medium"` + `--device parakeet` без `--model` → ясная ошибка (не traceback).
18. `test_cli_parakeet_warns_explicit_language` — config без `language`, явный `--language en --device parakeet` → warning.
Все юнит-тесты — чистые mocks, без реального скачивания.
## Long-audio verification gate — exit criteria, blocker для merge
**Метрики и нормы (единственный источник истины):**
| Метрика | Норма |
|---------|-------|
| Длина тестового файла | ≥30 минут, русский |
| Peak RSS | ≤4 GB |
| Первый `on_status` update | ≤10 сек от старта `_transcribe_file` |
| Максимальный интервал между `on_status` update'ами | ≤30 сек |
| Повторный прогон с `HF_HUB_OFFLINE=1` | без сетевых запросов |
**Методика измерения:**
- Peak RSS: `/usr/bin/time -v ...` (поле "Maximum resident set size") или `psutil.Process().memory_info().rss` из внешнего процесса раз в секунду.
- Интервалы `on_status`: специальный тестовый callback в CLI логирует `time.monotonic()` каждого вызова в файл; пост-анализ считает max interval.
- Offline: `HF_HUB_OFFLINE=1` через `env`; либо `strace -e trace=connect`, либо проверка отсутствия задержек на сетевых операциях в начале прогона.
**Blocker-правила (план заморожен до устранения):**
- Peak RSS > 4 GB → нужен ручной chunking; MVP не готов.
- Первый update > 10 сек ИЛИ любой интервал > 30 сек → нужен heartbeat-thread (паттерн `openvino.py::_generate_with_progress`).
- Offline-прогон делает сетевые обращения → VAD-кэш не работает, разбираться до merge.
Результаты (числа всех метрик) фиксируются в `docs/gpu.md`.
## Документация
1. **ADR-005** (`docs/adr/005-parakeet-backend.md`) — контекст, решение (ONNX Runtime + onnx-asr + Silero VAD), отклонённые альтернативы (NeMo toolkit, OpenVINO Parakeet, DirectML в MVP, VAD prefetch, скрытый override в `apply_device_defaults`, cross-backend fallback на Whisper), последствия (язык игнорируется, no fallback, `effective_model_name` через backend-атрибут, VAD runtime-зависимость с требованием интернета на первый запуск).
2. **README.md** — секция «Parakeet (экспериментально)»: установка, `--device parakeet`, ограничения (int8/fp32, язык auto, первый запуск — интернет для VAD ~15MB, память ~2GB, конфликт с `model` в конфиге).
3. **docs/gpu.md** — секция «Parakeet»: результаты long-audio gate + сравнение с OpenVINO medium / faster-whisper medium CPU на одном и том же RU-файле.
## Порядок шагов (для writing-plans)
Вся работа — в git worktree на ветке `feature/parakeet-backend` (через `superpowers:using-git-worktrees`).
1. **Scaffold + зависимости:** worktree, ветка, `uv add "onnx-asr[cpu,hub]>=0.7"`, `uv lock` → нет конфликтов `onnxruntime` с существующими deps (**блокер**). `uv sync`. Существующие `uv run pytest` зелёные.
2. **config.py:** `_VALID_DEVICES` + `DEVICE_DEFAULTS` для parakeet/parakeet-cpu. Тест на валидацию `device = "parakeet"` в конфиге.
3. **ParakeetBackend (заглушка):** файл `backends/parakeet.py` со stub'ами (`raise NotImplementedError`). Регистрация в `backends/__init__.py`. Импорт работает без ошибок.
4. **`ensure_model_available`:** валидация model_name / compute_type, `snapshot_download`, валидация каталога. Тесты 1-3.
5. **`create_model`:** `onnx_asr.load_model + load_vad + with_vad`, маппинг compute_type, cpu_threads. Тест 4.
6. **`transcribe`:** итерация `SegmentResult`, warning на язык, `language="multi"`, duration. Тесты 5-11.
7. **`transcriber.py` — no-fallback:** `_is_parakeet_error`, ветка в `_is_backend_error`, пропуск fallback при `actual_device.startswith("parakeet")`. Тест 12.
8. **cli.py:** перенос строки `Модель:` после `_transcribe_file`, чтение `effective_model_name` через `getattr`, force `language_mode="detected"` для Parakeet, warning на явный CLI `--language`, `_format_device_info` для `parakeet-cpu`. Regression-тесты 13-14, Parakeet-тесты 15-18.
9. **Long-audio verification gate:** прогон ≥30-мин RU-файла, замеры всех метрик, оффлайн-повтор. Фиксация в `docs/gpu.md`. **Blocker для merge.**
10. **ADR-005 + README** с финальными числами из шага 9.
11. **Финал:** `uv run pytest -v` зелёное, `uv run transcribe sample.mp3 --device parakeet` работает end-to-end, PR.
## Что НЕ делаем в MVP (отложено)
- DirectML EP для AMD iGPU / Intel iGPU (Windows).
- ORT-OpenVINO EP для Intel iGPU/NPU.
- Parakeet на CUDA (`onnxruntime-gpu`).
- Ручной chunking длинных файлов (доверяем `with_vad` + gate).
- `--device auto` включает Parakeet.
- Alias `parakeet-tdt-0.6b-v3` для моделей других бэкендов.
- Canary (ещё одна NeMo-модель, поддерживается `onnx-asr`).
- Prefetch Silero VAD в `ensure_model_available`.
- Изменение сигнатур `TranscribeFileResult` / `TranscribeResult` / `format_transcript`.
## Открытые риски и митигации
1. **Память на длинных файлах** — gate шага 9; провал → блокер MVP до ручного chunking.
2. **Progress UX (блокирующий `recognize`)** — gate шага 9; провал → heartbeat-thread обязателен.
3. **Качество на русском** — основная гипотеза; результат фиксируется в `docs/gpu.md`; при ухудшении по сравнению с Whisper — бэкенд остаётся с пометкой «EN лучше, чем RU».
4. **Конфликт версий `onnxruntime`** — блокер шага 1; решается через `uv lock` до любой имплементации.
5. **VAD offline** — gate шага 9 (`HF_HUB_OFFLINE=1`); провал → разбираться, при необходимости возвращаться к prefetch с реальной проверкой repo/layout.
6. **Config-conflict на `model`** — ясная ошибка, не скрытый override.