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

22 KiB
Raw Blame History

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). Отклонены: 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

_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

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

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

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.

Зависимости

# 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_aliasmodel_name="parakeet" → скачивает nvidia/parakeet-tdt-0.6b-v3.
  2. test_ensure_model_available_rejects_unknown_modelmodel_name="medium"ValueError с текстом-подсказкой.
  3. test_ensure_model_available_rejects_unknown_compute_typecompute_type="float16"ValueError.
  4. test_create_model_maps_compute_typeint8quantization="int8"; float32quantization=None.
  5. test_transcribe_produces_segments — mock recognize возвращает 3 SegmentResult → 3 Segment.
  6. test_transcribe_warns_on_explicit_cli_language — явный --languagewarnings.warn.
  7. test_transcribe_does_not_warn_on_config_languagelanguage из конфига → warning НЕ срабатывает.
  8. test_transcribe_calls_on_segment — callback на каждый сегмент.
  9. test_transcribe_returns_multi_languageresult.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

  1. test_load_model_parakeet_no_fallback — при ошибке Parakeet исключение пробрасывается (без fallback на CPU Whisper).

tests/test_cli.py — regression + новые

  1. test_cli_whisper_transcript_header_unchanged--device cpu --language ru → шапка содержит **Язык**: ru (forced) (регрессия отсутствует).
  2. test_cli_whisper_auto_language_header_unchanged--device cpu --language auto → шапка отражает detected-режим.
  3. test_cli_parakeet_transcript_header--device parakeet → шапка **Язык**: multi (detected) и **Модель**: parakeet-tdt-0.6b-v3.
  4. test_cli_parakeet_model_row_shows_effective — config model="medium" + --device parakeet --model parakeet → rich-вывод и format_transcript получают parakeet-tdt-0.6b-v3.
  5. test_cli_errors_on_config_model_mismatch_without_override — config model="medium" + --device parakeet без --model → ясная ошибка (не traceback).
  6. 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.