diff --git a/docs/superpowers/specs/2026-04-18-parakeet-backend-design.md b/docs/superpowers/specs/2026-04-18-parakeet-backend-design.md new file mode 100644 index 0000000..6d19452 --- /dev/null +++ b/docs/superpowers/specs/2026-04-18-parakeet-backend-design.md @@ -0,0 +1,277 @@ +# 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.