Compare commits
5
Commits
f043f4e3b5
...
dae9d107c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae9d107c9 | ||
|
|
9f11c32979 | ||
|
|
995dbe61f8 | ||
|
|
136e93765c | ||
|
|
12e17020bf |
No files matched your search
@@ -8,7 +8,7 @@ transcribe meeting.mp4
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **Полностью локально** — данные не покидают машину
|
- **Полностью локально** — данные не покидают машину
|
||||||
- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), ONNX (CPU), OpenVINO CPU или CPU fallback
|
- **Авто-ускорение** — NVIDIA CUDA при наличии GPU, иначе ONNX на CPU
|
||||||
- **Батч-режим** — обработка нескольких файлов за один вызов
|
- **Батч-режим** — обработка нескольких файлов за один вызов
|
||||||
- **Из проводника Windows** — пункт Transcribe в меню «Отправить» ([установка](#контекстное-меню-проводника-windows))
|
- **Из проводника Windows** — пункт Transcribe в меню «Отправить» ([установка](#контекстное-меню-проводника-windows))
|
||||||
- **Markdown с таймкодами** — удобен для суммаризации ИИ
|
- **Markdown с таймкодами** — удобен для суммаризации ИИ
|
||||||
@@ -33,18 +33,24 @@ uv tool install git+https://github.com/dementev-dev/local-transcriber
|
|||||||
|
|
||||||
**3. Ускорение (ставится автоматически):**
|
**3. Ускорение (ставится автоматически):**
|
||||||
|
|
||||||
- **Intel GPU** (Arc, встроенная графика): работает через OpenVINO — ускорение в ~2x vs CPU
|
|
||||||
- **OpenVINO** (Intel/AMD x86 CPU): ставится автоматически на Linux и Windows — ускорение в 2-4x
|
|
||||||
- **NVIDIA CUDA** (GPU): если есть GPU — транскрипция в 5-10× быстрее
|
- **NVIDIA CUDA** (GPU): если есть GPU — транскрипция в 5-10× быстрее
|
||||||
- **Windows**: `winget install -e --id Nvidia.CUDA --version 12.9` (от администратора), перезапустить терминал
|
- **Windows**: `winget install -e --id Nvidia.CUDA --version 12.9` (от администратора), перезапустить терминал
|
||||||
- **Linux / WSL2**: работает из коробки (нужен только драйвер: `nvidia-smi`)
|
- **Linux / WSL2**: работает из коробки (нужен только драйвер: `nvidia-smi`)
|
||||||
|
- **Без NVIDIA GPU**: автоматически используется ONNX с GigaAM RNN-T на CPU.
|
||||||
|
Эта модель понимает только русскую речь
|
||||||
|
- **Другие языки без NVIDIA**: выбирайте Whisper явно —
|
||||||
|
`--device openvino-cpu --model medium` на x86 или `--device cpu --model medium`
|
||||||
|
на любой платформе
|
||||||
|
- **OpenVINO** для Intel GPU или x86 CPU остаётся доступен через явный
|
||||||
|
`--device openvino`, `--device openvino-gpu` или `--device openvino-cpu`
|
||||||
|
|
||||||
**4. Готово:**
|
**4. Готово:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
transcribe meeting.mp4
|
transcribe meeting.mp4
|
||||||
```
|
```
|
||||||
Модели скачиваются автоматически при первом запуске (~1.5 GB для medium), нужен доступ в интернет.
|
Модели скачиваются автоматически при первом запуске; размер зависит от выбранного
|
||||||
|
профиля, нужен доступ в интернет.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><code>transcribe: command not found</code></summary>
|
<summary><code>transcribe: command not found</code></summary>
|
||||||
@@ -103,7 +109,7 @@ HuggingFace Hub использует симлинки для экономии м
|
|||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Простой запуск (medium, русский, автодетект устройства)
|
# Простой запуск (CUDA medium или ONNX GigaAM RNN-T, язык ru)
|
||||||
transcribe meeting.mp4
|
transcribe meeting.mp4
|
||||||
|
|
||||||
# Указать язык
|
# Указать язык
|
||||||
@@ -180,11 +186,11 @@ transcribe --uninstall-menu
|
|||||||
|
|
||||||
| Опция | Сокращение | По умолчанию | Описание |
|
| Опция | Сокращение | По умолчанию | Описание |
|
||||||
|-------|-----------|-------------|----------|
|
|-------|-----------|-------------|----------|
|
||||||
| `--model` | `-m` | `medium` | Модель Whisper |
|
| `--model` | `-m` | medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX) | Модель распознавания |
|
||||||
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) |
|
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.); автоматический профиль без NVIDIA понимает только русский |
|
||||||
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
|
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
|
||||||
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) |
|
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) |
|
||||||
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений |
|
| `--compute-type` | — | float16 (CUDA) / int8 (ONNX/OpenVINO) / float32 (CPU) | Тип вычислений |
|
||||||
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
|
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
|
||||||
| `--force` | `-f` | — | Перезаписать существующие транскрипты |
|
| `--force` | `-f` | — | Перезаписать существующие транскрипты |
|
||||||
| `--verbose` | `-v` | — | Подробный вывод |
|
| `--verbose` | `-v` | — | Подробный вывод |
|
||||||
@@ -193,12 +199,14 @@ transcribe --uninstall-menu
|
|||||||
|
|
||||||
| | Linux / WSL2 | macOS | Windows |
|
| | Linux / WSL2 | macOS | Windows |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| CPU | ✅ | ✅ | ✅ |
|
| CPU через ONNX | ✅ авто | ✅ авто | ✅ авто |
|
||||||
| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто |
|
| OpenVINO (x86 CPU) | ✅ явно | — | ✅ явно |
|
||||||
| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто |
|
| OpenVINO (Intel GPU) | ✅ явно | — | ✅ явно |
|
||||||
| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно |
|
|
||||||
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
|
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
|
||||||
|
|
||||||
|
Данные по macOS основаны на доступности пакетов onnxruntime: прогонов на этой
|
||||||
|
платформе не было.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Linux / WSL2</summary>
|
<summary>Linux / WSL2</summary>
|
||||||
|
|
||||||
@@ -237,8 +245,10 @@ transcribe --uninstall-menu
|
|||||||
Дефолтные параметры можно задать в `.transcriber.toml`:
|
Дефолтные параметры можно задать в `.transcriber.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
model = "large-v3"
|
device = "openvino-cpu"
|
||||||
language = "en"
|
model = "large-v3-turbo"
|
||||||
|
compute_type = "int8"
|
||||||
|
language = "ru"
|
||||||
```
|
```
|
||||||
|
|
||||||
Порядок поиска:
|
Порядок поиска:
|
||||||
@@ -247,6 +257,13 @@ language = "en"
|
|||||||
|
|
||||||
Приоритет: **CLI-аргумент > конфиг > device-aware дефолт > встроенный дефолт**.
|
Приоритет: **CLI-аргумент > конфиг > device-aware дефолт > встроенный дефолт**.
|
||||||
|
|
||||||
|
При `device = "auto"` выбирается CUDA, если доступен `nvidia-smi`, иначе ONNX.
|
||||||
|
Явный `device` из CLI или конфига отключает этот автоматический выбор.
|
||||||
|
Каталоги моделей различаются между бэкендами, поэтому при закреплении `model`
|
||||||
|
в конфиге рекомендуется явно закрепить и совместимый `device`. То же с языком:
|
||||||
|
автоматический ONNX-профиль рассчитан на русскую речь, а для остальных языков
|
||||||
|
нужен Whisper — например, `device = "openvino-cpu"` и `model = "medium"`.
|
||||||
|
|
||||||
Дефолты зависят от устройства:
|
Дефолты зависят от устройства:
|
||||||
|
|
||||||
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU |
|
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU |
|
||||||
@@ -258,7 +275,7 @@ language = "en"
|
|||||||
## Модели и GPU
|
## Модели и GPU
|
||||||
|
|
||||||
Рекомендации:
|
Рекомендации:
|
||||||
- **По умолчанию для ONNX:** `gigaam-v3-e2e-rnnt` — читаемый русский текст с
|
- **По умолчанию без CUDA:** ONNX `gigaam-v3-e2e-rnnt` — читаемый русский текст с
|
||||||
пунктуацией почти без потери скорости относительно сырого `gigaam-v3`
|
пунктуацией почти без потери скорости относительно сырого `gigaam-v3`
|
||||||
- **Макс. качество (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`
|
||||||
@@ -270,6 +287,8 @@ language = "en"
|
|||||||
<details>
|
<details>
|
||||||
<summary>Таблица моделей</summary>
|
<summary>Таблица моделей</summary>
|
||||||
|
|
||||||
|
#### Whisper через faster-whisper (`--device cuda`, `--device cpu`)
|
||||||
|
|
||||||
| Модель | Размер на диске | VRAM (int8) | Скорость (GPU) | Качество |
|
| Модель | Размер на диске | VRAM (int8) | Скорость (GPU) | Качество |
|
||||||
|--------|----------------|-------------|----------------|----------|
|
|--------|----------------|-------------|----------------|----------|
|
||||||
| `tiny` | ~75 MB | ~1 GB | ★★★★★ | ★ |
|
| `tiny` | ~75 MB | ~1 GB | ★★★★★ | ★ |
|
||||||
@@ -277,7 +296,15 @@ language = "en"
|
|||||||
| `small` | ~460 MB | ~1.5 GB | ★★★ | ★★★ |
|
| `small` | ~460 MB | ~1.5 GB | ★★★ | ★★★ |
|
||||||
| `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 | ★ | ★★★★★ |
|
||||||
| `large-v3-turbo` | ~1 GB | — | ★★★★ | ★★★★ |
|
|
||||||
|
#### Whisper через OpenVINO (`--device openvino-cpu`, `--device openvino-gpu`)
|
||||||
|
|
||||||
|
Здесь те же модели Whisper, но предквантизированные, поэтому на диске они
|
||||||
|
занимают меньше места: `medium` int8 — 748 MB, `large-v3-turbo` int8 — 790 MB,
|
||||||
|
`large-v3-turbo` fp16 — 1552 MB. Модель `large-v3-turbo` доступна только здесь:
|
||||||
|
faster-whisper её не поддерживает, и запуск с `--device cuda` завершится
|
||||||
|
ошибкой. Полный список репозиториев —
|
||||||
|
[docs/gpu.md](docs/gpu.md#доступные-openvino-модели).
|
||||||
|
|
||||||
#### ONNX-модели (`--device onnx`)
|
#### ONNX-модели (`--device onnx`)
|
||||||
|
|
||||||
@@ -310,6 +337,9 @@ language = "en"
|
|||||||
кыргызский и узбекский внутри одной записи. `onnx-asr` не передаёт этим моделям
|
кыргызский и узбекский внутри одной записи. `onnx-asr` не передаёт этим моделям
|
||||||
подсказку языка, поэтому `--language` не управляет выбором языка.
|
подсказку языка, поэтому `--language` не управляет выбором языка.
|
||||||
|
|
||||||
|
Если модель не понимает запрошенный язык, CLI предупреждает об этом до начала
|
||||||
|
распознавания и подсказывает совместимый профиль, но работу не прерывает.
|
||||||
|
|
||||||
Для моделей из таблицы опубликованы `int8` и `float32`. Если неявный
|
Для моделей из таблицы опубликованы `int8` и `float32`. Если неявный
|
||||||
device-aware дефолт недоступен для выбранной модели, CLI сообщит о подстановке
|
device-aware дефолт недоступен для выбранной модели, CLI сообщит о подстановке
|
||||||
доступного варианта. Явное значение из `--compute-type` или
|
доступного варианта. Явное значение из `--compute-type` или
|
||||||
@@ -351,7 +381,7 @@ device-aware дефолт недоступен для выбранной мод
|
|||||||
|
|
||||||
- **Дата транскрипции**: 2026-03-17 14:30:05
|
- **Дата транскрипции**: 2026-03-17 14:30:05
|
||||||
- **Модель**: large-v3
|
- **Модель**: large-v3
|
||||||
- **Язык**: ru (detected)
|
- **Язык**: ru (задан явно)
|
||||||
- **Длительность**: 01:23:45
|
- **Длительность**: 01:23:45
|
||||||
- **Устройство**: CUDA (NVIDIA GeForce RTX 3060)
|
- **Устройство**: CUDA (NVIDIA GeForce RTX 3060)
|
||||||
|
|
||||||
@@ -366,6 +396,14 @@ device-aware дефолт недоступен для выбранной мод
|
|||||||
Близкие по времени сегменты автоматически объединяются в абзацы (пауза > 2 сек или длительность > 60 сек разделяет абзацы).
|
Близкие по времени сегменты автоматически объединяются в абзацы (пауза > 2 сек или длительность > 60 сек разделяет абзацы).
|
||||||
Таймкоды: `MM:SS.ss`, для записей длиннее 1 часа — `HH:MM:SS.ss`.
|
Таймкоды: `MM:SS.ss`, для записей длиннее 1 часа — `HH:MM:SS.ss`.
|
||||||
|
|
||||||
|
В скобках после языка указан его источник:
|
||||||
|
|
||||||
|
- `задан явно` — язык взят из `--language` или конфига;
|
||||||
|
- `определён автоматически` — распознан моделью при `--language auto`;
|
||||||
|
- `из профиля модели` — у модели всего один язык, как у GigaAM.
|
||||||
|
|
||||||
|
Если язык определить не удалось, строка выглядит так: `- **Язык**: не определён`.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Поддерживаемые форматы
|
## Поддерживаемые форматы
|
||||||
|
|||||||
+34
-32
@@ -24,8 +24,9 @@ transcribe meeting-2026-03-17.mp4
|
|||||||
### 3.1. Основной flow
|
### 3.1. Основной flow
|
||||||
|
|
||||||
1. Пользователь вызывает CLI, передаёт путь к файлу (или glob-маску, post-MVP)
|
1. Пользователь вызывает CLI, передаёт путь к файлу (или glob-маску, post-MVP)
|
||||||
2. Проверка: ffmpeg доступен в PATH
|
2. Файл валидируется по пути, размеру и расширению
|
||||||
3. Файл передаётся в faster-whisper (он сам обрабатывает и аудио, и видео через libav/ffmpeg — отдельное извлечение аудиодорожки не нужно)
|
3. По `device` выбирается backend; аудио декодируется через PyAV без отдельного
|
||||||
|
извлечения дорожки
|
||||||
4. Результат форматируется в markdown с таймкодами
|
4. Результат форматируется в markdown с таймкодами
|
||||||
5. Файл `<имя>-transcript.md` сохраняется рядом с исходным (кодировка: UTF-8)
|
5. Файл `<имя>-transcript.md` сохраняется рядом с исходным (кодировка: UTF-8)
|
||||||
|
|
||||||
@@ -41,16 +42,16 @@ transcribe meeting-2026-03-17.mp4
|
|||||||
transcribe <путь_к_файлу> [опции]
|
transcribe <путь_к_файлу> [опции]
|
||||||
|
|
||||||
Опции:
|
Опции:
|
||||||
--model, -m Модель Whisper (tiny|base|small|medium|large-v3)
|
--model, -m Модель распознавания
|
||||||
По умолчанию: large-v3
|
По умолчанию: medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX)
|
||||||
--language, -l Язык (ru|en|auto)
|
--language, -l Язык (ru|en|auto)
|
||||||
По умолчанию: auto (автодетект)
|
По умолчанию: ru
|
||||||
--output, -o Путь к выходному файлу
|
--output, -o Путь к выходному файлу
|
||||||
По умолчанию: <input_stem>-transcript.md
|
По умолчанию: <input_stem>-transcript.md
|
||||||
--device, -d Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu)
|
--device, -d Устройство (auto|cpu|cuda|onnx|openvino|openvino-gpu|openvino-cpu)
|
||||||
По умолчанию: auto (CUDA → OpenVINO GPU → OpenVINO CPU → CPU)
|
По умолчанию: auto (CUDA при наличии, иначе ONNX CPU)
|
||||||
--compute-type Тип вычислений (float16|int8|int8_float16|float32)
|
--compute-type Тип вычислений
|
||||||
По умолчанию: int8 (универсален для GPU 4-8 GB и CPU)
|
По умолчанию: float16 (CUDA) / int8 (ONNX)
|
||||||
--verbose, -v Подробный вывод (прогресс сегментов)
|
--verbose, -v Подробный вывод (прогресс сегментов)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
|
|
||||||
- **Дата транскрипции**: 2026-03-17 14:30:05
|
- **Дата транскрипции**: 2026-03-17 14:30:05
|
||||||
- **Модель**: large-v3
|
- **Модель**: large-v3
|
||||||
- **Язык**: ru (detected) / ru (forced)
|
- **Язык**: ru (задан явно) / ru (определён автоматически)
|
||||||
- **Длительность**: 01:23:45
|
- **Длительность**: 01:23:45
|
||||||
- **Устройство**: CUDA (NVIDIA GeForce RTX 3060)
|
- **Устройство**: CUDA (NVIDIA GeForce RTX 3060)
|
||||||
|
|
||||||
@@ -82,16 +83,17 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
**Правила форматирования**:
|
**Правила форматирования**:
|
||||||
- Таймкоды в формате `[MM:SS.ss - MM:SS.ss]` (минуты:секунды.сотые)
|
- Таймкоды в формате `[MM:SS.ss - MM:SS.ss]` (минуты:секунды.сотые)
|
||||||
- Для записей длиннее 1 часа — `[HH:MM:SS.ss - HH:MM:SS.ss]`
|
- Для записей длиннее 1 часа — `[HH:MM:SS.ss - HH:MM:SS.ss]`
|
||||||
- Каждый сегмент — отдельный абзац
|
- Соседние сегменты объединяются в абзац до паузы 2 секунды или длительности 60 секунд
|
||||||
- Метаданные в шапке файла
|
- Метаданные в шапке файла
|
||||||
- Пустая строка между сегментами для читаемости
|
- Пустая строка между абзацами для читаемости
|
||||||
|
|
||||||
### 3.4. Поддерживаемые форматы
|
### 3.4. Поддерживаемые форматы
|
||||||
|
|
||||||
**Аудио**: mp3, wav, flac, ogg, m4a, wma, aac
|
**Аудио**: mp3, wav, flac, ogg, m4a, wma, aac
|
||||||
**Видео**: mp4, mkv, avi, mov, webm, ts
|
**Видео**: mp4, mkv, avi, mov, webm, ts
|
||||||
|
|
||||||
Определение типа — по расширению. Фактическое декодирование выполняет ffmpeg внутри faster-whisper; если формат не поддерживается, ошибка будет от ffmpeg.
|
Определение типа — по расширению. Фактическое декодирование выполняет PyAV с
|
||||||
|
встроенными библиотеками FFmpeg; системная установка `ffmpeg` не требуется.
|
||||||
|
|
||||||
## 4. Нефункциональные требования
|
## 4. Нефункциональные требования
|
||||||
|
|
||||||
@@ -102,6 +104,8 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
| RTX 3060 + large-v3 | ~10-15x (1 час аудио ≈ 4-6 мин) |
|
| RTX 3060 + large-v3 | ~10-15x (1 час аудио ≈ 4-6 мин) |
|
||||||
| RTX 4050 + large-v3 | ~12-18x (1 час аудио ≈ 3-5 мин) |
|
| RTX 4050 + large-v3 | ~12-18x (1 час аудио ≈ 3-5 мин) |
|
||||||
| Quadro M3000M + large-v3 | ~3-5x (1 час аудио ≈ 12-20 мин) |
|
| Quadro M3000M + large-v3 | ~3-5x (1 час аудио ≈ 12-20 мин) |
|
||||||
|
| CPU + ONNX GigaAM RNN-T | ~10-14x (1 час аудио ≈ 4-6 мин) |
|
||||||
|
| CPU + OpenVINO Turbo INT8 | ~7-10x (1 час аудио ≈ 6-9 мин) |
|
||||||
| CPU (modern) + large-v3 | ~0.5-1x (1 час аудио ≈ 60-120 мин) |
|
| CPU (modern) + large-v3 | ~0.5-1x (1 час аудио ≈ 60-120 мин) |
|
||||||
| CPU + small | ~3-5x (1 час аудио ≈ 12-20 мин) |
|
| CPU + small | ~3-5x (1 час аудио ≈ 12-20 мин) |
|
||||||
|
|
||||||
@@ -112,23 +116,22 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
| RTX 3060 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор |
|
| RTX 3060 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор |
|
||||||
| RTX 4050 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор |
|
| RTX 4050 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор |
|
||||||
| Quadro M3000M | 4 GB | ✅ | ⚠️ может OOM | int8 обязательно |
|
| Quadro M3000M | 4 GB | ✅ | ⚠️ может OOM | int8 обязательно |
|
||||||
| Без GPU | — | CPU int8 | — | int8 на CPU |
|
| Без GPU | — | ONNX GigaAM INT8 | — | auto выбирает ONNX |
|
||||||
|
|
||||||
Дефолт `int8` выбран как универсальный: работает на всех GPU от 4 GB и на CPU, при минимальной потере качества относительно float16.
|
Device-aware дефолты выбирают `float16` для CUDA и `int8` для ONNX/OpenVINO.
|
||||||
|
|
||||||
### 4.2. Требования к окружению
|
### 4.2. Требования к окружению
|
||||||
|
|
||||||
- Python ≥ 3.13
|
- Python ≥ 3.13
|
||||||
- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов)
|
|
||||||
- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001)
|
- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001)
|
||||||
- Дисковое пространство для моделей: ~3 GB (large-v3)
|
- Дисковое пространство для моделей: зависит от выбранного backend и модели
|
||||||
- Выходные файлы: UTF-8 (явная кодировка при записи)
|
- Выходные файлы: UTF-8 (явная кодировка при записи)
|
||||||
|
|
||||||
### 4.3. Кроссплатформенность
|
### 4.3. Кроссплатформенность
|
||||||
|
|
||||||
- Linux: нативный запуск
|
- Linux: нативный запуск
|
||||||
- Windows: нативный Python или WSL2
|
- Windows: нативный Python или WSL2
|
||||||
- macOS: не приоритет, но faster-whisper поддерживает CPU-режим
|
- macOS: ONNX CPU в auto-режиме; FasterWhisper CPU доступен явно
|
||||||
|
|
||||||
## 5. Технический стек
|
## 5. Технический стек
|
||||||
|
|
||||||
@@ -136,19 +139,18 @@ transcribe <путь_к_файлу> [опции]
|
|||||||
|---------------------|-------------------------------------------------|
|
|---------------------|-------------------------------------------------|
|
||||||
| Язык | Python 3.13+ |
|
| Язык | Python 3.13+ |
|
||||||
| Управление проектом | uv (pyproject.toml) |
|
| Управление проектом | uv (pyproject.toml) |
|
||||||
| Распознавание речи | faster-whisper (CTranslate2 backend) |
|
| Распознавание речи | faster-whisper, ONNX Runtime, OpenVINO GenAI |
|
||||||
| Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) |
|
| Медиа-декодирование | PyAV со встроенными библиотеками FFmpeg |
|
||||||
| CLI-фреймворк | typer |
|
| CLI-фреймворк | typer |
|
||||||
| Прогресс | rich (progress bar + статус) |
|
| Прогресс | rich (progress bar + статус) |
|
||||||
|
|
||||||
### 5.1. Почему faster-whisper
|
### 5.1. Почему несколько backend
|
||||||
|
|
||||||
- В 4× быстрее оригинального OpenAI Whisper при том же качестве
|
- faster-whisper оптимизирован для NVIDIA CUDA и поддерживает много языков
|
||||||
- Меньше потребление VRAM (large-v3 влезает в 6 GB с float16/int8)
|
- ONNX GigaAM RNN-T даёт быстрый читаемый результат на CPU
|
||||||
- Нативный Python API, без Docker
|
- OpenVINO предоставляет явные профили для Intel GPU и x86 CPU
|
||||||
- Поддержка CPU fallback из коробки
|
- Все backend работают локально через Python API, без Docker и облачных ключей
|
||||||
- Активное сообщество, регулярные обновления
|
- Модели загружаются автоматически и кешируются локально
|
||||||
- Автоматическая загрузка моделей из Hugging Face Hub
|
|
||||||
|
|
||||||
### 5.2. Структура проекта
|
### 5.2. Структура проекта
|
||||||
|
|
||||||
@@ -160,9 +162,10 @@ local-transcriber/
|
|||||||
│ └── local_transcriber/
|
│ └── local_transcriber/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── cli.py # CLI entry point (typer)
|
│ ├── cli.py # CLI entry point (typer)
|
||||||
│ ├── transcriber.py # Обёртка над faster-whisper
|
│ ├── transcriber.py # Оркестрация backend и fallback
|
||||||
|
│ ├── backends/ # Адаптеры FasterWhisper, ONNX и OpenVINO
|
||||||
│ ├── formatter.py # Форматирование в markdown
|
│ ├── formatter.py # Форматирование в markdown
|
||||||
│ └── utils.py # Проверки (ffmpeg), определение device и т.д.
|
│ └── utils.py # Проверки файлов и определение device
|
||||||
└── tests/
|
└── tests/
|
||||||
└── ...
|
└── ...
|
||||||
```
|
```
|
||||||
@@ -173,11 +176,10 @@ local-transcriber/
|
|||||||
|------|---------|-----------|
|
|------|---------|-----------|
|
||||||
| Качество распознавания русского текста | Среднее | large-v3 хорошо справляется с ru; при проблемах — попробовать `--language ru` вместо auto |
|
| Качество распознавания русского текста | Среднее | large-v3 хорошо справляется с ru; при проблемах — попробовать `--language ru` вместо auto |
|
||||||
| Нет разделения по спикерам | Низкое | Осознанно выведено за скоуп MVP; добавление diarization (pyannote.audio) — возможное расширение |
|
| Нет разделения по спикерам | Низкое | Осознанно выведено за скоуп MVP; добавление diarization (pyannote.audio) — возможное расширение |
|
||||||
| ffmpeg отсутствует в системе | Высокое | Проверка при старте + понятное сообщение об ошибке с инструкцией по установке |
|
|
||||||
| Первый запуск: долгая загрузка модели | Низкое | Прогресс-бар при скачивании; модели кешируются в `~/.cache/huggingface/` |
|
| Первый запуск: долгая загрузка модели | Низкое | Прогресс-бар при скачивании; модели кешируются в `~/.cache/huggingface/` |
|
||||||
| CUDA несовместимость на Windows | Среднее | Автоматический fallback на CPU + предупреждение; в README — инструкция по CUDA |
|
| CUDA несовместимость на Windows | Среднее | Автоматический fallback на CPU + предупреждение; в README — инструкция по CUDA |
|
||||||
| Большие файлы (>2 часов) | Низкое | faster-whisper работает потоково, не грузит всё в память |
|
| Большие файлы (>2 часов) | Среднее | Учитывать память выбранного backend; чанкование рассматривается отдельно |
|
||||||
| OOM на GPU с 4 GB VRAM | Среднее | Дефолт int8 (~2.5 GB); при OOM — fallback на CPU с предупреждением |
|
| OOM на GPU с 4 GB VRAM | Среднее | CUDA использует float16; при OOM — fallback на CPU с предупреждением или явный более лёгкий профиль |
|
||||||
| Файл без речи (тишина, музыка, шум) | Низкое | Создаётся транскрипт с шапкой метаданных и `*Речь не обнаружена.*` в теле + предупреждение в stderr |
|
| Файл без речи (тишина, музыка, шум) | Низкое | Создаётся транскрипт с шапкой метаданных и `*Речь не обнаружена.*` в теле + предупреждение в stderr |
|
||||||
|
|
||||||
## 7. Вне скоупа MVP
|
## 7. Вне скоупа MVP
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
**Статус**: Принято
|
**Статус**: Принято
|
||||||
**Дата**: 2026-03-21
|
**Дата**: 2026-03-21
|
||||||
|
**Обновлено**: 2026-08-12
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ shared libraries. Ни то, ни другое не должно происхо
|
|||||||
Вместо отдельного `--backend` флага устройство само определяет бэкенд:
|
Вместо отдельного `--backend` флага устройство само определяет бэкенд:
|
||||||
- `cuda`, `cpu` → FasterWhisperBackend
|
- `cuda`, `cpu` → FasterWhisperBackend
|
||||||
- `openvino` → OpenVINOBackend
|
- `openvino` → OpenVINOBackend
|
||||||
- `auto` → CUDA (nvidia-smi) → OpenVINO (import check + x86) → CPU
|
- `onnx` → OnnxAsrBackend
|
||||||
|
- `auto` → CUDA при наличии `nvidia-smi`, иначе ONNX на CPU
|
||||||
|
|
||||||
### load_model() — единственный владелец pipeline
|
### load_model() — единственный владелец pipeline
|
||||||
|
|
||||||
@@ -71,18 +73,19 @@ OpenVINO модели предквантизированы (int8/fp16), compute_
|
|||||||
- Из дефолтов: для large-v3 автоматически выбирается fp16 (стабильнее по качеству)
|
- Из дефолтов: для large-v3 автоматически выбирается fp16 (стабильнее по качеству)
|
||||||
- Несуществующая пара (model + compute_type) при явном выборе → ошибка
|
- Несуществующая пара (model + compute_type) при явном выборе → ошибка
|
||||||
|
|
||||||
### Обе зависимости по умолчанию
|
### Зависимости бэкендов по умолчанию
|
||||||
|
|
||||||
faster-whisper (~37MB) и openvino-genai (~69MB) ставятся вместе — суммарно ~106MB,
|
faster-whisper, onnx-asr/onnxruntime и openvino-genai ставятся вместе. Модели
|
||||||
приемлемо. Модели скачиваются только для активного бэкенда. CUDA (nvidia-cublas-cu12,
|
скачиваются только для активного бэкенда. CUDA (`nvidia-cublas-cu12`) остаётся
|
||||||
~554MB) остаётся conditional (Linux x86_64). OpenVINO — conditional (x86_64/AMD64, не macOS).
|
conditional для Linux x86_64. OpenVINO — conditional для x86_64/AMD64, кроме
|
||||||
|
macOS; ONNX обеспечивает автоматический CPU-путь на остальных платформах.
|
||||||
|
|
||||||
## Последствия
|
## Последствия
|
||||||
|
|
||||||
- Обратная совместимость: `transcribe()` сохранён; `load_model()` изменил сигнатуру (возвращает 4-tuple вместо 2-tuple, добавлен `compute_type_explicit`)
|
- Обратная совместимость: `transcribe()` сохранён; `load_model()` изменил сигнатуру (возвращает 4-tuple вместо 2-tuple, добавлен `compute_type_explicit`)
|
||||||
- Новый бэкенд добавляется одним файлом в `backends/` + регистрацией в `__init__.py`
|
- Новый бэкенд добавляется одним файлом в `backends/` + регистрацией в `__init__.py`
|
||||||
- Модели скачиваются по запросу — CUDA пользователь не качает OpenVINO модели, и наоборот
|
- Модели скачиваются по запросу — CUDA пользователь не качает OpenVINO модели, и наоборот
|
||||||
- ARM и macOS: OpenVINO не ставится (platform markers), работает CPU через faster-whisper
|
- ARM и macOS: OpenVINO не ставится (platform markers), auto использует ONNX
|
||||||
|
|
||||||
## Отклонённые альтернативы
|
## Отклонённые альтернативы
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Статус**: Принято
|
**Статус**: Принято
|
||||||
**Дата**: 2026-04-25
|
**Дата**: 2026-04-25
|
||||||
**Обновлено**: 2026-08-11
|
**Обновлено**: 2026-08-12
|
||||||
|
|
||||||
## Контекст
|
## Контекст
|
||||||
|
|
||||||
@@ -101,10 +101,16 @@ Mm-hmm-редукция и иностранные вставки **не обна
|
|||||||
|
|
||||||
## Решение
|
## Решение
|
||||||
|
|
||||||
**Принять onnx-asr как экспериментальный бэкенд с явным `--device onnx`.
|
**Принять onnx-asr как CPU-бэкенд автоматического профиля.
|
||||||
GigaAM v3 E2E RNN-T — модель по умолчанию для русских встреч на CPU.**
|
GigaAM v3 E2E RNN-T — модель по умолчанию на машинах без CUDA.**
|
||||||
|
|
||||||
Бэкенд **не в auto-detect** — только при явном указании пользователем (политика experimental backend, как для openvino).
|
Порядок `--device auto`: CUDA при наличии `nvidia-smi`, иначе ONNX. OpenVINO и
|
||||||
|
FasterWhisper CPU остаются доступными через явный CLI-аргумент или конфиг.
|
||||||
|
|
||||||
|
GigaAM понимает только русскую речь, поэтому для остальных языков автоматический
|
||||||
|
профиль не годится — нужен явный Whisper. Несовместимый язык работу не
|
||||||
|
останавливает: CLI предупреждает о нём до начала распознавания и называет
|
||||||
|
подходящий профиль.
|
||||||
|
|
||||||
Модели:
|
Модели:
|
||||||
- **`gigaam-v3-e2e-rnnt`** — модель по умолчанию: практически равна обычному
|
- **`gigaam-v3-e2e-rnnt`** — модель по умолчанию: практически равна обычному
|
||||||
@@ -122,16 +128,20 @@ GigaAM v3 E2E RNN-T — модель по умолчанию для русски
|
|||||||
- Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах).
|
- Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах).
|
||||||
- Пользователи ONNX без явного `--model` получают пунктуацию и нормализацию
|
- Пользователи ONNX без явного `--model` получают пунктуацию и нормализацию
|
||||||
RNN-T; более точный сырой CTC остаётся доступен как `--model gigaam-v3`.
|
RNN-T; более точный сырой CTC остаётся доступен как `--model gigaam-v3`.
|
||||||
- Whisper medium (`--device openvino-cpu`) **остаётся допустимым** для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с тихими участками он галлюцинирует целыми блоками — этот риск зафиксирован, но решение не выводит OpenVINO из списка дефолтов (часть пользователей всё ещё нуждается в пунктуации, и для коротких файлов галлюцинации не воспроизводятся).
|
- Whisper medium (`--device openvino-cpu`) **остаётся допустимым явным профилем**
|
||||||
|
для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с
|
||||||
|
тихими участками он может галлюцинировать целыми блоками.
|
||||||
- Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится.
|
- Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится.
|
||||||
- GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU).
|
- GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU).
|
||||||
- Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта.
|
- Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта.
|
||||||
|
|
||||||
## Открытые вопросы / следующие шаги
|
## Открытые вопросы / следующие шаги
|
||||||
|
|
||||||
- **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю.
|
- **Галлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю.
|
||||||
- **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация.
|
- **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация.
|
||||||
- **Auto-detect onnx**: после стабилизации в production-использовании (несколько недель) — рассмотреть включение в auto-detect как первый CPU-бэкенд (ниже CUDA, выше OpenVINO).
|
- Проверить качество на других языках и при необходимости дополнить
|
||||||
|
многоязычные рекомендации. Сама автоматическая политика от языка не зависит:
|
||||||
|
она предупреждает о несовместимости, но профиль за пользователя не меняет.
|
||||||
|
|
||||||
## Отклонённые альтернативы
|
## Отклонённые альтернативы
|
||||||
|
|
||||||
@@ -139,5 +149,4 @@ GigaAM v3 E2E RNN-T — модель по умолчанию для русски
|
|||||||
|---|---|
|
|---|---|
|
||||||
| NeMo Parakeet напрямую (без onnx-asr) | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI |
|
| NeMo Parakeet напрямую (без onnx-asr) | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI |
|
||||||
| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом |
|
| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом |
|
||||||
| GigaAM как auto-detect default | Экспериментальный бэкенд, политика — не сюрпризить существующих пользователей; включение в auto-detect — после периода стабилизации |
|
|
||||||
| Parakeet-v3 как multilingual default | Воспроизведённые проблемы из ADR-005 (Mm-hmm-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте |
|
| Parakeet-v3 как multilingual default | Воспроизведённые проблемы из ADR-005 (Mm-hmm-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте |
|
||||||
+10
-4
@@ -252,13 +252,19 @@ SQL`), словарь пользовательский в `.transcriber.toml`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Включение `onnx` в `--device auto`
|
### `large-v3-turbo` для faster-whisper
|
||||||
|
|
||||||
**Что:** После периода стабилизации `--device onnx` (несколько недель production-использования без жалоб) — рассмотреть включение в auto-detect chain.
|
**Что:** Добавить `large-v3-turbo` в каталог faster-whisper, чтобы модель была
|
||||||
|
доступна с `--device cuda` и `--device cpu`, а не только через OpenVINO.
|
||||||
|
|
||||||
**Порядок в chain (предложение):** CUDA → onnx (если CPU x86_64) → OpenVINO → CPU.
|
**Почему:** На CPU turbo INT8 оказался быстрее `medium` и точнее по WER
|
||||||
|
([сравнение от 2026-08-12](benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md)),
|
||||||
|
и владельцам NVIDIA этот профиль сейчас недоступен без ручного указания
|
||||||
|
репозитория HuggingFace.
|
||||||
|
|
||||||
**Почему откладывается:** политика experimental backend — не сюрпризить существующих пользователей до накопления опыта. Источник: [ADR-006](adr/006-onnx-asr-backend.md#решение).
|
**Почему откладывается:** каталог faster-whisper в проекте собран из
|
||||||
|
репозиториев `Systran`, и для turbo нужно сначала выяснить, какая CT2-сборка
|
||||||
|
годится, проверить её происхождение и качество, и только потом вносить в код.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Дата:** 2026-08-12
|
**Дата:** 2026-08-12
|
||||||
|
|
||||||
**Статус:** завершённое сравнение на трёх русскоязычных встречах
|
**Статус:** завершённое сравнение на трёх русскоязычных встречах и двух CPU
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
|
|
||||||
@@ -168,6 +168,158 @@ FineBI» вместо учётной записи, `NiFi` как `I5`, `edge cas
|
|||||||
проверяющие не нашли. Общий недостаток всех профилей — отсутствие надёжного
|
проверяющие не нашли. Общий недостаток всех профилей — отсутствие надёжного
|
||||||
разделения по участникам, что ограничивает пригодность для формального протокола.
|
разделения по участникам, что ограничивает пригодность для формального протокола.
|
||||||
|
|
||||||
|
## Повторный прогон на AMD Ryzen 7 8845H
|
||||||
|
|
||||||
|
Повторный прогон выполнен на втором ноутбуке тем же коммитом, на тех же шести
|
||||||
|
файлах и с теми же параметрами. Размеры и SHA-256 всех файлов совпали с
|
||||||
|
манифестом. Результаты Intel выше не перезаписывались.
|
||||||
|
|
||||||
|
### Оборудование и условия
|
||||||
|
|
||||||
|
- ноутбук Lenovo 83D5;
|
||||||
|
- AMD Ryzen 7 8845H, 8 ядер / 16 логических процессоров;
|
||||||
|
- 32 ГиБ LPDDR5X, четыре чипа Micron, фактическая скорость 6400 MT/s;
|
||||||
|
- Windows 11 Корпоративная, сборка 26200;
|
||||||
|
- схема питания «Сбалансированная»;
|
||||||
|
- OpenVINO 2026.3.0, OpenVINO GenAI и Tokenizers 2026.3.0.0;
|
||||||
|
- Python 3.13.13;
|
||||||
|
- язык во всех командах задан явно: `--language ru`;
|
||||||
|
- кэш каждой модели предварительно прогрет, скачивание не включено во время;
|
||||||
|
- загрузка CPU и свободная память опрашивались раз в две секунды.
|
||||||
|
|
||||||
|
Во время всех прогонов ноутбук оставался пригодным для интерактивной работы.
|
||||||
|
Минимум свободной памяти ниже, чем на Intel, из-за 32 ГиБ физической памяти и
|
||||||
|
фоновой нагрузки; это показатель всей системы, а не потребления одной модели.
|
||||||
|
|
||||||
|
### Сводка по трём записям
|
||||||
|
|
||||||
|
| Профиль | Время | RTFx | WER | Пунктуация / 1000 слов | Средний / пиковый CPU | Минимум свободной памяти |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| OpenVINO medium INT8 | 438,5 с | 5,97× | 22,9% | 193 | 59,2% / 89% | 6,65 ГиБ |
|
||||||
|
| **OpenVINO large-v3-turbo INT8** | **267,4 с** | **9,79×** | **19,6%** | 108 | 57,4% / 72% | 8,93 ГиБ |
|
||||||
|
| OpenVINO large-v3-turbo FP16 | 333,6 с | 7,85× | 20,1% | 114 | 59,0% / 78% | 8,39 ГиБ |
|
||||||
|
| **ONNX GigaAM v3 E2E RNN-T INT8** | **194,8 с** | **13,44×** | 26,9% | **290** | 56,2% / 82% | 11,71 ГиБ |
|
||||||
|
|
||||||
|
Для `medium` на RSQM получились три измеряемых времени: 158,8, 118,3 и
|
||||||
|
129,3 секунды. Все три транскрипта и отдельный прогревочный проход содержательно
|
||||||
|
совпали. В таблице использована медиана 129,3 секунды, чтобы не выбирать
|
||||||
|
произвольно единичный фоновый выброс.
|
||||||
|
|
||||||
|
### По отдельным записям
|
||||||
|
|
||||||
|
| Запись | Профиль | Время | RTFx | WER | Пунктуация / 1000 слов |
|
||||||
|
|---|---|---:|---:|---:|---:|
|
||||||
|
| BDMA | medium INT8 | 140,3 с | 6,35× | 25,0% | 202 |
|
||||||
|
| BDMA | turbo INT8 | **88,8 с** | **10,03×** | 24,5% | 55 |
|
||||||
|
| BDMA | turbo FP16 | 109,1 с | 8,16× | **24,4%** | 61 |
|
||||||
|
| BDMA | GigaAM RNN-T INT8 | **67,5 с** | **13,21×** | 29,0% | **326** |
|
||||||
|
| RSQM | medium INT8 | 129,3 с | 6,53× | 20,2% | **175** |
|
||||||
|
| RSQM | turbo INT8 | **82,1 с** | **10,28×** | **16,7%** | 61 |
|
||||||
|
| RSQM | turbo FP16 | 104,6 с | 8,07× | 20,4% | 48 |
|
||||||
|
| RSQM | GigaAM RNN-T INT8 | **58,6 с** | **14,41×** | 25,4% | **286** |
|
||||||
|
| DataCat | medium INT8 | 168,9 с | 5,22× | 23,2% | **199** |
|
||||||
|
| DataCat | turbo INT8 | **96,5 с** | **9,14×** | 18,0% | 178 |
|
||||||
|
| DataCat | turbo FP16 | 119,8 с | 7,36× | **16,8%** | 197 |
|
||||||
|
| DataCat | GigaAM RNN-T INT8 | **68,7 с** | **12,83×** | 26,4% | **265** |
|
||||||
|
|
||||||
|
### Повторы `medium` на паузах
|
||||||
|
|
||||||
|
На линии 2026.0 один контрольный прогон RSQM на Intel содержал 11 одинаковых
|
||||||
|
последовательных сегментов. После обновления на Ryzen выполнены четыре прохода
|
||||||
|
того же файла: один прогревочный и три измеряемых. Во всех четырёх получены
|
||||||
|
одинаковые 152 сегмента, объединённые форматтером в 17 абзацев. Детектор серий
|
||||||
|
из четырёх и более одинаковых сегментов не сработал, различий в содержательной
|
||||||
|
части файлов нет, ложного хвоста также нет.
|
||||||
|
|
||||||
|
На этом контрольном файле повтор после OpenVINO 2026.3 устойчиво не
|
||||||
|
воспроизводится на двух машинах. Это сильнее единичного успешного запуска, но
|
||||||
|
не доказывает устранение всего класса Whisper-галлюцинаций на тишине: набор
|
||||||
|
содержит только одну ранее проблемную запись.
|
||||||
|
|
||||||
|
### Отличия Ryzen от Intel
|
||||||
|
|
||||||
|
- `medium` показал практически одинаковое суммарное время: 438,5 против
|
||||||
|
436,7 секунды.
|
||||||
|
- Turbo INT8 на Ryzen быстрее на 27%: 267,4 против 365,5 секунды; FP16 быстрее
|
||||||
|
на 39%: 333,6 против 545,0 секунды.
|
||||||
|
- GigaAM на Ryzen быстрее на 27%: 194,8 против 265,7 секунды.
|
||||||
|
- На Ryzen Turbo INT8 ускоряет обработку относительно `medium` на 39%, а
|
||||||
|
GigaAM — на 56%.
|
||||||
|
- Численные WER на Ryzen ниже у всех четырёх профилей. Поскольку файлы,
|
||||||
|
версии библиотек и заявленная методика совпадают, но сами выходы различаются.
|
||||||
|
Сравнение характеризует всю среду выполнения; приписывать разницу только
|
||||||
|
архитектуре CPU без отдельного исследования нельзя.
|
||||||
|
- Пунктуация Turbo остаётся нестабильной: на Ryzen она лучше результата Intel
|
||||||
|
для RSQM, но всё ещё значительно реже, чем у `medium` и GigaAM.
|
||||||
|
|
||||||
|
### Независимая оценка качества на Ryzen
|
||||||
|
|
||||||
|
Как и на первом ноутбуке, два субагента читали полные транскрипты вслепую, без
|
||||||
|
названий профилей и таблицы численных метрик. Их оценки разошлись по ожидаемой
|
||||||
|
причине: один ставил выше готовность текста к чтению, второй — точность
|
||||||
|
технических деталей.
|
||||||
|
|
||||||
|
| Профиль | Читаемость | Для конспекта | Для протокола |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| ONNX GigaAM RNN-T INT8 | **3/5** | **4/5** | **3/5** |
|
||||||
|
| OpenVINO medium INT8 | **3/5** | 3/5 | 2/5 |
|
||||||
|
| OpenVINO turbo FP16 | 2/5 | 3/5 | 2/5 |
|
||||||
|
| OpenVINO turbo INT8 | 2/5 | 2/5 | 2/5 |
|
||||||
|
|
||||||
|
По читаемости GigaAM снова занял первое место: он лучше размечает смысловые
|
||||||
|
границы, сохраняет бытовые и организационные реплики и требует меньше ручной
|
||||||
|
чистки для чернового конспекта. `medium` оказался вторым. У обоих Turbo начало
|
||||||
|
нескольких записей превращается в длинные блоки нижнего регистра почти без
|
||||||
|
пунктуации. Ни один вариант не признан готовым формальным протоколом без сверки.
|
||||||
|
|
||||||
|
| Профиль | Смысл | Термины | Полнота | Нет повторов и выдумок |
|
||||||
|
|---|---:|---:|---:|---:|
|
||||||
|
| OpenVINO turbo FP16 | **4/5** | **4/5** | 4/5 | **4/5** |
|
||||||
|
| OpenVINO turbo INT8 | 4/5 | **4/5** | 4/5 | **4/5** |
|
||||||
|
| OpenVINO medium INT8 | 4/5 | **4/5** | 4/5 | 3/5 |
|
||||||
|
| ONNX GigaAM RNN-T INT8 | 4/5 | 3/5 | **5/5** | 3/5 |
|
||||||
|
|
||||||
|
По технической сохранности первым стал Turbo FP16, особенно на DataCat; Turbo
|
||||||
|
INT8 почти равен ему. Они лучше удерживают `FineBI`, `OpenLineage`, `Spark`,
|
||||||
|
`XML`, `DBC TablesV/ColumnsV`, `Open Platform` и архитектурные связи. GigaAM
|
||||||
|
самый полный, но чаще заменяет неразобранное гладко звучащими ложными терминами.
|
||||||
|
|
||||||
|
Наиболее опасные места:
|
||||||
|
|
||||||
|
- GigaAM потерял `IMSI` в требуемом ключе `IMSI плюс время`, исказил основание
|
||||||
|
оставить MPP-вычисления в Teradata и не сохранил Alation как технический
|
||||||
|
ориентир интеграции с FineBI;
|
||||||
|
- `medium` заменил Tele2 на `TeraData 2` и «админскую учётку» на «учётку от
|
||||||
|
Минска», то есть правдоподобно изменил адресата и источник доступа;
|
||||||
|
- Turbo INT8 почти потерял риск DDoS FineBI через API и необходимость
|
||||||
|
ограничивать частоту запросов; в другом месте ответ о production-базах
|
||||||
|
локально перевёрнут;
|
||||||
|
- Turbo FP16 хуже остальных передал `edge cases` на RSQM и потерял поручение
|
||||||
|
оценить требования и квоту Teradata, но лучше всего сохранил технически
|
||||||
|
насыщенный DataCat.
|
||||||
|
|
||||||
|
Ни один проверяющий не нашёл длинных повторяющихся или автономно выдуманных
|
||||||
|
блоков. Основной остаточный риск всех профилей — локальная правдоподобная
|
||||||
|
подстановка термина, числа или адресата.
|
||||||
|
|
||||||
|
### Что это означает для профиля по умолчанию
|
||||||
|
|
||||||
|
Результаты поддерживают ONNX GigaAM RNN-T как основной CPU-профиль: он быстрее
|
||||||
|
всех на обеих машинах, дважды занял первое место по читаемости и пригодности
|
||||||
|
для конспекта и использует VAD. После обсуждения принята простая политика:
|
||||||
|
`--device auto` выбирает CUDA при наличии `nvidia-smi`, иначе ONNX.
|
||||||
|
|
||||||
|
Ограничения выбора сохраняются:
|
||||||
|
|
||||||
|
- GigaAM ориентирован на русский язык и хуже сохраняет латиницу, аббревиатуры,
|
||||||
|
названия систем и компаний;
|
||||||
|
- OpenVINO Whisper остаётся многоязычным путём и умеет использовать Intel GPU;
|
||||||
|
- Turbo лучше GigaAM по WER и техническим терминам, хотя хуже подготовлен для
|
||||||
|
непосредственного чтения;
|
||||||
|
- изменение существующего auto-пути является несовместимым изменением поведения;
|
||||||
|
явные значения из CLI и конфигурации остаются способом сохранить прежний
|
||||||
|
OpenVINO/Whisper-профиль или выбрать Turbo для технически насыщенных встреч.
|
||||||
|
|
||||||
## Вывод
|
## Вывод
|
||||||
|
|
||||||
- `large-v3-turbo` реально работает с OpenVINO 2026.3 в вариантах INT8 и FP16.
|
- `large-v3-turbo` реально работает с OpenVINO 2026.3 в вариантах INT8 и FP16.
|
||||||
@@ -184,5 +336,13 @@ FineBI» вместо учётной записи, `NiFi` как `I5`, `edge cas
|
|||||||
протокола может потребоваться последующая расстановка пунктуации.
|
протокола может потребоваться последующая расстановка пунктуации.
|
||||||
- Автоматических предупреждений о повторах или потере хвоста после обновления
|
- Автоматических предупреждений о повторах или потере хвоста после обновления
|
||||||
не было; ручная проверка нашла у medium одиночный ложный сегмент в конце RSQM.
|
не было; ручная проверка нашла у medium одиночный ложный сегмент в конце RSQM.
|
||||||
- Модель по умолчанию и порядок `--device auto` оставлены без изменений. Решение
|
- В самом benchmark-коммите модель по умолчанию и порядок `--device auto` не
|
||||||
об их изменении требует отдельной продуктовой задачи.
|
менялись; по итогам анализа отдельно принято решение использовать ONNX на
|
||||||
|
машинах без CUDA.
|
||||||
|
- Повторный Ryzen-прогон подтвердил, что прежний блок из 11 повторов `medium`
|
||||||
|
не воспроизводится: четыре последовательных прохода дали одинаковый текст без
|
||||||
|
повторов и ложного хвоста. Это подтверждение для контрольного файла, а не
|
||||||
|
гарантия устранения всех Whisper-галлюцинаций на тишине.
|
||||||
|
- Для русской речи на CPU ONNX GigaAM RNN-T выглядит лучшим пользовательским
|
||||||
|
дефолтом по скорости и готовности текста к чтению. OpenVINO сохраняет сильные
|
||||||
|
аргументы для Intel GPU, других языков и технически насыщенных записей.
|
||||||
+19
-7
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
## Режимы `--device`
|
## Режимы `--device`
|
||||||
|
|
||||||
- `auto` (по умолчанию) — CUDA → OpenVINO GPU → OpenVINO CPU → CPU (первый доступный)
|
- `auto` (по умолчанию) — CUDA при наличии `nvidia-smi`, иначе ONNX на CPU
|
||||||
- `cuda` — строго NVIDIA GPU, ошибка если недоступен
|
- `cuda` — строго NVIDIA GPU, ошибка если недоступен
|
||||||
- `openvino` — авто-выбор OpenVINO GPU или CPU
|
- `openvino` — авто-выбор OpenVINO GPU или CPU
|
||||||
- `openvino-gpu` — строго Intel GPU через OpenVINO
|
- `openvino-gpu` — строго Intel GPU через OpenVINO
|
||||||
- `openvino-cpu` — строго CPU через OpenVINO (ускорение 2-4x на x86)
|
- `openvino-cpu` — строго CPU через OpenVINO (3-10x на x86, зависит от модели)
|
||||||
- `cpu` — строго CPU (faster-whisper/CTranslate2)
|
- `cpu` — строго CPU (faster-whisper/CTranslate2)
|
||||||
|
|
||||||
## Какой бэкенд на каком оборудовании
|
## Какой бэкенд на каком оборудовании
|
||||||
@@ -14,12 +14,17 @@
|
|||||||
| Оборудование | Рекомендуемый `--device` | Бэкенд | Ожидаемая скорость |
|
| Оборудование | Рекомендуемый `--device` | Бэкенд | Ожидаемая скорость |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| NVIDIA GPU (6+ GB VRAM) | `auto` / `cuda` | faster-whisper (CTranslate2) | 7-19x реалтайм |
|
| NVIDIA GPU (6+ GB VRAM) | `auto` / `cuda` | faster-whisper (CTranslate2) | 7-19x реалтайм |
|
||||||
| Intel Arc iGPU / dGPU | `auto` / `openvino-gpu` | OpenVINO GenAI (GPU) | TBD |
|
| Любой CPU без NVIDIA | `auto` / `onnx` | ONNX GigaAM RNN-T | 10-14x реалтайм* |
|
||||||
| Intel/AMD x86 CPU | `auto` / `openvino-cpu` | OpenVINO GenAI (CPU) | 3-6x реалтайм* |
|
| Intel Arc iGPU / dGPU | `openvino-gpu` | OpenVINO GenAI (GPU) | TBD |
|
||||||
| Любой CPU (fallback) | `cpu` | faster-whisper (CTranslate2) | ~1.5x реалтайм |
|
| Intel/AMD x86 CPU | `openvino-cpu` | OpenVINO GenAI (CPU) | 3-10x реалтайм* |
|
||||||
| Apple Silicon (macOS) | `cpu` | faster-whisper (CTranslate2) | ~2x реалтайм |
|
| Любой CPU, FasterWhisper | `cpu` | faster-whisper (CTranslate2) | ~1.5x реалтайм |
|
||||||
|
|
||||||
\* По результатам тестирования на Intel и AMD CPU. Реальная скорость зависит от CPU и модели.
|
\* По результатам контрольных прогонов на Intel и AMD CPU. Реальная скорость
|
||||||
|
зависит от CPU, модели и записи.
|
||||||
|
|
||||||
|
Автоматический профиль без NVIDIA рассчитан на русскую речь: GigaAM других
|
||||||
|
языков не понимает. Для них берите Whisper — `openvino-cpu` на x86 или `cpu`
|
||||||
|
на любой платформе.
|
||||||
|
|
||||||
## OpenVINO
|
## OpenVINO
|
||||||
|
|
||||||
@@ -43,6 +48,13 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
|
|||||||
| large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov |
|
| large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov |
|
||||||
| large-v3-turbo | OpenVINO/whisper-large-v3-turbo-int8-ov | OpenVINO/whisper-large-v3-turbo-fp16-ov |
|
| large-v3-turbo | OpenVINO/whisper-large-v3-turbo-int8-ov | OpenVINO/whisper-large-v3-turbo-fp16-ov |
|
||||||
|
|
||||||
|
Размер в кеше HuggingFace: `medium` int8 — 748 MB, `large-v3-turbo` int8 —
|
||||||
|
790 MB, `large-v3-turbo` fp16 — 1552 MB. Это меньше, чем у тех же моделей для
|
||||||
|
faster-whisper, потому что веса уже квантизированы.
|
||||||
|
|
||||||
|
Модель `large-v3-turbo` доступна только в OpenVINO: для `--device cuda` и
|
||||||
|
`--device cpu` каталог faster-whisper заканчивается на `large-v3`.
|
||||||
|
|
||||||
### Результаты тестирования OpenVINO
|
### Результаты тестирования OpenVINO
|
||||||
|
|
||||||
Актуальное сравнение OpenVINO 2026.3 на трёх русскоязычных встречах:
|
Актуальное сравнение OpenVINO 2026.3 на трёх русскоязычных встречах:
|
||||||
|
|||||||
@@ -2,45 +2,88 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from local_transcriber.types import Segment, TranscribeResult
|
from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class OnnxModelSpec:
|
class OnnxModelSpec:
|
||||||
"""Имя onnx-asr и опубликованные варианты квантизации модели."""
|
"""Имя onnx-asr, варианты квантизации и поддерживаемые языки."""
|
||||||
|
|
||||||
model_id: str
|
model_id: str
|
||||||
quantizations: frozenset[str | None]
|
quantizations: frozenset[str | None]
|
||||||
|
supported_languages: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
_INT8_AND_FLOAT32 = frozenset({"int8", None})
|
_INT8_AND_FLOAT32 = frozenset({"int8", None})
|
||||||
|
_RUSSIAN_ONLY = frozenset({"ru"})
|
||||||
|
_GIGAAM_MULTILINGUAL_LANGUAGES = frozenset({"ru", "en", "kk", "ky", "uz"})
|
||||||
|
_PARAKEET_V3_LANGUAGES = frozenset(
|
||||||
|
{
|
||||||
|
"bg",
|
||||||
|
"hr",
|
||||||
|
"cs",
|
||||||
|
"da",
|
||||||
|
"nl",
|
||||||
|
"en",
|
||||||
|
"et",
|
||||||
|
"fi",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"el",
|
||||||
|
"hu",
|
||||||
|
"it",
|
||||||
|
"lv",
|
||||||
|
"lt",
|
||||||
|
"mt",
|
||||||
|
"pl",
|
||||||
|
"pt",
|
||||||
|
"ro",
|
||||||
|
"sk",
|
||||||
|
"sl",
|
||||||
|
"es",
|
||||||
|
"sv",
|
||||||
|
"ru",
|
||||||
|
"uk",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_WHISPER_MODEL_NAMES = frozenset(
|
||||||
|
{"tiny", "base", "small", "medium", "large-v3", "large-v3-turbo"}
|
||||||
|
)
|
||||||
|
|
||||||
MODEL_CATALOG: dict[str, OnnxModelSpec] = {
|
MODEL_CATALOG: dict[str, OnnxModelSpec] = {
|
||||||
"gigaam-v3": OnnxModelSpec("gigaam-v3-ctc", _INT8_AND_FLOAT32),
|
"gigaam-v3": OnnxModelSpec(
|
||||||
|
"gigaam-v3-ctc", _INT8_AND_FLOAT32, _RUSSIAN_ONLY
|
||||||
|
),
|
||||||
"parakeet-v3": OnnxModelSpec(
|
"parakeet-v3": OnnxModelSpec(
|
||||||
"nemo-parakeet-tdt-0.6b-v3",
|
"nemo-parakeet-tdt-0.6b-v3",
|
||||||
_INT8_AND_FLOAT32,
|
_INT8_AND_FLOAT32,
|
||||||
|
_PARAKEET_V3_LANGUAGES,
|
||||||
),
|
),
|
||||||
"gigaam-multilingual-ctc": OnnxModelSpec(
|
"gigaam-multilingual-ctc": OnnxModelSpec(
|
||||||
"gigaam-multilingual-ctc",
|
"gigaam-multilingual-ctc",
|
||||||
_INT8_AND_FLOAT32,
|
_INT8_AND_FLOAT32,
|
||||||
|
_GIGAAM_MULTILINGUAL_LANGUAGES,
|
||||||
),
|
),
|
||||||
"gigaam-multilingual-large-ctc": OnnxModelSpec(
|
"gigaam-multilingual-large-ctc": OnnxModelSpec(
|
||||||
"gigaam-multilingual-large-ctc",
|
"gigaam-multilingual-large-ctc",
|
||||||
_INT8_AND_FLOAT32,
|
_INT8_AND_FLOAT32,
|
||||||
|
_GIGAAM_MULTILINGUAL_LANGUAGES,
|
||||||
),
|
),
|
||||||
"gigaam-v3-e2e-ctc": OnnxModelSpec(
|
"gigaam-v3-e2e-ctc": OnnxModelSpec(
|
||||||
"gigaam-v3-e2e-ctc",
|
"gigaam-v3-e2e-ctc",
|
||||||
_INT8_AND_FLOAT32,
|
_INT8_AND_FLOAT32,
|
||||||
|
_RUSSIAN_ONLY,
|
||||||
),
|
),
|
||||||
"gigaam-v3-e2e-rnnt": OnnxModelSpec(
|
"gigaam-v3-e2e-rnnt": OnnxModelSpec(
|
||||||
"gigaam-v3-e2e-rnnt",
|
"gigaam-v3-e2e-rnnt",
|
||||||
_INT8_AND_FLOAT32,
|
_INT8_AND_FLOAT32,
|
||||||
|
_RUSSIAN_ONLY,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +128,8 @@ class OnnxAsrBackend:
|
|||||||
self._compute_type_explicit = compute_type_explicit
|
self._compute_type_explicit = compute_type_explicit
|
||||||
self.actual_compute_type: str | None = None
|
self.actual_compute_type: str | None = None
|
||||||
self._resolved_model_id: str | None = None
|
self._resolved_model_id: str | None = None
|
||||||
|
self._model_name: str | None = None
|
||||||
|
self._model_spec: OnnxModelSpec | None = None
|
||||||
self._vad: Any = None
|
self._vad: Any = None
|
||||||
|
|
||||||
def ensure_model_available(
|
def ensure_model_available(
|
||||||
@@ -119,6 +164,8 @@ class OnnxAsrBackend:
|
|||||||
|
|
||||||
self.actual_compute_type = resolved_compute_type
|
self.actual_compute_type = resolved_compute_type
|
||||||
self._resolved_model_id = self._resolve_model(model_name)
|
self._resolved_model_id = self._resolve_model(model_name)
|
||||||
|
self._model_name = model_name
|
||||||
|
self._model_spec = spec
|
||||||
return self._resolved_model_id
|
return self._resolved_model_id
|
||||||
|
|
||||||
def create_model(
|
def create_model(
|
||||||
@@ -162,13 +209,16 @@ class OnnxAsrBackend:
|
|||||||
"""
|
"""
|
||||||
from faster_whisper import decode_audio
|
from faster_whisper import decode_audio
|
||||||
|
|
||||||
|
self._warn_if_language_unsupported(language)
|
||||||
_notify(on_status, "Загружаю аудио...")
|
_notify(on_status, "Загружаю аудио...")
|
||||||
audio_array = decode_audio(str(file_path), sampling_rate=16000)
|
audio_array = decode_audio(str(file_path), sampling_rate=16000)
|
||||||
duration = len(audio_array) / 16000.0
|
duration = len(audio_array) / 16000.0
|
||||||
|
|
||||||
_notify(on_status, "Транскрибирую (onnx-asr)...")
|
_notify(on_status, "Транскрибирую (onnx-asr)...")
|
||||||
segments: list[Segment] = []
|
segments: list[Segment] = []
|
||||||
detected_language = language or "unknown"
|
result_language = (
|
||||||
|
language or _model_language(self._model_spec) or UNKNOWN_LANGUAGE
|
||||||
|
)
|
||||||
|
|
||||||
for vad_seg in model.recognize(
|
for vad_seg in model.recognize(
|
||||||
audio_array, sample_rate=16000, language=language
|
audio_array, sample_rate=16000, language=language
|
||||||
@@ -192,7 +242,7 @@ class OnnxAsrBackend:
|
|||||||
|
|
||||||
return TranscribeResult(
|
return TranscribeResult(
|
||||||
segments=segments,
|
segments=segments,
|
||||||
language=detected_language,
|
language=result_language,
|
||||||
language_probability=1.0 if language else 0.0,
|
language_probability=1.0 if language else 0.0,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
device_used="", # оркестратор проставит
|
device_used="", # оркестратор проставит
|
||||||
@@ -202,6 +252,13 @@ class OnnxAsrBackend:
|
|||||||
"""Resolve alias to onnx-asr model name. Raw names pass through."""
|
"""Resolve alias to onnx-asr model name. Raw names pass through."""
|
||||||
if model_name in MODEL_ALIASES:
|
if model_name in MODEL_ALIASES:
|
||||||
return MODEL_ALIASES[model_name]
|
return MODEL_ALIASES[model_name]
|
||||||
|
if model_name in _WHISPER_MODEL_NAMES:
|
||||||
|
raise ValueError(
|
||||||
|
f"Модель '{model_name}' относится к Whisper и не поддерживается "
|
||||||
|
"ONNX-бэкендом. Без CUDA --device auto выбирает ONNX; "
|
||||||
|
f"укажите --device openvino-cpu --model {model_name} "
|
||||||
|
"или --device cuda --model medium."
|
||||||
|
)
|
||||||
if "/" in model_name or model_name.count("-") >= 2:
|
if "/" in model_name or model_name.count("-") >= 2:
|
||||||
# Looks like a raw onnx-asr name — allow passthrough
|
# Looks like a raw onnx-asr name — allow passthrough
|
||||||
return model_name
|
return model_name
|
||||||
@@ -211,6 +268,25 @@ class OnnxAsrBackend:
|
|||||||
f"Либо укажите полное имя модели onnx-asr."
|
f"Либо укажите полное имя модели onnx-asr."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _warn_if_language_unsupported(self, language: str | None) -> None:
|
||||||
|
if (
|
||||||
|
language is None
|
||||||
|
or self._model_spec is None
|
||||||
|
or language in self._model_spec.supported_languages
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
supported = ", ".join(sorted(self._model_spec.supported_languages))
|
||||||
|
warnings.warn(
|
||||||
|
f"Язык '{language}' не поддерживается моделью '{self._model_name}' "
|
||||||
|
f"(поддерживаются: {supported}). Результат может быть некорректным. "
|
||||||
|
"Для других языков используйте "
|
||||||
|
"--device openvino-cpu --model medium "
|
||||||
|
"или --device cuda --model medium.",
|
||||||
|
UserWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_compute_types(quantizations: frozenset[str | None]) -> str:
|
def _format_compute_types(quantizations: frozenset[str | None]) -> str:
|
||||||
values = [_compute_type_for_quantization(value) for value in quantizations]
|
values = [_compute_type_for_quantization(value) for value in quantizations]
|
||||||
@@ -228,6 +304,12 @@ def _preferred_compute_type(quantizations: frozenset[str | None]) -> str:
|
|||||||
raise ValueError("Для ONNX-модели не указаны доступные квантизации")
|
raise ValueError("Для ONNX-модели не указаны доступные квантизации")
|
||||||
|
|
||||||
|
|
||||||
|
def _model_language(spec: OnnxModelSpec | None) -> str | None:
|
||||||
|
if spec is not None and len(spec.supported_languages) == 1:
|
||||||
|
return next(iter(spec.supported_languages))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
|
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
|
||||||
if on_status is not None:
|
if on_status is not None:
|
||||||
on_status(message)
|
on_status(message)
|
||||||
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
from huggingface_hub.errors import LocalEntryNotFoundError
|
from huggingface_hub.errors import LocalEntryNotFoundError
|
||||||
|
|
||||||
from local_transcriber.types import Segment, TranscribeResult
|
from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult
|
||||||
|
|
||||||
# (model_alias, compute_type) → HF repo
|
# (model_alias, compute_type) → HF repo
|
||||||
MODEL_REPOS: dict[tuple[str, str], str] = {
|
MODEL_REPOS: dict[tuple[str, str], str] = {
|
||||||
@@ -159,7 +159,7 @@ class OpenVINOBackend:
|
|||||||
f"Транскрибирую (OpenVINO)... [{len(segments)} сегм.]",
|
f"Транскрибирую (OpenVINO)... [{len(segments)} сегм.]",
|
||||||
)
|
)
|
||||||
|
|
||||||
detected_language = language or "auto"
|
detected_language = language or UNKNOWN_LANGUAGE
|
||||||
language_probability = 1.0 if language else 0.0
|
language_probability = 1.0 if language else 0.0
|
||||||
|
|
||||||
return TranscribeResult(
|
return TranscribeResult(
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ from .config import apply_device_defaults, load_config, resolve_defaults
|
|||||||
from .context_menu import install_menu as install_context_menu
|
from .context_menu import install_menu as install_context_menu
|
||||||
from .context_menu import uninstall_menu as uninstall_context_menu
|
from .context_menu import uninstall_menu as uninstall_context_menu
|
||||||
from .formatter import (
|
from .formatter import (
|
||||||
|
LANGUAGE_DETECTED,
|
||||||
|
LANGUAGE_FORCED,
|
||||||
|
LANGUAGE_FROM_MODEL,
|
||||||
|
LANGUAGE_UNKNOWN,
|
||||||
format_duration,
|
format_duration,
|
||||||
format_timestamp,
|
format_timestamp,
|
||||||
format_transcript,
|
format_transcript,
|
||||||
@@ -30,6 +34,7 @@ from .transcriber import (
|
|||||||
_transcribe_file,
|
_transcribe_file,
|
||||||
load_model,
|
load_model,
|
||||||
)
|
)
|
||||||
|
from .types import UNKNOWN_LANGUAGE
|
||||||
from .utils import (
|
from .utils import (
|
||||||
build_output_path,
|
build_output_path,
|
||||||
detect_device,
|
detect_device,
|
||||||
@@ -57,6 +62,19 @@ def _format_device_info(device_used: str) -> str:
|
|||||||
return "CPU"
|
return "CPU"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_language_mode(
|
||||||
|
requested_language: str, result: TranscribeResult
|
||||||
|
) -> str:
|
||||||
|
"""Описывает источник языка, не выдавая профиль модели за детектор."""
|
||||||
|
if requested_language != "auto":
|
||||||
|
return LANGUAGE_FORCED
|
||||||
|
if result.language_probability > 0:
|
||||||
|
return LANGUAGE_DETECTED
|
||||||
|
if result.language not in {"", UNKNOWN_LANGUAGE}:
|
||||||
|
return LANGUAGE_FROM_MODEL
|
||||||
|
return LANGUAGE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
def _format_repetition_blocks(
|
def _format_repetition_blocks(
|
||||||
blocks: list[RepetitionBlock],
|
blocks: list[RepetitionBlock],
|
||||||
use_hours: bool,
|
use_hours: bool,
|
||||||
@@ -114,7 +132,11 @@ def _print_quality_warnings(result: TranscribeResult, file_name: str | None = No
|
|||||||
def main(
|
def main(
|
||||||
files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"),
|
files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"),
|
||||||
model: str | None = typer.Option(
|
model: str | None = typer.Option(
|
||||||
None, "--model", "-m", show_default=False, help="Модель Whisper [по умолч.: medium]"
|
None,
|
||||||
|
"--model",
|
||||||
|
"-m",
|
||||||
|
show_default=False,
|
||||||
|
help="Модель [по умолч.: medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX)]",
|
||||||
),
|
),
|
||||||
language: str | None = typer.Option(
|
language: str | None = typer.Option(
|
||||||
None, "--language", "-l", show_default=False, help="Язык [по умолч.: ru]"
|
None, "--language", "-l", show_default=False, help="Язык [по умолч.: ru]"
|
||||||
@@ -126,7 +148,10 @@ def main(
|
|||||||
),
|
),
|
||||||
compute_type: str | None = typer.Option(
|
compute_type: str | None = typer.Option(
|
||||||
None, "--compute-type", show_default=False,
|
None, "--compute-type", show_default=False,
|
||||||
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]"
|
help=(
|
||||||
|
"Тип вычислений [по умолч.: float16 (CUDA) / "
|
||||||
|
"int8 (ONNX/OpenVINO) / float32 (CPU)]"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
threads: int = typer.Option(
|
threads: int = typer.Option(
|
||||||
0, "--threads", "-t", show_default=False, min=0,
|
0, "--threads", "-t", show_default=False, min=0,
|
||||||
@@ -308,7 +333,7 @@ def _run_single(
|
|||||||
)
|
)
|
||||||
|
|
||||||
device_info = _format_device_info(result.device_used)
|
device_info = _format_device_info(result.device_used)
|
||||||
language_mode = "detected" if defaults["language"] == "auto" else "forced"
|
language_mode = _format_language_mode(defaults["language"], result)
|
||||||
|
|
||||||
content = format_transcript(
|
content = format_transcript(
|
||||||
result=result,
|
result=result,
|
||||||
@@ -394,8 +419,6 @@ def _run_batch(
|
|||||||
# Phase 3: Transcribe
|
# Phase 3: Transcribe
|
||||||
processed = 0
|
processed = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
language_mode = "detected" if defaults["language"] == "auto" else "forced"
|
|
||||||
|
|
||||||
batch_start = time.monotonic()
|
batch_start = time.monotonic()
|
||||||
|
|
||||||
for i, file in enumerate(to_process, 1):
|
for i, file in enumerate(to_process, 1):
|
||||||
@@ -435,6 +458,7 @@ def _run_batch(
|
|||||||
model_path = tfr.model_path
|
model_path = tfr.model_path
|
||||||
|
|
||||||
result = tfr.result
|
result = tfr.result
|
||||||
|
language_mode = _format_language_mode(defaults["language"], result)
|
||||||
|
|
||||||
if len(result.segments) == 0:
|
if len(result.segments) == 0:
|
||||||
console.print(
|
console.print(
|
||||||
|
|||||||
@@ -10,6 +10,19 @@ from .types import Segment, TranscribeResult
|
|||||||
_PAUSE_THRESHOLD_S = 2.0 # пауза между сегментами для разбиения на абзацы
|
_PAUSE_THRESHOLD_S = 2.0 # пауза между сегментами для разбиения на абзацы
|
||||||
_MAX_PARAGRAPH_S = 60.0 # максимальная длительность абзаца
|
_MAX_PARAGRAPH_S = 60.0 # максимальная длительность абзаца
|
||||||
|
|
||||||
|
# Источник языка в шапке транскрипта
|
||||||
|
LANGUAGE_FORCED = "задан явно"
|
||||||
|
LANGUAGE_DETECTED = "определён автоматически"
|
||||||
|
LANGUAGE_FROM_MODEL = "из профиля модели"
|
||||||
|
LANGUAGE_UNKNOWN = "не определён"
|
||||||
|
|
||||||
|
LANGUAGE_MODES = (
|
||||||
|
LANGUAGE_FORCED,
|
||||||
|
LANGUAGE_DETECTED,
|
||||||
|
LANGUAGE_FROM_MODEL,
|
||||||
|
LANGUAGE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _Paragraph:
|
class _Paragraph:
|
||||||
@@ -80,7 +93,7 @@ def format_transcript(
|
|||||||
source_filename: str,
|
source_filename: str,
|
||||||
model_name: str,
|
model_name: str,
|
||||||
device_info: str,
|
device_info: str,
|
||||||
language_mode: str, # "detected" | "forced"
|
language_mode: str, # см. LANGUAGE_MODES
|
||||||
transcription_date: datetime | None = None, # None -> datetime.now()
|
transcription_date: datetime | None = None, # None -> datetime.now()
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Собирает markdown-транскрипт: шапка с метаданными + абзацы с таймкодами."""
|
"""Собирает markdown-транскрипт: шапка с метаданными + абзацы с таймкодами."""
|
||||||
@@ -92,7 +105,10 @@ def format_transcript(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}")
|
lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
lines.append(f"- **Модель**: {model_name}")
|
lines.append(f"- **Модель**: {model_name}")
|
||||||
lines.append(f"- **Язык**: {result.language} ({language_mode})")
|
if language_mode == LANGUAGE_UNKNOWN:
|
||||||
|
lines.append(f"- **Язык**: {LANGUAGE_UNKNOWN}")
|
||||||
|
else:
|
||||||
|
lines.append(f"- **Язык**: {result.language} ({language_mode})")
|
||||||
lines.append(f"- **Длительность**: {format_duration(result.duration)}")
|
lines.append(f"- **Длительность**: {format_duration(result.duration)}")
|
||||||
if tail_gap(result) > TAIL_GAP_WARN_S:
|
if tail_gap(result) > TAIL_GAP_WARN_S:
|
||||||
last_end = result.segments[-1].end
|
last_end = result.segments[-1].end
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
# Единый признак «язык неизвестен» для всех бэкендов
|
||||||
|
UNKNOWN_LANGUAGE = "unknown"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Segment:
|
class Segment:
|
||||||
@@ -15,10 +18,10 @@ class Segment:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class TranscribeResult:
|
class TranscribeResult:
|
||||||
segments: list[Segment]
|
segments: list[Segment]
|
||||||
language: str
|
language: str # код языка или UNKNOWN_LANGUAGE, если он неизвестен
|
||||||
language_probability: float
|
language_probability: float
|
||||||
duration: float # seconds
|
duration: float # seconds
|
||||||
device_used: str # "cpu" / "cuda" / "openvino-gpu" / "openvino-cpu"
|
device_used: str # "cpu" / "cuda" / "onnx" / "openvino-gpu" / "openvino-cpu"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -16,18 +16,16 @@ SUPPORTED_EXTENSIONS = {
|
|||||||
def detect_device(requested: str = "auto") -> str:
|
def detect_device(requested: str = "auto") -> str:
|
||||||
"""Определяет устройство для вычислений.
|
"""Определяет устройство для вычислений.
|
||||||
|
|
||||||
При ``requested="auto"`` проверяет: CUDA → OpenVINO GPU → OpenVINO CPU → CPU.
|
При ``requested="auto"`` выбирает CUDA при наличии ``nvidia-smi``,
|
||||||
``"openvino"`` — алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU).
|
иначе ONNX на CPU.
|
||||||
|
``"openvino"`` — алиас для авто-детекта внутри OpenVINO
|
||||||
|
(GPU если доступен, иначе CPU).
|
||||||
Возвращает всегда конкретное значение (не абстрактный ``"openvino"``).
|
Возвращает всегда конкретное значение (не абстрактный ``"openvino"``).
|
||||||
"""
|
"""
|
||||||
if requested == "auto":
|
if requested == "auto":
|
||||||
if shutil.which("nvidia-smi") is not None:
|
if shutil.which("nvidia-smi") is not None:
|
||||||
return "cuda"
|
return "cuda"
|
||||||
if _is_openvino_gpu_available():
|
return "onnx"
|
||||||
return "openvino-gpu"
|
|
||||||
if _is_openvino_available():
|
|
||||||
return "openvino-cpu"
|
|
||||||
return "cpu"
|
|
||||||
if requested == "openvino":
|
if requested == "openvino":
|
||||||
if _is_openvino_gpu_available():
|
if _is_openvino_gpu_available():
|
||||||
return "openvino-gpu"
|
return "openvino-gpu"
|
||||||
|
|||||||
@@ -11,21 +11,16 @@ from local_transcriber.backends.openvino import (
|
|||||||
OpenVINOBackend,
|
OpenVINOBackend,
|
||||||
_validate_model_dir,
|
_validate_model_dir,
|
||||||
)
|
)
|
||||||
from local_transcriber.types import Segment
|
from local_transcriber.types import UNKNOWN_LANGUAGE, Segment
|
||||||
|
|
||||||
|
|
||||||
# === _resolve_repo ===
|
# === _resolve_repo ===
|
||||||
|
|
||||||
|
|
||||||
def test_model_catalog_contains_supported_profiles():
|
def test_model_catalog_contains_large_v3_turbo_profiles():
|
||||||
assert MODEL_REPOS == {
|
assert {
|
||||||
("tiny", "int8"): "OpenVINO/whisper-tiny-int8-ov",
|
pair: repo for pair, repo in MODEL_REPOS.items() if pair[0] == "large-v3-turbo"
|
||||||
("base", "fp16"): "OpenVINO/whisper-base-fp16-ov",
|
} == {
|
||||||
("small", "int8"): "OpenVINO/whisper-small-int8-ov",
|
|
||||||
("medium", "int8"): "OpenVINO/whisper-medium-int8-ov",
|
|
||||||
("medium", "fp16"): "OpenVINO/whisper-medium-fp16-ov",
|
|
||||||
("large-v3", "int8"): "OpenVINO/whisper-large-v3-int8-ov",
|
|
||||||
("large-v3", "fp16"): "OpenVINO/whisper-large-v3-fp16-ov",
|
|
||||||
("large-v3-turbo", "int8"): "OpenVINO/whisper-large-v3-turbo-int8-ov",
|
("large-v3-turbo", "int8"): "OpenVINO/whisper-large-v3-turbo-int8-ov",
|
||||||
("large-v3-turbo", "fp16"): "OpenVINO/whisper-large-v3-turbo-fp16-ov",
|
("large-v3-turbo", "fp16"): "OpenVINO/whisper-large-v3-turbo-fp16-ov",
|
||||||
}
|
}
|
||||||
@@ -61,11 +56,20 @@ def test_resolve_repo_implicit_fallback():
|
|||||||
assert backend._resolve_repo("base", "int8") == ("OpenVINO/whisper-base-fp16-ov", "fp16")
|
assert backend._resolve_repo("base", "int8") == ("OpenVINO/whisper-base-fp16-ov", "fp16")
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_repo_implicit_large_v3_prefers_fp16():
|
@pytest.mark.parametrize(
|
||||||
"""Неявный compute_type: large-v3 автоматически получает fp16."""
|
("model_name", "expected_compute_type"),
|
||||||
|
[("large-v3", "fp16"), ("large-v3-turbo", "int8")],
|
||||||
|
)
|
||||||
|
def test_resolve_repo_implicit_large_v3_profiles(
|
||||||
|
model_name, expected_compute_type
|
||||||
|
):
|
||||||
|
"""Неявный compute_type различает обычную и turbo-модель."""
|
||||||
backend = OpenVINOBackend(compute_type_explicit=False)
|
backend = OpenVINOBackend(compute_type_explicit=False)
|
||||||
# Дефолт int8, но для large-v3 override на fp16
|
|
||||||
assert backend._resolve_repo("large-v3", "int8") == ("OpenVINO/whisper-large-v3-fp16-ov", "fp16")
|
assert backend._resolve_repo(model_name, "int8") == (
|
||||||
|
f"OpenVINO/whisper-{model_name}-{expected_compute_type}-ov",
|
||||||
|
expected_compute_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_repo_explicit_large_v3_int8_respected():
|
def test_resolve_repo_explicit_large_v3_int8_respected():
|
||||||
@@ -297,7 +301,7 @@ def test_transcribe_no_language_auto():
|
|||||||
|
|
||||||
call_kwargs = mock_model.generate.call_args.kwargs
|
call_kwargs = mock_model.generate.call_args.kwargs
|
||||||
assert "language" not in call_kwargs
|
assert "language" not in call_kwargs
|
||||||
assert result.language == "auto"
|
assert result.language == UNKNOWN_LANGUAGE
|
||||||
assert result.language_probability == 0.0
|
assert result.language_probability == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+77
-8
@@ -5,8 +5,15 @@ import pytest
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from typer.testing import CliRunner
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
from local_transcriber.cli import _format_device_info, app
|
from local_transcriber.cli import _format_device_info, _format_language_mode, app
|
||||||
|
from local_transcriber.formatter import (
|
||||||
|
LANGUAGE_DETECTED,
|
||||||
|
LANGUAGE_FORCED,
|
||||||
|
LANGUAGE_FROM_MODEL,
|
||||||
|
LANGUAGE_UNKNOWN,
|
||||||
|
)
|
||||||
from local_transcriber.transcriber import Segment, TranscribeFileResult, TranscribeResult
|
from local_transcriber.transcriber import Segment, TranscribeFileResult, TranscribeResult
|
||||||
|
from local_transcriber.types import UNKNOWN_LANGUAGE
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
@@ -42,6 +49,24 @@ def _make_tfr(result=None, model=None, actual_device="cpu", backend=None, model_
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("requested_language", "language", "probability", "expected"),
|
||||||
|
[
|
||||||
|
("ru", "ru", 1.0, LANGUAGE_FORCED),
|
||||||
|
("auto", "ru", 0.95, LANGUAGE_DETECTED),
|
||||||
|
("auto", "ru", 0.0, LANGUAGE_FROM_MODEL),
|
||||||
|
("auto", UNKNOWN_LANGUAGE, 0.0, LANGUAGE_UNKNOWN),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_format_language_mode(
|
||||||
|
requested_language, language, probability, expected
|
||||||
|
):
|
||||||
|
result = _make_result(language=language)
|
||||||
|
result.language_probability = probability
|
||||||
|
|
||||||
|
assert _format_language_mode(requested_language, result) == expected
|
||||||
|
|
||||||
|
|
||||||
def _single_patches(result=None, tmp_file=None, actual_device="cpu"):
|
def _single_patches(result=None, tmp_file=None, actual_device="cpu"):
|
||||||
"""Patches for a standard single-file CLI happy path."""
|
"""Patches for a standard single-file CLI happy path."""
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -73,27 +98,32 @@ def test_cli_happy_path_exit_code_zero(tmp_path):
|
|||||||
def test_cli_default_options_passed_to_transcribe(tmp_path):
|
def test_cli_default_options_passed_to_transcribe(tmp_path):
|
||||||
audio = tmp_path / "test.mp3"
|
audio = tmp_path / "test.mp3"
|
||||||
audio.write_bytes(b"fake")
|
audio.write_bytes(b"fake")
|
||||||
result = _make_result()
|
result = _make_result(device_used="onnx")
|
||||||
model = _make_model()
|
model = _make_model()
|
||||||
backend = _make_backend()
|
backend = _make_backend()
|
||||||
tfr = _make_tfr(result=result, model=model, backend=backend)
|
tfr = _make_tfr(result=result, model=model, actual_device="onnx", backend=backend)
|
||||||
mock_transcribe_file = MagicMock(return_value=tfr)
|
mock_transcribe_file = MagicMock(return_value=tfr)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("local_transcriber.cli.load_config", return_value={}),
|
patch("local_transcriber.cli.load_config", return_value={}),
|
||||||
patch("local_transcriber.cli.validate_input_file", return_value=audio),
|
patch("local_transcriber.cli.validate_input_file", return_value=audio),
|
||||||
patch("local_transcriber.cli.detect_device", return_value="cpu"),
|
patch("local_transcriber.cli.detect_device", return_value="onnx"),
|
||||||
patch("local_transcriber.cli.load_model", return_value=(model, "cpu", backend, "/models/medium")),
|
patch(
|
||||||
|
"local_transcriber.cli.load_model",
|
||||||
|
return_value=(model, "onnx", backend, "/models/gigaam-v3-e2e-rnnt"),
|
||||||
|
),
|
||||||
patch("local_transcriber.cli._transcribe_file", mock_transcribe_file),
|
patch("local_transcriber.cli._transcribe_file", mock_transcribe_file),
|
||||||
patch("local_transcriber.cli.write_transcript"),
|
patch("local_transcriber.cli.write_transcript"),
|
||||||
):
|
):
|
||||||
runner.invoke(app, [str(audio)])
|
out = runner.invoke(app, [str(audio)])
|
||||||
|
|
||||||
call_kwargs = mock_transcribe_file.call_args[1]
|
call_kwargs = mock_transcribe_file.call_args[1]
|
||||||
assert call_kwargs["model_name"] == "medium"
|
assert call_kwargs["model_name"] == "gigaam-v3-e2e-rnnt"
|
||||||
assert call_kwargs["compute_type"] == "float32"
|
assert call_kwargs["compute_type"] == "int8"
|
||||||
assert call_kwargs["language"] == "ru"
|
assert call_kwargs["language"] == "ru"
|
||||||
assert call_kwargs["on_segment"] is None # verbose=False
|
assert call_kwargs["on_segment"] is None # verbose=False
|
||||||
|
assert "Модель: gigaam-v3-e2e-rnnt" in out.output
|
||||||
|
assert "Устройство: onnx" in out.output
|
||||||
|
|
||||||
|
|
||||||
def test_cli_custom_options(tmp_path):
|
def test_cli_custom_options(tmp_path):
|
||||||
@@ -660,6 +690,45 @@ def test_cli_config_applied(tmp_path):
|
|||||||
assert mock_load_model.call_args[0][0] == "tiny"
|
assert mock_load_model.call_args[0][0] == "tiny"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_config_overrides_auto_device(tmp_path):
|
||||||
|
audio = tmp_path / "test.mp3"
|
||||||
|
audio.write_bytes(b"fake")
|
||||||
|
model = _make_model()
|
||||||
|
backend = _make_backend()
|
||||||
|
result = _make_result(device_used="openvino-cpu")
|
||||||
|
tfr = _make_tfr(
|
||||||
|
result=result,
|
||||||
|
model=model,
|
||||||
|
actual_device="openvino-cpu",
|
||||||
|
backend=backend,
|
||||||
|
)
|
||||||
|
mock_detect_device = MagicMock(return_value="openvino-cpu")
|
||||||
|
mock_load_model = MagicMock(
|
||||||
|
return_value=(model, "openvino-cpu", backend, "/models/medium")
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"local_transcriber.cli.load_config",
|
||||||
|
return_value={
|
||||||
|
"device": "openvino-cpu",
|
||||||
|
"model": "medium",
|
||||||
|
"compute_type": "int8",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
patch("local_transcriber.cli.validate_input_file", return_value=audio),
|
||||||
|
patch("local_transcriber.cli.detect_device", mock_detect_device),
|
||||||
|
patch("local_transcriber.cli.load_model", mock_load_model),
|
||||||
|
patch("local_transcriber.cli._transcribe_file", return_value=tfr),
|
||||||
|
patch("local_transcriber.cli.write_transcript"),
|
||||||
|
):
|
||||||
|
out = runner.invoke(app, [str(audio)])
|
||||||
|
|
||||||
|
assert out.exit_code == 0
|
||||||
|
assert mock_detect_device.call_args_list[0].args == ("openvino-cpu",)
|
||||||
|
assert mock_load_model.call_args.args[:3] == ("medium", "openvino-cpu", "int8")
|
||||||
|
|
||||||
|
|
||||||
def test_cli_cli_overrides_config(tmp_path):
|
def test_cli_cli_overrides_config(tmp_path):
|
||||||
audio = tmp_path / "test.mp3"
|
audio = tmp_path / "test.mp3"
|
||||||
audio.write_bytes(b"fake")
|
audio.write_bytes(b"fake")
|
||||||
|
|||||||
@@ -71,11 +71,17 @@ def test_load_config_invalid_device(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_resolve_defaults_cli_wins():
|
def test_resolve_defaults_cli_wins():
|
||||||
config = {"model": "tiny", "language": "en"}
|
config = {"model": "tiny", "language": "en", "device": "openvino-cpu"}
|
||||||
cli = {"model": "small", "language": None, "device": None, "compute_type": None}
|
cli = {
|
||||||
|
"model": "small",
|
||||||
|
"language": None,
|
||||||
|
"device": "onnx",
|
||||||
|
"compute_type": None,
|
||||||
|
}
|
||||||
result = resolve_defaults(cli, config)
|
result = resolve_defaults(cli, config)
|
||||||
assert result["model"] == "small"
|
assert result["model"] == "small"
|
||||||
assert result["language"] == "en"
|
assert result["language"] == "en"
|
||||||
|
assert result["device"] == "onnx"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_defaults_config_wins():
|
def test_resolve_defaults_config_wins():
|
||||||
|
|||||||
+37
-12
@@ -2,12 +2,16 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from local_transcriber.formatter import (
|
from local_transcriber.formatter import (
|
||||||
|
LANGUAGE_DETECTED,
|
||||||
|
LANGUAGE_FORCED,
|
||||||
|
LANGUAGE_UNKNOWN,
|
||||||
_group_segments,
|
_group_segments,
|
||||||
format_timestamp,
|
format_timestamp,
|
||||||
format_transcript,
|
format_transcript,
|
||||||
write_transcript,
|
write_transcript,
|
||||||
)
|
)
|
||||||
from local_transcriber.transcriber import Segment, TranscribeResult
|
from local_transcriber.transcriber import Segment, TranscribeResult
|
||||||
|
from local_transcriber.types import UNKNOWN_LANGUAGE
|
||||||
|
|
||||||
|
|
||||||
def test_format_timestamp_minutes():
|
def test_format_timestamp_minutes():
|
||||||
@@ -40,14 +44,14 @@ def test_format_transcript_basic():
|
|||||||
source_filename="meeting.mp4",
|
source_filename="meeting.mp4",
|
||||||
model_name="large-v3",
|
model_name="large-v3",
|
||||||
device_info="CUDA (NVIDIA GeForce RTX 3060)",
|
device_info="CUDA (NVIDIA GeForce RTX 3060)",
|
||||||
language_mode="detected",
|
language_mode=LANGUAGE_DETECTED,
|
||||||
transcription_date=datetime(2026, 3, 17, 14, 30, 5),
|
transcription_date=datetime(2026, 3, 17, 14, 30, 5),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "# Транскрипт: meeting.mp4" in content
|
assert "# Транскрипт: meeting.mp4" in content
|
||||||
assert "**Дата транскрипции**: 2026-03-17 14:30:05" in content
|
assert "**Дата транскрипции**: 2026-03-17 14:30:05" in content
|
||||||
assert "**Модель**: large-v3" in content
|
assert "**Модель**: large-v3" in content
|
||||||
assert "**Язык**: ru (detected)" in content
|
assert "**Язык**: ru (определён автоматически)" in content
|
||||||
assert "**Длительность**: 02:00" in content
|
assert "**Длительность**: 02:00" in content
|
||||||
assert "**Устройство**: CUDA (NVIDIA GeForce RTX 3060)" in content
|
assert "**Устройство**: CUDA (NVIDIA GeForce RTX 3060)" in content
|
||||||
assert "---" in content
|
assert "---" in content
|
||||||
@@ -55,6 +59,27 @@ def test_format_transcript_basic():
|
|||||||
assert "[00:00.00 - 00:09.15] Добрый день, коллеги. Первый вопрос." in content
|
assert "[00:00.00 - 00:09.15] Добрый день, коллеги. Первый вопрос." in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_transcript_unknown_language_without_placeholder():
|
||||||
|
"""Неизвестный язык печатается одной строкой, без служебного значения."""
|
||||||
|
result = TranscribeResult(
|
||||||
|
segments=[Segment(start=0.0, end=4.0, text=" Добрый день.")],
|
||||||
|
language=UNKNOWN_LANGUAGE,
|
||||||
|
language_probability=0.0,
|
||||||
|
duration=120.0,
|
||||||
|
device_used="openvino-cpu",
|
||||||
|
)
|
||||||
|
content = format_transcript(
|
||||||
|
result,
|
||||||
|
source_filename="meeting.mp4",
|
||||||
|
model_name="medium",
|
||||||
|
device_info="OpenVINO (CPU)",
|
||||||
|
language_mode=LANGUAGE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "**Язык**: не определён" in content
|
||||||
|
assert UNKNOWN_LANGUAGE not in content
|
||||||
|
|
||||||
|
|
||||||
def test_format_transcript_segment_no_leading_space():
|
def test_format_transcript_segment_no_leading_space():
|
||||||
"""Сегменты без ведущего пробела должны форматироваться корректно."""
|
"""Сегменты без ведущего пробела должны форматироваться корректно."""
|
||||||
result = TranscribeResult(
|
result = TranscribeResult(
|
||||||
@@ -69,7 +94,7 @@ def test_format_transcript_segment_no_leading_space():
|
|||||||
source_filename="f.mp3",
|
source_filename="f.mp3",
|
||||||
model_name="tiny",
|
model_name="tiny",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="detected",
|
language_mode=LANGUAGE_DETECTED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
assert "[00:00.00 - 00:02.00] Hello" in content
|
assert "[00:00.00 - 00:02.00] Hello" in content
|
||||||
@@ -88,7 +113,7 @@ def test_format_transcript_empty():
|
|||||||
source_filename="silence.wav",
|
source_filename="silence.wav",
|
||||||
model_name="tiny",
|
model_name="tiny",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="detected",
|
language_mode=LANGUAGE_DETECTED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,12 +138,12 @@ def test_format_transcript_long():
|
|||||||
source_filename="long.mp4",
|
source_filename="long.mp4",
|
||||||
model_name="large-v3",
|
model_name="large-v3",
|
||||||
device_info="CUDA",
|
device_info="CUDA",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 3, 17, 10, 0, 0),
|
transcription_date=datetime(2026, 3, 17, 10, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert "**Длительность**: 01:03:20" in content
|
assert "**Длительность**: 01:03:20" in content
|
||||||
assert "**Язык**: en (forced)" in content
|
assert "**Язык**: en (задан явно)" in content
|
||||||
# Timestamps should use hours format
|
# Timestamps should use hours format
|
||||||
assert "[00:00:00.00 - 00:00:10.50] Начало." in content
|
assert "[00:00:00.00 - 00:00:10.50] Начало." in content
|
||||||
assert "[01:01:40.00 - 01:01:50.25] Конец." in content
|
assert "[01:01:40.00 - 01:01:50.25] Конец." in content
|
||||||
@@ -188,7 +213,7 @@ def test_format_transcript_tail_gap_warning():
|
|||||||
source_filename="tail.mp3",
|
source_filename="tail.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -210,7 +235,7 @@ def test_format_transcript_no_tail_gap_warning_for_small_gap():
|
|||||||
source_filename="ok.mp3",
|
source_filename="ok.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,7 +256,7 @@ def test_format_transcript_no_tail_gap_warning_for_exact_threshold():
|
|||||||
source_filename="ok.mp3",
|
source_filename="ok.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,7 +282,7 @@ def test_format_transcript_repetition_warning():
|
|||||||
source_filename="repeat.mp3",
|
source_filename="repeat.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -284,7 +309,7 @@ def test_format_transcript_repetition_warning_uses_hours():
|
|||||||
source_filename="long-repeat.mp3",
|
source_filename="long-repeat.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,7 +330,7 @@ def test_format_transcript_without_anomalies_has_no_warning_lines():
|
|||||||
source_filename="ok.mp3",
|
source_filename="ok.mp3",
|
||||||
model_name="medium",
|
model_name="medium",
|
||||||
device_info="CPU",
|
device_info="CPU",
|
||||||
language_mode="forced",
|
language_mode=LANGUAGE_FORCED,
|
||||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+86
-2
@@ -1,9 +1,11 @@
|
|||||||
"""Tests for onnx-asr backend."""
|
"""Tests for onnx-asr backend."""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from local_transcriber.backends.onnx_asr import OnnxAsrBackend
|
from local_transcriber.backends.onnx_asr import OnnxAsrBackend
|
||||||
from local_transcriber.types import Segment, TranscribeResult
|
from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult
|
||||||
|
|
||||||
|
|
||||||
class FakeVadSegment:
|
class FakeVadSegment:
|
||||||
@@ -224,6 +226,68 @@ class TestCreateModel:
|
|||||||
|
|
||||||
|
|
||||||
class TestTranscribe:
|
class TestTranscribe:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("model_name", "language", "expects_warning"),
|
||||||
|
[
|
||||||
|
("gigaam-v3-e2e-rnnt", "en", True),
|
||||||
|
("gigaam-v3-e2e-rnnt", "ru", False),
|
||||||
|
("gigaam-multilingual-ctc", "en", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_warns_when_language_is_not_supported(
|
||||||
|
self, monkeypatch, tmp_path, model_name, language, expects_warning
|
||||||
|
):
|
||||||
|
wav_file = tmp_path / "test.wav"
|
||||||
|
wav_file.write_bytes(b"fake audio")
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"faster_whisper.decode_audio",
|
||||||
|
lambda path, sampling_rate=16000: [0.0] * 16000,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeModel:
|
||||||
|
def recognize(self, waveform, sample_rate, language=None):
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
backend = OnnxAsrBackend()
|
||||||
|
backend.ensure_model_available(model_name, "int8")
|
||||||
|
|
||||||
|
if expects_warning:
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match=(
|
||||||
|
r"Язык 'en'.*--device openvino-cpu --model medium.*"
|
||||||
|
r"--device cuda --model medium"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
backend.transcribe(FakeModel(), wav_file, language=language)
|
||||||
|
else:
|
||||||
|
with warnings.catch_warnings(record=True) as caught:
|
||||||
|
backend.transcribe(FakeModel(), wav_file, language=language)
|
||||||
|
assert caught == []
|
||||||
|
|
||||||
|
def test_auto_language_uses_single_supported_model_language(
|
||||||
|
self, monkeypatch, tmp_path
|
||||||
|
):
|
||||||
|
wav_file = tmp_path / "test.wav"
|
||||||
|
wav_file.write_bytes(b"fake audio")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"faster_whisper.decode_audio",
|
||||||
|
lambda path, sampling_rate=16000: [0.0] * 16000,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeModel:
|
||||||
|
def recognize(self, waveform, sample_rate, language=None):
|
||||||
|
return iter(())
|
||||||
|
|
||||||
|
backend = OnnxAsrBackend()
|
||||||
|
backend.ensure_model_available("gigaam-v3-e2e-rnnt", "int8")
|
||||||
|
|
||||||
|
result = backend.transcribe(FakeModel(), wav_file, language=None)
|
||||||
|
|
||||||
|
assert result.language == "ru"
|
||||||
|
assert result.language_probability == 0.0
|
||||||
|
|
||||||
def test_transcribe_collects_segments(self, monkeypatch, tmp_path):
|
def test_transcribe_collects_segments(self, monkeypatch, tmp_path):
|
||||||
"""Verify transcribe maps VAD segments to project Segments."""
|
"""Verify transcribe maps VAD segments to project Segments."""
|
||||||
wav_file = tmp_path / "test.wav"
|
wav_file = tmp_path / "test.wav"
|
||||||
@@ -326,7 +390,7 @@ class TestTranscribe:
|
|||||||
result = backend.transcribe(FakeModel(), wav_file, language=None)
|
result = backend.transcribe(FakeModel(), wav_file, language=None)
|
||||||
|
|
||||||
assert len(result.segments) == 0
|
assert len(result.segments) == 0
|
||||||
assert result.language == "unknown"
|
assert result.language == UNKNOWN_LANGUAGE
|
||||||
assert result.duration == 1.0
|
assert result.duration == 1.0
|
||||||
|
|
||||||
def test_transcribe_skips_zero_length_vad_segments(self, monkeypatch, tmp_path):
|
def test_transcribe_skips_zero_length_vad_segments(self, monkeypatch, tmp_path):
|
||||||
@@ -386,3 +450,23 @@ class TestModelAliases:
|
|||||||
backend = OnnxAsrBackend()
|
backend = OnnxAsrBackend()
|
||||||
with pytest.raises(ValueError, match="Неподдерживаемая модель"):
|
with pytest.raises(ValueError, match="Неподдерживаемая модель"):
|
||||||
backend._resolve_model("nonexistent-model")
|
backend._resolve_model("nonexistent-model")
|
||||||
|
|
||||||
|
def test_whisper_alias_error_suggests_explicit_backend(self):
|
||||||
|
backend = OnnxAsrBackend()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match=r"Whisper.*--device openvino-cpu.*--device cuda",
|
||||||
|
):
|
||||||
|
backend._resolve_model("medium")
|
||||||
|
|
||||||
|
def test_turbo_whisper_error_suggests_models_supported_by_backends(self):
|
||||||
|
backend = OnnxAsrBackend()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
backend._resolve_model("large-v3-turbo")
|
||||||
|
|
||||||
|
message = str(exc_info.value)
|
||||||
|
assert "--device openvino-cpu --model large-v3-turbo" in message
|
||||||
|
assert "--device cuda --model medium" in message
|
||||||
|
assert "--device cuda --model large-v3-turbo" not in message
|
||||||
+9
-32
@@ -61,46 +61,23 @@ def test_detect_device_explicit_passthrough():
|
|||||||
"""Явные device strings проходят без изменений."""
|
"""Явные device strings проходят без изменений."""
|
||||||
assert detect_device("cpu") == "cpu"
|
assert detect_device("cpu") == "cpu"
|
||||||
assert detect_device("cuda") == "cuda"
|
assert detect_device("cuda") == "cuda"
|
||||||
|
assert detect_device("onnx") == "onnx"
|
||||||
assert detect_device("openvino-gpu") == "openvino-gpu"
|
assert detect_device("openvino-gpu") == "openvino-gpu"
|
||||||
assert detect_device("openvino-cpu") == "openvino-cpu"
|
assert detect_device("openvino-cpu") == "openvino-cpu"
|
||||||
|
|
||||||
|
|
||||||
def test_detect_device_auto_openvino_gpu():
|
def test_detect_device_auto_cuda_when_nvidia_smi_available():
|
||||||
"""auto + нет nvidia-smi + есть OpenVINO GPU → openvino-gpu."""
|
"""При доступном nvidia-smi auto выбирает CUDA."""
|
||||||
with (
|
with patch(
|
||||||
patch("local_transcriber.utils.shutil.which", return_value=None),
|
"local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi"
|
||||||
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True),
|
|
||||||
):
|
|
||||||
assert detect_device("auto") == "openvino-gpu"
|
|
||||||
|
|
||||||
|
|
||||||
def test_detect_device_auto_openvino_cpu():
|
|
||||||
"""auto + нет nvidia-smi + есть OpenVINO, нет GPU → openvino-cpu."""
|
|
||||||
with (
|
|
||||||
patch("local_transcriber.utils.shutil.which", return_value=None),
|
|
||||||
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False),
|
|
||||||
patch("local_transcriber.utils._is_openvino_available", return_value=True),
|
|
||||||
):
|
|
||||||
assert detect_device("auto") == "openvino-cpu"
|
|
||||||
|
|
||||||
|
|
||||||
def test_detect_device_cuda_over_openvino():
|
|
||||||
"""nvidia-smi доступен и openvino тоже → cuda побеждает."""
|
|
||||||
with (
|
|
||||||
patch("local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi"),
|
|
||||||
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True),
|
|
||||||
):
|
):
|
||||||
assert detect_device("auto") == "cuda"
|
assert detect_device("auto") == "cuda"
|
||||||
|
|
||||||
|
|
||||||
def test_detect_device_auto_cpu_fallback():
|
def test_detect_device_auto_onnx_without_cuda():
|
||||||
"""Ни nvidia-smi, ни openvino → cpu."""
|
"""Без CUDA auto выбирает ONNX CPU."""
|
||||||
with (
|
with patch("local_transcriber.utils.shutil.which", return_value=None):
|
||||||
patch("local_transcriber.utils.shutil.which", return_value=None),
|
assert detect_device("auto") == "onnx"
|
||||||
patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False),
|
|
||||||
patch("local_transcriber.utils._is_openvino_available", return_value=False),
|
|
||||||
):
|
|
||||||
assert detect_device("auto") == "cpu"
|
|
||||||
|
|
||||||
|
|
||||||
def test_detect_device_openvino_resolves_to_gpu():
|
def test_detect_device_openvino_resolves_to_gpu():
|
||||||
|
|||||||
Reference in new issue
Block a user