From 82596187a42c157ba903f5784e1c43fe3ded2349 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:09:43 +0300 Subject: [PATCH 01/16] =?UTF-8?q?docs(superpowers):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20spec=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=20=D0=B4=D0=BB=D1=8F=20onnx-asr=20=D0=B1?= =?UTF-8?q?=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - задизайнить эксперимент с Parakeet/GigaAM через onnx-asr как третий бэкенд. - Что: - spec: архитектура, модель-алиасы (gigaam-v3, parakeet-v3), API, регистрация. - plan: 8 задач TDD: зависимость → скелет → ensure → create → transcribe → регистрация. - Проверка: - gh pr diff, ревью spec/plan. --- ...6-04-25-parakeet-backend-implementation.md | 824 ++++++++++++++++++ .../2026-04-25-parakeet-backend-design.md | 160 ++++ 2 files changed, 984 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-25-parakeet-backend-implementation.md create mode 100644 docs/superpowers/specs/2026-04-25-parakeet-backend-design.md diff --git a/docs/superpowers/plans/2026-04-25-parakeet-backend-implementation.md b/docs/superpowers/plans/2026-04-25-parakeet-backend-implementation.md new file mode 100644 index 0000000..46932e0 --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-parakeet-backend-implementation.md @@ -0,0 +1,824 @@ +# onnx-asr Backend Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `onnx-asr` as a third pluggable backend (device `"onnx"`) supporting GigaAM v3 (Russian, 4.7% WER, 59x RTF) and Parakeet v3 (multilingual, 11% WER, 34x RTF). + +**Architecture:** Follow `OpenVINOBackend` pattern in `backends/openvino.py`. Implement the structural Backend protocol (`ensure_model_available`, `create_model`, `transcribe`). Register via `get_backend("onnx")` in `__init__.py`. Audio loading reuses `faster_whisper.decode_audio()`. VAD via Silero built into onnx-asr. + +**Tech Stack:** `onnx-asr>=0.11.0`, `onnxruntime` (transitive), `huggingface-hub` (transitive), existing `faster-whisper` for audio decode. + +**Refs:** Spec at `docs/superpowers/specs/2026-04-25-parakeet-backend-design.md`, Backend protocol at `src/local_transcriber/backends/base.py`, Pattern to follow at `src/local_transcriber/backends/openvino.py`, Types at `src/local_transcriber/types.py`. + +--- + +### Task 1: Add onnx-asr dependency + +**Files:** +- Modify: `pyproject.toml:7-15` + +- [ ] **Step 1: Add `onnx-asr[cpu,hub]` to dependencies** + +Open `pyproject.toml`. In the `dependencies` list, add `"onnx-asr[cpu,hub]>=0.11.0"`: + +```toml +dependencies = [ + "typer", + "rich", + "faster-whisper>=1.2.1", + "socksio>=1.0.0", + "nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'", + "openvino-genai>=2025.0; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", + "tomli>=2.0; python_version < '3.11'", + "onnx-asr[cpu,hub]>=0.11.0", +] +``` + +- [ ] **Step 2: Install the dependency** + +Run: `uv sync` +Expected: onnx-asr and onnxruntime installed without errors. + +- [ ] **Step 3: Verify import works** + +Run: `uv run python -c "import onnx_asr; print(onnx_asr.__version__)"` +Expected: prints version (e.g. `0.11.0`), no errors. + +- [ ] **Step 4: Commit** + +```bash +git add pyproject.toml uv.lock +git commit -m "build: add onnx-asr[cpu,hub]>=0.11.0 dependency" +``` + +--- + +### Task 2: Backend skeleton — model aliases and class stub + +**Files:** +- Create: `src/local_transcriber/backends/onnx_asr.py` +- Create: `tests/test_onnx_asr.py` +- Read: `src/local_transcriber/backends/openvino.py` (for pattern) +- Read: `src/local_transcriber/backends/base.py` (for protocol) +- Read: `src/local_transcriber/types.py` (for Segment, TranscribeResult) + +- [ ] **Step 1: Write the failing test for model alias resolution** + +Create `tests/test_onnx_asr.py`: + +```python +"""Tests for onnx-asr backend.""" + +import pytest +from local_transcriber.backends.onnx_asr import OnnxAsrBackend, MODEL_ALIASES + + +class TestModelAliases: + def test_gigaam_v3_resolves(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("gigaam-v3") + assert result == "gigaam-v3-ctc" + + def test_parakeet_v3_resolves(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("parakeet-v3") + assert result == "nemo-parakeet-tdt-0.6b-v3" + + def test_raw_name_passes_through(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("nemo-canary-1b-v2") + assert result == "nemo-canary-1b-v2" + + def test_unknown_alias_raises(self): + backend = OnnxAsrBackend() + with pytest.raises(ValueError, match="Неподдерживаемая модель"): + backend._resolve_model("nonexistent-model") +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/test_onnx_asr.py -v` +Expected: FAIL — `OnnxAsrBackend` not defined. + +- [ ] **Step 3: Write minimal implementation** + +Create `src/local_transcriber/backends/onnx_asr.py`: + +```python +"""Бэкенд транскрипции на основе onnx-asr (GigaAM, Parakeet, FastConformer).""" + +from __future__ import annotations + +MODEL_ALIASES: dict[str, str] = { + "gigaam-v3": "gigaam-v3-ctc", + "parakeet-v3": "nemo-parakeet-tdt-0.6b-v3", +} + +SUPPORTED_ALIASES = ", ".join(MODEL_ALIASES) + + +class OnnxAsrBackend: + """Бэкенд транскрипции через onnx-asr (ONNX Runtime).""" + + def _resolve_model(self, model_name: str) -> str: + """Resolve alias to onnx-asr model name. Raw names pass through.""" + if model_name in MODEL_ALIASES: + return MODEL_ALIASES[model_name] + if "/" in model_name or "-" in model_name: + # Looks like a raw onnx-asr name — allow passthrough + return model_name + raise ValueError( + f"Неподдерживаемая модель '{model_name}'. " + f"Доступные алиасы: {SUPPORTED_ALIASES}. " + f"Либо укажите полное имя модели onnx-asr." + ) +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/test_onnx_asr.py::TestModelAliases -v` +Expected: 4 PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/local_transcriber/backends/onnx_asr.py tests/test_onnx_asr.py +git commit -m "feat: add onnx-asr backend skeleton with model alias resolution" +``` + +--- + +### Task 3: Implement ensure_model_available + +**Files:** +- Modify: `src/local_transcriber/backends/onnx_asr.py` +- Modify: `tests/test_onnx_asr.py` + +- [ ] **Step 1: Write failing tests** + +Append to `tests/test_onnx_asr.py`: + +```python +class TestEnsureModelAvailable: + def test_returns_model_id_for_gigaam(self): + backend = OnnxAsrBackend() + result = backend.ensure_model_available("gigaam-v3", "int8") + assert result == "gigaam-v3-ctc" + + def test_returns_model_id_for_parakeet(self): + backend = OnnxAsrBackend() + result = backend.ensure_model_available("parakeet-v3", "fp16") + assert result == "nemo-parakeet-tdt-0.6b-v3" + + def test_stores_compute_type(self): + backend = OnnxAsrBackend() + backend.ensure_model_available("gigaam-v3", "float32") + assert backend._resolved_model_id == "gigaam-v3-ctc" + assert backend.actual_compute_type == "float32" +``` + +- [ ] **Step 2: Run tests — expect FAIL** + +Run: `uv run pytest tests/test_onnx_asr.py::TestEnsureModelAvailable -v` +Expected: FAIL — `ensure_model_available` not defined. + +- [ ] **Step 3: Implement ensure_model_available** + +Append to `OnnxAsrBackend` class in `src/local_transcriber/backends/onnx_asr.py`: + +```python + def __init__(self): + self.actual_compute_type: str | None = None + self._resolved_model_id: str | None = None + self._vad: Any = None + + def ensure_model_available( + self, + model_name: str, + compute_type: str, + on_status: Callable[[str], None] | None = None, + ) -> str: + """Resolves model alias and returns the onnx-asr model identifier. + + onnx-asr downloads models automatically via load_model(), + so this just validates the alias and returns the identifier string. + """ + self.actual_compute_type = compute_type + self._resolved_model_id = self._resolve_model(model_name) + return self._resolved_model_id +``` + +Add the import at the top of the file: + +```python +from collections.abc import Callable +from typing import Any +``` + +- [ ] **Step 4: Run tests — expect PASS** + +Run: `uv run pytest tests/test_onnx_asr.py::TestEnsureModelAvailable -v` +Expected: 3 PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/local_transcriber/backends/onnx_asr.py tests/test_onnx_asr.py +git commit -m "feat: implement OnnxAsrBackend.ensure_model_available" +``` + +--- + +### Task 4: Implement create_model + +**Files:** +- Modify: `src/local_transcriber/backends/onnx_asr.py` +- Modify: `tests/test_onnx_asr.py` + +- [ ] **Step 1: Write failing tests** + +Append to `tests/test_onnx_asr.py`: + +```python +class TestCreateModel: + def test_calls_load_model_with_correct_args(self, monkeypatch): + """Verify create_model passes correct args to onnx_asr.load_model.""" + calls = [] + + def fake_load_model(model=None, path=None, quantization=None, + cpu_preprocessing=None, **kwargs): + calls.append({ + "model": model, "path": path, "quantization": quantization, + "cpu_preprocessing": cpu_preprocessing, + }) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + + backend = OnnxAsrBackend() + backend.actual_compute_type = "int8" + model = backend.create_model("gigaam-v3-ctc", "onnx", "int8") + + assert len(calls) == 1 + assert calls[0]["quantization"] == "int8" + assert calls[0]["cpu_preprocessing"] is True + assert model is not None + + def test_loads_silero_vad(self, monkeypatch): + """Verify Silero VAD is loaded and attached to model.""" + vad_calls = [] + + def fake_load_vad(model, **kwargs): + vad_calls.append(model) + return "fake_vad" + + def fake_load_model(**kwargs): + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + self._vad = vad + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", fake_load_vad) + + backend = OnnxAsrBackend() + model = backend.create_model("gigaam-v3-ctc", "onnx", "int8") + + assert vad_calls == ["silero"] + + def test_fp16_compute_type(self, monkeypatch): + """Verify fp16 compute_type is passed through.""" + calls = [] + + def fake_load_model(model=None, quantization=None, **kwargs): + calls.append(quantization) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda **kw: None) + + backend = OnnxAsrBackend() + backend.create_model("parakeet-v3", "onnx", "fp16") + + assert calls == ["fp16"] +``` + +- [ ] **Step 2: Run tests — expect FAIL** + +Run: `uv run pytest tests/test_onnx_asr.py::TestCreateModel -v` +Expected: FAIL — `create_model` not defined. + +- [ ] **Step 3: Implement create_model** + +Append to `OnnxAsrBackend` class: + +```python + def create_model( + self, + model_path: str, + device: str, + compute_type: str, + cpu_threads: int = 0, + ) -> Any: + """Creates onnx-asr model with VAD. + + model_path: onnx-asr model identifier (e.g. "gigaam-v3-ctc"). + compute_type: "int8", "fp16", or "float32" — passed as quantization. + cpu_threads: not used by onnx-asr (onnxruntime manages threads internally). + """ + import onnx_asr + + ct = compute_type if compute_type in ("int8", "fp16", "float32") else "int8" + + model = onnx_asr.load_model( + model=model_path, + quantization=ct, + cpu_preprocessing=True, + ) + vad = onnx_asr.load_vad("silero") + self._vad = vad + return model.with_vad(vad) +``` + +- [ ] **Step 4: Run tests — expect PASS** + +Run: `uv run pytest tests/test_onnx_asr.py::TestCreateModel -v` +Expected: 3 PASS. + +Note: These tests mock `onnx_asr.load_model` and `onnx_asr.load_vad`, so no real model download happens. + +- [ ] **Step 5: Commit** + +```bash +git add src/local_transcriber/backends/onnx_asr.py tests/test_onnx_asr.py +git commit -m "feat: implement OnnxAsrBackend.create_model with VAD" +``` + +--- + +### Task 5: Implement transcribe + +**Files:** +- Modify: `src/local_transcriber/backends/onnx_asr.py` +- Modify: `tests/test_onnx_asr.py` + +- [ ] **Step 1: Write failing tests** + +Append to `tests/test_onnx_asr.py`: + +```python +from pathlib import Path +from local_transcriber.types import Segment, TranscribeResult + + +class FakeVadSegment: + """Mimics onnx-asr SegmentResult.""" + def __init__(self, start_ts, end_ts, text): + self.start_ts = start_ts + self.end_ts = end_ts + self.text = text + + +class TestTranscribe: + def test_transcribe_collects_segments(self, monkeypatch, tmp_path): + """Verify transcribe maps VAD segments to project Segments.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + audio_samples = [0.0] * 16000 # 1 second of silence + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array(audio_samples, dtype=np.float32) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + yield FakeVadSegment(0.0, 1.0, "hello") + yield FakeVadSegment(1.0, 2.5, "world") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + backend.actual_compute_type = "int8" + result = backend.transcribe( + FakeModel(), wav_file, language=None, + ) + + assert isinstance(result, TranscribeResult) + assert len(result.segments) == 2 + assert result.segments[0] == Segment(start=0.0, end=1.0, text="hello") + assert result.segments[1] == Segment(start=1.0, end=2.5, text="world") + assert result.duration == 1.0 # 16000 samples / 16000 Hz + + def test_transcribe_calls_on_segment(self, monkeypatch, tmp_path): + """Verify on_segment callback is invoked per segment.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + segments_captured = [] + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + yield FakeVadSegment(0.0, 2.0, "one") + yield FakeVadSegment(2.0, 4.0, "two") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + result = backend.transcribe( + FakeModel(), wav_file, language=None, + on_segment=lambda s: segments_captured.append(s), + ) + + assert len(segments_captured) == 2 + assert segments_captured[0].text == "one" + assert segments_captured[1].text == "two" + + def test_transcribe_passes_language(self, monkeypatch, tmp_path): + """Verify language is passed to recognize().""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + lang_received = [] + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + lang_received.append(language) + yield FakeVadSegment(0.0, 1.0, "text") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + backend.transcribe(FakeModel(), wav_file, language="ru") + + assert lang_received == ["ru"] + + def test_transcribe_empty_audio(self, monkeypatch, tmp_path): + """Verify zero segments for silent audio.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + # No segments yielded + if False: + yield + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + result = backend.transcribe(FakeModel(), wav_file) + + assert len(result.segments) == 0 + assert result.language == "unknown" + assert result.duration == 1.0 +``` + +- [ ] **Step 2: Run tests — expect FAIL** + +Run: `uv run pytest tests/test_onnx_asr.py::TestTranscribe -v` +Expected: FAIL — `transcribe` not defined. + +- [ ] **Step 3: Implement transcribe** + +Append to `OnnxAsrBackend` class: + +```python + def transcribe( + self, + model: Any, + file_path: Path, + language: str | None, + on_segment: Callable[[Segment], None] | None = None, + on_status: Callable[[str], None] | None = None, + ) -> TranscribeResult: + """Transcribes audio file using onnx-asr model with VAD. + + model: result of create_model() — a SegmentResultsAsrAdapter. + file_path: path to audio/video file (any format supported by faster-whisper decode). + language: language code (e.g. "ru", "en") — only meaningful for multilingual models. + """ + from faster_whisper import decode_audio + + _notify(on_status, "Загружаю аудио...") + audio_array = decode_audio(str(file_path), sampling_rate=16000) + duration = len(audio_array) / 16000.0 + + _notify(on_status, "Транскрибирую (onnx-asr)...") + segments: list[Segment] = [] + detected_language = language or "unknown" + + for vad_seg in model.recognize(audio_array, 16000, language=language): + seg = Segment( + start=max(0.0, vad_seg.start_ts), + end=max(0.0, vad_seg.end_ts), + text=vad_seg.text, + ) + if on_segment is not None: + on_segment(seg) + segments.append(seg) + _notify( + on_status, + f"Транскрибирую (onnx-asr)... [{len(segments)} сегм.]", + ) + + return TranscribeResult( + segments=segments, + language=detected_language, + language_probability=1.0 if language else 0.0, + duration=duration, + device_used="", # оркестратор проставит + ) +``` + +Add the helper function at the bottom of the file (before class): + +```python +def _notify(on_status: Callable[[str], None] | None, message: str) -> None: + if on_status is not None: + on_status(message) +``` + +Update imports at the top of `onnx_asr.py` — the full import block should be: + +```python +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from local_transcriber.types import Segment, TranscribeResult +``` + +- [ ] **Step 4: Run tests — verify all pass** + +Run: `uv run pytest tests/test_onnx_asr.py -v` +Expected: ALL tests pass (4 alias + 3 ensure + 3 create + 4 transcribe = 14 PASS). + +- [ ] **Step 5: Commit** + +```bash +git add src/local_transcriber/backends/onnx_asr.py tests/test_onnx_asr.py +git commit -m "feat: implement OnnxAsrBackend.transcribe with VAD segments" +``` + +--- + +### Task 6: Register backend in __init__.py + +**Files:** +- Modify: `src/local_transcriber/backends/__init__.py` +- Modify: `tests/test_onnx_asr.py` + +- [ ] **Step 1: Write failing test for get_backend("onnx")** + +Append to `tests/test_onnx_asr.py`: + +```python +class TestBackendRegistration: + def test_get_backend_returns_onnx_backend(self): + from local_transcriber.backends import get_backend + backend = get_backend("onnx") + assert isinstance(backend, OnnxAsrBackend) +``` + +- [ ] **Step 2: Run test — expect FAIL** + +Run: `uv run pytest tests/test_onnx_asr.py::TestBackendRegistration -v` +Expected: FAIL — `ValueError` or some error from `get_backend("onnx")`. + +- [ ] **Step 3: Register "onnx" device** + +Open `src/local_transcriber/backends/__init__.py` and add the `"onnx"` case before the fallback. The function should look like: + +```python +def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend: + """Возвращает экземпляр бэкенда для указанного устройства. + + Импорты ленивые — бэкенд загружается только при запросе. + compute_type_explicit: False если compute_type пришёл из дефолтов (влияет на fallback). + """ + if device in ("openvino", "openvino-gpu", "openvino-cpu"): + try: + from .openvino import OpenVINOBackend + except ImportError: + raise ValueError( + "OpenVINO бэкенд недоступен. Установите: pip install openvino-genai" + ) from None + return OpenVINOBackend( + ov_device=device, compute_type_explicit=compute_type_explicit + ) + + if device == "onnx": + try: + from .onnx_asr import OnnxAsrBackend + except ImportError: + raise ValueError( + "onnx-asr бэкенд недоступен. Установите: pip install onnx-asr[cpu,hub]" + ) from None + return OnnxAsrBackend() + + # cuda, cpu и всё остальное → faster-whisper + from .faster_whisper import FasterWhisperBackend + + return FasterWhisperBackend() +``` + +- [ ] **Step 4: Run test — expect PASS** + +Run: `uv run pytest tests/test_onnx_asr.py::TestBackendRegistration -v` +Expected: 1 PASS. + +- [ ] **Step 5: Run ALL tests to ensure nothing broken** + +Run: `uv run pytest -v` +Expected: all existing tests + new onnx-asr tests pass. No regressions in faster-whisper or OpenVINO. + +- [ ] **Step 6: Commit** + +```bash +git add src/local_transcriber/backends/__init__.py tests/test_onnx_asr.py +git commit -m "feat: register onnx-asr backend for --device onnx" +``` + +--- + +### Task 7: Manual smoke test with real model + +**Files:** +- No code changes. Manual verification only. + +- [ ] **Step 1: Verify CLI shows onnx device option** + +Run: `uv run transcribe --help` +Expected: shows `--device` option, verify `onnx` is listed. + +- [ ] **Step 2: Smoke test with GigaAM v3 on a short audio file** + +Requires a real audio file (WAV or MP3). Use a small test file if available. + +Run: `uv run transcribe /path/to/test.wav --device onnx --model gigaam-v3 --verbose` + +Expected: +- Model downloads from HuggingFace (first run, ~600 MB) +- Transcription runs +- Output `.md` file created with segments and timestamps +- No errors + +- [ ] **Step 3: Smoke test with Parakeet v3** + +Run: `uv run transcribe /path/to/test.wav --device onnx --model parakeet-v3 --language ru --verbose` + +Expected: +- Model downloads (first run) +- Transcription with Russian language +- Output `.md` created + +- [ ] **Step 4: Verify unknown alias error** + +Run: `uv run transcribe /path/to/test.wav --device onnx --model nonexisent` + +Expected: error message with available aliases (`gigaam-v3`, `parakeet-v3`). + +- [ ] **Step 5: Verify compute-type flag works** + +Run: `uv run transcribe /path/to/test.wav --device onnx --model gigaam-v3 --compute-type fp16` + +Expected: model loads with fp16 quantization (slightly larger download), transcription works. + +--- + +### Task 8: Write comparison script (optional, for experiment) + +**Files:** +- Create: `scripts/compare_backends.py` + +- [ ] **Step 1: Create comparison script** + +Create `scripts/compare_backends.py`: + +```python +"""Compare onnx-asr vs OpenVINO backends on real audio files. + +Usage: python scripts/compare_backends.py /path/to/audio.mp3 +""" +import sys +import time +from pathlib import Path +from local_transcriber.transcriber import load_model, _transcribe_file +from local_transcriber.backends import get_backend +from local_transcriber.types import Segment + + +def transcribe_with_backend(file_path: Path, device: str, model_name: str, + compute_type: str, language: str | None) -> tuple[float, int, str]: + """Run transcription and return (elapsed_sec, segment_count, transcript_text).""" + start = time.monotonic() + model_obj, actual_device, backend, model_path = load_model( + model_name, device, compute_type, + strict_device=True, + ) + tfr = _transcribe_file( + model_obj, actual_device, backend, model_path, + file_path, model_name, compute_type, + language=language, + ) + elapsed = time.monotonic() - start + text = " ".join(s.text for s in tfr.result.segments) + return elapsed, len(tfr.result.segments), text + + +def main(): + if len(sys.argv) < 2: + print("Usage: python scripts/compare_backends.py ") + sys.exit(1) + + file_path = Path(sys.argv[1]) + if not file_path.exists(): + print(f"File not found: {file_path}") + sys.exit(1) + + models_to_test = [ + ("gigaam-v3", "onnx", "int8"), + ("parakeet-v3", "onnx", "int8"), + ("medium", "openvino-cpu", "int8"), + ] + + print(f"File: {file_path.name} ({file_path.stat().st_size / 1e6:.1f} MB)") + print() + + for model_name, device, ct in models_to_test: + print(f"--- {model_name} on {device} (compute={ct}) ---") + try: + elapsed, seg_count, text = transcribe_with_backend( + file_path, device, model_name, ct, language="ru" if "gigaam" in model_name else None, + ) + print(f" Time: {elapsed:.1f}s") + print(f" Segments: {seg_count}") + print(f" Text preview: {text[:200]}...") + print() + except Exception as e: + print(f" ERROR: {e}") + print() + + +if __name__ == "__main__": + main() +``` + +- [ ] **Step 2: Run the comparison** + +Run: `uv run python scripts/compare_backends.py /path/to/real/audio.mp3` +Expected: timing and transcript preview for each model. + +- [ ] **Step 3: Commit** + +```bash +git add scripts/compare_backends.py +git commit -m "test: add onnx-asr vs OpenVINO comparison script" +``` + +--- + +## Self-Review + +1. **Spec coverage:** Each spec requirement maps to a task: + - Model aliases (gigaam-v3, parakeet-v3) → Task 2 + - ensure_model_available → Task 3 + - create_model with VAD → Task 4 + - transcribe with decode_audio → Task 5 + - Registration via get_backend("onnx") → Task 6 + - Dependencies → Task 1 + - Comparison → Task 8 + - Error handling (ValueError for bad alias) → Task 2 tests + +2. **Placeholder scan:** No TBD, TODO, or vague descriptions. All code is concrete. + +3. **Type consistency:** + - `_resolved_model_id` set in Task 3, used in Task 4 (consistent) + - `actual_compute_type` set in Task 3, checked in Task 4 tests + - `Segment` imported from types.py — matches project type + - `TranscribeResult` fields match types.py definition diff --git a/docs/superpowers/specs/2026-04-25-parakeet-backend-design.md b/docs/superpowers/specs/2026-04-25-parakeet-backend-design.md new file mode 100644 index 0000000..4e14f49 --- /dev/null +++ b/docs/superpowers/specs/2026-04-25-parakeet-backend-design.md @@ -0,0 +1,160 @@ +# Design: onnx-asr Backend (Parakeet / GigaAM) + +**Status**: Draft +**Date**: 2026-04-25 +**Branch**: experiment/onnx-asr-backend + +## Motivation + +Current CPU backends (faster-whisper, OpenVINO on CPU) run at 0.5-1.5x realtime for large models. Target audience has Intel integrated GPU or CPU only — no discrete NVIDIA GPU. + +[onnx-asr](https://github.com/istupakov/onnx-asr) is a lightweight ONNX Runtime wrapper supporting Parakeet, GigaAM, and FastConformer models. Key advantages: + +- **30-90x realtime on CPU** (vs 0.5-1.5x for faster-whisper) +- **Lightweight**: `numpy` + `onnxruntime` + `huggingface-hub` (~no added weight) +- **Python >= 3.10** — compatible with project +- **Russian-optimized models** with WER 4-5% (vs 10%+ for Whisper) + +## Goals + +1. Add `onnx-asr` as a third pluggable backend (alongside FasterWhisper and OpenVINO) +2. Compare speed and quality against OpenVINO on real Russian audio +3. Experiment is on a separate branch — merge only if results are compelling + +## Models + +Two model aliases exposed via CLI `--model`: + +| Alias | onnx-asr name | Language | WER (ru) | RTFx CPU | +|-------|--------------|----------|----------|----------| +| `gigaam-v3` | `gigaam-v3-ctc` | ru only | 4.72% | 59x | +| `parakeet-v3` | `nemo-parakeet-tdt-0.6b-v3` | 25 lang (auto-detect) | 10.95% | 34x | + +`gigaam-v3` is the default for `--device onnx` (best Russian quality + speed). +`parakeet-v3` is the multilingual fallback. + +User can also pass any valid onnx-asr model name directly (e.g. `nemo-canary-1b-v2`). + +## Architecture + +### New file + +`src/local_transcriber/backends/onnx_asr.py` — mirrors `openvino.py` structure. + +Implements the [Backend protocol](../adr/003-pluggable-backends.md) (structural typing): + +``` +class OnnxAsrBackend: + def ensure_model_available(model_name, compute_type, on_status) -> str + def create_model(model_path, device, compute_type, cpu_threads) -> Any + def transcribe(model, file_path, language, on_segment, on_status) -> TranscribeResult +``` + +### Model resolution (`ensure_model_available`) + +1. Resolve alias → onnx-asr model name via `MODEL_ALIASES` dict +2. Allow raw onnx-asr names (e.g. `nemo-parakeet-tdt-0.6b-v3`) to pass through +3. Return resolved model identifier string (onnx-asr handles download internally via `load_model`) + +### Model creation (`create_model`) + +```python +import onnx_asr +model = onnx_asr.load_model( + model_id, quantization=compute_type, cpu_preprocessing=True, +) +vad = onnx_asr.load_vad("silero") +model = model.with_vad(vad) +``` + +- `compute_type`: `int8` (default, quantized, fast), `fp16`, `float32` +- `cpu_preprocessing=True`: keeps mel-spectrogram computation on CPU (faster for CPU-only inference) +- VAD (Silero): always enabled — splits audio by voice activity, handles any length +- VAD segments naturally carry `start_ts`/`end_ts` — no separate `.with_timestamps()` needed + +### Transcription (`transcribe`) + +1. `faster_whisper.decode_audio(file, 16000)` → numpy float32 array (reuses same audio loader as OpenVINO backend, supports all media formats) +2. `model.recognize(audio_array, 16000, language=lang)` → iterator of VAD segments with `start_ts`, `end_ts`, `text` +3. Map each segment → project's `Segment(start, end, text)` dataclass +4. Return `TranscribeResult` with segments, language, duration + +Language handling: +- `gigaam-v3`: Russian only, `language` parameter ignored +- `parakeet-v3`: auto-detect (when `--language auto` / `None`) or explicit `--language ru/en/...` + +### Quantization support + +onnx-asr supports quantized ONNX models via `quantization` parameter: + +| compute_type | Description | RAM | Quality impact | +|-------------|-------------|-----|---------------| +| `int8` | 8-bit quantized (default) | ~300 MB | Minimal | +| `fp16` | Half precision | ~600 MB | None | +| `float32` | Full precision | ~1.2 GB | None | + +### Error handling + +- `RuntimeError` (OOM, ONNX session failure) → warning + fallback to CPU via `CPUExecutionProvider` +- Invalid model name → `ValueError` with list of supported aliases +- Corrupt audio / unsupported format → propagated from `faster_whisper.decode_audio` + +### Registration + +In `backends/__init__.py`: + +```python +if device == "onnx": + from .onnx_asr import OnnxAsrBackend + return OnnxAsrBackend() +``` + +Device `"onnx"` is NOT in auto-detect chain. Only explicit `--device onnx`. +Rationale: experimental backend, don't surprise existing users. + +### Dependencies + +Add to `pyproject.toml`: + +```toml +dependencies = [ + # ... existing ... + "onnx-asr[cpu,hub]>=0.11.0", +] +``` + +`onnxruntime` pulled transitively by `onnx-asr`. + +### CLI integration + +No CLI changes needed — existing `--device`, `--model`, `--compute-type`, `--language` flags work: + +```bash +# Russian, best quality/speed +uv run transcribe meeting.mp4 --device onnx --model gigaam-v3 + +# Multilingual +uv run transcribe podcast.mp3 --device onnx --model parakeet-v3 --language auto + +# With int8 quantization +uv run transcribe lecture.mp4 --device onnx --compute-type int8 +``` + +## Comparison approach + +Experiment compares onnx-asr against OpenVINO backend on real audio files: + +1. Pick 2-3 Russian audio files of varying length (1 min, 5 min, 30 min) +2. Run both backends, measure: elapsed time, segment count, dump transcripts +3. Qualitative: can a readable summary/conspect be made from the transcript? +4. Decision criteria: + - Noticeably faster at comparable quality → keep + - Noticeably better quality at comparable speed → keep + - Neither → discard + +## Non-goals + +- No auto-detect (device `"onnx"` must be explicit) +- No OpenVINO execution provider for onnx-asr (onnx-asr can use it, but out of scope) +- No German/French/etc language optimization — only Russian is benchmarked +- No replacing existing backends From b5c1da24ce3c92ba0141dd677d0336629238a212 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:17:24 +0300 Subject: [PATCH 02/16] =?UTF-8?q?build(deps):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20onnx-asr[cpu,hub]>=3D0.1?= =?UTF-8?q?1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - onnx-asr бэкенд для транскрипции (Parakeet/GigaAM) даёт 30-90x realtime на CPU. - Что: - добавлен пакет onnx-asr[cpu,hub]>=0.11.0 в зависимости проекта. - uv.lock обновлён. - Проверка: - uv run python -c "import onnx_asr; print(onnx_asr.__version__)" --- pyproject.toml | 1 + uv.lock | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9108485..b35f94a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'", "openvino-genai>=2025.0; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", "tomli>=2.0; python_version < '3.11'", + "onnx-asr[cpu,hub]>=0.11.0", ] [project.scripts] diff --git a/uv.lock b/uv.lock index f65985b..abbff45 100644 --- a/uv.lock +++ b/uv.lock @@ -301,6 +301,7 @@ source = { editable = "." } dependencies = [ { name = "faster-whisper" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "onnx-asr", extra = ["cpu", "hub"] }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, { name = "rich" }, { name = "socksio" }, @@ -317,6 +318,7 @@ dev = [ requires-dist = [ { name = "faster-whisper", specifier = ">=1.2.1" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4" }, + { name = "onnx-asr", extras = ["cpu", "hub"], specifier = ">=0.11.0" }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2025.0" }, { name = "rich" }, { name = "socksio", specifier = ">=1.0.0" }, @@ -512,6 +514,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:453611eb21a7c1f2c2156ed9f3a45b691deda0440ec550860290dc901af5b4c2", size = 581242350, upload-time = "2025-06-05T20:04:51.979Z" }, ] +[[package]] +name = "onnx-asr" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/f6/b154881761a593312f509522f99542acffa2516f7a1df6ddf5660ad4a162/onnx_asr-0.11.0.tar.gz", hash = "sha256:57ad8d9571dc17db95f0daf9ba432b9472383de320c610735850e56b5375a37d", size = 43665, upload-time = "2026-03-23T02:30:57.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/04/bdffd682cc38b43144b6528186c80451f219a05e3fd0eb331a548f455b9a/onnx_asr-0.11.0-py3-none-any.whl", hash = "sha256:142d8b3ce7716684992826a269304f5ce9cf1c0fe704b751358e223f45d2a5cf", size = 138349, upload-time = "2026-03-23T02:30:58.566Z" }, +] + +[package.optional-dependencies] +cpu = [ + { name = "onnxruntime" }, +] +hub = [ + { name = "huggingface-hub" }, +] + [[package]] name = "onnxruntime" version = "1.24.3" From a3d0213cb3c6731876cb039a85859ea147ca9f5c Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:19:33 +0300 Subject: [PATCH 03/16] =?UTF-8?q?feat(onnx-asr):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=81=D0=BA=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4=D0=B0=20=D1=81=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=B0=D0=BB=D0=B8=D0=B0=D1=81=D0=BE=D0=B2=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - onnx-asr бэкенд должен распознавать короткие алиасы (gigaam-v3, parakeet-v3) и raw-имена (nemo-canary-1b-v2). - Что: - создан файл backends/onnx_asr.py с MODEL_ALIASES и классом OnnxAsrBackend. - метод _resolve_model: преобразует алиасы, пропускает raw-имена, выдаёт ValueError для неизвестных. - написаны 4 теста на разрешение алиасов. - Проверка: - uv run pytest tests/test_onnx_asr.py::TestModelAliases -v --- src/local_transcriber/backends/onnx_asr.py | 27 ++++++++++++++++++++++ tests/test_onnx_asr.py | 26 +++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 src/local_transcriber/backends/onnx_asr.py create mode 100644 tests/test_onnx_asr.py diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py new file mode 100644 index 0000000..190c6f2 --- /dev/null +++ b/src/local_transcriber/backends/onnx_asr.py @@ -0,0 +1,27 @@ +"""Бэкенд транскрипции на основе onnx-asr (GigaAM, Parakeet, FastConformer).""" + +from __future__ import annotations + +MODEL_ALIASES: dict[str, str] = { + "gigaam-v3": "gigaam-v3-ctc", + "parakeet-v3": "nemo-parakeet-tdt-0.6b-v3", +} + +SUPPORTED_ALIASES = ", ".join(MODEL_ALIASES) + + +class OnnxAsrBackend: + """Бэкенд транскрипции через onnx-asr (ONNX Runtime).""" + + def _resolve_model(self, model_name: str) -> str: + """Resolve alias to onnx-asr model name. Raw names pass through.""" + if model_name in MODEL_ALIASES: + return MODEL_ALIASES[model_name] + if "/" in model_name or model_name.count("-") >= 2: + # Looks like a raw onnx-asr name — allow passthrough + return model_name + raise ValueError( + f"Неподдерживаемая модель '{model_name}'. " + f"Доступные алиасы: {SUPPORTED_ALIASES}. " + f"Либо укажите полное имя модели onnx-asr." + ) diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py new file mode 100644 index 0000000..4cceb3a --- /dev/null +++ b/tests/test_onnx_asr.py @@ -0,0 +1,26 @@ +"""Tests for onnx-asr backend.""" + +import pytest +from local_transcriber.backends.onnx_asr import OnnxAsrBackend, MODEL_ALIASES + + +class TestModelAliases: + def test_gigaam_v3_resolves(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("gigaam-v3") + assert result == "gigaam-v3-ctc" + + def test_parakeet_v3_resolves(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("parakeet-v3") + assert result == "nemo-parakeet-tdt-0.6b-v3" + + def test_raw_name_passes_through(self): + backend = OnnxAsrBackend() + result = backend._resolve_model("nemo-canary-1b-v2") + assert result == "nemo-canary-1b-v2" + + def test_unknown_alias_raises(self): + backend = OnnxAsrBackend() + with pytest.raises(ValueError, match="Неподдерживаемая модель"): + backend._resolve_model("nonexistent-model") From f25a546754828a11dc283553ebe35042038bbd1f Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:20:35 +0300 Subject: [PATCH 04/16] =?UTF-8?q?feat(onnx-asr):=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20ensure=5Fmod?= =?UTF-8?q?el=5Favailable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Бэкенд должен резолвить алиасы моделей и сохранять compute_type для дальнейшего использования. - Что: - добавлен конструктор __init__ с полями actual_compute_type, _resolved_model_id, _vad. - метод ensure_model_available валидирует алиас и возвращает идентификатор модели onnx-asr. - написаны 3 теста на резолвинг и сохранение compute_type. - Проверка: - uv run pytest tests/test_onnx_asr.py -v --- src/local_transcriber/backends/onnx_asr.py | 23 ++++++++++++++++++++++ tests/test_onnx_asr.py | 18 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 190c6f2..770c412 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -2,6 +2,9 @@ from __future__ import annotations +from collections.abc import Callable +from typing import Any + MODEL_ALIASES: dict[str, str] = { "gigaam-v3": "gigaam-v3-ctc", "parakeet-v3": "nemo-parakeet-tdt-0.6b-v3", @@ -13,6 +16,26 @@ SUPPORTED_ALIASES = ", ".join(MODEL_ALIASES) class OnnxAsrBackend: """Бэкенд транскрипции через onnx-asr (ONNX Runtime).""" + def __init__(self): + self.actual_compute_type: str | None = None + self._resolved_model_id: str | None = None + self._vad: Any = None + + def ensure_model_available( + self, + model_name: str, + compute_type: str, + on_status: Callable[[str], None] | None = None, + ) -> str: + """Resolves model alias and returns the onnx-asr model identifier. + + onnx-asr downloads models automatically via load_model(), + so this just validates the alias and returns the identifier string. + """ + self.actual_compute_type = compute_type + self._resolved_model_id = self._resolve_model(model_name) + return self._resolved_model_id + def _resolve_model(self, model_name: str) -> str: """Resolve alias to onnx-asr model name. Raw names pass through.""" if model_name in MODEL_ALIASES: diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index 4cceb3a..a1f842d 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -4,6 +4,24 @@ import pytest from local_transcriber.backends.onnx_asr import OnnxAsrBackend, MODEL_ALIASES +class TestEnsureModelAvailable: + def test_returns_model_id_for_gigaam(self): + backend = OnnxAsrBackend() + result = backend.ensure_model_available("gigaam-v3", "int8") + assert result == "gigaam-v3-ctc" + + def test_returns_model_id_for_parakeet(self): + backend = OnnxAsrBackend() + result = backend.ensure_model_available("parakeet-v3", "fp16") + assert result == "nemo-parakeet-tdt-0.6b-v3" + + def test_stores_compute_type(self): + backend = OnnxAsrBackend() + backend.ensure_model_available("gigaam-v3", "float32") + assert backend._resolved_model_id == "gigaam-v3-ctc" + assert backend.actual_compute_type == "float32" + + class TestModelAliases: def test_gigaam_v3_resolves(self): backend = OnnxAsrBackend() From d9c9aefdb385cb7db8730e64be3c8e3b95d560d7 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:22:02 +0300 Subject: [PATCH 05/16] =?UTF-8?q?feat(onnx-asr):=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20create=5Fmod?= =?UTF-8?q?el=20=D1=81=20Silero=20VAD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - onnx-asr модель должна создаваться с квантизацией и VAD для разбивки аудио на сегменты. - Что: - метод create_model вызывает onnx_asr.load_model с квантизацией и cpu_preprocessing=True. - подгружается Silero VAD, прикрепляется к модели через with_vad. - написаны 3 теста: проверка аргументов load_model, загрузка VAD, передача fp16. - Проверка: - uv run pytest tests/test_onnx_asr.py -v --- src/local_transcriber/backends/onnx_asr.py | 26 ++++++++ tests/test_onnx_asr.py | 73 ++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 770c412..7411767 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -36,6 +36,32 @@ class OnnxAsrBackend: self._resolved_model_id = self._resolve_model(model_name) return self._resolved_model_id + def create_model( + self, + model_path: str, + device: str, + compute_type: str, + cpu_threads: int = 0, + ) -> Any: + """Creates onnx-asr model with VAD. + + model_path: onnx-asr model identifier (e.g. "gigaam-v3-ctc"). + compute_type: "int8", "fp16", or "float32" — passed as quantization. + cpu_threads: not used by onnx-asr (onnxruntime manages threads internally). + """ + import onnx_asr + + ct = compute_type if compute_type in ("int8", "fp16", "float32") else "int8" + + model = onnx_asr.load_model( + model=model_path, + quantization=ct, + cpu_preprocessing=True, + ) + vad = onnx_asr.load_vad("silero") + self._vad = vad + return model.with_vad(vad) + def _resolve_model(self, model_name: str) -> str: """Resolve alias to onnx-asr model name. Raw names pass through.""" if model_name in MODEL_ALIASES: diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index a1f842d..7a6a81b 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -22,6 +22,79 @@ class TestEnsureModelAvailable: assert backend.actual_compute_type == "float32" +class TestCreateModel: + def test_calls_load_model_with_correct_args(self, monkeypatch): + """Verify create_model passes correct args to onnx_asr.load_model.""" + calls = [] + + def fake_load_model(model=None, path=None, quantization=None, + cpu_preprocessing=None, **kwargs): + calls.append({ + "model": model, "path": path, "quantization": quantization, + "cpu_preprocessing": cpu_preprocessing, + }) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + + backend = OnnxAsrBackend() + backend.actual_compute_type = "int8" + model = backend.create_model("gigaam-v3-ctc", "onnx", "int8") + + assert len(calls) == 1 + assert calls[0]["quantization"] == "int8" + assert calls[0]["cpu_preprocessing"] is True + assert model is not None + + def test_loads_silero_vad(self, monkeypatch): + """Verify Silero VAD is loaded and attached to model.""" + vad_calls = [] + + def fake_load_vad(model, **kwargs): + vad_calls.append(model) + return "fake_vad" + + def fake_load_model(**kwargs): + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + self._vad = vad + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", fake_load_vad) + + backend = OnnxAsrBackend() + model = backend.create_model("gigaam-v3-ctc", "onnx", "int8") + + assert vad_calls == ["silero"] + + def test_fp16_compute_type(self, monkeypatch): + """Verify fp16 compute_type is passed through.""" + calls = [] + + def fake_load_model(model=None, quantization=None, **kwargs): + calls.append(quantization) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None) + + backend = OnnxAsrBackend() + backend.create_model("parakeet-v3", "onnx", "fp16") + + assert calls == ["fp16"] + + class TestModelAliases: def test_gigaam_v3_resolves(self): backend = OnnxAsrBackend() From 9e04dc8c257408ecf97dfb9a6d9b78a8451b335a Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:24:31 +0300 Subject: [PATCH 06/16] =?UTF-8?q?feat(onnx-asr):=20=D1=80=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20transcribe?= =?UTF-8?q?=20=D1=81=20=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B5=D0=B9=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20VAD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Основной метод бэкенда — транскрипция аудиофайла в сегменты с временными метками. - Что: - метод transcribe декодирует аудио через faster_whisper.decode_audio, затем вызывает model.recognize() с VAD-сегментацией. - каждый VAD-сегмент преобразуется в проектную структуру Segment. - поддержка колбэков on_segment, on_status. - написаны 4 теста: сбор сегментов, вызов on_segment, передача языка, обработка пустого аудио. - Проверка: - uv run pytest tests/test_onnx_asr.py -v (14 passed) --- src/local_transcriber/backends/onnx_asr.py | 54 ++++++++++ tests/test_onnx_asr.py | 119 +++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 7411767..43f186c 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -3,8 +3,11 @@ from __future__ import annotations from collections.abc import Callable +from pathlib import Path from typing import Any +from local_transcriber.types import Segment, TranscribeResult + MODEL_ALIASES: dict[str, str] = { "gigaam-v3": "gigaam-v3-ctc", "parakeet-v3": "nemo-parakeet-tdt-0.6b-v3", @@ -62,6 +65,52 @@ class OnnxAsrBackend: self._vad = vad return model.with_vad(vad) + def transcribe( + self, + model: Any, + file_path: Path, + language: str | None, + on_segment: Callable[[Segment], None] | None = None, + on_status: Callable[[str], None] | None = None, + ) -> TranscribeResult: + """Transcribes audio file using onnx-asr model with VAD. + + model: result of create_model() — a SegmentResultsAsrAdapter. + file_path: path to audio/video file (any format supported by faster-whisper decode). + language: language code (e.g. "ru", "en") — only meaningful for multilingual models. + """ + from faster_whisper import decode_audio + + _notify(on_status, "Загружаю аудио...") + audio_array = decode_audio(str(file_path), sampling_rate=16000) + duration = len(audio_array) / 16000.0 + + _notify(on_status, "Транскрибирую (onnx-asr)...") + segments: list[Segment] = [] + detected_language = language or "unknown" + + for vad_seg in model.recognize(audio_array, 16000, language=language): + seg = Segment( + start=max(0.0, vad_seg.start_ts), + end=max(0.0, vad_seg.end_ts), + text=vad_seg.text, + ) + if on_segment is not None: + on_segment(seg) + segments.append(seg) + _notify( + on_status, + f"Транскрибирую (onnx-asr)... [{len(segments)} сегм.]", + ) + + return TranscribeResult( + segments=segments, + language=detected_language, + language_probability=1.0 if language else 0.0, + duration=duration, + device_used="", # оркестратор проставит + ) + def _resolve_model(self, model_name: str) -> str: """Resolve alias to onnx-asr model name. Raw names pass through.""" if model_name in MODEL_ALIASES: @@ -74,3 +123,8 @@ class OnnxAsrBackend: f"Доступные алиасы: {SUPPORTED_ALIASES}. " f"Либо укажите полное имя модели onnx-asr." ) + + +def _notify(on_status: Callable[[str], None] | None, message: str) -> None: + if on_status is not None: + on_status(message) diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index 7a6a81b..f495519 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -1,7 +1,19 @@ """Tests for onnx-asr backend.""" import pytest +from pathlib import Path + from local_transcriber.backends.onnx_asr import OnnxAsrBackend, MODEL_ALIASES +from local_transcriber.types import Segment, TranscribeResult + + +class FakeVadSegment: + """Mimics onnx-asr SegmentResult.""" + + def __init__(self, start_ts, end_ts, text): + self.start_ts = start_ts + self.end_ts = end_ts + self.text = text class TestEnsureModelAvailable: @@ -95,6 +107,113 @@ class TestCreateModel: assert calls == ["fp16"] +class TestTranscribe: + def test_transcribe_collects_segments(self, monkeypatch, tmp_path): + """Verify transcribe maps VAD segments to project Segments.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + audio_samples = [0.0] * 16000 # 1 second of silence + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array(audio_samples, dtype=np.float32) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + yield FakeVadSegment(0.0, 1.0, "hello") + yield FakeVadSegment(1.0, 2.5, "world") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + backend.actual_compute_type = "int8" + result = backend.transcribe( + FakeModel(), wav_file, language=None, + ) + + assert isinstance(result, TranscribeResult) + assert len(result.segments) == 2 + assert result.segments[0] == Segment(start=0.0, end=1.0, text="hello") + assert result.segments[1] == Segment(start=1.0, end=2.5, text="world") + assert result.duration == 1.0 # 16000 samples / 16000 Hz + + def test_transcribe_calls_on_segment(self, monkeypatch, tmp_path): + """Verify on_segment callback is invoked per segment.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + segments_captured = [] + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + yield FakeVadSegment(0.0, 2.0, "one") + yield FakeVadSegment(2.0, 4.0, "two") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + result = backend.transcribe( + FakeModel(), wav_file, language=None, + on_segment=lambda s: segments_captured.append(s), + ) + + assert len(segments_captured) == 2 + assert segments_captured[0].text == "one" + assert segments_captured[1].text == "two" + + def test_transcribe_passes_language(self, monkeypatch, tmp_path): + """Verify language is passed to recognize().""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + lang_received = [] + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + lang_received.append(language) + yield FakeVadSegment(0.0, 1.0, "text") + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + backend.transcribe(FakeModel(), wav_file, language="ru") + + assert lang_received == ["ru"] + + def test_transcribe_empty_audio(self, monkeypatch, tmp_path): + """Verify zero segments for silent audio.""" + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + def fake_decode_audio(path, sampling_rate=16000): + import numpy as np + return np.array([0.0] * 16000, dtype=np.float32) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + # No segments yielded + if False: + yield + + monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio) + + backend = OnnxAsrBackend() + result = backend.transcribe(FakeModel(), wav_file, language=None) + + assert len(result.segments) == 0 + assert result.language == "unknown" + assert result.duration == 1.0 + + class TestModelAliases: def test_gigaam_v3_resolves(self): backend = OnnxAsrBackend() From 79dbd170ce754d18b1bbe00ab4a87870f277f65f Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:25:28 +0300 Subject: [PATCH 07/16] =?UTF-8?q?feat(onnx-asr):=20=D0=B7=D0=B0=D1=80?= =?UTF-8?q?=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20--device=20onnx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Бэкенд должен быть доступен через CLI по --device onnx. - Что: - в backends/__init__.py добавлена ветка device == "onnx" с ленивым импортом OnnxAsrBackend. - при отсутствии onnx-asr выдаётся понятная ошибка. - добавлен тест TestBackendRegistration. - Проверка: - uv run pytest -v (172 passed) --- src/local_transcriber/backends/__init__.py | 9 +++++++++ tests/test_onnx_asr.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/local_transcriber/backends/__init__.py b/src/local_transcriber/backends/__init__.py index 7a11cdb..99fcfa3 100644 --- a/src/local_transcriber/backends/__init__.py +++ b/src/local_transcriber/backends/__init__.py @@ -25,6 +25,15 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend: ov_device=device, compute_type_explicit=compute_type_explicit ) + if device == "onnx": + try: + from .onnx_asr import OnnxAsrBackend + except ImportError: + raise ValueError( + "onnx-asr бэкенд недоступен. Установите: pip install onnx-asr[cpu,hub]" + ) from None + return OnnxAsrBackend() + # cuda, cpu и всё остальное → faster-whisper from .faster_whisper import FasterWhisperBackend diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index f495519..31e31f3 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -214,6 +214,13 @@ class TestTranscribe: assert result.duration == 1.0 +class TestBackendRegistration: + def test_get_backend_returns_onnx_backend(self): + from local_transcriber.backends import get_backend + backend = get_backend("onnx") + assert isinstance(backend, OnnxAsrBackend) + + class TestModelAliases: def test_gigaam_v3_resolves(self): backend = OnnxAsrBackend() From 7d39ab908b291f0fdf18f58d24d627176611b7f1 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:29:08 +0300 Subject: [PATCH 08/16] =?UTF-8?q?test:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20?= =?UTF-8?q?=D1=81=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20onnx-a?= =?UTF-8?q?sr=20=D0=B8=20OpenVINO=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - Экспериментальное сравнение скорости и качества транскрипции на реальных аудиофайлах. - Что: - скрипт compare_backends.py запускает транскрипцию одного файла через gigaam-v3, parakeet-v3 и OpenVINO medium. - выводит время, количество сегментов и превью текста для каждого. - Проверка: - python scripts/compare_backends.py /path/to/audio.mp3 --- scripts/compare_backends.py | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 scripts/compare_backends.py diff --git a/scripts/compare_backends.py b/scripts/compare_backends.py new file mode 100644 index 0000000..d8ee696 --- /dev/null +++ b/scripts/compare_backends.py @@ -0,0 +1,66 @@ +"""Compare onnx-asr vs OpenVINO backends on real audio files. + +Usage: python scripts/compare_backends.py /path/to/audio.mp3 +""" +import sys +import time +from pathlib import Path +from local_transcriber.transcriber import load_model, _transcribe_file +from local_transcriber.backends import get_backend +from local_transcriber.types import Segment + + +def transcribe_with_backend(file_path: Path, device: str, model_name: str, + compute_type: str, language: str | None) -> tuple[float, int, str]: + """Run transcription and return (elapsed_sec, segment_count, transcript_text).""" + start = time.monotonic() + model_obj, actual_device, backend, model_path = load_model( + model_name, device, compute_type, + strict_device=True, + ) + tfr = _transcribe_file( + model_obj, actual_device, backend, model_path, + file_path, model_name, compute_type, + language=language, + ) + elapsed = time.monotonic() - start + text = " ".join(s.text for s in tfr.result.segments) + return elapsed, len(tfr.result.segments), text + + +def main(): + if len(sys.argv) < 2: + print("Usage: python scripts/compare_backends.py ") + sys.exit(1) + + file_path = Path(sys.argv[1]) + if not file_path.exists(): + print(f"File not found: {file_path}") + sys.exit(1) + + models_to_test = [ + ("gigaam-v3", "onnx", "int8"), + ("parakeet-v3", "onnx", "int8"), + ("medium", "openvino-cpu", "int8"), + ] + + print(f"File: {file_path.name} ({file_path.stat().st_size / 1e6:.1f} MB)") + print() + + for model_name, device, ct in models_to_test: + print(f"--- {model_name} on {device} (compute={ct}) ---") + try: + elapsed, seg_count, text = transcribe_with_backend( + file_path, device, model_name, ct, language="ru" if "gigaam" in model_name else None, + ) + print(f" Time: {elapsed:.1f}s") + print(f" Segments: {seg_count}") + print(f" Text preview: {text[:200]}...") + print() + except Exception as e: + print(f" ERROR: {e}") + print() + + +if __name__ == "__main__": + main() From b1cbdc3e0ba56fd88e911321728e2757e44442d5 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 21:48:54 +0300 Subject: [PATCH 09/16] =?UTF-8?q?fix(onnx-asr):=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B1=D0=B0=D0=B3=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20smoke-=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - при smoke-тесте выявлены расхождения между onnx-asr API и мок-тестами. - Что: - убран deprecated cpu_preprocessing из load_model. - sample_rate передан как keyword-аргумент в recognize(). - исправлены имена полей SegmentResult: start_ts→start, end_ts→end. - добавлен device "onnx" в DEVICE_DEFAULTS и _VALID_DEVICES. - Проверка: - uv run pytest -q (172 passed). - uv run transcribe file.mp4 --device onnx --model gigaam-v3. --- src/local_transcriber/backends/onnx_asr.py | 7 +++---- src/local_transcriber/config.py | 3 ++- tests/test_onnx_asr.py | 10 ++++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 43f186c..664cba0 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -59,7 +59,6 @@ class OnnxAsrBackend: model = onnx_asr.load_model( model=model_path, quantization=ct, - cpu_preprocessing=True, ) vad = onnx_asr.load_vad("silero") self._vad = vad @@ -89,10 +88,10 @@ class OnnxAsrBackend: segments: list[Segment] = [] detected_language = language or "unknown" - for vad_seg in model.recognize(audio_array, 16000, language=language): + for vad_seg in model.recognize(audio_array, sample_rate=16000, language=language): seg = Segment( - start=max(0.0, vad_seg.start_ts), - end=max(0.0, vad_seg.end_ts), + start=max(0.0, vad_seg.start), + end=max(0.0, vad_seg.end), text=vad_seg.text, ) if on_segment is not None: diff --git a/src/local_transcriber/config.py b/src/local_transcriber/config.py index 5d7ab3f..8b85234 100644 --- a/src/local_transcriber/config.py +++ b/src/local_transcriber/config.py @@ -22,11 +22,12 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = { "openvino": {"model": "medium", "compute_type": "int8"}, "openvino-gpu": {"model": "medium", "compute_type": "int8"}, "openvino-cpu": {"model": "medium", "compute_type": "int8"}, + "onnx": {"model": "gigaam-v3", "compute_type": "int8"}, } # Одно место правды для допустимых ключей конфига _VALID_KEYS = set(HARDCODED_DEFAULTS) -_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu"} +_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu", "onnx"} def find_config_file() -> Path | None: diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index 31e31f3..207e60f 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -10,9 +10,9 @@ from local_transcriber.types import Segment, TranscribeResult class FakeVadSegment: """Mimics onnx-asr SegmentResult.""" - def __init__(self, start_ts, end_ts, text): - self.start_ts = start_ts - self.end_ts = end_ts + def __init__(self, start, end, text): + self.start = start + self.end = end self.text = text @@ -40,10 +40,9 @@ class TestCreateModel: calls = [] def fake_load_model(model=None, path=None, quantization=None, - cpu_preprocessing=None, **kwargs): + **kwargs): calls.append({ "model": model, "path": path, "quantization": quantization, - "cpu_preprocessing": cpu_preprocessing, }) return FakeAsrAdapter() @@ -59,7 +58,6 @@ class TestCreateModel: assert len(calls) == 1 assert calls[0]["quantization"] == "int8" - assert calls[0]["cpu_preprocessing"] is True assert model is not None def test_loads_silero_vad(self, monkeypatch): From 53142fd341f20f1664e9fa6f1e98a920feaffd32 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 22:31:51 +0300 Subject: [PATCH 10/16] =?UTF-8?q?docs(adr):=20ADR-005=20=E2=80=94=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D1=8B=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=20=D1=81=20onnx-asr=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - задокументировать результаты сравнения gigaam-v3 vs OpenVINO на CPU. - Что: - таблица скорости по 3 файлам (3-5x ускорение). - сравнение качества русской речи с GPU faster-whisper. - решение: оставить бэкенд как экспериментальный (--device onnx). - Проверка: - просмотр docs/adr/005-onnx-asr-backend.md. --- docs/adr/005-onnx-asr-backend.md | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/adr/005-onnx-asr-backend.md diff --git a/docs/adr/005-onnx-asr-backend.md b/docs/adr/005-onnx-asr-backend.md new file mode 100644 index 0000000..e036051 --- /dev/null +++ b/docs/adr/005-onnx-asr-backend.md @@ -0,0 +1,61 @@ +# ADR-005: onnx-asr бэкенд (Parakeet / GigaAM) — эксперимент и решение + +**Статус**: Принято +**Дата**: 2026-04-25 + +## Контекст + +Целевая аудитория `local-transcriber` — пользователи с Intel iGPU / CPU, без дискретного NVIDIA GPU. +Существующие CPU-бэкенды (faster-whisper, OpenVINO) дают 0.5-6x RTF для средних моделей — транскрипция часовой записи занимает 10-120 минут. + +[onnx-asr](https://github.com/istupakov/onnx-asr) — легковесная обёртка (onnxruntime + numpy) над ONNX-моделями Parakeet, GigaAM, FastConformer и Canary. Заявляет 30-90x RTF на CPU при сравнимом с Whisper качестве для русского языка. + +## Эксперимент + +Проведено сравнение на трёх реальных русскоязычных записях встреч (22-81 мин, mp4), ноутбук с Intel i7-11800H (CPU-only). + +Сравнивались: **onnx-asr GigaAM v3 CTC** (int8, CPU) против **OpenVINO Whisper medium** (int8, CPU). Эталон — faster-whisper large-v3 на RTX 3060 (GPU). + +### Результаты + +| Файл | Длит. | gigaam-v3 (CPU) | OpenVINO medium (CPU) | Ускорение | +|------|-------|-----------------|----------------------|-----------| +| 10-59-59 | 22:26 | 81с (16.6×) | 265с (5.1×) | **3.3×** | +| Vasya | 45:51 | 95с (29×) | 455с (6×) | **4.8×** | +| 12-02-37 | 1:20:44 | 170с (28.5×) | 779с (6.2×) | **4.6×** | + +### Качество (русская речь, файл 12-02-37) + +| Модель | Пунктуация | Читаемость | Контекст | +|--------|-----------|-----------|----------| +| GPU f-whisper large-v3 | ✅ | Отлично | ✅ | +| **GigaAM v3 CTC (CPU)** | ❌ | Хорошо | ✅ | +| OpenVINO medium (CPU) | ✅ | Хорошо | ✅ | + +GigaAM v3 не расставляет знаки препинания, но контекст разговора полностью сохраняется — пригоден для конспектирования и дальнейшей LLM-обработки. VAD даёт более дробные сегменты (удобнее для навигации). Английскую речь не понимает (модель обучена только на русском). + +## Решение + +**Оставить onnx-asr как экспериментальный бэкенд.** Доступен через `--device onnx`, не в цепочке авто-детекта. + +Модели: +- `gigaam-v3` — по умолчанию для `--device onnx`. Русский, 4.7% WER, 17-29x RTF на CPU. +- `parakeet-v3` — мультиязычный fallback (25 языков, включая русский). 11% WER, 34x RTF. + +Обе модели в int8-квантизации (~300 MB, минимальная потеря качества). + +## Последствия + +- Пользователи CPU получают 3-5x ускорение для русской речи по сравнению с OpenVINO +- Пунктуация отсутствует (GigaAM не обучен её ставить) — приемлемо для конспектирования +- Мультиязычные записи требуют `--model parakeet-v3` +- Бэкенд не в авто-детекте — пользователь должен явно указать `--device onnx` +- В будущем: добавить parakeet-v3 в авто-детект для мультиязыка, рассмотреть Canary для лучшего качества + +## Отклонённые альтернативы + +| Альтернатива | Почему отклонена | +|---|---| +| NeMo Parakeet напрямую | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI | +| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом | +| Интеграция в авто-детект | Экспериментальный бэкенд, не хотим сюрпризов у пользователей | From 854bf55145a6241c3a50a6cd5b9668f61ff97716 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 23:13:41 +0300 Subject: [PATCH 11/16] =?UTF-8?q?docs:=20=D0=B7=D0=B0=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20onnx-=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD=D0=B4=20?= =?UTF-8?q?=D0=B2=20README,=20CLI-help=20=D0=B8=20ADR-005?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - пользователь должен знать о --device onnx и поддерживаемых моделях. - Что: - CLI help: добавлен onnx в список устройств. - README: секция примеров, таблица платформ, device-aware дефолты. - ADR-005: результаты parakeet-v3 --language ru, уточнённые рекомендации. - Проверка: - uv run pytest -q (172 passed). - uv run transcribe --help показывает onnx. --- README.md | 23 +++++++++++++++-------- docs/adr/005-onnx-asr-backend.md | 30 ++++++++++++++++-------------- src/local_transcriber/cli.py | 2 +- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d792660..17cdcd6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ transcribe meeting.mp4 ``` - **Полностью локально** — данные не покидают машину -- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), OpenVINO CPU или CPU fallback +- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), ONNX (CPU), OpenVINO CPU или CPU fallback - **Батч-режим** — обработка нескольких файлов за один вызов - **Markdown с таймкодами** — удобен для суммаризации ИИ - **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы) @@ -114,6 +114,12 @@ transcribe podcast.wav --model large-v3 --compute-type float16 # Максимальное качество на Intel GPU transcribe podcast.wav --model large-v3 --device openvino-gpu +# Максимальная скорость на CPU (русский) +transcribe meeting.mp4 --device onnx --model gigaam-v3 + +# Мультиязычный CPU (25 языков, медленнее) +transcribe podcast.wav --device onnx --model parakeet-v3 --language auto + # Сохранить в конкретный файл transcribe interview.m4a --output result.md ``` @@ -145,8 +151,8 @@ transcribe *.mp4 --force | `--model` | `-m` | `medium` | Модель Whisper | | `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) | | `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу | -| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu) | -| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU) | Тип вычислений | +| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) | +| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений | | `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) | | `--force` | `-f` | — | Перезаписать существующие транскрипты | | `--verbose` | `-v` | — | Подробный вывод | @@ -158,6 +164,7 @@ transcribe *.mp4 --force | CPU | ✅ | ✅ | ✅ | | OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто | | OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто | +| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно | | GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
@@ -210,11 +217,11 @@ language = "en" Дефолты зависят от устройства: -| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | CPU | -|----------|------|----------------|----------------|-----| -| model | medium | medium | medium | medium | -| compute_type | float16 | int8 | int8 | float32 | -| language | ru | ru | ru | ru | +| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU | +|----------|------|----------------|----------------|------|-----| +| model | medium | medium | medium | gigaam-v3 | medium | +| compute_type | float16 | int8 | int8 | int8 | float32 | +| language | ru | ru | ru | ru | ru | ## Модели и GPU diff --git a/docs/adr/005-onnx-asr-backend.md b/docs/adr/005-onnx-asr-backend.md index e036051..cb68bcb 100644 --- a/docs/adr/005-onnx-asr-backend.md +++ b/docs/adr/005-onnx-asr-backend.md @@ -18,19 +18,20 @@ ### Результаты -| Файл | Длит. | gigaam-v3 (CPU) | OpenVINO medium (CPU) | Ускорение | -|------|-------|-----------------|----------------------|-----------| -| 10-59-59 | 22:26 | 81с (16.6×) | 265с (5.1×) | **3.3×** | -| Vasya | 45:51 | 95с (29×) | 455с (6×) | **4.8×** | -| 12-02-37 | 1:20:44 | 170с (28.5×) | 779с (6.2×) | **4.6×** | +| Файл | Длит. | gigaam-v3 (CPU) | parakeet-v3 --ru (CPU) | OpenVINO medium (CPU) | +|------|-------|-----------------|----------------------|----------------------| +| 10-59-59 | 22:26 | 81с (16.6×) | 116с (11.6×) | 265с (5.1×) | +| Vasya | 45:51 | 95с (29×) | 138с (20×) | 455с (6×) | +| 12-02-37 | 1:20:44 | 170с (28.5×) | 251с (19.3×) | 779с (6.2×) | ### Качество (русская речь, файл 12-02-37) -| Модель | Пунктуация | Читаемость | Контекст | -|--------|-----------|-----------|----------| -| GPU f-whisper large-v3 | ✅ | Отлично | ✅ | -| **GigaAM v3 CTC (CPU)** | ❌ | Хорошо | ✅ | -| OpenVINO medium (CPU) | ✅ | Хорошо | ✅ | +| Модель | Пунктуация | Читаемость | Контекст | Особенности | +|--------|-----------|-----------|----------|-------------| +| GPU f-whisper large-v3 | ✅ | Отлично | ✅ | — | +| **GigaAM v3 CTC (CPU)** | ❌ | Хорошо | ✅ | Самый быстрый, без пунктуации | +| **Parakeet v3 --ru (CPU)** | ✅ | Хорошо | ✅ | Пунктуация, `` токены, ловит англ. вкрапления | +| OpenVINO medium (CPU) | ✅ | Хорошо | ✅ | Самый медленный | GigaAM v3 не расставляет знаки препинания, но контекст разговора полностью сохраняется — пригоден для конспектирования и дальнейшей LLM-обработки. VAD даёт более дробные сегменты (удобнее для навигации). Английскую речь не понимает (модель обучена только на русском). @@ -39,18 +40,19 @@ GigaAM v3 не расставляет знаки препинания, но ко **Оставить onnx-asr как экспериментальный бэкенд.** Доступен через `--device onnx`, не в цепочке авто-детекта. Модели: -- `gigaam-v3` — по умолчанию для `--device onnx`. Русский, 4.7% WER, 17-29x RTF на CPU. -- `parakeet-v3` — мультиязычный fallback (25 языков, включая русский). 11% WER, 34x RTF. +- `gigaam-v3` — по умолчанию для `--device onnx`. Русский, 4.7% WER, 17-29x RTF на CPU. Без пунктуации. +- `parakeet-v3` — мультиязычный (25 языков). 11% WER, 12-20x RTF. Для русского рекомендуется `--language ru`. С пунктуацией, но возможны `` токены. Обе модели в int8-квантизации (~300 MB, минимальная потеря качества). ## Последствия - Пользователи CPU получают 3-5x ускорение для русской речи по сравнению с OpenVINO -- Пунктуация отсутствует (GigaAM не обучен её ставить) — приемлемо для конспектирования +- gigaam-v3: максимальная скорость, без пунктуации — пригоден для конспектирования +- parakeet-v3: с пунктуацией, медленнее gigaam на 40%, для русского нужен явный `--language ru` - Мультиязычные записи требуют `--model parakeet-v3` - Бэкенд не в авто-детекте — пользователь должен явно указать `--device onnx` -- В будущем: добавить parakeet-v3 в авто-детект для мультиязыка, рассмотреть Canary для лучшего качества +- В будущем: добавить onnx в авто-детект, рассмотреть Canary для лучшего качества ## Отклонённые альтернативы diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 497cc84..4c543d4 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -55,7 +55,7 @@ def main( output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"), device: str | None = typer.Option( None, "--device", "-d", show_default=False, - help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) [по умолч.: auto]" + help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu|onnx) [по умолч.: auto]" ), compute_type: str | None = typer.Option( None, "--compute-type", show_default=False, From 503f2a37329c59da21c3841922ef4d6bc625ad38 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 23:16:13 +0300 Subject: [PATCH 12/16] =?UTF-8?q?docs(readme):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B5=D0=BA=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B4=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D0=BE=20onnx-?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - в README не было рекомендаций по выбору между gigaam-v3 и parakeet-v3. - Что: - добавлена таблица ONNX-моделей с RTFx, языками и пунктуацией. - пример parakeet-v3 изменён на --language ru. - рекомендации: gigaam-v3 для скорости, parakeet-v3 для пунктуации. - Проверка: - uv run pytest -q (172 passed). --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17cdcd6..4a7d608 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ transcribe podcast.wav --model large-v3 --device openvino-gpu # Максимальная скорость на CPU (русский) transcribe meeting.mp4 --device onnx --model gigaam-v3 -# Мультиязычный CPU (25 языков, медленнее) -transcribe podcast.wav --device onnx --model parakeet-v3 --language auto +# CPU с пунктуацией (русский, для parakeet-v3 нужен явный язык) +transcribe podcast.wav --device onnx --model parakeet-v3 --language ru # Сохранить в конкретный файл transcribe interview.m4a --output result.md @@ -229,6 +229,8 @@ language = "en" - **По умолчанию:** `medium` — хороший баланс скорости и качества - **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` +- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации) +- **CPU с пунктуацией (русский):** `--device onnx --model parakeet-v3 --language ru` (12-20× RTF, возможны ``) - **Быстрый тест:** `tiny` — для проверки пайплайна
@@ -242,6 +244,17 @@ language = "en" | `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ | | `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ | +#### ONNX-модели (`--device onnx`) + +Другие архитектуры, не Whisper. Работают через onnxruntime на CPU: + +| Модель | Размер (int8) | RTFx CPU | Языки | Пунктуация | +|--------|--------------|----------|-------|-----------| +| `gigaam-v3` | ~300 MB | 17-29× | ru | ❌ | +| `parakeet-v3` | ~600 MB | 12-20× | 25 языков | ✅ | + +> **Рекомендация**: для русского — `gigaam-v3` (быстрее, чище). Для мультиязыка или пунктуации — `parakeet-v3 --language ru`. Подробнее в [ADR-005](docs/adr/005-onnx-asr-backend.md). +
From 5990d85a58e4c49f6a5738045ace3e5a89e40392 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 23:17:30 +0300 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91?= =?UTF-8?q?=D0=BD=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D1=81=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20(=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B3=D0=BE=D0=B4=D0=B8=D0=BB=D1=81=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - сравнение сделано вручную, скрипт не использовался. - Что: - удалён scripts/compare_backends.py. - Проверка: - uv run pytest -q (172 passed). --- scripts/compare_backends.py | 66 ------------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 scripts/compare_backends.py diff --git a/scripts/compare_backends.py b/scripts/compare_backends.py deleted file mode 100644 index d8ee696..0000000 --- a/scripts/compare_backends.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Compare onnx-asr vs OpenVINO backends on real audio files. - -Usage: python scripts/compare_backends.py /path/to/audio.mp3 -""" -import sys -import time -from pathlib import Path -from local_transcriber.transcriber import load_model, _transcribe_file -from local_transcriber.backends import get_backend -from local_transcriber.types import Segment - - -def transcribe_with_backend(file_path: Path, device: str, model_name: str, - compute_type: str, language: str | None) -> tuple[float, int, str]: - """Run transcription and return (elapsed_sec, segment_count, transcript_text).""" - start = time.monotonic() - model_obj, actual_device, backend, model_path = load_model( - model_name, device, compute_type, - strict_device=True, - ) - tfr = _transcribe_file( - model_obj, actual_device, backend, model_path, - file_path, model_name, compute_type, - language=language, - ) - elapsed = time.monotonic() - start - text = " ".join(s.text for s in tfr.result.segments) - return elapsed, len(tfr.result.segments), text - - -def main(): - if len(sys.argv) < 2: - print("Usage: python scripts/compare_backends.py ") - sys.exit(1) - - file_path = Path(sys.argv[1]) - if not file_path.exists(): - print(f"File not found: {file_path}") - sys.exit(1) - - models_to_test = [ - ("gigaam-v3", "onnx", "int8"), - ("parakeet-v3", "onnx", "int8"), - ("medium", "openvino-cpu", "int8"), - ] - - print(f"File: {file_path.name} ({file_path.stat().st_size / 1e6:.1f} MB)") - print() - - for model_name, device, ct in models_to_test: - print(f"--- {model_name} on {device} (compute={ct}) ---") - try: - elapsed, seg_count, text = transcribe_with_backend( - file_path, device, model_name, ct, language="ru" if "gigaam" in model_name else None, - ) - print(f" Time: {elapsed:.1f}s") - print(f" Segments: {seg_count}") - print(f" Text preview: {text[:200]}...") - print() - except Exception as e: - print(f" ERROR: {e}") - print() - - -if __name__ == "__main__": - main() From 3b7eb603aef44ff181f5f3bc477bce07b24229ae Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 23:35:04 +0300 Subject: [PATCH 14/16] =?UTF-8?q?docs(adr):=20ADR-006=20onnx-asr=20?= =?UTF-8?q?=E2=80=94=20=D0=BD=D0=B5=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=D0=B0=D1=8F=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20agent-judge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переименовано из 005 в 006: на master уже есть ADR-005-parakeet-evaluation от прошлого эксперимента (Opus, отклонение Parakeet), нумерация бы конфликтовала. Качество замерено независимым agent-judge'ем по методологии ADR-005 на 9 транскриптах (3 файла × 3 backend), а не субъективными пометками "Отлично/Хорошо" на одном файле, как в первой версии. Артефакты транскриптов лежат в /mnt/c/ddmitry/Videos/OBS/ для верификации. Подтверждены три класса проблем Parakeet из ADR-005 (Mm-hmm-редукция, иноязычные вставки, искажение IT-терминов) на новом наборе файлов с конкретными цитатами и таймкодами. Обнаружен новый класс ошибок Whisper medium: галлюцинации на длинных файлах с тихими фрагментами (до 17 минут потеряно на одном файле, многоминутные повторы, приписывание несуществующих имён). Не описано в ADR-005, потому что там были 15-минутные отрывки. GigaAM v3 подтверждён как лучший backend для русских встреч на CPU (summary utility 4/5 на всех файлах, единственный без потерь содержания). Рекомендация в README уточнена: parakeet-v3 для русского не подходит, openvino-cpu medium — только для встреч ≤30 мин с равномерной громкостью. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +- docs/adr/005-onnx-asr-backend.md | 63 --------------- docs/adr/006-onnx-asr-backend.md | 135 +++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 66 deletions(-) delete mode 100644 docs/adr/005-onnx-asr-backend.md create mode 100644 docs/adr/006-onnx-asr-backend.md diff --git a/README.md b/README.md index 4a7d608..a2db285 100644 --- a/README.md +++ b/README.md @@ -229,8 +229,8 @@ language = "en" - **По умолчанию:** `medium` — хороший баланс скорости и качества - **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` -- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации) -- **CPU с пунктуацией (русский):** `--device onnx --model parakeet-v3 --language ru` (12-20× RTF, возможны ``) +- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после) +- **CPU с пунктуацией (русский):** `--device openvino-cpu --model medium` (5-6× RTF; для встреч ≤30 мин с равномерной громкостью — на длинных файлах с тихими фрагментами возможны галлюцинации) - **Быстрый тест:** `tiny` — для проверки пайплайна
@@ -253,7 +253,7 @@ language = "en" | `gigaam-v3` | ~300 MB | 17-29× | ru | ❌ | | `parakeet-v3` | ~600 MB | 12-20× | 25 языков | ✅ | -> **Рекомендация**: для русского — `gigaam-v3` (быстрее, чище). Для мультиязыка или пунктуации — `parakeet-v3 --language ru`. Подробнее в [ADR-005](docs/adr/005-onnx-asr-backend.md). +> **Рекомендация**: для русского — `gigaam-v3` (единственный из onnx-моделей, дающий пригодный для конспекта транскрипт на русских встречах; см. [ADR-006](docs/adr/006-onnx-asr-backend.md)). `parakeet-v3` уместен только для англоязычного / multilingual контента — на русском воспроизводит проблемы из [ADR-005](docs/adr/005-parakeet-evaluation.md) (Mm-hmm-редукция тихих реплик, иноязычные вставки).
diff --git a/docs/adr/005-onnx-asr-backend.md b/docs/adr/005-onnx-asr-backend.md deleted file mode 100644 index cb68bcb..0000000 --- a/docs/adr/005-onnx-asr-backend.md +++ /dev/null @@ -1,63 +0,0 @@ -# ADR-005: onnx-asr бэкенд (Parakeet / GigaAM) — эксперимент и решение - -**Статус**: Принято -**Дата**: 2026-04-25 - -## Контекст - -Целевая аудитория `local-transcriber` — пользователи с Intel iGPU / CPU, без дискретного NVIDIA GPU. -Существующие CPU-бэкенды (faster-whisper, OpenVINO) дают 0.5-6x RTF для средних моделей — транскрипция часовой записи занимает 10-120 минут. - -[onnx-asr](https://github.com/istupakov/onnx-asr) — легковесная обёртка (onnxruntime + numpy) над ONNX-моделями Parakeet, GigaAM, FastConformer и Canary. Заявляет 30-90x RTF на CPU при сравнимом с Whisper качестве для русского языка. - -## Эксперимент - -Проведено сравнение на трёх реальных русскоязычных записях встреч (22-81 мин, mp4), ноутбук с Intel i7-11800H (CPU-only). - -Сравнивались: **onnx-asr GigaAM v3 CTC** (int8, CPU) против **OpenVINO Whisper medium** (int8, CPU). Эталон — faster-whisper large-v3 на RTX 3060 (GPU). - -### Результаты - -| Файл | Длит. | gigaam-v3 (CPU) | parakeet-v3 --ru (CPU) | OpenVINO medium (CPU) | -|------|-------|-----------------|----------------------|----------------------| -| 10-59-59 | 22:26 | 81с (16.6×) | 116с (11.6×) | 265с (5.1×) | -| Vasya | 45:51 | 95с (29×) | 138с (20×) | 455с (6×) | -| 12-02-37 | 1:20:44 | 170с (28.5×) | 251с (19.3×) | 779с (6.2×) | - -### Качество (русская речь, файл 12-02-37) - -| Модель | Пунктуация | Читаемость | Контекст | Особенности | -|--------|-----------|-----------|----------|-------------| -| GPU f-whisper large-v3 | ✅ | Отлично | ✅ | — | -| **GigaAM v3 CTC (CPU)** | ❌ | Хорошо | ✅ | Самый быстрый, без пунктуации | -| **Parakeet v3 --ru (CPU)** | ✅ | Хорошо | ✅ | Пунктуация, `` токены, ловит англ. вкрапления | -| OpenVINO medium (CPU) | ✅ | Хорошо | ✅ | Самый медленный | - -GigaAM v3 не расставляет знаки препинания, но контекст разговора полностью сохраняется — пригоден для конспектирования и дальнейшей LLM-обработки. VAD даёт более дробные сегменты (удобнее для навигации). Английскую речь не понимает (модель обучена только на русском). - -## Решение - -**Оставить onnx-asr как экспериментальный бэкенд.** Доступен через `--device onnx`, не в цепочке авто-детекта. - -Модели: -- `gigaam-v3` — по умолчанию для `--device onnx`. Русский, 4.7% WER, 17-29x RTF на CPU. Без пунктуации. -- `parakeet-v3` — мультиязычный (25 языков). 11% WER, 12-20x RTF. Для русского рекомендуется `--language ru`. С пунктуацией, но возможны `` токены. - -Обе модели в int8-квантизации (~300 MB, минимальная потеря качества). - -## Последствия - -- Пользователи CPU получают 3-5x ускорение для русской речи по сравнению с OpenVINO -- gigaam-v3: максимальная скорость, без пунктуации — пригоден для конспектирования -- parakeet-v3: с пунктуацией, медленнее gigaam на 40%, для русского нужен явный `--language ru` -- Мультиязычные записи требуют `--model parakeet-v3` -- Бэкенд не в авто-детекте — пользователь должен явно указать `--device onnx` -- В будущем: добавить onnx в авто-детект, рассмотреть Canary для лучшего качества - -## Отклонённые альтернативы - -| Альтернатива | Почему отклонена | -|---|---| -| NeMo Parakeet напрямую | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI | -| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом | -| Интеграция в авто-детект | Экспериментальный бэкенд, не хотим сюрпризов у пользователей | diff --git a/docs/adr/006-onnx-asr-backend.md b/docs/adr/006-onnx-asr-backend.md new file mode 100644 index 0000000..c9b3ddb --- /dev/null +++ b/docs/adr/006-onnx-asr-backend.md @@ -0,0 +1,135 @@ +# ADR-006: onnx-asr бэкенд — GigaAM v3 как CPU-default для русских встреч + +**Статус**: Принято +**Дата**: 2026-04-25 + +## Контекст + +Целевая аудитория `local-transcriber` — пользователи с Intel iGPU / CPU, без дискретного NVIDIA GPU. +Существующие CPU-бэкенды (faster-whisper, OpenVINO) дают 0.5-6x RTF для средних моделей — транскрипция часовой записи занимает 10-120 минут. + +[onnx-asr](https://github.com/istupakov/onnx-asr) — легковесная обёртка (onnxruntime + numpy) над ONNX-моделями Parakeet, GigaAM, FastConformer и Canary. Заявляет 30-90x RTF на CPU при сравнимом с Whisper качестве для русского языка. + +[ADR-005](005-parakeet-evaluation.md) ранее отклонил Parakeet TDT 0.6B v3 для целевого use case на двух 15-минутных файлах с тихим микрофоном менти и плотной IT-терминологией. ADR-006 повторяет эксперимент на новом наборе файлов (22-81 мин, разной громкости), добавляет GigaAM v3 как кандидата для русской речи и проверяет три класса проблем Parakeet из ADR-005 на актуальном материале. + +## Эксперимент + +Три реальных русскоязычных записи установочных встреч (формат ментор↔менти, mp4, 22-81 мин), ноутбук с Intel i7-11800H (CPU-only). Сравнивались три CPU-бэкенда: + +- **gigaam-v3** (onnx-asr GigaAM v3 CTC, int8, monolingual ru, без пунктуации) +- **parakeet-v3** (onnx-asr Parakeet TDT 0.6B v3, int8, multilingual) +- **ov-medium** (OpenVINO Whisper medium int8) — baseline + +Качественная оценка проведена независимым agent-judge'ем по методологии ADR-005 (4 критерия: completeness / term accuracy / fluency / summary utility, шкала 1-5). Транскрипты сохранены в `/mnt/c/ddmitry/Videos/OBS/.{onnx-gigaam,parakeet-v3,ov-medium}.md` и доступны для верификации. + +### Скорость + +| Файл | Длит. | gigaam-v3 | parakeet-v3 --ru | OpenVINO medium | +|---|---|---|---|---| +| 10-59-59 | 22:26 | 81с (16.6×) | 116с (11.6×) | 265с (5.1×) | +| Vasya | 45:51 | 95с (29×) | 138с (20×) | 455с (6×) | +| 12-02-37 | 1:20:44 | 170с (28.5×) | 251с (19.3×) | 779с (6.2×) | + +GigaAM в 3-5× быстрее OpenVINO medium, Parakeet в 2.5-3.5× быстрее. + +### Качество (agent-judge, 1-5) + +| Файл | Backend | Completeness | Term accuracy | Fluency | Summary utility | +|------|---------|:-:|:-:|:-:|:-:| +| 10-59-59 | **gigaam** | **5** | **4** | **4** | **4** | +| 10-59-59 | parakeet | 4 | 2 | 2 | 2 | +| 10-59-59 | ov-medium | 3 | 4 | 4 | 2 | +| Vasya | **gigaam** | 3 | **4** | **4** | **4** | +| Vasya | parakeet | 2 | 2 | 2 | 2 | +| Vasya | ov-medium | 2 | 3 | 2 | 2 | +| 12-02-37 | **gigaam** | 4 | 3 | 3 | **4** | +| 12-02-37 | parakeet | 3 | 2 | 1 | 1 | +| 12-02-37 | ov-medium | 2 | 3 | 2 | 1 | + +GigaAM — единственный backend, дающий summary utility 4/5 на всех трёх файлах. Parakeet и ov-medium систематически уступают по разным причинам (см. ниже). + +### Класс ошибок: Parakeet — три проблемы из ADR-005 воспроизведены + +Все три класса систематических ошибок Parakeet, описанные в [ADR-005](005-parakeet-evaluation.md), воспроизводятся на новом наборе файлов: + +**1. Mm-hmm/Yeah-редукция тихих реплик менти.** Массово на всех трёх файлах: +- Vasya `[04:56-09:33]` блок из ~10 реплик: `Mm-hmm. Mm-hmm. Mm. That's nice. Mm-hmm. Mm-hmm.` — полностью утеряны ответы менти на вопросы ментора. +- 10-59-59 `[19:03]` `Yeah. Иногда лучше дышали в облаке`. +- 12-02-37 `[00:00:01]` `I mean.` вместо «не пони…». + +**2. Вставки иностранных языков посреди русского.** На этом наборе ещё агрессивнее, чем в ADR-005 (там был только польский): +- 10-59-59 `[00:08]` `Secondo, Alice. The mutual microphone.` — итальянский+английский для «секунду, Алиса, замьючен микрофон». +- 10-59-59 `[22:02]` `Ah si va sur. Well.` — испано-французская смесь в финальном прощании. +- Vasya `[27:31]` `Mas o żegnienie.` — польский в полностью русской встрече. +- 12-02-37 `[10:38]` `Запроси к Każdemu Actually, таблица классная` — русско-польско-английский в одной фразе. +- 12-02-37 `[01:03:23]` `No już je wsie.` — польский («ну уже всё»). + +**3. Искажение IT-терминов и имён компаний:** +- 10-59-59 `[02:21]` `не Аринадата и не Терринте игра` вместо «Аренадата и Тере-Интегра» (имена работодателей). +- 10-59-59 `[01:23]` `Запромбанке` (с unk-токенами) вместо «Газпромбанк». +- 12-02-37 `[02:35]` `Basic space clear cause` вместо «база данных кликхаус». +- 12-02-37 `[06:12]` `Поскре это не колочный, чтобы это греплан. Ловочная.` — Postgres/Greenplum/«колоночная» искажены до неразборчивости. +- 12-02-37 `[16:53]` `своеобресть` вместо «Wildberries» — целевой работодатель в задаче, имя потеряно. + +### Класс ошибок: Whisper medium — галлюцинации на длинных файлах с тихими фрагментами + +Не описано в ADR-005 (там были 15-минутные отрывки) — обнаружено только на длинных файлах: + +- 12-02-37 `[01:03:43-01:20:14]` — **17 минут хвоста встречи** забиты галлюцинированными повторами: `«Вместе с вами мы решим, как мы будем работать с вами»`, `«Это не то, чтобы не было»`, `«Выбор? Нет. Выбор? Нет.»`. Бытовая часть встречи целиком потеряна. +- 12-02-37 `[19:54-20:49]` — 11 повторов `«И вот, как я вам рассказываю, это очень интересно»` вместо реального решения SQL-задачи. +- Vasya `[10:08-11:59]` — ~6 повторов `«Но если вы хотите, чтобы мы не разговаривали, то вы можете.»` (~2 минуты галлюцинации). +- Vasya `[36:00-36:30]` — 14 повторов `«Ага. Ага.»` (loop). +- Vasya `[45:51]` — `«Субтитры сделаны с помощью СМС, аппарата — Лариса.»` — классический Whisper-артефакт «титров». +- 10-59-59 `[10:56-11:40]` — строка из ~1000 символов `«ааааа…»` — галлюцинация на тихом фрагменте, проглатывает 30 секунд аудио. +- 12-02-37 `[01:18:47]` — приписан несуществующий человек `«Валерий Сюткин»`. + +Это критичный класс ошибок: текст выглядит правдоподобно, и читатель конспекта не отличит галлюцинацию от реального содержания без возврата к аудио. Хуже потери — потому что вводит в заблуждение. + +### Класс ошибок: GigaAM — локальные искажения латиницы и имён + +GigaAM monolingual ru, латиницу не выдаёт. На транскрипте: +- `«эскель»`/`«эсквель»` вместо `SQL` (везде кириллицей). +- `«гитам ардауна»` вместо `git и markdown` (Vasya `[14:14]`). +- `«арендата»`/`«арендат»`/`«арендода»` для «Аренадата» (10-59-59 `[02:21]`) — три разных варианта одного имени. +- `«дв один»` вместо `DEV1` (12-02-37 `[00:50:56]`). +- `«яндекс тим под яндекс тим»` для «Яндекс ТимКод» (12-02-37 `[00:14:15]`). + +Mm-hmm-редукция и иностранные вставки **не обнаружены**: monolingual архитектура исключает language-confusion, тихие реплики менти остаются как русские «угу/да/ну». + +Эти ошибки локальны, предсказуемы и легко чинятся LLM-этапом нормализации без знания исходного аудио (восстановить SQL, Greenplum, ClickHouse, имена компаний из контекста). + +## Решение + +**Принять onnx-asr как экспериментальный бэкенд с явным `--device onnx`. GigaAM v3 — рекомендуемая модель для русских встреч на CPU.** + +Бэкенд **не в auto-detect** — только при явном указании пользователем (политика experimental backend, как для openvino). + +Модели: +- **`gigaam-v3`** — рекомендуемая для русских встреч на CPU. 17-29× RTF, summary utility 4/5 на всех протестированных файлах. Без пунктуации, без латиницы; ошибки локальны, чинятся LLM-нормализацией. +- **`parakeet-v3`** — multilingual (25 языков), формально доступен. **Не рекомендуется для русских встреч**: Mm-hmm-редукция и иноязычные вставки воспроизводятся систематически (см. выше). Уместен только для англоязычного контента. + +Обе модели в int8-квантизации (~300 MB). + +## Последствия + +- Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах). +- Whisper medium (`--device openvino-cpu`) **остаётся допустимым** для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с тихими участками он галлюцинирует целыми блоками — этот риск зафиксирован, но решение не выводит OpenVINO из списка дефолтов (часть пользователей всё ещё нуждается в пунктуации, и для коротких файлов галлюцинации не воспроизводятся). +- Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится. +- GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU). +- Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта. + +## Открытые вопросы / следующие шаги + +- **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю. +- **GigaAM v3 RNN-T** (вариант `gigaam-v3-rnnt` вместо `gigaam-v3-ctc`) — заявлен как немного качественнее CTC, не тестировался. Может закрыть часть GigaAM-ошибок на латинице. +- **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация. +- **Auto-detect onnx**: после стабилизации в production-использовании (несколько недель) — рассмотреть включение в auto-detect как первый CPU-бэкенд (ниже CUDA, выше OpenVINO). + +## Отклонённые альтернативы + +| Альтернатива | Почему отклонена | +|---|---| +| NeMo Parakeet напрямую (без onnx-asr) | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI | +| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом | +| GigaAM как auto-detect default | Экспериментальный бэкенд, политика — не сюрпризить существующих пользователей; включение в auto-detect — после периода стабилизации | +| Parakeet-v3 как multilingual default | Воспроизведённые проблемы из ADR-005 (Mm-hmm-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте | From 0c3a67c4ead171c7449738e32a893f5bbc2e4884 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sat, 25 Apr 2026 23:48:36 +0300 Subject: [PATCH 15/16] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20backlog.md=20=E2=80=94=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B9=D1=87=D0=B8=D0=B2=D1=8B=D0=B9=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=BA=20=D0=B1=D1=83=D0=B4=D1=83=D1=89=D0=B8?= =?UTF-8?q?=D1=85=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В проекте не было общего файла для будущих идей: docs/plan.md — историческая летопись (всё [x]), docs/gpu.md — потенциальные направления привязаны к GPU/CPU производительности, открытые вопросы из ADR легко теряются среди следующих ADR. Создан docs/backlog.md как канонический хаб для будущих экспериментов с обоснованием, цифрами и ссылками на источники. Первые записи: - GigaAM v3 RNN-T — потенциальная замена CTC-дефолта (WER 2.6% vs 13.2% на сложных текстах, e2e_rnnt c пунктуацией, 70:30 vs Whisper-large-v3 LLM-judge) - Canary 1B — multilingual + пунктуация на CPU - Whisper medium галлюцинации на длинных файлах (chunk_length / pipeline-фильтры) - Качественный pipeline для OpenVINO (давно зафиксировано в gpu.md) - Включение onnx в --device auto после стабилизации Все направления связаны ссылками на источники (ADR-006, gpu.md, публикации SberDevices), чтобы при возврате к работе не пришлось воспроизводить контекст. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/backlog.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/backlog.md diff --git a/docs/backlog.md b/docs/backlog.md new file mode 100644 index 0000000..7581b21 --- /dev/null +++ b/docs/backlog.md @@ -0,0 +1,87 @@ +# Backlog — будущие эксперименты и направления + +Список открытых направлений, которые имеют смысл, но не реализованы. Каждый пункт содержит обоснование и ссылку на источник (ADR / статья), чтобы при возврате не пришлось воспроизводить контекст с нуля. + +Когда направление становится в работу — переносится в spec/план или соответствующий ADR. Когда отклоняется — остаётся в backlog с пометкой «отклонено» и причиной (для истории решений). + +--- + +## ASR-бэкенды и модели + +### GigaAM v3 RNN-T — потенциальная замена CTC-дефолта для `--device onnx` + +**Что:** Прогнать `gigaam-v3-rnnt` (или `v3_e2e_rnnt` если доступен через onnx-asr) по той же методологии, что и в [ADR-006](adr/006-onnx-asr-backend.md): 3 файла × agent-judge × 4 критерия. + +**Почему интересно:** + +- **WER 2.6% vs 13.2%** для CTC на сложных текстах — в 5 раз ниже на разговорной речи и доменной лексике (источник: SberDevices / Хабр-публикация GigaAM-v3). +- **Контекстный декодер** — структурно решает основную проблему GigaAM-CTC из ADR-006: кириллизация латиницы и искажения имён компаний (`Запромбанк` → `Газпромбанк`, `яндекс тим под яндекс тим` → `Яндекс ТимКод`). RNN-T видит контекст уже сгенерированных токенов и может «дотянуть» имена. +- **`v3_e2e_rnnt` с пунктуацией и нормализацией** — закрывает главное ограничение GigaAM-CTC, ради которого в README сейчас стоит fallback на `openvino-cpu medium` (с задокументированными в ADR-006 галлюцинациями на длинных файлах). +- **70:30 vs Whisper-large-v3** — GigaAM-v3 (CTC и RNN-T) выигрывает у `large-v3` по LLM-as-Judge (Gemini 2.5 Pro). Если переносится на наш use case — RNN-T на CPU становится сильнее GPU faster-whisper large-v3. +- **30% лучше на «новых доменах»** (callcenter-like речь, нестандартные характеристики) — это и есть домен установочных встреч. + +**Tradeoff:** + +- Скорость ниже CTC (RNN-T декодинг последовательный). Реалистичная оценка: 10-15× RTF на CPU вместо 17-29× у CTC. Всё ещё в 1.5-2× быстрее Whisper medium. +- Размер модели больше (~500 MB int8 против ~300 MB у CTC) — оценка, нужна верификация. + +**Если подтвердится бенчмарком:** + +- Дефолт в `--device onnx` меняется с `gigaam-v3-ctc` на `gigaam-v3-rnnt`. +- `openvino-cpu medium` уходит из рекомендаций для русского. +- Формулировка «GPU faster-whisper large-v3 — эталон» в README может стать неточной. +- Пишется ADR-007 с переоценкой дефолта. + +**Уточнить перед запуском:** какой именно вариант RNN-T доступен в onnx-asr (`gigaam-v3-rnnt` без пунктуации vs `v3_e2e_rnnt` с пунктуацией). Проверить через `huggingface_hub` listing для `istupakov/gigaam-v3-onnx`. + +--- + +### Canary 1B — multilingual + пунктуация на CPU + +**Что:** Протестировать `nemo-canary-1b-v2` через onnx-asr на тех же 3 файлах. + +**Почему:** Multilingual + пунктуация в одной модели. Кандидат на «лучшее качество за разумную скорость» для пользователей, которым нужны и не-русский контент, и пунктуация одновременно. Упомянут в [ADR-006](adr/006-onnx-asr-backend.md#открытые-вопросы--следующие-шаги). + +**Tradeoff:** Тяжелее GigaAM (~1 GB vs ~300 MB), скорость на CPU ожидаемо ниже. Если RNN-T закроет потребность в пунктуации — Canary становится менее приоритетным. + +--- + +## Качество и устойчивость + +### Whisper medium галлюцинации на длинных файлах с тихими фрагментами + +**Что:** Воспроизвести и зафиксировать класс ошибок: `12-02-37` теряет 17 минут хвоста, `Vasya` имеет 5-минутные блоки повторов, появляются несуществующие имена (`Валерий Сюткин`). Источник: agent-judge в [ADR-006](adr/006-onnx-asr-backend.md#класс-ошибок-whisper-medium--галлюцинации-на-длинных-файлах-с-тихими-фрагментами). + +**Возможные направления:** + +- Ограничить длину чанка для openvino-medium (chunk_length параметр в WhisperPipeline). +- Внедрить `compression_ratio_threshold` / `log_prob_threshold` фильтры через переписывание pipeline (как у CTranslate2). Уже частично описано в [docs/gpu.md «Качественный pipeline для OpenVINO»](gpu.md#качественный-pipeline-для-openvino). +- Предупреждать пользователя при `--device openvino-cpu` для файлов >30 мин. + +**Приоритет:** средний — пока есть `gigaam-v3` как альтернатива для русского. Критично, если openvino остаётся единственным вариантом для пунктуации (отпадает после теста RNN-T). + +--- + +### Качественный pipeline для OpenVINO (temperature fallback + фильтры) + +**Что:** Реализовать temperature fallback, compression_ratio и log_prob фильтры поверх OpenVINO GenAI WhisperPipeline. Эвристики — логика на Python (~50-100 строк), не зависящая от inference engine. + +**Почему:** Дать Intel Arc / AMD GPU и AMD CPU то же качество, что сейчас есть только у CUDA-пользователей через CTranslate2. Полностью описано в [docs/gpu.md](gpu.md#качественный-pipeline-для-openvino). + +--- + +## Авто-детект и UX + +### Включение `onnx` в `--device auto` + +**Что:** После периода стабилизации `--device onnx` (несколько недель production-использования без жалоб) — рассмотреть включение в auto-detect chain. + +**Порядок в chain (предложение):** CUDA → onnx (если CPU x86_64) → OpenVINO → CPU. + +**Почему откладывается:** политика experimental backend — не сюрпризить существующих пользователей до накопления опыта. Источник: [ADR-006](adr/006-onnx-asr-backend.md#решение). + +--- + +## Отклонённые направления + +*(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)* From 9cfa437b3338be27a4fd0260c658175b23286e8c Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 26 Apr 2026 00:06:22 +0300 Subject: [PATCH 16/16] =?UTF-8?q?fix(onnx-asr):=20=D0=BA=D0=BE=D1=80=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D0=BC=D0=B0=D0=BF=D0=BF?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=20compute=5Ftype=20=D0=B2=20onnx-asr=20quant?= =?UTF-8?q?ization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit При adversarial review (gpt-5.5) обнаружено: пользователь с config compute_type = "float32" и --device onnx падал на старте. HARDCODED_DEFAULTS["compute_type"] = "float32", apply_device_defaults не заменяет значение если оно есть в config — старая проверка (compute_type in ("int8", "fp16", "float32")) пропускала "float32" дальше как onnx_asr quantization, что заставляло искать несуществующий файл с суффиксом _float32. - _normalize_quantization() — explicit маппинг в onnx-asr quantization. - float32/fp32 → None (unquantized loading в onnx-asr — это None, не строка). - float16 → fp16 (CUDA-naming → onnx-asr-naming). - int8/fp16 → pass-through. - Неизвестные compute_type (например, int8_float32 от CTranslate2) → ValueError вместо silent fallback на int8 — пользователь раньше получал не ту модель без предупреждения. 4 новых теста: float32→None, fp32→None, float16→fp16, unknown→raises. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/local_transcriber/backends/onnx_asr.py | 35 +++++++++-- tests/test_onnx_asr.py | 73 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 664cba0..f3d7b87 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -15,6 +15,32 @@ MODEL_ALIASES: dict[str, str] = { SUPPORTED_ALIASES = ", ".join(MODEL_ALIASES) +# compute_type проекта → onnx-asr quantization (file suffix; None = unquantized). +_QUANTIZATION_MAP: dict[str, str | None] = { + "int8": "int8", + "fp16": "fp16", + "float16": "fp16", + "float32": None, + "fp32": None, +} + + +def _normalize_quantization(compute_type: str) -> str | None: + """Маппит compute_type проекта в значение onnx-asr ``quantization``. + + onnx-asr использует ``quantization`` как суффикс имени файла модели: + ``int8``/``fp16`` подгружают квантизованные веса, ``None`` — unquantized + (float32). Передача ``"float32"`` строкой пытается найти несуществующий + файл с суффиксом ``_float32`` и приводит к ошибке загрузки. + """ + if compute_type not in _QUANTIZATION_MAP: + supported = ", ".join(sorted(_QUANTIZATION_MAP)) + raise ValueError( + f"Неподдерживаемый compute_type '{compute_type}' для onnx-asr. " + f"Допустимо: {supported}." + ) + return _QUANTIZATION_MAP[compute_type] + class OnnxAsrBackend: """Бэкенд транскрипции через onnx-asr (ONNX Runtime).""" @@ -48,17 +74,16 @@ class OnnxAsrBackend: ) -> Any: """Creates onnx-asr model with VAD. - model_path: onnx-asr model identifier (e.g. "gigaam-v3-ctc"). - compute_type: "int8", "fp16", or "float32" — passed as quantization. - cpu_threads: not used by onnx-asr (onnxruntime manages threads internally). + compute_type маппится в onnx-asr ``quantization`` — это суффикс файла + модели; для unquantized (float32/fp32) нужно None, не строку. """ import onnx_asr - ct = compute_type if compute_type in ("int8", "fp16", "float32") else "int8" + quantization = _normalize_quantization(compute_type) model = onnx_asr.load_model( model=model_path, - quantization=ct, + quantization=quantization, ) vad = onnx_asr.load_vad("silero") self._vad = vad diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index 207e60f..ff192eb 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -104,6 +104,79 @@ class TestCreateModel: assert calls == ["fp16"] + def test_float32_maps_to_none(self, monkeypatch): + """compute_type='float32' маппится в quantization=None (unquantized). + + onnx-asr использует quantization как суффикс файла; для float32 нужен None, + строка "float32" приведёт к попытке загрузить несуществующий файл. + """ + calls = [] + + def fake_load_model(model=None, quantization="MISSING", **kwargs): + calls.append(quantization) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None) + + backend = OnnxAsrBackend() + backend.create_model("gigaam-v3-ctc", "onnx", "float32") + + assert calls == [None] + + def test_fp32_maps_to_none(self, monkeypatch): + """compute_type='fp32' тоже маппится в quantization=None.""" + calls = [] + + def fake_load_model(model=None, quantization="MISSING", **kwargs): + calls.append(quantization) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None) + + backend = OnnxAsrBackend() + backend.create_model("gigaam-v3-ctc", "onnx", "fp32") + + assert calls == [None] + + def test_float16_alias_maps_to_fp16(self, monkeypatch): + """compute_type='float16' (CUDA-naming) маппится в onnx-asr 'fp16'.""" + calls = [] + + def fake_load_model(model=None, quantization=None, **kwargs): + calls.append(quantization) + return FakeAsrAdapter() + + class FakeAsrAdapter: + def with_vad(self, vad): + return self + + monkeypatch.setattr("onnx_asr.load_model", fake_load_model) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None) + + backend = OnnxAsrBackend() + backend.create_model("gigaam-v3-ctc", "onnx", "float16") + + assert calls == ["fp16"] + + def test_unknown_compute_type_raises(self, monkeypatch): + """Неподдерживаемый compute_type → ValueError, не silent fallback.""" + monkeypatch.setattr("onnx_asr.load_model", lambda **kw: None) + monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None) + + backend = OnnxAsrBackend() + with pytest.raises(ValueError, match="Неподдерживаемый compute_type"): + backend.create_model("gigaam-v3-ctc", "onnx", "int8_float32") + class TestTranscribe: def test_transcribe_collects_segments(self, monkeypatch, tmp_path):