Revert "docs(specs): дизайн-спек Parakeet backend"

This reverts commit cb6c1d7728.
This commit is contained in:
2026-04-18 20:32:48 +03:00
parent cb6c1d7728
commit 2c1d798eeb
@@ -1,277 +0,0 @@
# 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.