Merge branch 'experiment/parakeet-backend-opencode'
Эксперимент с DeepSeek V4 Pro: добавление onnx-asr бэкенда (Parakeet/GigaAM/ FastConformer) с независимой валидацией качества через agent-judge. Реализация: - backends/onnx_asr.py — третий pluggable backend через onnxruntime + Silero VAD. - --device onnx с моделями gigaam-v3 (рекомендуется для русского) и parakeet-v3. - _normalize_quantization() — маппинг compute_type → onnx-asr quantization (int8/fp16 pass-through, float16→fp16, float32/fp32→None, остальные→ValueError). - DEVICE_DEFAULTS для onnx, _VALID_DEVICES расширен. Документация: - ADR-006 — итоги: GigaAM v3 как best-CPU choice для русских встреч, Parakeet непригоден для русского (воспроизведены три класса проблем из ADR-005), новый класс ошибок Whisper medium на длинных файлах с тихими фрагментами. - backlog.md — устойчивый список будущих экспериментов (GigaAM v3 RNN-T и др.). Контекст эксперимента: - ADR-005 (Opus, master) отклонил Parakeet на двух 15-мин файлах с тихим микрофоном. ADR-006 повторяет на 22-81-мин файлах + добавляет GigaAM. - Реализация прошла три фазы валидации: smoke-test (4 mock-API расхождения → fix), agent-judge на 9 транскриптах (подтверждение проблем Parakeet, обнаружение Whisper-галлюцинаций), adversarial-review через GPT-5.5 (compute_type=float32 ломал onnx-загрузку → fix + 4 теста). Не включено в merge: docs/superpowers/specs/ и plans/ от DeepSeek (stale assumptions, итоговый канон в ADR-006; полные документы — в истории ветки). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 с таймкодами** — удобен для суммаризации ИИ
|
- **Markdown с таймкодами** — удобен для суммаризации ИИ
|
||||||
- **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы)
|
- **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы)
|
||||||
@@ -114,6 +114,12 @@ transcribe podcast.wav --model large-v3 --compute-type float16
|
|||||||
# Максимальное качество на Intel GPU
|
# Максимальное качество на Intel GPU
|
||||||
transcribe podcast.wav --model large-v3 --device openvino-gpu
|
transcribe podcast.wav --model large-v3 --device openvino-gpu
|
||||||
|
|
||||||
|
# Максимальная скорость на CPU (русский)
|
||||||
|
transcribe meeting.mp4 --device onnx --model gigaam-v3
|
||||||
|
|
||||||
|
# CPU с пунктуацией (русский, для parakeet-v3 нужен явный язык)
|
||||||
|
transcribe podcast.wav --device onnx --model parakeet-v3 --language ru
|
||||||
|
|
||||||
# Сохранить в конкретный файл
|
# Сохранить в конкретный файл
|
||||||
transcribe interview.m4a --output result.md
|
transcribe interview.m4a --output result.md
|
||||||
```
|
```
|
||||||
@@ -145,8 +151,8 @@ transcribe *.mp4 --force
|
|||||||
| `--model` | `-m` | `medium` | Модель Whisper |
|
| `--model` | `-m` | `medium` | Модель Whisper |
|
||||||
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) |
|
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) |
|
||||||
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
|
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
|
||||||
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu) |
|
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) |
|
||||||
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU) | Тип вычислений |
|
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений |
|
||||||
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
|
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
|
||||||
| `--force` | `-f` | — | Перезаписать существующие транскрипты |
|
| `--force` | `-f` | — | Перезаписать существующие транскрипты |
|
||||||
| `--verbose` | `-v` | — | Подробный вывод |
|
| `--verbose` | `-v` | — | Подробный вывод |
|
||||||
@@ -158,6 +164,7 @@ transcribe *.mp4 --force
|
|||||||
| CPU | ✅ | ✅ | ✅ |
|
| CPU | ✅ | ✅ | ✅ |
|
||||||
| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто |
|
| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто |
|
||||||
| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто |
|
| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто |
|
||||||
|
| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно |
|
||||||
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
|
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -210,11 +217,11 @@ language = "en"
|
|||||||
|
|
||||||
Дефолты зависят от устройства:
|
Дефолты зависят от устройства:
|
||||||
|
|
||||||
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | CPU |
|
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU |
|
||||||
|----------|------|----------------|----------------|-----|
|
|----------|------|----------------|----------------|------|-----|
|
||||||
| model | medium | medium | medium | medium |
|
| model | medium | medium | medium | gigaam-v3 | medium |
|
||||||
| compute_type | float16 | int8 | int8 | float32 |
|
| compute_type | float16 | int8 | int8 | int8 | float32 |
|
||||||
| language | ru | ru | ru | ru |
|
| language | ru | ru | ru | ru | ru |
|
||||||
|
|
||||||
## Модели и GPU
|
## Модели и GPU
|
||||||
|
|
||||||
@@ -222,6 +229,8 @@ language = "en"
|
|||||||
- **По умолчанию:** `medium` — хороший баланс скорости и качества
|
- **По умолчанию:** `medium` — хороший баланс скорости и качества
|
||||||
- **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16`
|
- **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16`
|
||||||
- **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu`
|
- **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu`
|
||||||
|
- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после)
|
||||||
|
- **CPU с пунктуацией (русский):** `--device openvino-cpu --model medium` (5-6× RTF; для встреч ≤30 мин с равномерной громкостью — на длинных файлах с тихими фрагментами возможны галлюцинации)
|
||||||
- **Быстрый тест:** `tiny` — для проверки пайплайна
|
- **Быстрый тест:** `tiny` — для проверки пайплайна
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -235,6 +244,17 @@ language = "en"
|
|||||||
| `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ |
|
| `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ |
|
||||||
| `large-v3` | ~3 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` (единственный из onnx-моделей, дающий пригодный для конспекта транскрипт на русских встречах; см. [ADR-006](docs/adr/006-onnx-asr-backend.md)). `parakeet-v3` уместен только для англоязычного / multilingual контента — на русском воспроизводит проблемы из [ADR-005](docs/adr/005-parakeet-evaluation.md) (Mm-hmm-редукция тихих реплик, иноязычные вставки).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -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/<basename>.{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-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте |
|
||||||
@@ -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#решение).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Отклонённые направления
|
||||||
|
|
||||||
|
*(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)*
|
||||||
@@ -12,6 +12,7 @@ dependencies = [
|
|||||||
"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
"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')",
|
"openvino-genai>=2025.0; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')",
|
||||||
"tomli>=2.0; python_version < '3.11'",
|
"tomli>=2.0; python_version < '3.11'",
|
||||||
|
"onnx-asr[cpu,hub]>=0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend:
|
|||||||
ov_device=device, compute_type_explicit=compute_type_explicit
|
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
|
# cuda, cpu и всё остальное → faster-whisper
|
||||||
from .faster_whisper import FasterWhisperBackend
|
from .faster_whisper import FasterWhisperBackend
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""Бэкенд транскрипции на основе onnx-asr (GigaAM, Parakeet, FastConformer)."""
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
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)."""
|
||||||
|
|
||||||
|
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 create_model(
|
||||||
|
self,
|
||||||
|
model_path: str,
|
||||||
|
device: str,
|
||||||
|
compute_type: str,
|
||||||
|
cpu_threads: int = 0,
|
||||||
|
) -> Any:
|
||||||
|
"""Creates onnx-asr model with VAD.
|
||||||
|
|
||||||
|
compute_type маппится в onnx-asr ``quantization`` — это суффикс файла
|
||||||
|
модели; для unquantized (float32/fp32) нужно None, не строку.
|
||||||
|
"""
|
||||||
|
import onnx_asr
|
||||||
|
|
||||||
|
quantization = _normalize_quantization(compute_type)
|
||||||
|
|
||||||
|
model = onnx_asr.load_model(
|
||||||
|
model=model_path,
|
||||||
|
quantization=quantization,
|
||||||
|
)
|
||||||
|
vad = onnx_asr.load_vad("silero")
|
||||||
|
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, sample_rate=16000, language=language):
|
||||||
|
seg = Segment(
|
||||||
|
start=max(0.0, vad_seg.start),
|
||||||
|
end=max(0.0, vad_seg.end),
|
||||||
|
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:
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
|
||||||
|
if on_status is not None:
|
||||||
|
on_status(message)
|
||||||
@@ -55,7 +55,7 @@ def main(
|
|||||||
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
|
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
|
||||||
device: str | None = typer.Option(
|
device: str | None = typer.Option(
|
||||||
None, "--device", "-d", show_default=False,
|
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(
|
compute_type: str | None = typer.Option(
|
||||||
None, "--compute-type", show_default=False,
|
None, "--compute-type", show_default=False,
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
|
|||||||
"openvino": {"model": "medium", "compute_type": "int8"},
|
"openvino": {"model": "medium", "compute_type": "int8"},
|
||||||
"openvino-gpu": {"model": "medium", "compute_type": "int8"},
|
"openvino-gpu": {"model": "medium", "compute_type": "int8"},
|
||||||
"openvino-cpu": {"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_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:
|
def find_config_file() -> Path | None:
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"""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, end, text):
|
||||||
|
self.start = start
|
||||||
|
self.end = end
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
**kwargs):
|
||||||
|
calls.append({
|
||||||
|
"model": model, "path": path, "quantization": quantization,
|
||||||
|
})
|
||||||
|
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 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"]
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""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 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()
|
||||||
|
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")
|
||||||
@@ -301,6 +301,7 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "faster-whisper" },
|
{ name = "faster-whisper" },
|
||||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ 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 = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "socksio" },
|
{ name = "socksio" },
|
||||||
@@ -317,6 +318,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "faster-whisper", specifier = ">=1.2.1" },
|
{ name = "faster-whisper", specifier = ">=1.2.1" },
|
||||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4" },
|
{ 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 = "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 = "rich" },
|
||||||
{ name = "socksio", specifier = ">=1.0.0" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "onnxruntime"
|
name = "onnxruntime"
|
||||||
version = "1.24.3"
|
version = "1.24.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user