Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7392d72bca | ||
|
|
0af2dbdd17 | ||
|
|
3c519c6860 | ||
|
|
80b864077a | ||
|
|
579cb87bc0 | ||
|
|
b302e3dbb2 | ||
|
|
f8c6a4fc7e | ||
|
|
5a7fc0d613 | ||
|
|
b1dfd9dcca | ||
|
|
650e7580e5 | ||
|
|
df204e5ae4 | ||
|
|
9307486ceb | ||
|
|
f18ad6ea2a | ||
|
|
966efc3bfe | ||
|
|
e41600510b | ||
|
|
0059e28f77 | ||
|
|
1c779be139 | ||
|
|
9cfa437b33 | ||
|
|
0c3a67c4ea | ||
|
|
3b7eb603ae | ||
|
|
5990d85a58 | ||
|
|
503f2a3732 | ||
|
|
854bf55145 | ||
|
|
53142fd341 | ||
|
|
b1cbdc3e0b | ||
|
|
7d39ab908b | ||
|
|
79dbd170ce | ||
|
|
9e04dc8c25 | ||
|
|
d9c9aefdb3 | ||
|
|
f25a546754 | ||
|
|
a3d0213cb3 | ||
|
|
b5c1da24ce | ||
|
|
82596187a4 | ||
|
|
0b5703a0e3 | ||
|
|
2c1d798eeb | ||
|
|
cb6c1d7728 | ||
|
|
749ede090c | ||
|
|
59b74f166c | ||
|
|
857267763c | ||
|
|
8ae74d2748 | ||
|
|
248f260005 | ||
|
|
413cb3e6d8 | ||
|
|
9d41b893c3 |
@@ -4,3 +4,6 @@ __pycache__/
|
||||
.mypy_cache/
|
||||
dist/
|
||||
*.pyc
|
||||
.codex
|
||||
.qwen/
|
||||
.scratch/
|
||||
@@ -0,0 +1 @@
|
||||
3.13.13
|
||||
@@ -0,0 +1,90 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project
|
||||
|
||||
Local audio/video transcription CLI — no cloud, no API keys. Outputs markdown with timestamps.
|
||||
|
||||
**Language conventions**: code identifiers in English; docstrings, comments, UI strings, and commit messages in Russian. Style is ruff-compatible. Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
uv sync # install dependencies
|
||||
uv run transcribe meeting.mp4 # run CLI
|
||||
uv run pytest # run all tests
|
||||
uv run pytest tests/test_cli.py # run one test file
|
||||
uv run pytest -k test_name # run single test by name
|
||||
uv run pytest -v # verbose output
|
||||
```
|
||||
|
||||
Package manager is **uv** (not pip). Build backend is hatchling.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
CLI (cli.py)
|
||||
→ config.py cascade: CLI arg → .transcriber.toml → device-aware default → hardcoded
|
||||
→ utils.py detect_device(), validate files, expand globs (Windows workaround)
|
||||
→ context_menu.py Windows SendTo: Transcribe.cmd install/uninstall (--install-menu / --uninstall-menu)
|
||||
→ transcriber.py load_model() → get_backend(device) → ensure_model_available → create_model
|
||||
_transcribe_file() with mid-stream CUDA→CPU fallback
|
||||
→ formatter.py segments → markdown with timestamps, paragraph grouping (>2s pause or >60s)
|
||||
```
|
||||
|
||||
### Backend system (`src/local_transcriber/backends/`)
|
||||
|
||||
Three backends implement the `Backend` Protocol (structural typing, no inheritance required):
|
||||
|
||||
| Backend | Module | Devices | Library |
|
||||
|---------|--------|---------|---------|
|
||||
| FasterWhisper | `faster_whisper.py` | `cpu`, `cuda` | `faster_whisper` (CTranslate2) |
|
||||
| OpenVINO | `openvino.py` | `openvino`, `openvino-gpu`, `openvino-cpu` | `openvino_genai` |
|
||||
| OnnxAsr | `onnx_asr.py` | `onnx` | `onnx_asr` (onnxruntime) |
|
||||
|
||||
`get_backend(device)` in `backends/__init__.py` maps device string to backend with lazy imports.
|
||||
|
||||
### Key design decisions
|
||||
|
||||
- **Two-level fallback**: GPU→CPU at model load time AND mid-stream during transcription (GPU visible via nvidia-smi but insufficient VRAM).
|
||||
- **CUDA bootstrap** (`_cuda_bootstrap.py`): preloads `libcublas.so.12` via `ctypes.CDLL(RTLD_GLOBAL)` before importing ctranslate2, because pip's `nvidia-cublas-cu12` installs to a non-standard path and `LD_LIBRARY_PATH` can't be changed at runtime (glibc caches it).
|
||||
- **Batch mode**: 3-phase pipeline (prescan → load model once → transcribe all). `TranscribeFileResult` carries updated model/backend/device state between files.
|
||||
- **Device-aware defaults**: `compute_type` and `model` vary by device (float16 for CUDA, int8 for OpenVINO, float32 for CPU). Defined in `config.py` `DEVICE_DEFAULTS`.
|
||||
- **OpenVINO uses pre-quantized models** — `compute_type` selects which HF repo to download, not a runtime parameter.
|
||||
|
||||
## Testing
|
||||
|
||||
All tests mock backends — no real model downloads or transcription. Key test patterns:
|
||||
|
||||
- CLI tests: `typer.testing.CliRunner` + mocks for `load_config`, `detect_device`, `load_model`, `_transcribe_file`, `write_transcript`
|
||||
- `_single_patches()` — helper assembling standard happy-path mock set
|
||||
- `_make_result()` / `_make_tfr()` — factories for test data
|
||||
|
||||
## Common tasks
|
||||
|
||||
- **New CLI option**: add `typer.Option` in `cli.py:main()` → add key to `HARDCODED_DEFAULTS` in `config.py` → write test
|
||||
- **New audio/video format**: add extension to `SUPPORTED_EXTENSIONS` in `utils.py`
|
||||
- **New backend**: implement `Backend` protocol → add device mapping in `backends/__init__.py` → add device-aware defaults in `config.py`
|
||||
- **Change output format**: edit `format_transcript()` in `formatter.py`
|
||||
|
||||
## Project docs
|
||||
|
||||
- `docs/PRD.md` — product requirements and scope
|
||||
- `docs/backlog.md` — future experiments and ideas
|
||||
- `docs/gpu.md` — GPU benchmarks, platform compatibility details
|
||||
- `docs/adr/` — architecture decision records (CUDA bootstrap, batch mode, pluggable backends, compute-type defaults, ONNX-ASR evaluation)
|
||||
|
||||
## Agent skills
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Задачи ведутся в Gitea через `tea`; GitHub используется только как зеркало, внешние PR не входят в triage. См. `docs/agents/issue-tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
Используются стандартные пять triage-меток. См. `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Репозиторий использует single-context layout. См. `docs/agents/domain.md`.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Локальная транскрипция
|
||||
|
||||
Контекст описывает язык проекта для локального распознавания аудио и видео.
|
||||
|
||||
## Language
|
||||
|
||||
**Движок распознавания**:
|
||||
Программная среда, которая загружает и исполняет модели распознавания. Обновление движка само по себе не означает изменение выбранной модели или рекомендаций пользователю.
|
||||
_Avoid_: Модель, ASR-модель
|
||||
|
||||
**Поддерживаемая модель**:
|
||||
Модель, которую пользователь может выбрать явно и для которой проект обеспечивает работоспособный путь транскрипции. Этот статус не означает автоматический выбор или рекомендацию для большинства пользователей.
|
||||
_Avoid_: Доступная модель, дефолт
|
||||
|
||||
**Модель по умолчанию**:
|
||||
Поддерживаемая модель, которую проект выбирает без явного указания модели пользователем для определённого пути выполнения.
|
||||
_Avoid_: Рекомендуемая модель, поддерживаемая модель
|
||||
@@ -8,8 +8,9 @@ transcribe meeting.mp4
|
||||
```
|
||||
|
||||
- **Полностью локально** — данные не покидают машину
|
||||
- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), OpenVINO CPU или CPU fallback
|
||||
- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), ONNX (CPU), OpenVINO CPU или CPU fallback
|
||||
- **Батч-режим** — обработка нескольких файлов за один вызов
|
||||
- **Из проводника Windows** — пункт Transcribe в меню «Отправить» ([установка](#контекстное-меню-проводника-windows))
|
||||
- **Markdown с таймкодами** — удобен для суммаризации ИИ
|
||||
- **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы)
|
||||
|
||||
@@ -114,6 +115,15 @@ transcribe podcast.wav --model large-v3 --compute-type float16
|
||||
# Максимальное качество на Intel GPU
|
||||
transcribe podcast.wav --model large-v3 --device openvino-gpu
|
||||
|
||||
# Максимальная скорость на CPU (русский)
|
||||
transcribe meeting.mp4 --device onnx --model gigaam-v3
|
||||
|
||||
# CPU с пунктуацией и нормализацией русского текста
|
||||
transcribe podcast.wav --device onnx --model gigaam-v3-e2e-ctc
|
||||
|
||||
# Смешанная русско-английская речь
|
||||
transcribe meeting.wav --device onnx --model gigaam-multilingual-ctc
|
||||
|
||||
# Сохранить в конкретный файл
|
||||
transcribe interview.m4a --output result.md
|
||||
```
|
||||
@@ -138,6 +148,31 @@ transcribe *.mp4 --force
|
||||
- При ошибке в одном файле остальные продолжают обрабатываться
|
||||
- `--output` несовместим с несколькими файлами
|
||||
|
||||
### Контекстное меню проводника (Windows)
|
||||
|
||||
Установить пункт `Transcribe` в меню «Отправить»:
|
||||
|
||||
```bash
|
||||
transcribe --install-menu
|
||||
```
|
||||
|
||||
(при запуске из клона репозитория — `uv run transcribe --install-menu`)
|
||||
|
||||
Использование: выделите один или несколько аудио/видеофайлов в проводнике, откройте контекстное меню правой кнопкой. В Windows 11 выберите «Показать дополнительные параметры» или нажмите Shift+F10, затем «Отправить» → «Transcribe». Несколько выделенных файлов передаются в один процесс и обрабатываются одним батчем.
|
||||
|
||||
Удалить пункт меню:
|
||||
|
||||
```bash
|
||||
transcribe --uninstall-menu
|
||||
```
|
||||
|
||||
Если что-то пошло не так, пункт можно удалить вручную: Win+R → `shell:sendto` → удалить `Transcribe.cmd`.
|
||||
|
||||
Известные ограничения:
|
||||
|
||||
- После переноса или пересоздания проекта/venv выполните `--install-menu` заново: внутри `Transcribe.cmd` хранится абсолютный путь к `transcribe.exe`.
|
||||
- Очень большой мультивыбор с суммарной длиной путей ≳8000 символов упирается в лимит командной строки cmd.exe. Обрабатывайте такие файлы частями.
|
||||
|
||||
### Опции CLI
|
||||
|
||||
| Опция | Сокращение | По умолчанию | Описание |
|
||||
@@ -145,8 +180,9 @@ transcribe *.mp4 --force
|
||||
| `--model` | `-m` | `medium` | Модель Whisper |
|
||||
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) |
|
||||
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
|
||||
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu) |
|
||||
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU) | Тип вычислений |
|
||||
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) |
|
||||
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений |
|
||||
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
|
||||
| `--force` | `-f` | — | Перезаписать существующие транскрипты |
|
||||
| `--verbose` | `-v` | — | Подробный вывод |
|
||||
|
||||
@@ -157,6 +193,7 @@ transcribe *.mp4 --force
|
||||
| CPU | ✅ | ✅ | ✅ |
|
||||
| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто |
|
||||
| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто |
|
||||
| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно |
|
||||
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
|
||||
|
||||
<details>
|
||||
@@ -209,11 +246,11 @@ language = "en"
|
||||
|
||||
Дефолты зависят от устройства:
|
||||
|
||||
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | CPU |
|
||||
|----------|------|----------------|----------------|-----|
|
||||
| model | medium | medium | medium | medium |
|
||||
| compute_type | float16 | int8 | int8 | float32 |
|
||||
| language | ru | ru | ru | ru |
|
||||
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU |
|
||||
|----------|------|----------------|----------------|------|-----|
|
||||
| model | medium | medium | medium | gigaam-v3 | medium |
|
||||
| compute_type | float16 | int8 | int8 | int8 | float32 |
|
||||
| language | ru | ru | ru | ru | ru |
|
||||
|
||||
## Модели и GPU
|
||||
|
||||
@@ -221,6 +258,8 @@ language = "en"
|
||||
- **По умолчанию:** `medium` — хороший баланс скорости и качества
|
||||
- **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16`
|
||||
- **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu`
|
||||
- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после)
|
||||
- **CPU с пунктуацией (русский):** `--device openvino-cpu --model medium` (5-6× RTF; для встреч ≤30 мин с равномерной громкостью — на длинных файлах с тихими фрагментами возможны галлюцинации)
|
||||
- **Быстрый тест:** `tiny` — для проверки пайплайна
|
||||
|
||||
<details>
|
||||
@@ -234,6 +273,37 @@ language = "en"
|
||||
| `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ |
|
||||
| `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ |
|
||||
|
||||
#### ONNX-модели (`--device onnx`)
|
||||
|
||||
Другие архитектуры, не Whisper. Работают через onnxruntime на CPU:
|
||||
|
||||
| Модель | Размер (int8) | RTFx CPU | Языки | Пунктуация |
|
||||
|--------|--------------|----------|-------|-----------|
|
||||
| `gigaam-v3` | ~300 MB | 17-29× | ru | ❌ |
|
||||
| `gigaam-multilingual-ctc` | ~300 MB | 10,5×* | ru, en, kk, ky, uz | ❌ |
|
||||
| `gigaam-v3-e2e-ctc` | ~300 MB | 12,3×* | ru | ✅ |
|
||||
| `gigaam-v3-e2e-rnnt` | ~300 MB | 11,7×* | ru | ✅ |
|
||||
| `parakeet-v3` | ~600 MB | 12-20× | 25 языков | ✅ |
|
||||
|
||||
\* Наблюдение на AMD Ryzen 7 8845H, Windows, `int8`, запись 14:51. Это не
|
||||
приёмочный замер для целевого Intel Core i5.
|
||||
|
||||
> **Рекомендация**: для русского по-прежнему используйте проверенный `gigaam-v3`
|
||||
> (см. [ADR-006](docs/adr/006-onnx-asr-backend.md)). E2E-модели добавляют
|
||||
> пунктуацию и нормализацию, но их скорость на слабых CPU не измерялась;
|
||||
> `gigaam-v3-e2e-rnnt` декодирует последовательно и особенно медленна на длинных
|
||||
> записях. `parakeet-v3` на русском воспроизводит проблемы из
|
||||
> [ADR-005](docs/adr/005-parakeet-evaluation.md).
|
||||
|
||||
GigaAM Multilingual сама распознаёт русский, английский, казахский, кыргызский и
|
||||
узбекский внутри одной записи. `onnx-asr` не передаёт этой модели подсказку
|
||||
языка, поэтому `--language` не управляет её выбором языка.
|
||||
|
||||
Для моделей из таблицы опубликованы `int8` и `float32`. Если неявный
|
||||
device-aware дефолт недоступен для выбранной модели, CLI сообщит о подстановке
|
||||
доступного варианта. Явное значение из `--compute-type` или
|
||||
`.transcriber.toml` вместо подстановки завершится ошибкой.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -243,9 +313,10 @@ language = "en"
|
||||
|-----|--------|----------|----------|--------------------|
|
||||
| `float16` | CUDA | ~4.5-5 GB | Отлично | **По умолчанию для CUDA** |
|
||||
| `int8_float16` | CUDA | ~4.7 GB | Отлично | GPU от 6 GB, альтернатива float16 |
|
||||
| `int8` | CUDA / OpenVINO | Низкое | Хорошо, но бывают галлюцинации | **По умолчанию для OpenVINO** |
|
||||
| `int8_float32` | CPU | Среднее | Отлично | **Рекомендуется для CPU** — 1.5x быстрее float32 при том же качестве |
|
||||
| `int8` | CUDA / OpenVINO / ONNX | Низкое | Хорошо, но бывают галлюцинации | **По умолчанию для OpenVINO и ONNX** |
|
||||
| `fp16` | OpenVINO | Низкое | Отлично | OpenVINO large-v3 (выбирается автоматически) |
|
||||
| `float32` | CPU | Среднее | Отлично | **По умолчанию для CPU** |
|
||||
| `float32` | CPU / ONNX | Среднее | Отлично | **По умолчанию для CPU** |
|
||||
|
||||
**Важно:** `int8` на длинных записях может давать галлюцинации (повтор фраз, потеря контента).
|
||||
`float16`/`fp16` и `float32` значительно стабильнее на записях >20 минут.
|
||||
|
||||
+2
-2
@@ -118,7 +118,7 @@ transcribe <путь_к_файлу> [опции]
|
||||
|
||||
### 4.2. Требования к окружению
|
||||
|
||||
- Python ≥ 3.10
|
||||
- Python ≥ 3.13
|
||||
- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов)
|
||||
- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001)
|
||||
- Дисковое пространство для моделей: ~3 GB (large-v3)
|
||||
@@ -134,7 +134,7 @@ transcribe <путь_к_файлу> [опции]
|
||||
|
||||
| Компонент | Технология |
|
||||
|---------------------|-------------------------------------------------|
|
||||
| Язык | Python 3.10+ |
|
||||
| Язык | Python 3.13+ |
|
||||
| Управление проектом | uv (pyproject.toml) |
|
||||
| Распознавание речи | faster-whisper (CTranslate2 backend) |
|
||||
| Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) |
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# ADR-005: NVIDIA Parakeet TDT — оценено, отклонено для дефолтного use case
|
||||
|
||||
**Статус**: Отклонено (оставлено в экспериментальной ветке)
|
||||
**Дата**: 2026-04-19
|
||||
|
||||
## Контекст
|
||||
|
||||
ADR-003 и ADR-004 зафиксировали стратегию: CTranslate2 на CPU (качество) +
|
||||
OpenVINO (скорость на x86). Оба бэкенда — Whisper. Возникла гипотеза, что
|
||||
**NVIDIA Parakeet TDT 0.6B v3** может выйти за пределы этого trade-off: модель
|
||||
заявлена multilingual (25 языков включая русский), имеет ONNX-порт
|
||||
[istupakov/parakeet-tdt-0.6b-v3-onnx](https://huggingface.co/istupakov/parakeet-tdt-0.6b-v3-onnx),
|
||||
работает через `onnx-asr` + Silero VAD. Есть позитивные отзывы (напр.
|
||||
приложение Handy использует Parakeet для dictation в реальном времени).
|
||||
|
||||
Основное применение проекта — транскрипция русскоязычных ИТ-встреч
|
||||
(ментор ↔ менти, ~15–60 мин, ноутбучный микрофон). Если Parakeet обойдёт
|
||||
Whisper medium int8 по скорости при сравнимом качестве, или по качеству при
|
||||
сравнимой скорости — это основание поменять дефолт.
|
||||
|
||||
## Эксперимент
|
||||
|
||||
Реализован полный бэкенд в ветке `feature/parakeet-backend` (ONNX Runtime CPU EP,
|
||||
Silero VAD, cache-first model loading). Прогнан автоматический бенчмарк
|
||||
на двух 15-минутных отрывках реальных установочных встреч
|
||||
(`scripts/quality_bench.py` в ветке), CPU Intel i7-11800H.
|
||||
|
||||
Качество оценивал агент-судья по шкале 1–5 по критериям completeness /
|
||||
term accuracy / fluency / summary utility — на пригодность транскрипта
|
||||
для построения конспекта без возврата к аудио.
|
||||
|
||||
### Скорость и ресурсы
|
||||
|
||||
| Отрывок | Бэкенд | Wall | RTFx | Peak RSS |
|
||||
|---|---|---|---|---|
|
||||
| artur-intro (15 мин) | **parakeet-int8** | 129.5s | **6.92x** | 2.21 GB |
|
||||
| artur-intro (15 мин) | openvino-medium-int8 | 354.2s | 2.54x | 3.50 GB |
|
||||
| mar15-mid (15 мин) | **parakeet-int8** | 159.6s | **5.64x** | 2.65 GB |
|
||||
| mar15-mid (15 мин) | openvino-medium-int8 | 428.4s | 2.10x | 3.52 GB |
|
||||
|
||||
**Parakeet в 2.7x быстрее и использует на ~35% меньше RAM.**
|
||||
|
||||
### Качество (Summary utility, 1–5)
|
||||
|
||||
| Отрывок | Parakeet int8 | Whisper medium int8 |
|
||||
|---|---|---|
|
||||
| artur-intro | **2** | **4** |
|
||||
| mar15-mid | **3** | **4** |
|
||||
|
||||
Систематические проблемы Parakeet на русской речи с низкой громкостью:
|
||||
|
||||
1. **Редукция коротких реплик менти до `Mm-hmm`/`Yeah`** — теряется
|
||||
значительная часть диалога (низкий уровень микрофона менти).
|
||||
2. **Вставки кусков польского/испанского/немецкого** посреди русской речи
|
||||
(`Miejsce w sześć zajmie`, `Que salviosa`, `Genial positions`).
|
||||
3. **Сильное искажение ИТ-терминов и бытовых выражений** —
|
||||
`Pretty subs` вместо «пройти собес», `Состем основ` вместо «система контроля
|
||||
версий», `І на глуп Джой` вместо `inner loop join`.
|
||||
|
||||
Whisper medium int8 даёт связный русский текст с нормальной пунктуацией;
|
||||
искажения присутствуют (`капка` вместо Kafka, одна галлюцинация
|
||||
«Добро пожаловать в наш канал!» на паузе), но не критичны для конспекта.
|
||||
|
||||
### Попытка тюнинга — Silero VAD threshold
|
||||
|
||||
Гипотеза: дефолтный `threshold=0.5` обрезает тихие реплики менти. Проверено
|
||||
на 0.5/0.3/0.2 — снижение порога **уменьшает** объём транскрипта
|
||||
(Parakeet хуже декодирует длинные склеенные сегменты с разреженной речью)
|
||||
и **не снижает** частоту `Mm-hmm`-редукций. Проблема inherent для модели
|
||||
на тихом русском диалоге, не артефакт VAD.
|
||||
|
||||
## Решение
|
||||
|
||||
**Parakeet TDT 0.6B v3 не принимается как дефолтный или опциональный бэкенд
|
||||
для целевого use case.** Whisper medium int8 остаётся предпочтительным —
|
||||
несмотря на 2.7x проигрыш по скорости, он даёт транскрипт, из которого можно
|
||||
сделать конспект без возврата к аудио. Для русской речи с варьирующейся
|
||||
громкостью микрофона Parakeet v3 недостаточно устойчив.
|
||||
|
||||
Код бэкенда **не мержится в master**. Экспериментальная ветка
|
||||
`feature/parakeet-backend` сохраняется как reference для будущих экспериментов.
|
||||
|
||||
## Последствия
|
||||
|
||||
- Архитектура pluggable backends (ADR-003) подтвердила ценность: интегрировать
|
||||
и оценить новый бэкенд оказалось недорого.
|
||||
- Surface area master не растёт: нет `--device parakeet-cpu`, нет зависимости
|
||||
`onnx-asr`, нет новой ветки в CLI.
|
||||
- Если в будущем появится интерес — ветка `feature/parakeet-backend` содержит
|
||||
готовый бэкенд (13 задач, ~19 тестов), скрипты бенчмарка и VAD-тюнинга.
|
||||
|
||||
## Когда стоит пересмотреть решение
|
||||
|
||||
1. **Англоязычный контент** — Parakeet обучался преимущественно на английском,
|
||||
для native English встреч выигрыш в скорости может окупить качество.
|
||||
2. **Live-captioning / dictation** (как в Handy) — другой use case, близкий
|
||||
микрофон + короткие фразы, проблемы редукции неактуальны.
|
||||
3. **Русские specialized-модели через ту же обвязку** `onnx-asr`:
|
||||
`gigaam-v3-rnnt` (SberDevices), `nemo-fastconformer-ru-rnnt` (NVIDIA),
|
||||
`t-one-ctc` (T-Bank). Это отдельная ветка экспериментов, не Parakeet.
|
||||
4. **Слабые машины с ограниченной RAM** — Parakeet 2.2 GB vs Whisper 3.5 GB
|
||||
на 15-мин аудио. Если важна RAM-экономия и английский контент.
|
||||
@@ -0,0 +1,135 @@
|
||||
# ADR-006: onnx-asr бэкенд — GigaAM v3 как CPU-default для русских встреч
|
||||
|
||||
**Статус**: Принято
|
||||
**Дата**: 2026-04-25
|
||||
|
||||
## Контекст
|
||||
|
||||
Целевая аудитория `local-transcriber` — пользователи с Intel iGPU / CPU, без дискретного NVIDIA GPU.
|
||||
Существующие CPU-бэкенды (faster-whisper, OpenVINO) дают 0.5-6x RTF для средних моделей — транскрипция часовой записи занимает 10-120 минут.
|
||||
|
||||
[onnx-asr](https://github.com/istupakov/onnx-asr) — легковесная обёртка (onnxruntime + numpy) над ONNX-моделями Parakeet, GigaAM, FastConformer и Canary. Заявляет 30-90x RTF на CPU при сравнимом с Whisper качестве для русского языка.
|
||||
|
||||
[ADR-005](005-parakeet-evaluation.md) ранее отклонил Parakeet TDT 0.6B v3 для целевого use case на двух 15-минутных файлах с тихим микрофоном менти и плотной IT-терминологией. ADR-006 повторяет эксперимент на новом наборе файлов (22-81 мин, разной громкости), добавляет GigaAM v3 как кандидата для русской речи и проверяет три класса проблем Parakeet из ADR-005 на актуальном материале.
|
||||
|
||||
## Эксперимент
|
||||
|
||||
Три реальных русскоязычных записи установочных встреч (формат ментор↔менти, mp4, 22-81 мин), ноутбук с Intel i7-11800H (CPU-only). Сравнивались три CPU-бэкенда:
|
||||
|
||||
- **gigaam-v3** (onnx-asr GigaAM v3 CTC, int8, monolingual ru, без пунктуации)
|
||||
- **parakeet-v3** (onnx-asr Parakeet TDT 0.6B v3, int8, multilingual)
|
||||
- **ov-medium** (OpenVINO Whisper medium int8) — baseline
|
||||
|
||||
Качественная оценка проведена независимым agent-judge'ем по методологии ADR-005 (4 критерия: completeness / term accuracy / fluency / summary utility, шкала 1-5). Транскрипты сохранены в `/mnt/c/ddmitry/Videos/OBS/<basename>.{onnx-gigaam,parakeet-v3,ov-medium}.md` и доступны для верификации.
|
||||
|
||||
### Скорость
|
||||
|
||||
| Файл | Длит. | gigaam-v3 | parakeet-v3 --ru | OpenVINO medium |
|
||||
|---|---|---|---|---|
|
||||
| 10-59-59 | 22:26 | 81с (16.6×) | 116с (11.6×) | 265с (5.1×) |
|
||||
| Vasya | 45:51 | 95с (29×) | 138с (20×) | 455с (6×) |
|
||||
| 12-02-37 | 1:20:44 | 170с (28.5×) | 251с (19.3×) | 779с (6.2×) |
|
||||
|
||||
GigaAM в 3-5× быстрее OpenVINO medium, Parakeet в 2.5-3.5× быстрее.
|
||||
|
||||
### Качество (agent-judge, 1-5)
|
||||
|
||||
| Файл | Backend | Completeness | Term accuracy | Fluency | Summary utility |
|
||||
|------|---------|:-:|:-:|:-:|:-:|
|
||||
| 10-59-59 | **gigaam** | **5** | **4** | **4** | **4** |
|
||||
| 10-59-59 | parakeet | 4 | 2 | 2 | 2 |
|
||||
| 10-59-59 | ov-medium | 3 | 4 | 4 | 2 |
|
||||
| Vasya | **gigaam** | 3 | **4** | **4** | **4** |
|
||||
| Vasya | parakeet | 2 | 2 | 2 | 2 |
|
||||
| Vasya | ov-medium | 2 | 3 | 2 | 2 |
|
||||
| 12-02-37 | **gigaam** | 4 | 3 | 3 | **4** |
|
||||
| 12-02-37 | parakeet | 3 | 2 | 1 | 1 |
|
||||
| 12-02-37 | ov-medium | 2 | 3 | 2 | 1 |
|
||||
|
||||
GigaAM — единственный backend, дающий summary utility 4/5 на всех трёх файлах. Parakeet и ov-medium систематически уступают по разным причинам (см. ниже).
|
||||
|
||||
### Класс ошибок: Parakeet — три проблемы из ADR-005 воспроизведены
|
||||
|
||||
Все три класса систематических ошибок Parakeet, описанные в [ADR-005](005-parakeet-evaluation.md), воспроизводятся на новом наборе файлов:
|
||||
|
||||
**1. Mm-hmm/Yeah-редукция тихих реплик менти.** Массово на всех трёх файлах:
|
||||
- Vasya `[04:56-09:33]` блок из ~10 реплик: `Mm-hmm. Mm-hmm. Mm. That's nice. Mm-hmm. Mm-hmm.` — полностью утеряны ответы менти на вопросы ментора.
|
||||
- 10-59-59 `[19:03]` `Yeah. Иногда лучше дышали в облаке`.
|
||||
- 12-02-37 `[00:00:01]` `I mean.` вместо «не пони…».
|
||||
|
||||
**2. Вставки иностранных языков посреди русского.** На этом наборе ещё агрессивнее, чем в ADR-005 (там был только польский):
|
||||
- 10-59-59 `[00:08]` `Secondo, Alice. The mutual microphone.` — итальянский+английский для «секунду, Алиса, замьючен микрофон».
|
||||
- 10-59-59 `[22:02]` `Ah si va sur. Well.` — испано-французская смесь в финальном прощании.
|
||||
- Vasya `[27:31]` `Mas o żegnienie.` — польский в полностью русской встрече.
|
||||
- 12-02-37 `[10:38]` `Запроси к Każdemu Actually, таблица классная` — русско-польско-английский в одной фразе.
|
||||
- 12-02-37 `[01:03:23]` `No już je wsie.` — польский («ну уже всё»).
|
||||
|
||||
**3. Искажение IT-терминов и имён компаний:**
|
||||
- 10-59-59 `[02:21]` `не Аринадата и не Терринте игра` вместо «Аренадата и Тере-Интегра» (имена работодателей).
|
||||
- 10-59-59 `[01:23]` `Запромбанке` (с unk-токенами) вместо «Газпромбанк».
|
||||
- 12-02-37 `[02:35]` `Basic space clear cause` вместо «база данных кликхаус».
|
||||
- 12-02-37 `[06:12]` `Поскре это не колочный, чтобы это греплан. Ловочная.` — Postgres/Greenplum/«колоночная» искажены до неразборчивости.
|
||||
- 12-02-37 `[16:53]` `своеобресть` вместо «Wildberries» — целевой работодатель в задаче, имя потеряно.
|
||||
|
||||
### Класс ошибок: Whisper medium — галлюцинации на длинных файлах с тихими фрагментами
|
||||
|
||||
Не описано в ADR-005 (там были 15-минутные отрывки) — обнаружено только на длинных файлах:
|
||||
|
||||
- 12-02-37 `[01:03:43-01:20:14]` — **17 минут хвоста встречи** забиты галлюцинированными повторами: `«Вместе с вами мы решим, как мы будем работать с вами»`, `«Это не то, чтобы не было»`, `«Выбор? Нет. Выбор? Нет.»`. Бытовая часть встречи целиком потеряна.
|
||||
- 12-02-37 `[19:54-20:49]` — 11 повторов `«И вот, как я вам рассказываю, это очень интересно»` вместо реального решения SQL-задачи.
|
||||
- Vasya `[10:08-11:59]` — ~6 повторов `«Но если вы хотите, чтобы мы не разговаривали, то вы можете.»` (~2 минуты галлюцинации).
|
||||
- Vasya `[36:00-36:30]` — 14 повторов `«Ага. Ага.»` (loop).
|
||||
- Vasya `[45:51]` — `«Субтитры сделаны с помощью СМС, аппарата — Лариса.»` — классический Whisper-артефакт «титров».
|
||||
- 10-59-59 `[10:56-11:40]` — строка из ~1000 символов `«ааааа…»` — галлюцинация на тихом фрагменте, проглатывает 30 секунд аудио.
|
||||
- 12-02-37 `[01:18:47]` — приписан несуществующий человек `«Валерий Сюткин»`.
|
||||
|
||||
Это критичный класс ошибок: текст выглядит правдоподобно, и читатель конспекта не отличит галлюцинацию от реального содержания без возврата к аудио. Хуже потери — потому что вводит в заблуждение.
|
||||
|
||||
### Класс ошибок: GigaAM — локальные искажения латиницы и имён
|
||||
|
||||
GigaAM monolingual ru, латиницу не выдаёт. На транскрипте:
|
||||
- `«эскель»`/`«эсквель»` вместо `SQL` (везде кириллицей).
|
||||
- `«гитам ардауна»` вместо `git и markdown` (Vasya `[14:14]`).
|
||||
- `«арендата»`/`«арендат»`/`«арендода»` для «Аренадата» (10-59-59 `[02:21]`) — три разных варианта одного имени.
|
||||
- `«дв один»` вместо `DEV1` (12-02-37 `[00:50:56]`).
|
||||
- `«яндекс тим под яндекс тим»` для «Яндекс ТимКод» (12-02-37 `[00:14:15]`).
|
||||
|
||||
Mm-hmm-редукция и иностранные вставки **не обнаружены**: monolingual архитектура исключает language-confusion, тихие реплики менти остаются как русские «угу/да/ну».
|
||||
|
||||
Эти ошибки локальны, предсказуемы и легко чинятся LLM-этапом нормализации без знания исходного аудио (восстановить SQL, Greenplum, ClickHouse, имена компаний из контекста).
|
||||
|
||||
## Решение
|
||||
|
||||
**Принять onnx-asr как экспериментальный бэкенд с явным `--device onnx`. GigaAM v3 — рекомендуемая модель для русских встреч на CPU.**
|
||||
|
||||
Бэкенд **не в auto-detect** — только при явном указании пользователем (политика experimental backend, как для openvino).
|
||||
|
||||
Модели:
|
||||
- **`gigaam-v3`** — рекомендуемая для русских встреч на CPU. 17-29× RTF, summary utility 4/5 на всех протестированных файлах. Без пунктуации, без латиницы; ошибки локальны, чинятся LLM-нормализацией.
|
||||
- **`parakeet-v3`** — multilingual (25 языков), формально доступен. **Не рекомендуется для русских встреч**: Mm-hmm-редукция и иноязычные вставки воспроизводятся систематически (см. выше). Уместен только для англоязычного контента.
|
||||
|
||||
Обе модели в int8-квантизации (~300 MB).
|
||||
|
||||
## Последствия
|
||||
|
||||
- Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах).
|
||||
- Whisper medium (`--device openvino-cpu`) **остаётся допустимым** для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с тихими участками он галлюцинирует целыми блоками — этот риск зафиксирован, но решение не выводит OpenVINO из списка дефолтов (часть пользователей всё ещё нуждается в пунктуации, и для коротких файлов галлюцинации не воспроизводятся).
|
||||
- Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится.
|
||||
- GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU).
|
||||
- Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта.
|
||||
|
||||
## Открытые вопросы / следующие шаги
|
||||
|
||||
- **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю.
|
||||
- **GigaAM v3 RNN-T** (вариант `gigaam-v3-rnnt` вместо `gigaam-v3-ctc`) — заявлен как немного качественнее CTC, не тестировался. Может закрыть часть GigaAM-ошибок на латинице.
|
||||
- **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация.
|
||||
- **Auto-detect onnx**: после стабилизации в production-использовании (несколько недель) — рассмотреть включение в auto-detect как первый CPU-бэкенд (ниже CUDA, выше OpenVINO).
|
||||
|
||||
## Отклонённые альтернативы
|
||||
|
||||
| Альтернатива | Почему отклонена |
|
||||
|---|---|
|
||||
| NeMo Parakeet напрямую (без onnx-asr) | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI |
|
||||
| Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом |
|
||||
| GigaAM как auto-detect default | Экспериментальный бэкенд, политика — не сюрпризить существующих пользователей; включение в auto-detect — после периода стабилизации |
|
||||
| Parakeet-v3 как multilingual default | Воспроизведённые проблемы из ADR-005 (Mm-hmm-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте |
|
||||
@@ -0,0 +1,47 @@
|
||||
# Domain Docs
|
||||
|
||||
Репозиторий использует single-context layout.
|
||||
|
||||
## Перед исследованием кода
|
||||
|
||||
- Прочитать корневой `CONTEXT.md`.
|
||||
- Прочитать относящиеся к задаче решения из `docs/adr/`.
|
||||
- Проверить относящиеся к задаче исследования в `docs/research/`.
|
||||
- Проверить относящиеся к задаче спецификации в `docs/specs/`.
|
||||
- Если документа нет, продолжить молча: доменные документы создаются лениво
|
||||
соответствующими навыками.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── docs/
|
||||
│ ├── adr/
|
||||
│ ├── research/
|
||||
│ │ └── YYYY-MM-DD-slug.md
|
||||
│ └── specs/
|
||||
│ └── YYYY-MM-DD-slug.md
|
||||
└── src/
|
||||
```
|
||||
|
||||
## Назначение документов
|
||||
|
||||
- `CONTEXT.md` — каноническая терминология предметной области.
|
||||
- `docs/adr/` — принятые архитектурные решения и их обоснование.
|
||||
- `docs/research/YYYY-MM-DD-slug.md` — результаты исследований, основанные на
|
||||
источниках и экспериментах.
|
||||
- `docs/specs/YYYY-MM-DD-slug.md` — согласованные спецификации изменений.
|
||||
|
||||
## Терминология
|
||||
|
||||
В задачах, тестах, предложениях и документации использовать термины из
|
||||
`CONTEXT.md`. Не заменять их синонимами, перечисленными в `_Avoid_`.
|
||||
|
||||
Если нужного понятия нет, проверить, действительно ли это доменный термин.
|
||||
Существенный пробел передать в `domain-modeling`.
|
||||
|
||||
## Конфликты с ADR
|
||||
|
||||
Если предлагаемое изменение противоречит существующему ADR, указать конфликт
|
||||
явно и объяснить, почему решение стоит пересмотреть.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Issue Tracker
|
||||
|
||||
Задачи проекта ведутся в Gitea-репозитории `ddmitry/local-transcriber`.
|
||||
|
||||
- Основной remote: `origin`
|
||||
- Gitea: `https://git.dementev.space`
|
||||
- CLI: `tea`
|
||||
- Remote `github` является зеркалом и не используется для управления задачами
|
||||
- Внешние pull request не входят в очередь triage
|
||||
|
||||
## Доступ
|
||||
|
||||
Перед операциями с задачами проверить наличие `tea`.
|
||||
|
||||
Если команда недоступна, остановиться и предложить пользователю установку:
|
||||
|
||||
```powershell
|
||||
winget install --id Gitea.tea --exact
|
||||
```
|
||||
|
||||
Не переключаться автоматически на GitHub Issues или локальные markdown-задачи.
|
||||
|
||||
Проверить настроенные подключения:
|
||||
|
||||
```powershell
|
||||
tea login list
|
||||
```
|
||||
|
||||
Если подходящего подключения нет, предложить пользователю настроить его через
|
||||
`tea login add`. Не запрашивать и не выводить токены в переписке или логах.
|
||||
|
||||
## Прокси
|
||||
|
||||
Рабочее окружение использует корпоративный прокси (`HTTP_PROXY` и `HTTPS_PROXY`),
|
||||
через который `git.dementev.space` недоступен: запрос к API завершается ошибкой
|
||||
`EOF`. Хост нужно добавить в `NO_PROXY`.
|
||||
|
||||
Разделитель — **запятая**, не точка с запятой: `tea` написан на Go, а Go
|
||||
разбирает `NO_PROXY` по запятым, и хост после `;` не распознаётся.
|
||||
|
||||
На текущую сессию:
|
||||
|
||||
```powershell
|
||||
$env:NO_PROXY = "$env:NO_PROXY,git.dementev.space"
|
||||
```
|
||||
|
||||
Постоянно, в пользовательских переменных окружения (значение подхватят только
|
||||
новые процессы):
|
||||
|
||||
```powershell
|
||||
[Environment]::SetEnvironmentVariable("NO_PROXY", "$env:NO_PROXY,git.dementev.space", "User")
|
||||
```
|
||||
|
||||
## Работа с задачами
|
||||
|
||||
Из рабочего дерева использовать Gitea remote `origin`:
|
||||
|
||||
```powershell
|
||||
tea issues list --remote origin
|
||||
tea issues create --remote origin
|
||||
tea issues edit <index> --remote origin
|
||||
tea labels list --remote origin
|
||||
```
|
||||
|
||||
За пределами рабочего дерева явно указывать репозиторий
|
||||
`ddmitry/local-transcriber` и настроенный Gitea login.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Triage Labels
|
||||
|
||||
Инженерные навыки используют пять канонических triage-ролей. В Gitea им
|
||||
соответствуют одноимённые метки.
|
||||
|
||||
| Роль навыка | Метка Gitea | Значение |
|
||||
| --- | --- | --- |
|
||||
| `needs-triage` | `needs-triage` | Требует оценки сопровождающим |
|
||||
| `needs-info` | `needs-info` | Ожидает дополнительной информации от автора |
|
||||
| `ready-for-agent` | `ready-for-agent` | Полностью описана и готова для автономного агента |
|
||||
| `ready-for-human` | `ready-for-human` | Требует реализации человеком |
|
||||
| `wontfix` | `wontfix` | Выполняться не будет |
|
||||
|
||||
Когда навык упоминает triage-роль, следует использовать соответствующую метку
|
||||
из этой таблицы.
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
# Backlog — будущие эксперименты и направления
|
||||
|
||||
Список открытых направлений, которые имеют смысл, но не реализованы. Каждый пункт содержит обоснование и ссылку на источник (ADR / статья), чтобы при возврате не пришлось воспроизводить контекст с нуля.
|
||||
|
||||
Когда направление становится в работу — переносится в spec/план или соответствующий ADR. Когда отклоняется — остаётся в backlog с пометкой «отклонено» и причиной (для истории решений).
|
||||
|
||||
---
|
||||
|
||||
## ASR-бэкенды и модели
|
||||
|
||||
### Переоценка CPU-дефолта после обновления `onnx-asr` 0.12
|
||||
|
||||
**Проблема:** текущий CPU-путь через OpenVINO Whisper medium нестабилен на
|
||||
длинных записях с тихими участками: модель генерирует правдоподобные повторы и
|
||||
несуществующий текст. На файле `2026-07-29 13-58-39.mp4` разговор заканчивается
|
||||
примерно на 02:28, после чего OpenVINO создаёт десятки повторяющихся блоков до
|
||||
конца 59-минутной записи. Изолированный тест окна 02:00–03:30 дал одинаковую
|
||||
серию из 14 повторов на `openvino-genai` 2026.0 и 2026.3; обновление движка само
|
||||
по себе проблему не устраняет. Подробнее — в пункте
|
||||
[«Whisper medium галлюцинации»](#whisper-medium-галлюцинации-на-длинных-файлах-с-тихими-фрагментами).
|
||||
|
||||
**Порядок работы:**
|
||||
|
||||
1. Сначала обновить совместимый стек по
|
||||
[исследованию обновлений](research/2026-08-10-engine-model-updates.md): снять
|
||||
ограничение `onnx-asr<0.12.0`, проверить VAD и квантованные модели; OpenVINO и
|
||||
OpenVINO GenAI обновлять согласованно. Не фиксировать ONNX Runtime 1.28 для
|
||||
Python 3.10.
|
||||
Шаг 1 в части onnx-пути вынесен в отдельную работу —
|
||||
[спека «Onnx-каталог, Python 3.13 и границы версий»](specs/2026-08-11-onnx-model-catalog.md): движок
|
||||
обновляется, три модели становятся поддерживаемыми, дефолт и OpenVINO не
|
||||
трогаются намеренно, чтобы сохранить точку отсчёта для будущего сравнения.
|
||||
2. После интеграционных тестов провести сравнительный бенчмарк моделей. Не
|
||||
менять CPU-дефолт только по model card или результату на одном файле.
|
||||
3. Решение зафиксировать в ADR-007, обновить рекомендации README и только затем
|
||||
менять auto-detect/defaults.
|
||||
|
||||
**Кандидаты:**
|
||||
|
||||
- `gigaam-v3-ctc` — текущий baseline для русского CPU.
|
||||
- `gigaam-v3-rnnt` — контекстный декодер без пунктуации.
|
||||
- `gigaam-v3-e2e-ctc` и `gigaam-v3-e2e-rnnt` — варианты с пунктуацией и
|
||||
нормализацией текста.
|
||||
- `gigaam-multilingual-ctc` и `gigaam-multilingual-large-ctc`, добавленные в
|
||||
`onnx-asr` 0.12 — кандидаты для смешанной речи, но не априори для русского
|
||||
дефолта.
|
||||
- OpenVINO medium + VAD — альтернатива смене модели, сохраняющая Whisper и
|
||||
пунктуацию.
|
||||
- Parakeet v3 — контрольный multilingual-вариант, но не кандидат на русский
|
||||
дефолт без новых данных, опровергающих ADR-005/006.
|
||||
|
||||
**Матрица качества:**
|
||||
|
||||
- те же три полные записи и четыре критерия agent-judge из
|
||||
[ADR-006](adr/006-onnx-asr-backend.md): completeness, term accuracy, fluency,
|
||||
summary utility;
|
||||
- новый негативный пример `2026-07-29 13-58-39.mp4` с длинной тишиной;
|
||||
- небольшой вручную проверенный набор сложных фрагментов для WER/CER и разбора
|
||||
критичных смысловых ошибок: тихие реплики, имена компаний, латиница и
|
||||
IT-термины, числа, русско-английское переключение;
|
||||
- автоматические проверки потери хвоста, повторов, пустых/нулевых VAD-сегментов
|
||||
и выдуманного текста на тишине.
|
||||
|
||||
**Матрица производительности:**
|
||||
|
||||
- обязательный прогон на реальном Intel Core i5 11-го поколения; точный SKU,
|
||||
число ядер, объём RAM, power mode и число потоков записать вместе с результатом;
|
||||
- Ryzen 7 8845H разработчика и ограничение числа потоков использовать только для
|
||||
предварительного smoke-теста, не как приёмку производительности i5;
|
||||
- измерять отдельно cold start/загрузку модели и warm transcription, медиану
|
||||
трёх прогонов, RTFx, peak RSS и размер скачиваемой модели;
|
||||
- короткий 15-минутный фрагмент нужен для итераций, полные записи 22–81 мин —
|
||||
для итогового решения и проверки устойчивости.
|
||||
|
||||
**Почему интересно:**
|
||||
|
||||
- **WER 2.6% vs 13.2%** для CTC на сложных текстах — в 5 раз ниже на разговорной речи и доменной лексике (источник: SberDevices / Хабр-публикация GigaAM-v3).
|
||||
- **Контекстный декодер** — структурно решает основную проблему GigaAM-CTC из ADR-006: кириллизация латиницы и искажения имён компаний (`Запромбанк` → `Газпромбанк`, `яндекс тим под яндекс тим` → `Яндекс ТимКод`). RNN-T видит контекст уже сгенерированных токенов и может «дотянуть» имена.
|
||||
- **`v3_e2e_rnnt` с пунктуацией и нормализацией** — закрывает главное ограничение GigaAM-CTC, ради которого в README сейчас стоит fallback на `openvino-cpu medium` (с задокументированными в ADR-006 галлюцинациями на длинных файлах).
|
||||
- **70:30 vs Whisper-large-v3** — GigaAM-v3 (CTC и RNN-T) выигрывает у `large-v3` по LLM-as-Judge (Gemini 2.5 Pro). Если переносится на наш use case — RNN-T на CPU становится сильнее GPU faster-whisper large-v3.
|
||||
- **30% лучше на «новых доменах»** (callcenter-like речь, нестандартные характеристики) — это и есть домен установочных встреч.
|
||||
|
||||
**Tradeoff:**
|
||||
|
||||
- Скорость ниже CTC (RNN-T декодинг последовательный). Реалистичная оценка: 10-15× RTF на CPU вместо 17-29× у CTC. Всё ещё в 1.5-2× быстрее Whisper medium.
|
||||
- Размер модели больше (~500 MB int8 против ~300 MB у CTC) — оценка, нужна верификация.
|
||||
|
||||
**Если подтвердится бенчмарком:**
|
||||
|
||||
- Если E2E RNN-T сохраняет качество и приемлемую скорость на i5 — сделать его
|
||||
русским CPU-дефолтом, OpenVINO оставить явной опцией.
|
||||
- Если лучший вариант зависит от языка — выбрать language-aware default:
|
||||
GigaAM v3 для русского, multilingual-модель для смешанной речи.
|
||||
- Если модели GigaAM проигрывают по пунктуации/смыслу — сохранить текущий
|
||||
model default и лечить OpenVINO через VAD/качественный pipeline.
|
||||
- Если ни один вариант не проходит порог качества и скорости — не менять
|
||||
дефолт, оставить предупреждения и явный выбор backend.
|
||||
|
||||
---
|
||||
|
||||
### Canary 1B — multilingual + пунктуация на CPU
|
||||
|
||||
**Что:** Протестировать `nemo-canary-1b-v2` через onnx-asr на тех же 3 файлах.
|
||||
|
||||
**Почему:** Multilingual + пунктуация в одной модели. Кандидат на «лучшее качество за разумную скорость» для пользователей, которым нужны и не-русский контент, и пунктуация одновременно. Упомянут в [ADR-006](adr/006-onnx-asr-backend.md#открытые-вопросы--следующие-шаги).
|
||||
|
||||
**Tradeoff:** Тяжелее GigaAM (~1 GB vs ~300 MB), скорость на CPU ожидаемо ниже. Если RNN-T закроет потребность в пунктуации — Canary становится менее приоритетным.
|
||||
|
||||
---
|
||||
|
||||
## Качество и устойчивость
|
||||
|
||||
### Whisper medium галлюцинации на длинных файлах с тихими фрагментами
|
||||
|
||||
**Статус:** воспроизведено 2026-08-10. Помимо примеров из
|
||||
[ADR-006](adr/006-onnx-asr-backend.md#класс-ошибок-whisper-medium--галлюцинации-на-длинных-файлах-с-тихими-фрагментами),
|
||||
на записи `2026-07-29 13-58-39.mp4` минимальный тест 02:00–03:30 стабильно
|
||||
получает 14 одинаковых сегментов после окончания речи. На 30-секундном окне
|
||||
только с речью повторов нет. `openvino-genai` 2026.3 и
|
||||
`no_repeat_ngram_size=3` результат не меняют. Корень проблемы — длинные
|
||||
безречевые участки, которые текущий OpenVINO backend целиком передаёт в
|
||||
`WhisperPipeline` без VAD.
|
||||
|
||||
**Возможные направления:**
|
||||
|
||||
- Добавить VAD перед OpenVINO WhisperPipeline и сохранить исходные таймкоды —
|
||||
наиболее прямое лечение подтверждённой причины.
|
||||
- Ограничить длину чанка для openvino-medium (chunk_length параметр в WhisperPipeline).
|
||||
- Внедрить `compression_ratio_threshold` / `log_prob_threshold` фильтры через переписывание pipeline (как у CTranslate2). Уже частично описано в [docs/gpu.md «Качественный pipeline для OpenVINO»](gpu.md#качественный-pipeline-для-openvino).
|
||||
- Переключить русский CPU-дефолт на победителя сравнительного GigaAM-бенчмарка.
|
||||
- Предупреждать пользователя при `--device openvino-cpu` для файлов >30 мин.
|
||||
|
||||
**Приоритет:** высокий — проблема затрагивает текущий OpenVINO CPU-путь, а
|
||||
целевая аудитория включает ноутбуки с Intel Core i5 11-го поколения. GigaAM v3
|
||||
остаётся рабочей явной альтернативой для русского, но выбор безопасного
|
||||
auto/default требует сравнительного бенчмарка.
|
||||
|
||||
---
|
||||
|
||||
### Интеллектуальное чанкование длинных файлов по паузам
|
||||
|
||||
**Что:** Резать длинные файлы на чанки (~90 с) не по фиксированной сетке, а по ближайшей тишине: `ffmpeg -af silencedetect=noise=-40dB:d=0.5` → парсинг stderr → выбор точки разреза в окне ±30 с вокруг целевой границы (с минимальным зазором между разрезами, чтобы не получить нулевые чанки).
|
||||
|
||||
**Почему:** Разрез посреди слова/фразы портит распознавание на границах чанков; разрез по паузе — нет. Потенциально смягчает класс ошибок Whisper medium на длинных файлах (потеря хвоста, блоки повторов — см. пункт выше): короткие чанки не дают декодеру «уплыть».
|
||||
|
||||
**Источник:** референсная реализация Parakeet-сервера (Flask, OpenAI-совместимый API), лежавшая в репо как `app.py` в период эксперимента ADR-005/006 (апрель 2026); удалена при чистке 2026-07-09 — рабочие константы: порог -40dB, мин. тишина 0.5 с, окно поиска 30 с, мин. зазор 5 с.
|
||||
|
||||
**Tradeoff:** дополнительный проход ffmpeg по всему файлу (silencedetect) перед транскрипцией; для часового файла — десятки секунд.
|
||||
|
||||
---
|
||||
|
||||
### Качественный pipeline для OpenVINO (temperature fallback + фильтры)
|
||||
|
||||
**Что:** Реализовать temperature fallback, compression_ratio и log_prob фильтры поверх OpenVINO GenAI WhisperPipeline. Эвристики — логика на Python (~50-100 строк), не зависящая от inference engine.
|
||||
|
||||
**Почему:** Дать Intel Arc / AMD GPU и AMD CPU то же качество, что сейчас есть только у CUDA-пользователей через CTranslate2. Полностью описано в [docs/gpu.md](gpu.md#качественный-pipeline-для-openvino).
|
||||
|
||||
---
|
||||
|
||||
## Структура транскрипта (конспекты и MoM)
|
||||
|
||||
Источник раздела: внешнее сравнение локального транскрипта (medium,
|
||||
openvino-cpu, запись 25:59) с облачным сервисом Hypescribe, критерий —
|
||||
пригодность как сырья для конспекта и протокола встречи (GPT-ревью,
|
||||
2026-07-10). Итог: по смыслу локальная модель почти равна облаку
|
||||
(6.5/10 против 7/10), главный разрыв — **не качество распознавания,
|
||||
а структура**: разделение говорящих (2/10 против 8/10) и нарезка на
|
||||
реплики. Приоритеты ревьюера: 1) смысл, 2) спикеры, 3) техтермины,
|
||||
4) разбивка на фразы, 5) таймкоды. Вывод: локальная диаризация +
|
||||
словарь терминов закрывают потребность в облачном сервисе для
|
||||
внутренних встреч.
|
||||
|
||||
Сознательно вне ядра CLI (максимум — рецепт в README): второй проход
|
||||
LLM для чистки текста, сопоставление Speaker N с именами — это работа
|
||||
поверх готового транскрипта.
|
||||
|
||||
### Диаризация — разделение говорящих
|
||||
|
||||
**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): сегменты
|
||||
уже несут таймкоды; диаризация даёт интервалы «кто когда говорил»;
|
||||
merge по перекрытию интервалов; formatter ломает абзац на смене спикера
|
||||
и подписывает `Speaker 1:`. Ставится как extra:
|
||||
`uv sync --extra diarization`.
|
||||
|
||||
**Почему:** Без спикеров MoM не собрать — это ключевой разрыв с облаком
|
||||
по внешнему ревью, и никакое качество распознавания его не компенсирует.
|
||||
Заодно естественно решает «разбивку на реплики» (приоритет №4).
|
||||
|
||||
**Варианты реализации (ключевое решение, нужен ADR):**
|
||||
|
||||
- **sherpa-onnx** — диаризация целиком на onnxruntime (сегментация
|
||||
pyannote в ONNX + спикер-эмбеддинги), без torch, в духе нашего
|
||||
onnx-стека и «no cloud, no API keys».
|
||||
- **pyannote.audio** — стандарт качества, но тянет torch и требует
|
||||
HF-токен с принятием лицензии моделей — трение с духом проекта.
|
||||
|
||||
**Уточнить перед запуском:** качество обоих вариантов на русской речи
|
||||
и перекрывающихся репликах; скорость на CPU (диаризация — второй проход
|
||||
по всему аудио); лицензии моделей сегментации/эмбеддингов.
|
||||
|
||||
---
|
||||
|
||||
### Ручка нарезки абзацев в formatter
|
||||
|
||||
**Что:** «Минутные простыни» в транскрипте — не свойство модели, а наши
|
||||
константы группировки `_PAUSE_THRESHOLD_S = 2.0` / `_MAX_PARAGRAPH_S =
|
||||
60.0` в `formatter.py` (сырых сегментов много: 23-минутная запись — 360
|
||||
сегментов, ~4 с на реплику). Вынести в опцию/конфиг или уменьшить
|
||||
дефолт.
|
||||
|
||||
**Почему откладывается:** при диаризации абзацы будут ломаться по смене
|
||||
спикера естественно — сначала решить с диаризацией, чтобы не делать
|
||||
ручку, которая устареет.
|
||||
|
||||
---
|
||||
|
||||
### Словарь замен технических терминов — запасной план
|
||||
|
||||
**Что:** Пост-обработка текста сегментов словарём замен по границам слов
|
||||
(`CSW → CSV`, `софтп → SFTP`, `ямлик → YAML`, `Spark и Scale → Spark
|
||||
SQL`), словарь пользовательский в `.transcriber.toml`.
|
||||
|
||||
**Почему запасной:** это тот же класс ошибок, что «кириллизация латиницы
|
||||
и искажение имён» из [ADR-006](adr/006-onnx-asr-backend.md), и первым
|
||||
его должен попробовать закрыть контекстный декодер GigaAM v3 RNN-T
|
||||
(первый пункт бэклога). Заводить словарь — только если бенчмарк RNN-T
|
||||
термины не вытянет.
|
||||
|
||||
---
|
||||
|
||||
## Авто-детект и UX
|
||||
|
||||
### Профили намерения вместо выбора модели
|
||||
|
||||
**Что:** Вместо `--model gigaam-v3-e2e-rnnt` пользователь выбирает намерение —
|
||||
условные `ru-fast`, `ru-readable`, `mixed`, — а проект разворачивает его в пару
|
||||
модель + квантизация с учётом устройства.
|
||||
|
||||
**Почему:** Имена onnx-моделей ничего не говорят о том, что получит
|
||||
пользователь, и различие «поддерживаемая модель ≠ рекомендуемая» через них не
|
||||
выражается.
|
||||
|
||||
**Почему откладывается:** профиль осмыслен, когда известно, какой профиль чем
|
||||
закрывается — то есть после сравнительной оценки. Введение понятия раньше
|
||||
данных закрепит догадку в интерфейсе. Источник:
|
||||
[спека «Onnx-каталог, Python 3.13 и границы версий»](specs/2026-08-11-onnx-model-catalog.md).
|
||||
|
||||
---
|
||||
|
||||
### Включение `onnx` в `--device auto`
|
||||
|
||||
**Что:** После периода стабилизации `--device onnx` (несколько недель production-использования без жалоб) — рассмотреть включение в auto-detect chain.
|
||||
|
||||
**Порядок в chain (предложение):** CUDA → onnx (если CPU x86_64) → OpenVINO → CPU.
|
||||
|
||||
**Почему откладывается:** политика experimental backend — не сюрпризить существующих пользователей до накопления опыта. Источник: [ADR-006](adr/006-onnx-asr-backend.md#решение).
|
||||
|
||||
---
|
||||
|
||||
## Отклонённые направления
|
||||
|
||||
*(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)*
|
||||
@@ -1,123 +0,0 @@
|
||||
# Бенчмарк: качество и скорость транскрипции
|
||||
|
||||
**Дата**: 2026-03-23
|
||||
|
||||
## Цель
|
||||
|
||||
Сравнить качество распознавания нашего приложения (local-transcriber) с облачным
|
||||
сервисом Whisper.ai, подобрать оптимальную конфигурацию бэкенд/модель/compute_type.
|
||||
|
||||
## Тестовый стенд
|
||||
|
||||
- **CPU**: AMD Ryzen 7 8845H w/ Radeon 780M (3.80 GHz), 8 ядер
|
||||
- **GPU**: нет NVIDIA (нет CUDA), есть встроенный Radeon 780M iGPU + Ryzen AI NPU (XDNA)
|
||||
- **ОС**: Windows 11 Enterprise
|
||||
- **Запись**: совещание 3 участников, 14:41, русский язык, микрофон ноутбука
|
||||
|
||||
## Конфигурации
|
||||
|
||||
| # | Модель | Бэкенд | compute_type | Устройство |
|
||||
|---|--------|--------|--------------|------------|
|
||||
| 1 | medium | OpenVINO | fp16 | CPU |
|
||||
| 2 | large-v3 | OpenVINO | fp16 | CPU |
|
||||
| 3 | medium | CTranslate2 (faster-whisper) | float32 | CPU |
|
||||
| 4 | medium | CTranslate2 (faster-whisper) | int8_float32 | CPU |
|
||||
| 5 | — | Whisper.ai (облако) | — | — |
|
||||
|
||||
## Результаты: скорость
|
||||
|
||||
| # | Конфигурация | Время, с | Коэфф. к реалтайму (14:41 = 881с) |
|
||||
|---|---|---|---|
|
||||
| 1 | medium, OpenVINO fp16 | **~240** | 0.27x |
|
||||
| 2 | large-v3, OpenVINO fp16 | ~503 | 0.57x |
|
||||
| 4 | medium, CT2 int8_float32 | ~599 | 0.68x |
|
||||
| 3 | medium, CT2 float32 | ~881 | 1.0x |
|
||||
|
||||
## Результаты: качество
|
||||
|
||||
Для оценки выбраны маркерные фразы — технические термины, сложная разговорная речь,
|
||||
имена собственные. Каждый маркер проверен вручную по контексту записи.
|
||||
|
||||
### Техтермины (английские в русской речи)
|
||||
|
||||
| Термин | #1 OV fp16 | #2 large OV | #3 CT2 f32 | #4 CT2 int8f32 | #5 Whisper.ai |
|
||||
|--------|-----------|-------------|------------|----------------|---------------|
|
||||
| DWH | ✅ DWH | ❌ ДВХ | ⚠️ ДВХ | ✅ DWH | ⚠️ dvh |
|
||||
| Teradata | ✅ TeraData | ⚠️ тирадат | ⚠️ Тирадату | ✅ Teradata | ⚠️ тирадата |
|
||||
| NiFi | ✅ NiFi | ❌ "на файл" | ⚠️ Найфай | ✅ NiFi | ⚠️ фай |
|
||||
| FineBI | ✅ fineBI | ❌ "фанбой" | ✅ FNBI | ✅ finebi/FNBI | ⚠️ FineBI/NBI |
|
||||
| Alation | ❌ "лэйшене" | ❌ "валаши" | ✅ Allation | ✅ Allation | ✅ Allation |
|
||||
| ETL | ❌ ETA | ❌ "и ты или" | ✅ ETL | ✅ ETL | ✅ ETL |
|
||||
| DBC Tables V | ✅ | ❌ "деби си" | ✅ | ✅ | ✅ |
|
||||
| OpenLineage | ⚠️ OpenLinage | ⚠️ "open lineage и о" | ✅ OpenLinage.io | ✅ OpenLinage.io | ✅ Open Lineage.io |
|
||||
|
||||
### Русская разговорная речь
|
||||
|
||||
| Фраза (правильно) | #1 OV fp16 | #2 large OV | #3 CT2 f32 | #4 CT2 int8f32 | #5 Whisper.ai |
|
||||
|--------------------|-----------|-------------|------------|----------------|---------------|
|
||||
| "на другую мониторочку" | ❌ "другом не торчка" | ❌ "другому и" | ✅ "мониторчику" | ⚠️ "мою точку" | ✅ "мониторочку" |
|
||||
| "верхнеуровневое" | ⚠️ "верхверх верхне..." | ✅ | ✅ | ✅ | ✅ |
|
||||
| "датарентген" | ❌ "госпиталин ген" | ❌ "господа тренды" | ✅ "даторентген" | ✅ "даторентген" | ✅ "датарентген" |
|
||||
| "девовские схемы" | ✅ | ❌ "девушки схемы" | ✅ | ✅ | ✅ |
|
||||
| "с накиданными правами" | ✅ | ❌ "накиданных бровами" | ✅ | ✅ | ✅ |
|
||||
| "Вьюхи" | ❌ "в юхе" | — | ✅ "Вьюхи" | ⚠️ "Yuhi" | ❌ "в юхе" |
|
||||
|
||||
### Структура и пунктуация
|
||||
|
||||
| Аспект | #1 OV fp16 | #2 large OV | #3 CT2 f32 | #4 CT2 int8f32 | #5 Whisper.ai |
|
||||
|--------|-----------|-------------|------------|----------------|---------------|
|
||||
| Пунктуация | слабая | слабая | хорошая | хорошая | средняя |
|
||||
| Длина сегментов | ~1 мин | ~1 мин | ~1 мин | ~1 мин | 2–30 сек |
|
||||
| Диаризация | нет | нет | нет | нет | есть (условная) |
|
||||
|
||||
## Ключевые выводы
|
||||
|
||||
### 1. OpenVINO fp16 ухудшает качество medium
|
||||
|
||||
OpenVINO даёт **3.7x ускорение**, но fp16-квантизация на CPU вносит заметные ошибки
|
||||
в распознавание нечёткой русской речи ("госпиталин ген", "ETA"). При этом английские
|
||||
техтермины (DWH, NiFi, FineBI) распознаются хорошо — видимо, они более «чёткие»
|
||||
акустически.
|
||||
|
||||
### 2. large-v3 через OpenVINO — хуже medium
|
||||
|
||||
Удивительный результат: large-v3 с OpenVINO fp16 деградировал по всем параметрам.
|
||||
FineBI → "фанбой", NiFi → "на файл", девовские → "девушки". Гипотеза: fp16 на CPU
|
||||
бьёт сильнее по большим моделям из-за накопления ошибок округления.
|
||||
|
||||
### 3. CTranslate2 float32 — лучшее качество
|
||||
|
||||
Сравнимо с облачным Whisper.ai, а на некоторых маркерах лучше (Вьюхи, DWH).
|
||||
Но **3.7x медленнее** OpenVINO — неприемлемо для длинных записей.
|
||||
|
||||
### 4. int8_float32 — оптимальный баланс
|
||||
|
||||
Mixed precision (int8 матрицы, float32 аккумулятор) даёт:
|
||||
- **1.5x ускорение** vs float32 (599с vs 881с)
|
||||
- Качество практически идентичное float32
|
||||
- Техтермины DWH/Teradata/NiFi даже лучше, чем у float32
|
||||
|
||||
### 5. Whisper.ai — диаризация как преимущество
|
||||
|
||||
Единственное значимое преимущество облачного сервиса — диаризация спикеров.
|
||||
По качеству распознавания наше приложение с CT2 int8_float32 на уровне или лучше.
|
||||
|
||||
## Рекомендации
|
||||
|
||||
### Краткосрочно
|
||||
|
||||
- **Дефолт для CPU без CUDA**: рассмотреть `int8_float32` вместо `float32` — 1.5x быстрее
|
||||
при сопоставимом качестве.
|
||||
- **OpenVINO fp16 оставить** как быстрый вариант — для случаев, когда скорость важнее
|
||||
точности (превью, черновые транскрипты).
|
||||
|
||||
### Среднесрочно
|
||||
|
||||
- **DirectML бэкенд** для AMD Radeon iGPU — через onnxruntime-directml можно задействовать
|
||||
Radeon 780M. Float16 на GPU (в отличие от CPU-квантизации) не должен терять качество.
|
||||
Потенциально 3–5x ускорение при сохранении качества float32.
|
||||
|
||||
### Долгосрочно
|
||||
|
||||
- **Ryzen AI NPU (XDNA)** — AMD развивает поддержку, но tooling пока сырой.
|
||||
- **Диаризация** — добавить идентификацию спикеров (pyannote-audio или аналоги).
|
||||
+181
-20
@@ -37,7 +37,7 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
|
||||
| tiny | OpenVINO/whisper-tiny-int8-ov | — |
|
||||
| base | — | OpenVINO/whisper-base-fp16-ov |
|
||||
| small | OpenVINO/whisper-small-int8-ov | — |
|
||||
| medium | OpenVINO/whisper-medium-int8-ov | — |
|
||||
| medium | OpenVINO/whisper-medium-int8-ov | OpenVINO/whisper-medium-fp16-ov |
|
||||
| large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov |
|
||||
|
||||
### Результаты тестирования OpenVINO
|
||||
@@ -50,7 +50,7 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
|
||||
|---|---|---|---|
|
||||
| Intel Ultra 7 255H | **122с** | **411с** | — |
|
||||
| AMD Ryzen 7 8845H | 185с | 416с | 734с |
|
||||
| Intel i7 (WSL2) | 171-205с | — | 658с |
|
||||
| Intel i7-11800H (WSL2) | 171-205с | — | 658с |
|
||||
|
||||
**Ускорение vs CPU float32:** **3-6x** в зависимости от CPU.
|
||||
|
||||
@@ -77,6 +77,113 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
|
||||
- **medium** — для повседневного использования и обработки ИИ. Ключевые термины верные, единичные ляпы не влияют на смысл конспекта. Оптимальный баланс скорости и качества.
|
||||
- **large-v3** — для важных записей, где нужна дословная точность. Лучшая пунктуация и связность. На OpenVINO (416с) быстрее, чем medium на чистом CPU (734с) — лучшее качество при выше скорости.
|
||||
|
||||
## CPU бэкенд (CTranslate2 / faster-whisper)
|
||||
|
||||
При `--device cpu` используется faster-whisper на основе CTranslate2. Этот бэкенд медленнее
|
||||
OpenVINO, но обеспечивает **лучшее качество** благодаря развитому pipeline декодирования.
|
||||
|
||||
### Рекомендуемый compute_type: `int8_float32`
|
||||
|
||||
```bash
|
||||
transcribe meeting.mp4 --device cpu --compute-type int8_float32
|
||||
```
|
||||
|
||||
`int8_float32` — int8 квантизация весов с float32 аккумулятором. Даёт **1.5x ускорение**
|
||||
vs float32 при сопоставимом качестве (протестировано на русском языке с техтерминами).
|
||||
|
||||
| compute_type | Скорость* | Качество | Когда использовать |
|
||||
|---|---|---|---|
|
||||
| `int8_float32` | **~460с** | Отлично | **Рекомендуется** — лучший баланс |
|
||||
| `float32` | ~880с | Отлично (эталон) | Если важна максимальная точность |
|
||||
| `int8` | быстрее | Хорошо, но бывают галлюцинации | Не рекомендуется для длинных записей |
|
||||
|
||||
\* Замеры на AMD Ryzen 7 8845H, medium, запись 14:41, 8 потоков.
|
||||
|
||||
### Оптимизация потоков
|
||||
|
||||
CTranslate2 по умолчанию использует 4 потока. На многоядерных CPU рекомендуется
|
||||
задать число потоков равным числу **физических ядер** (не виртуальных):
|
||||
|
||||
```bash
|
||||
transcribe meeting.mp4 --device cpu --compute-type int8_float32 --threads 8
|
||||
```
|
||||
|
||||
**Замеры (Intel i7-11800H, 8 ядер / 16 потоков, medium int8_float32, 16 мин файл):**
|
||||
|
||||
| --threads | Время | Ускорение |
|
||||
|---|---|---|
|
||||
| 0 (дефолт = 4) | 320с | baseline |
|
||||
| 8 (физ. ядра) | **277с** | **+13%** |
|
||||
|
||||
SMT/Hyper-Threading не помогает — 16 потоков на 8-ядерном CPU медленнее, чем 8.
|
||||
|
||||
### Почему CPU бэкенд качественнее OpenVINO
|
||||
|
||||
При одной и той же модели (medium) CTranslate2 даёт заметно лучше распознавание,
|
||||
чем OpenVINO. Разница **не в квантизации**, а в pipeline декодирования:
|
||||
|
||||
| Механизм | CTranslate2 (faster-whisper) | OpenVINO GenAI |
|
||||
|---|---|---|
|
||||
| Temperature fallback | ✅ до 5 попыток с ростом temperature | ❌ один проход |
|
||||
| Фильтр по compression_ratio | ✅ отсекает повторы | ❌ нет |
|
||||
| Фильтр по log_prob | ✅ отсекает неуверенные сегменты | ❌ нет |
|
||||
| no_speech_threshold | ✅ детекция тишины | ❌ нет |
|
||||
| VAD (Silero) | ✅ опционально | ❌ нет |
|
||||
| condition_on_previous_text | ✅ с умным сбросом | ⚠️ базовый |
|
||||
|
||||
Эти эвристики критичны для русской разговорной речи с паузами и перебивками.
|
||||
|
||||
### Выбор между OpenVINO и CPU
|
||||
|
||||
| Сценарий | Рекомендация |
|
||||
|---|---|
|
||||
| Быстрый черновой транскрипт | `--device openvino-cpu` (int8, ~240с) |
|
||||
| Качественный транскрипт для суммаризации | `--device cpu --compute-type int8_float32` (~460с) |
|
||||
| Максимальная точность | `--device cpu --compute-type float32` (~880с) |
|
||||
|
||||
## CUDA: compute_type и качество
|
||||
|
||||
На NVIDIA GPU дефолт — `float16`, и для большинства случаев это оптимальный выбор.
|
||||
|
||||
### Результаты тестирования compute_type на GPU (RTX 3060 Laptop, 6 GB)
|
||||
|
||||
**Скорость (файл 16 мин, русский язык):**
|
||||
|
||||
| Модель | float16 | int8_float32 | int8_float16 |
|
||||
|---|---|---|---|
|
||||
| medium | **53с** (~18x) | 64с (~15x) | 54с (~18x) |
|
||||
| large-v3 | **86с** (~11x) | 113с (~8.5x) | 125с (~7.7x) |
|
||||
|
||||
**Скорость (файл 46 мин, русский язык):**
|
||||
|
||||
| Модель | float16 |
|
||||
|---|---|
|
||||
| medium | **127с** (~22x реалтайм) |
|
||||
| large-v3 | 271с (~10x реалтайм) |
|
||||
|
||||
**Качество (файл 16 мин):**
|
||||
|
||||
| Модель | float16 | int8_float32 | int8_float16 |
|
||||
|---|---|---|---|
|
||||
| medium | **Отлично** | **Отлично** (≈float16) | Хорошо (1-2 ошибки) |
|
||||
| large-v3 | Хорошо (ед. галлюцинации) | **Плохо** (повтор "Ага" ×18) | **Неприемлемо** (мусор, иероглифы) |
|
||||
|
||||
**Качество (файл 46 мин):**
|
||||
|
||||
| Модель | float16 | Проблемы |
|
||||
|---|---|---|
|
||||
| medium | **Отлично** | Единичные ляпы ("email" вместо "ML"), стабилен |
|
||||
| large-v3 | **Плохо** | Галлюцинации на 3+ языках (китайский, арабский), повторы фраз, "Аминь" вместо "Угу" |
|
||||
|
||||
### Выводы по CUDA compute_type
|
||||
|
||||
- **`float16` — оптимальный дефолт**: самый быстрый и стабильный
|
||||
- **`int8_float32` не дал выигрыша**: на 20% медленнее float16, качество для medium сопоставимо, для large-v3 — деградация
|
||||
- **`int8_float16`**: по скорости ≈ float16 для medium, но large-v3 даёт мусор
|
||||
- **`int8`**: рискует галлюцинациями на large-v3 и длинных записях (повтор фраз ×25)
|
||||
- **medium устойчивее к квантизации**, чем large-v3 — все варианты compute_type дают приемлемый результат
|
||||
- **large-v3 на длинных записях (>20 мин)**: галлюцинации даже с float16 — medium надёжнее
|
||||
|
||||
## Настройка по платформам
|
||||
|
||||
### Linux / WSL2 (x86_64)
|
||||
@@ -98,45 +205,99 @@ winget install -e --id Nvidia.CUDA --version 12.9 # требует запус
|
||||
|
||||
## Совместимость GPU
|
||||
|
||||
| GPU | VRAM | medium float16 | large-v3 float16 | Рекомендация |
|
||||
|-----|------|---------------|-----------------|--------------|
|
||||
| RTX 3060 | 6 GB | ✅ | ✅ | medium float16 (дефолт) |
|
||||
| RTX 4050 | 6 GB | ✅ | ✅ | medium float16 |
|
||||
| Quadro M3000M | 4 GB | ✅ | ⚠️ tight | medium float16 или int8 |
|
||||
| GPU | VRAM | CC | medium float16 | large-v3 float16 | Рекомендация |
|
||||
|-----|------|----|---------------|-----------------|--------------|
|
||||
| RTX 3060 | 6 GB | 8.6 | ✅ | ✅ | medium float16 (дефолт) |
|
||||
| RTX 4050 | 6 GB | 8.9 | ✅ | ✅ | medium float16 |
|
||||
| Quadro M3000M | 4 GB | 5.0 | ❌ | ❌ | `--device cpu` или `openvino` |
|
||||
|
||||
> **Quadro M3000M и другие GPU с CC < 7.0 (Maxwell, Pascal)**: CUDA не работает —
|
||||
> float16 требует CC ≥ 7.0, float32 падает с ошибкой (CTranslate2 4.x не включает
|
||||
> sm_50/sm_60 в пребилды). Используйте `--device cpu --compute-type int8_float32`
|
||||
> или `--device openvino`.
|
||||
|
||||
## Ожидаемая скорость
|
||||
|
||||
Замеры на RTX 3060 Laptop (6 GB) и Intel CPU (WSL2):
|
||||
|
||||
| Конфигурация | 16 мин файл | 42 мин файл | Отн. скорость |
|
||||
| Конфигурация | 16 мин файл | 46 мин файл | Отн. скорость |
|
||||
|-------------|-------------|-------------|---------------|
|
||||
| GPU + medium float16 | ~35с | ~133с | ~19x реалтайм |
|
||||
| GPU + large-v3 float16 | ~90с | ~350с | ~7x реалтайм |
|
||||
| GPU + medium float16 | **53с** | **127с** | **~18-22x реалтайм** |
|
||||
| GPU + large-v3 float16 | 86с | 271с | ~10-11x реалтайм |
|
||||
| GPU + medium int8_float32 | 64с | — | ~15x реалтайм |
|
||||
| GPU + medium int8_float16 | 54с | — | ~18x реалтайм |
|
||||
| **OpenVINO + small int8** | **93с** | **153с** | **~10-16x реалтайм** |
|
||||
| **OpenVINO + medium int8** | **171-205с** | **413с** | **~4-6x реалтайм** |
|
||||
| **OpenVINO + large-v3 fp16** | **416с** | — | **~2.3x реалтайм** |
|
||||
| **CPU + medium int8_float32** | **~460с*** | — | **~2x реалтайм** |
|
||||
| CPU + medium float32 | 658с (11 мин) | ~26 мин | ~1.5x реалтайм |
|
||||
| CPU + large-v3 int8 | 839с (14 мин) | ~37 мин | ~1:1 реалтайм |
|
||||
|
||||
\* Замер CPU int8_float32 на AMD Ryzen 7 8845H (8 потоков), файл 14:41.
|
||||
|
||||
## Результаты тестирования качества
|
||||
|
||||
Тесты проведены на реальных записях рабочих созвонов (русский язык, технические термины:
|
||||
SQL, PostgreSQL, Greenplum, Airflow, ClickHouse, Docker, CDR, GTP, MAP).
|
||||
|
||||
| Конфигурация | Качество (длинная запись, 42 мин) | Проблемы |
|
||||
|---|---|---|
|
||||
| large-v3 int8 GPU | Плохо | Галлюцинации (фразы ×25), потеря контента |
|
||||
| large-v3 float16 GPU | Отлично | — |
|
||||
| medium float16 GPU | Хорошо | Редкие мелкие ляпы в терминах |
|
||||
| medium float32 CPU | Хорошо | Сопоставимо с large-v3 int8, без галлюцинаций |
|
||||
| large-v3 int8 CPU | Хорошо | Без галлюцинаций (на коротких файлах) |
|
||||
| Конфигурация | Качество (короткие, ≤16 мин) | Качество (длинные, >40 мин) | Проблемы |
|
||||
|---|---|---|---|
|
||||
| medium float16 GPU | **Отлично** | **Отлично** | Единичные ляпы в терминах ("email" вместо "ML") |
|
||||
| medium int8_float32 GPU | **Отлично** | — | Качество ≈ float16, но на 20% медленнее |
|
||||
| medium int8_float16 GPU | Хорошо | — | 1-2 ошибки |
|
||||
| large-v3 float16 GPU | Хорошо | **Плохо** | Галлюцинации на длинных записях (мусор на 3+ языках) |
|
||||
| large-v3 int8_float32 GPU | **Плохо** | — | Повтор фраз ("Ага" ×18), искажения |
|
||||
| large-v3 int8_float16 GPU | **Неприемлемо** | — | Мусор, китайские/арабские символы, потеря текста |
|
||||
| large-v3 int8 GPU | **Плохо** | **Плохо** | Галлюцинации (фразы ×25), потеря контента |
|
||||
| medium int8_float32 CPU | **Отлично** | — | Качество ≈ float32, на уровне облачных сервисов |
|
||||
| medium float32 CPU | **Отлично** | Хорошо | Эталон качества |
|
||||
| large-v3 int8 CPU | Хорошо | — | Без галлюцинаций (на коротких файлах) |
|
||||
|
||||
### Ключевые выводы
|
||||
|
||||
1. **Указание языка (`--language ru`) критично** — auto-detect может ошибиться и выдать мусор
|
||||
2. **float16/float32 стабильнее int8** — особенно на записях >20 минут
|
||||
3. **medium + float16 на GPU — лучший баланс** скорости и качества для повседневного использования
|
||||
4. **large-v3 + float16 на GPU** — для максимального качества важных записей
|
||||
2. **medium + float16 на GPU — лучший баланс** скорости и качества для любых записей
|
||||
3. **large-v3 ненадёжен на длинных записях (>20 мин)** — галлюцинации даже с float16;
|
||||
medium стабильнее на любой длине
|
||||
4. **medium устойчив к квантизации** — все варианты compute_type дают приемлемый результат;
|
||||
large-v3 деградирует катастрофически при любом int8
|
||||
5. **CPU: int8_float32 — лучший баланс** — 1.5x быстрее float32 при том же качестве
|
||||
6. **OpenVINO быстрее, но CPU бэкенд качественнее** — разница в pipeline декодирования
|
||||
(temperature fallback, фильтры галлюцинаций), а не в квантизации
|
||||
|
||||
## Потенциальные направления развития
|
||||
|
||||
### Качественный pipeline для OpenVINO
|
||||
|
||||
Сейчас качественный pipeline декодирования (temperature fallback, фильтры галлюцинаций,
|
||||
VAD) доступен только через CTranslate2, т.е. на CPU и NVIDIA CUDA. Пользователи Intel Arc
|
||||
и AMD Radeon получают скорость OpenVINO, но без защиты от галлюцинаций.
|
||||
|
||||
**Решение**: реализовать temperature fallback, compression_ratio и log_prob фильтры
|
||||
поверх OpenVINO GenAI WhisperPipeline. Эти эвристики — логика на Python (~50-100 строк),
|
||||
не зависящая от inference engine. Это даст Intel Arc / AMD GPU то же качество,
|
||||
что сейчас есть только у CUDA-пользователей.
|
||||
|
||||
### CUDA: int8_float32 / int8_float16 — протестировано
|
||||
|
||||
На CPU `int8_float32` показал качество на уровне float32 при 1.5x ускорении.
|
||||
На GPU (RTX 3060) результат другой: для **medium** int8_float32 даёт сопоставимое
|
||||
качество, но на 20% медленнее float16 — выигрыша нет. Для **large-v3** любой int8
|
||||
вариант (включая int8_float32) вызывает галлюцинации — float32 аккумулятор не спасает
|
||||
глубокую модель на GPU. Вывод: **float16 остаётся оптимальным дефолтом для CUDA**.
|
||||
|
||||
### NVIDIA Parakeet TDT — проверено, отклонено
|
||||
|
||||
Оценивалась гипотеза: multilingual Parakeet TDT 0.6B v3 через `onnx-asr` может обойти
|
||||
Whisper medium int8 на x86 CPU. На двух 15-минутных отрывках реальных русских ИТ-встреч
|
||||
(Intel i7-11800H) Parakeet показал **2.7x преимущество по скорости** и **-35% RAM**, но
|
||||
**качество ниже** — систематическая редукция тихих реплик менти до `Mm-hmm`, вставки
|
||||
кусков польского/испанского, сильное искажение ИТ-терминов. Попытка тюнинга Silero VAD
|
||||
threshold не помогла — проблема inherent для модели на тихой русской речи.
|
||||
|
||||
Код бэкенда не вмержен в master; эксперимент сохранён в ветке
|
||||
`feature/parakeet-backend`. Подробности и когда стоит пересмотреть —
|
||||
[ADR-005](adr/005-parakeet-evaluation.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
# Исследование: ускорение на AMD Radeon / NPU
|
||||
|
||||
**Дата**: 2026-03-23
|
||||
**Железо**: AMD Ryzen 7 8845H, Radeon 780M iGPU (RDNA3, 12 CU), XDNA 1 NPU (16 TOPS)
|
||||
|
||||
## Мотивация
|
||||
|
||||
Бенчмарк ([benchmark-2026-03-23.md](benchmark-2026-03-23.md)) показал: лучшее качество
|
||||
даёт CTranslate2 float32/int8_float32, но это 600–880с на 15-минутную запись.
|
||||
OpenVINO fp16 быстрее (240с), но теряет качество на русской речи.
|
||||
|
||||
Нужен путь: качество float32 + скорость GPU.
|
||||
|
||||
## Вариант 1: DirectML (onnxruntime-directml)
|
||||
|
||||
### Вердикт: ❌ НЕ рекомендуется
|
||||
|
||||
**Блокирующая проблема**: оператор `DecoderMaskedMultiHeadAttention` не реализован
|
||||
в DirectML ([Olive#1213](https://github.com/microsoft/Olive/issues/1213), открыт с июня 2024).
|
||||
Whisper-декодер fallback-ится на CPU — GPU ускоряет только encoder, выигрыш минимален.
|
||||
|
||||
**DirectML в maintenance mode** — Microsoft переключился на Windows ML (WinML).
|
||||
Команда DML расформирована, новых операторов не будет. Фикс маловероятен.
|
||||
|
||||
**WinML** (замена DirectML) — GA с сентября 2025, но Python-пакет `onnxruntime-winml`
|
||||
ещё не доступен. Требует Windows 11 25H2+. Потенциально интересен в будущем.
|
||||
|
||||
## Вариант 2: whisper.cpp + Vulkan ✅
|
||||
|
||||
### Вердикт: рекомендуется как основной путь
|
||||
|
||||
**Почему:**
|
||||
- Проверен на AMD Radeon 780M — баг softmax ([#2596](https://github.com/ggml-org/whisper.cpp/issues/2596))
|
||||
исправлен в декабре 2024
|
||||
- **3–4x ускорение** vs CPU на Radeon 680M (RDNA2), 780M (RDNA3) должен быть не хуже
|
||||
- Весь pipeline внутри: аудио → mel → encoder → decoder → beam search
|
||||
- GGUF-модели компактнее ONNX
|
||||
- Активная разработка, Vulkan-бэкенд зрелый
|
||||
|
||||
**Бенчмарки (чужие, похожее железо):**
|
||||
|
||||
| GPU | Модель | Результат |
|
||||
|-----|--------|-----------|
|
||||
| Radeon 680M (RDNA2) | medium | ~3–4x быстрее CPU |
|
||||
| Ryzen 5 4500U (Vega) | medium | 1ч аудио за ~25 мин |
|
||||
| Ryzen AI Max+ 395 (Strix Halo) | large-v3-turbo | 1ч20м аудио за ~3 мин |
|
||||
|
||||
**Память:** Radeon 780M использует системную RAM. whisper-medium в GGUF ≈ 1.5 ГБ —
|
||||
помещается с запасом.
|
||||
|
||||
### Интеграция в проект
|
||||
|
||||
Новый бэкенд `backends/whisper_cpp.py`, реализующий Backend Protocol:
|
||||
|
||||
```
|
||||
uv add pywhispercpp # Python-биндинги к whisper.cpp
|
||||
```
|
||||
|
||||
Либо Vulkan-сборка whisper.cpp как subprocess (более надёжно,
|
||||
но менее удобно для пользователя).
|
||||
|
||||
**Зависимость в pyproject.toml:**
|
||||
```toml
|
||||
# Опциональная, т.к. требует Vulkan-совместимый GPU
|
||||
"pywhispercpp>=0.4; sys_platform == 'win32'"
|
||||
```
|
||||
|
||||
Либо через dependency group:
|
||||
```toml
|
||||
[dependency-groups]
|
||||
vulkan = ["pywhispercpp>=0.4"]
|
||||
```
|
||||
|
||||
Тогда установка: `uv sync --group vulkan`
|
||||
|
||||
**Открытые вопросы:**
|
||||
- pywhispercpp собран с Vulkan или нужна своя сборка?
|
||||
- Качество GGUF-квантизации vs CTranslate2 float32 — нужен бенчмарк
|
||||
- Формат моделей другой (GGUF vs CTranslate2/OpenVINO) — ещё одно скачивание
|
||||
|
||||
## Вариант 3: AMD NPU (XDNA)
|
||||
|
||||
### Вердикт: ⏳ рано для XDNA 1, перспективно для XDNA 2
|
||||
|
||||
**Что есть:**
|
||||
- Ryzen AI Software 1.7.0 (февраль 2026) поддерживает 8845H (Hawk Point)
|
||||
- [AMD LIRA](https://github.com/amd/LIRA) — CLI для ASR на NPU, поддерживает whisper base/small/medium
|
||||
- [AMD-форк whisper.cpp](https://github.com/amd/whisper.cpp) с NPU-бэкендом
|
||||
- Pre-квантизированные модели на HuggingFace (`amd/NPU-Whisper-Base-Small`)
|
||||
|
||||
**Ограничения на XDNA 1 (8845H):**
|
||||
- 16 TOPS — в 3 раза слабее XDNA 2 (50 TOPS)
|
||||
- **Только encoder** на NPU, decoder на CPU — ускорение частичное
|
||||
- whisper-large-v3 не помещается
|
||||
- Установка через отдельный installer AMD, не pip/uv
|
||||
- Форк whisper.cpp не в mainline — риск отставания
|
||||
|
||||
**Когда станет интересно:**
|
||||
- На железе с XDNA 2 (Ryzen AI 300+, 50 TOPS)
|
||||
- Когда AMD выпустит pip-пакет или upstreamит в whisper.cpp
|
||||
- Для сценариев, где CPU/GPU заняты другой работой
|
||||
|
||||
## Сводка
|
||||
|
||||
| Подход | Применимость | Ускорение | Сложность | Стабильность |
|
||||
|--------|-------------|-----------|-----------|--------------|
|
||||
| **whisper.cpp + Vulkan** | ✅ высокая | 3–4x vs CPU | низкая | хорошая |
|
||||
| DirectML (onnxruntime) | ❌ заблокирован | — | — | — |
|
||||
| AMD NPU (XDNA 1) | ⚠️ ограничен | частичное | высокая | средняя |
|
||||
| WinML (будущее) | ⏳ не готов | неизвестно | средняя | — |
|
||||
|
||||
## Детали по whisper.cpp + Vulkan
|
||||
|
||||
### Модели (GGML .bin формат, НЕ GGUF)
|
||||
|
||||
Скачивать с https://huggingface.co/ggerganov/whisper.cpp/tree/main
|
||||
|
||||
| Модель | Файл | Размер |
|
||||
|--------|------|--------|
|
||||
| medium f16 | `ggml-medium.bin` | 1.53 ГБ |
|
||||
| medium q8_0 | `ggml-medium-q8_0.bin` | 823 МБ |
|
||||
| medium q5_0 | `ggml-medium-q5_0.bin` | 539 МБ |
|
||||
| large-v3 f16 | `ggml-large-v3.bin` | 3.1 ГБ |
|
||||
| large-v3 q5_0 | `ggml-large-v3-q5_0.bin` | 1.08 ГБ |
|
||||
| large-v3-turbo f16 | `ggml-large-v3-turbo.bin` | 1.62 ГБ |
|
||||
| large-v3-turbo q8_0 | `ggml-large-v3-turbo-q8_0.bin` | 874 МБ |
|
||||
|
||||
### Python-биндинги
|
||||
|
||||
**pywhispercpp** (v1.4.1) — pip wheel CPU-only. Для Vulkan нужна сборка из исходников:
|
||||
```bash
|
||||
GGML_VULKAN=1 pip install git+https://github.com/absadiki/pywhispercpp
|
||||
```
|
||||
Требует: VulkanSDK, cmake, C++ компилятор.
|
||||
|
||||
### Subprocess-подход (альтернатива)
|
||||
|
||||
whisper-cli принимает WAV 16kHz mono, выдаёт JSON с таймкодами:
|
||||
```bash
|
||||
whisper-cli -m ggml-medium.bin -f audio.wav -l ru -oj -of output
|
||||
```
|
||||
|
||||
Для видео/mp3 нужна предварительная конвертация через ffmpeg (PyAV уже есть в зависимостях).
|
||||
|
||||
### Сборка whisper.cpp с Vulkan
|
||||
|
||||
```bash
|
||||
# Linux/WSL
|
||||
sudo apt install cmake build-essential libvulkan-dev
|
||||
git clone https://github.com/ggml-org/whisper.cpp
|
||||
cd whisper.cpp
|
||||
cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build --config Release -j$(nproc)
|
||||
# -> build/bin/whisper-cli
|
||||
```
|
||||
|
||||
## Прототип: whisper.cpp в WSL2 (2026-03-23)
|
||||
|
||||
### Сборка
|
||||
|
||||
whisper.cpp собран с `-DGGML_VULKAN=ON`, но **Vulkan GPU не обнаружен**:
|
||||
|
||||
```
|
||||
ggml_vulkan: No devices found.
|
||||
whisper_backend_init_gpu: no GPU found
|
||||
```
|
||||
|
||||
**Причина**: WSL2 не предоставляет Vulkan-доступ к AMD GPU.
|
||||
- `/dev/dxg` есть (DirectX 12 paravirtualization), но **dozen** (Vulkan-over-D3D12) драйвер
|
||||
отсутствует в текущей mesa (25.2.8)
|
||||
- RADV (нативный Vulkan) не работает — нет `/dev/dri/renderD128`
|
||||
- Единственный Vulkan-девайс — `llvmpipe` (CPU-эмуляция), whisper.cpp его игнорирует
|
||||
|
||||
Whisper.cpp отработал **на CPU** (4 потока, AVX512).
|
||||
|
||||
### Скорость (CPU fallback)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Модель | ggml-medium.bin (f16, 1533 МБ) |
|
||||
| Аудио | 14:41 (881с), русский, 3 участника |
|
||||
| Общее время | **886с** |
|
||||
| encode | 368с (32 прохода, 11.5с/проход) |
|
||||
| batchd (decode) | 424с |
|
||||
| Потоки | 4 / 16 |
|
||||
| Beam search | 5 beams + best of 5 |
|
||||
|
||||
Сравнение с бенчмарком (тот же аудиофайл):
|
||||
|
||||
| Конфигурация | Время, с |
|
||||
|---|---|
|
||||
| OpenVINO fp16 (medium) | ~240 |
|
||||
| CT2 int8_float32 (medium) | ~599 |
|
||||
| CT2 float32 (medium) | ~881 |
|
||||
| **whisper.cpp CPU (medium f16)** | **~886** |
|
||||
|
||||
На CPU whisper.cpp ≈ CTranslate2 float32. Без GPU ускорения нет.
|
||||
|
||||
### Качество (сравнение с бенчмарком)
|
||||
|
||||
#### Техтермины
|
||||
|
||||
| Термин | whisper.cpp (CPU) | CT2 f32 | CT2 int8f32 |
|
||||
|--------|------------------|---------|-------------|
|
||||
| DWH | ✅ DWH | ⚠️ ДВХ | ✅ DWH |
|
||||
| Teradata | ✅ TeraData | ⚠️ Тирадату | ✅ Teradata |
|
||||
| NiFi | ⚠️ Найфай/Найфаю | ⚠️ Найфай | ✅ NiFi |
|
||||
| FineBI | ✅ fineBI/FNBI | ✅ FNBI | ✅ finebi/FNBI |
|
||||
| Alation | ✅ Allation | ✅ Allation | ✅ Allation |
|
||||
| ETL | ✅ ETL | ✅ ETL | ✅ ETL |
|
||||
| DBC Tables V | ✅ DBC tables-V | ✅ | ✅ |
|
||||
| OpenLineage | ✅ OpenLinage.io | ✅ OpenLinage.io | ✅ OpenLinage.io |
|
||||
|
||||
#### Русская разговорная речь
|
||||
|
||||
| Фраза | whisper.cpp (CPU) | CT2 f32 | CT2 int8f32 |
|
||||
|-------|------------------|---------|-------------|
|
||||
| "на другую мониторочку" | ✅ "на другом мониторчике" | ✅ "мониторчику" | ⚠️ "мою точку" |
|
||||
| "верхнеуровневое" | ✅ | ✅ | ✅ |
|
||||
| "датарентген" | ✅ "даторентген" | ✅ "даторентген" | ✅ "даторентген" |
|
||||
| "девовские схемы" | ✅ | ✅ | ✅ |
|
||||
| "с накиданными правами" | ✅ | ✅ | ✅ |
|
||||
| "Вьюхи" | ⚠️ "Yuhi" | ✅ "Вьюхи" | ⚠️ "Yuhi" |
|
||||
|
||||
#### Структура и пунктуация
|
||||
|
||||
| Аспект | whisper.cpp (CPU) | CT2 f32 | CT2 int8f32 |
|
||||
|--------|------------------|---------|-------------|
|
||||
| Пунктуация | хорошая | хорошая | хорошая |
|
||||
| Длина сегментов | 1–10 сек | ~1 мин | ~1 мин |
|
||||
|
||||
**Вывод по качеству**: whisper.cpp medium f16 на уровне CTranslate2 float32/int8_float32.
|
||||
Качество сопоставимо, различия минимальны. Сегменты короче (1–10 сек vs ~1 мин) —
|
||||
это даже лучше для навигации по транскрипту.
|
||||
|
||||
### Блокер: Vulkan в WSL2 для AMD
|
||||
|
||||
Для получения GPU-ускорения нужно одно из:
|
||||
|
||||
1. **Нативный Linux** (dual boot / bare metal) — RADV драйвер работает с Radeon 780M
|
||||
2. **Windows-сборка** — нативный Vulkan через AMD Adrenalin драйвер
|
||||
3. **WSL2 + dozen** — экспериментальный Vulkan-over-D3D12 драйвер mesa.
|
||||
Статус: [в разработке](https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests?search=dozen),
|
||||
не включён в стандартную mesa Ubuntu.
|
||||
Потенциально можно собрать mesa из исходников с `-Dvulkan-drivers=microsoft-experimental`
|
||||
|
||||
**Рекомендация**: попробовать Windows-сборку whisper.cpp — это самый простой путь
|
||||
к Vulkan + Radeon 780M. Либо собрать mesa с dozen для WSL2.
|
||||
|
||||
## Прототип: whisper.cpp + Vulkan на Windows (2026-03-23)
|
||||
|
||||
### Сборка
|
||||
|
||||
whisper.cpp собран на Windows с MinGW (MSYS2) + Vulkan:
|
||||
```
|
||||
cmake -B build-vulkan -G "MinGW Makefiles" -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release
|
||||
```
|
||||
|
||||
Vulkan GPU обнаружен:
|
||||
```
|
||||
ggml_vulkan: Found 1 Vulkan devices:
|
||||
ggml_vulkan: 0 = AMD Radeon(TM) 780M (AMD proprietary driver) | uma: 1 | fp16: 1 |
|
||||
warp size: 64 | shared memory: 32768 | int dot: 1 | matrix cores: KHR_coopmat
|
||||
```
|
||||
|
||||
### Проблема: зацикливание (repetition loop)
|
||||
|
||||
Первый запуск (дефолтные параметры): **520с**, но с ~07:00 модель зациклилась
|
||||
на фразе "Ну, опять же, они могут завести…" — повторяется ~170 раз до конца записи.
|
||||
Потеряна половина транскрипта.
|
||||
|
||||
**Причина**: whisper.cpp передаёт контекст предыдущего сегмента в декодер (`-mc -1`),
|
||||
что провоцирует repetition loop на длинных записях с паузами/перебивками.
|
||||
|
||||
**Решение**: `-mc 0` (не передавать контекст между сегментами).
|
||||
|
||||
### Скорость (Vulkan, -mc 0)
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Модель | ggml-medium.bin (f16, 1533 МБ) на Vulkan GPU |
|
||||
| Аудио | 14:41 (881с), русский, 3 участника |
|
||||
| Общее время | **432с** |
|
||||
| encode | 123.5с (35 проходов, 3.5с/проход) |
|
||||
| batchd (decode) | 252.7с |
|
||||
| Потоки | 4 / 16 |
|
||||
|
||||
Сравнение с бенчмарком (тот же аудиофайл):
|
||||
|
||||
| Конфигурация | Время, с | Ускорение vs CPU f32 |
|
||||
|---|---|---|
|
||||
| OpenVINO fp16 (medium) | ~240 | 3.7x |
|
||||
| **whisper.cpp Vulkan + Radeon 780M** | **~432** | **2.0x** |
|
||||
| **CT2 int8_float32, 8 threads** | **~458** | **1.9x** |
|
||||
| CT2 int8_float32, 16 threads | ~498 | 1.8x |
|
||||
| CT2 int8_float32, default (4 threads) | ~599 | 1.5x |
|
||||
| CT2 float32 / whisper.cpp CPU | ~881-886 | 1.0x |
|
||||
|
||||
**Оптимизация потоков**: CTranslate2 по умолчанию использует 4 потока (`cpu_threads=0`).
|
||||
На Ryzen 7 8845H (8 ядер / 16 потоков) оптимум — **8 потоков** (по числу физических ядер).
|
||||
16 потоков (SMT/hyperthreading) не помогают — контекст-свитчинг нивелирует выигрыш.
|
||||
Управление: `OMP_NUM_THREADS=8` или параметр `cpu_threads` в WhisperModel.
|
||||
|
||||
CT2 int8_float32 с 8 потоками (**458с**) почти догоняет whisper.cpp Vulkan (**432с**)
|
||||
без дополнительных зависимостей.
|
||||
|
||||
### Качество (Vulkan, -mc 0)
|
||||
|
||||
#### Техтермины
|
||||
|
||||
| Термин | whisper.cpp Vulkan | CT2 int8f32 |
|
||||
|--------|-------------------|-------------|
|
||||
| DWH | ✅ DWH | ✅ DWH |
|
||||
| Teradata | ✅ TeraData | ✅ Teradata |
|
||||
| NiFi | ⚠️ IFI | ✅ NiFi |
|
||||
| FineBI | ✅ FindBI/FineBI/FNBI | ✅ finebi/FNBI |
|
||||
| Alation | ✅ Allation | ✅ Allation |
|
||||
| ETL | ✅ ETL | ✅ ETL |
|
||||
| DBC Tables V | ⚠️ dbc tables v (lowercase) | ✅ DBC Tables V |
|
||||
| OpenLineage | ✅ OpenLinage.io | ✅ OpenLinage.io |
|
||||
| Вьюхи | ✅ Вьюхи | ⚠️ Yuhi |
|
||||
|
||||
#### Русская разговорная речь и пунктуация
|
||||
|
||||
Качество сопоставимо с CT2 int8_float32. Пунктуация хорошая в обоих случаях.
|
||||
Сегменты у whisper.cpp короче (2-10 сек vs ~1 мин) — удобнее для навигации.
|
||||
|
||||
### Вывод
|
||||
|
||||
whisper.cpp + Vulkan на Radeon 780M даёт **432с** — всего **28% быстрее** CT2 int8_float32
|
||||
(599с). Разница недостаточна, чтобы оправдать сложность интеграции:
|
||||
|
||||
- Отдельная сборка whisper.cpp (cmake + MinGW + VulkanSDK)
|
||||
- Другой формат моделей (GGML вместо CTranslate2)
|
||||
- Workaround `-mc 0` от зацикливания
|
||||
- Конвертация в WAV (whisper.cpp не принимает mp4 напрямую)
|
||||
- Ещё одна зависимость для пользователя
|
||||
|
||||
**CT2 int8_float32 остаётся оптимальным выбором**: уже встроен в проект, zero-config,
|
||||
хорошее качество, приемлемая скорость.
|
||||
|
||||
## Рекомендуемый план (обновлённый)
|
||||
|
||||
1. ~~**Прототип в WSL** — собрать whisper.cpp с Vulkan, прогнать тестовый файл~~ ✅ Сделано
|
||||
2. ~~**Качество подтверждено** — whisper.cpp medium f16 ≈ CT2 float32/int8_float32~~ ✅
|
||||
3. ~~**GPU-ускорение в WSL2** — Vulkan не видит AMD GPU~~ ✅ Подтверждено
|
||||
4. ~~**Windows-сборка с Vulkan** — 432с, 2x vs CPU, но всего 28% vs CT2 int8_float32~~ ✅
|
||||
5. **Решение**: whisper.cpp + Vulkan не оправдывает себя на Radeon 780M iGPU.
|
||||
CT2 int8_float32 — оптимальный дефолт для CPU без CUDA.
|
||||
6. NPU — отложить до XDNA 2 или до появления pip-пакета от AMD
|
||||
7. Пересмотреть при появлении дискретного AMD GPU или WinML с Python-поддержкой
|
||||
@@ -0,0 +1,53 @@
|
||||
# Обновления движков и моделей на 2026-08-10
|
||||
|
||||
Исследование выполнено по первичным источникам: официальным release notes, changelog, PyPI и model cards Hugging Face. Текущее состояние проекта зафиксировано в [`pyproject.toml`](../../pyproject.toml) и [`uv.lock`](../../uv.lock).
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
В проекте уже используется актуальный `faster-whisper` 1.2.1, однако связанные движки и модельный каталог можно обновить. Наиболее полезная последовательность: согласованно поднять OpenVINO и OpenVINO GenAI до 2026.3, разрешить `onnx-asr` 0.12 и проверить VAD, обновить CTranslate2 до 4.8.1, затем добавить `large-v3-turbo` для FasterWhisper и OpenVINO. ONNX Runtime 1.28 нельзя безусловно фиксировать, пока проект поддерживает Python 3.10, поскольку эта версия ORT требует Python 3.11 или новее ([ONNX Runtime 1.28.0 на PyPI](https://pypi.org/project/onnxruntime/1.28.0/)).
|
||||
|
||||
## Движки
|
||||
|
||||
| Компонент | Сейчас в проекте | Актуальная стабильная версия | Существенные изменения и риски |
|
||||
|---|---:|---:|---|
|
||||
| faster-whisper | 1.2.1 | 1.2.1 | Проект уже на последнем релизе. В 1.2.1 обновлён Silero VAD до v6, доработаны retry Hugging Face Hub и `clip_timestamps`; в 1.2.0 добавлена поддержка `distil-large-v3.5` ([официальные releases](https://github.com/SYSTRAN/faster-whisper/releases), [PyPI](https://pypi.org/project/faster-whisper/)). Для актуального GPU-стека документация указывает CUDA 12 и cuDNN 9; для старых комбинаций требуются специальные версии CTranslate2 ([официальная установка](https://github.com/SYSTRAN/faster-whisper#gpu)). |
|
||||
| CTranslate2 | 4.7.1 | 4.8.1 | В 4.8.0 `PACKED_GEMM` включён по умолчанию для Intel MKL; 4.8.1 исправляет heap overflow при загрузке модели и аварийное деление на ноль в Whisper `align()` при отсутствии кадров ([официальный changelog](https://github.com/OpenNMT/CTranslate2/blob/master/CHANGELOG.md), [релиз 4.8.1](https://github.com/OpenNMT/CTranslate2/releases/tag/v4.8.1), [PyPI](https://pypi.org/project/ctranslate2/)). Публичного breaking API для используемого пути не заявлено, но из-за нативных библиотек нужно проверить Windows CPU, Linux CUDA и проектный CUDA bootstrap. Требование cuDNN 9 появилось в CTranslate2 4.5.0 ([changelog](https://github.com/OpenNMT/CTranslate2/blob/master/CHANGELOG.md#450)). |
|
||||
| OpenVINO | 2026.0.0 | 2026.3.0 | В 2026.3 добавлены общий `ASRPipeline`, Qwen3-ASR и метрики задержки стадий распознавания; в ветке 2026.x также появились word-level timestamps и определённый язык в результатах Whisper ([официальные release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html), [PyPI](https://pypi.org/project/openvino/)). Начиная с 2026.0 удалена поддержка устаревшего stateless Whisper decoder, поэтому модели должны быть stateful; та же версия требует минимум AVX2, использует manylinux_2_28 и больше не поддерживает CentOS 7 ([release notes 2026.0](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0)). |
|
||||
| openvino-genai | 2026.0.0.0 | 2026.3.0.0 | OpenVINO GenAI необходимо обновлять вместе с OpenVINO: официальная документация требует совпадения `major.minor.patch` у OpenVINO, OpenVINO Tokenizers и GenAI, иначе возможны ABI/import errors. PyPI-пакеты собраны с `_GLIBCXX_USE_CXX11_ABI=0`, а архивы C++ — с ABI=1, поэтому смешивать PyPI GenAI с OpenVINO из C++ archive нельзя ([официальная страница PyPI](https://pypi.org/project/openvino-genai/), [официальные releases](https://github.com/openvinotoolkit/openvino.genai/releases)). |
|
||||
| onnx-asr | 0.11.0 | 0.12.0 | Обновление сейчас **явно заблокировано** зависимостью `onnx-asr[cpu,hub]>=0.11.0,<0.12.0` в [`pyproject.toml`](../../pyproject.toml). В 0.12 добавлены GigaAM Multilingual CTC/Large CTC, свёрточные ONNX preprocessors с ускорением CUDA EP и ORT fallback; также исправлены сегменты нулевой длины после VAD, что напрямую относится к используемому проектом `.with_vad()` ([релиз 0.12.0](https://github.com/istupakov/onnx-asr/releases/tag/v0.12.0), [официальные release notes](https://istupakov.github.io/onnx-asr/release-notes/), [PyPI](https://pypi.org/project/onnx-asr/)). Breaking Python API не объявлен, но проект должен проверить загрузку квантованных моделей, VAD и форму возвращаемых сегментов. |
|
||||
| ONNX Runtime | 1.24.3 | 1.28.0 | В 1.28 обновлены ONNX и protobuf и включены security fixes, в том числе проверка границ в `WhisperDecoderSubgraph` ([официальный релиз 1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0), [PyPI](https://pypi.org/project/onnxruntime/1.28.0/)). Версия 1.28 требует Python 3.11+, тогда как проект допускает Python 3.10, поэтому нужен условный lock/constraint либо осознанное повышение минимальной версии Python ([метаданные PyPI 1.28.0](https://pypi.org/project/onnxruntime/1.28.0/)). Для GPU-пакетов ORT 1.27+ используется CUDA 13.0 и cuDNN 9; major-версии CUDA и cuDNN должны совпадать с runtime ([официальная матрица CUDA EP](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements)). |
|
||||
|
||||
## Модели и варианты
|
||||
|
||||
### Whisper large-v3-turbo
|
||||
|
||||
`whisper-large-v3-turbo` — мультиязычная модель на 99 языков с 809 млн параметров вместо 1550 млн у `large-v3`; число decoder layers сокращено с 32 до 4, что заметно ускоряет вывод ценой небольшой потери качества ([официальная model card OpenAI](https://huggingface.co/openai/whisper-large-v3-turbo)). Это наиболее полезный новый вариант для русского и смешанного аудио.
|
||||
|
||||
FasterWhisper уже содержит стандартные aliases `large-v3-turbo` и `turbo` в собственной таблице моделей ([официальный `utils.py`](https://github.com/SYSTRAN/faster-whisper/blob/master/faster_whisper/utils.py)); готовая CTranslate2-конверсия опубликована как [`dropbox-dash/faster-whisper-large-v3-turbo`](https://huggingface.co/dropbox-dash/faster-whisper-large-v3-turbo). Проект использует собственную ограниченную таблицу aliases, поэтому модель нужно добавить явно либо перейти на стандартное разрешение имён FasterWhisper ([текущая реализация проекта](../../src/local_transcriber/backends/faster_whisper.py)).
|
||||
|
||||
Для OpenVINO опубликованы официальные варианты [`whisper-large-v3-turbo-fp16-ov`](https://huggingface.co/OpenVINO/whisper-large-v3-turbo-fp16-ov) и [`whisper-large-v3-turbo-int8-ov`](https://huggingface.co/OpenVINO/whisper-large-v3-turbo-int8-ov). Их model cards требуют OpenVINO 2026.1 или новее, поэтому текущий OpenVINO 2026.0 недостаточен; после согласованного обновления до 2026.3 можно добавить общий alias `turbo` для FasterWhisper и OpenVINO.
|
||||
|
||||
### GigaAM Multilingual
|
||||
|
||||
`onnx-asr` 0.12 добавляет модели `gigaam-multilingual-ctc` и `gigaam-multilingual-large-ctc` ([официальный релиз 0.12.0](https://github.com/istupakov/onnx-asr/releases/tag/v0.12.0), [документация использования](https://istupakov.github.io/onnx-asr/usage/)). Конвертированные репозитории опубликованы как [`gigaam-multilingual-ctc-onnx`](https://huggingface.co/istupakov/gigaam-multilingual-ctc-onnx) и [`gigaam-multilingual-large-ctc-onnx`](https://huggingface.co/istupakov/gigaam-multilingual-large-ctc-onnx), исходная модель — [`ai-sage/GigaAM-Multilingual`](https://huggingface.co/ai-sage/GigaAM-Multilingual). Они полезны для смешанной речи на поддерживаемых пяти языках, но требуют снятия ограничения `<0.12.0`.
|
||||
|
||||
### GigaAM v3 E2E
|
||||
|
||||
Текущий ONNX-репозиторий GigaAM v3 содержит не только CTC/RNNT, но и E2E-варианты с пунктуацией и нормализацией текста ([официальная model card `istupakov/gigaam-v3-onnx`](https://huggingface.co/istupakov/gigaam-v3-onnx)). E2E-вариант может улучшить читаемость русского текста, но меняет семантику вывода относительно текущего `gigaam-v3-ctc`; перед заменой default нужны сравнительные тесты транскрипции и форматирования.
|
||||
|
||||
### Distil-Whisper large-v3.5
|
||||
|
||||
`distil-large-v3.5` поддерживается faster-whisper начиная с 1.2.0 ([официальные releases](https://github.com/SYSTRAN/faster-whisper/releases)); доступны [исходная модель](https://huggingface.co/distil-whisper/distil-large-v3.5) и [CTranslate2-конверсия](https://huggingface.co/distil-whisper/distil-large-v3.5-ct2). Модель предназначена только для английского языка, поэтому она не подходит как русский default, но может быть отдельной English-only опцией.
|
||||
|
||||
### Qwen3-ASR как R&D
|
||||
|
||||
OpenVINO 2026.3 добавляет раннюю поддержку Qwen3-ASR через новый общий `ASRPipeline` ([официальные release notes 2026.3](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)). Текущий backend проекта построен вокруг `WhisperPipeline` и Whisper-совместимых сегментов ([текущая реализация](../../src/local_transcriber/backends/openvino.py)), поэтому Qwen3-ASR не является drop-in обновлением модели: это отдельная R&D-задача с новым интерфейсом, модельным контрактом и тестами качества.
|
||||
|
||||
## Приоритет действий
|
||||
|
||||
1. Согласованно обновить `openvino` до 2026.3.0 и `openvino-genai` до 2026.3.0.0, не смешивая источники сборок; проверить существующие stateful Whisper-модели, CPU/GPU и минимальные платформенные требования ([совместимость GenAI](https://pypi.org/project/openvino-genai/), [release notes OpenVINO](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html)).
|
||||
2. Убрать блокирующее ограничение `<0.12.0`, поднять `onnx-asr` до 0.12.0 и прогнать интеграционные тесты VAD, сегментов и квантованных GigaAM-моделей ([релиз 0.12.0](https://github.com/istupakov/onnx-asr/releases/tag/v0.12.0)).
|
||||
3. Обновить CTranslate2 с 4.7.1 до 4.8.1 и проверить Windows CPU/Linux CUDA, включая CUDA bootstrap ([changelog](https://github.com/OpenNMT/CTranslate2/blob/master/CHANGELOG.md)).
|
||||
4. Добавить alias `turbo` и репозитории `large-v3-turbo` для FasterWhisper и OpenVINO после обновления OpenVINO ([OpenAI model card](https://huggingface.co/openai/whisper-large-v3-turbo), [OpenVINO FP16](https://huggingface.co/OpenVINO/whisper-large-v3-turbo-fp16-ov), [OpenVINO INT8](https://huggingface.co/OpenVINO/whisper-large-v3-turbo-int8-ov)).
|
||||
5. Не фиксировать ONNX Runtime 1.28 для всех окружений, пока поддерживается Python 3.10; сначала выбрать условные зависимости либо официально поднять Python floor ([PyPI 1.28.0](https://pypi.org/project/onnxruntime/1.28.0/)).
|
||||
6. Рассматривать GigaAM Multilingual, GigaAM v3 E2E и Qwen3-ASR как отдельные эксперименты с качеством и совместимостью, а `distil-large-v3.5` — только как English-only профиль ([GigaAM Multilingual](https://huggingface.co/ai-sage/GigaAM-Multilingual), [GigaAM v3 ONNX](https://huggingface.co/istupakov/gigaam-v3-onnx), [OpenVINO 2026.3](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0), [Distil-Whisper](https://huggingface.co/distil-whisper/distil-large-v3.5)).
|
||||
@@ -0,0 +1,160 @@
|
||||
# Onnx-каталог, Python 3.13 и границы версий
|
||||
|
||||
## Проблема
|
||||
|
||||
Смешанная русско-английская речь и отсутствие пунктуации у `gigaam-v3-ctc` —
|
||||
известные ограничения onnx-пути ([ADR-006](../adr/006-onnx-asr-backend.md)). В
|
||||
`onnx-asr` 0.12 появились модели, которые их адресуют, но обновление
|
||||
заблокировано ограничением `onnx-asr[cpu,hub]>=0.11.0,<0.12.0` в
|
||||
`pyproject.toml`.
|
||||
|
||||
Состояние движков и первичные источники —
|
||||
[исследование обновлений](../research/2026-08-10-engine-model-updates.md).
|
||||
|
||||
## Что делаем
|
||||
|
||||
Снимаем ограничение версии, поднимаем `onnx-asr` до 0.12 и делаем
|
||||
поддерживаемыми три модели:
|
||||
|
||||
- `gigaam-multilingual-ctc` — смешанная речь с англоязычными терминами;
|
||||
- `gigaam-v3-e2e-ctc` и `gigaam-v3-e2e-rnnt` — пунктуация и нормализация текста.
|
||||
|
||||
Каталог моделей в бэкенде хранит, помимо имени, опубликованные для модели
|
||||
квантизации — сейчас такого места нет, потому что алиасы отображают имя в имя.
|
||||
Расширять существующую таблицу или заводить рядом отдельную структуру —
|
||||
решается при реализации. Passthrough сырых имён onnx-asr сохраняется: он не
|
||||
мешает и оставляет дверь для экспериментов.
|
||||
|
||||
Заодно поднимаем минимальную версию Python до 3.13 и ONNX Runtime до последней
|
||||
доступной (1.28 на момент написания). Python 3.10 в режиме security-only и
|
||||
снимается с поддержки в октябре 2026, а ORT 1.28 требует минимум 3.11 — так что
|
||||
подъём floor и обновление рантайма идут вместе. Следствие: зависимость `tomli` и
|
||||
условный импорт в `config.py` становятся мёртвым кодом и убираются — начиная с
|
||||
3.11 есть `tomllib`.
|
||||
|
||||
Floor берётся сразу 3.13, а не минимально достаточный 3.11: любой подъём версии
|
||||
всё равно требует прогона на всех трёх платформах, и дорого именно тестирование,
|
||||
а не строка в `pyproject.toml`. Один переезд вместо двух. Проект ставится там,
|
||||
где доступны uv и PyPI, поэтому uv сам поднимет нужный интерпретатор; закрытых
|
||||
контуров и оффлайн-зеркал среди пользователей нет.
|
||||
|
||||
Рабочая версия фиксируется файлом `.python-version` (сейчас его в репозитории
|
||||
нет). Floor разрешает любой интерпретатор от 3.13 и выше, а пин делает окружение
|
||||
одинаковым на разных машинах — иначе расхождение вылезает именно тогда, когда
|
||||
что-то отлаживаешь.
|
||||
|
||||
Колёса cp313 проверены для всех нативных зависимостей и всех трёх целевых
|
||||
платформ — Windows, Linux, macOS: `openvino-genai` 2026.0.0.0, `ctranslate2`
|
||||
4.7.1, `onnxruntime` 1.28.0. На macOS `openvino-genai` не ставится по
|
||||
существующему маркеру `sys_platform != 'darwin'`, так что там остаются
|
||||
faster-whisper на CPU и onnx-путь — это не меняется этой работой, но стоит
|
||||
помнить, раз появились пользователи на Mac.
|
||||
|
||||
GPU-пакеты ORT с их требованием CUDA 13 нас не касаются: CUDA идёт через
|
||||
CTranslate2, а onnx-путь ставится в CPU-варианте.
|
||||
|
||||
## Границы версий
|
||||
|
||||
Каждая прямая зависимость получает верхнюю границу по мажорной версии. Причина
|
||||
не в осторожности вообще, а в канале установки: README предлагает
|
||||
`uv tool install git+…`, а этот путь резолвит зависимости заново из метаданных
|
||||
пакета — `uv.lock` в колесо не попадает и не участвует. Диапазоны в
|
||||
`[project.dependencies]` — единственное, что ограничивает версии на машине
|
||||
пользователя. Установки в разные месяцы иначе дают разные наборы библиотек при
|
||||
одном и том же коде, и такие расхождения дороже разбирать, чем поднимать
|
||||
границы вручную.
|
||||
|
||||
Три случая, которые сами собой не решаются:
|
||||
|
||||
- **`onnxruntime` объявляется прямой зависимостью с границей**, хотя проект его
|
||||
не импортирует. `[tool.uv] constraint-dependencies` для этого не годится:
|
||||
по документации uv он действует только когда uv резолвит сам проект, и
|
||||
игнорируется, когда пакет ставят как зависимость.
|
||||
- **`nvidia-cublas-cu12` получает границу по мажорной версии.**
|
||||
`_cuda_bootstrap.py` преднагружает `libcublas.so.12` по точному soname
|
||||
([ADR-001](../adr/001-cuda-bootstrap.md)); мажорный апгрейд даёт другой soname
|
||||
и ломает bootstrap при неизменном коде.
|
||||
- **`openvino-genai` фиксируется на проверенной линии 2026.0**, а не просто «до
|
||||
следующего года». Иначе свежая установка подтянет 2026.3 и незаметно поменяет
|
||||
ту самую точку отсчёта, которую мы договорились не трогать. Границу снимает
|
||||
отдельная работа по обновлению OpenVINO — осознанно.
|
||||
|
||||
Конкретные номера берутся из `uv.lock` при реализации.
|
||||
|
||||
## Чего не делаем
|
||||
|
||||
- Не меняем модель по умолчанию и auto-detect: `gigaam-v3` остаётся дефолтом
|
||||
`--device onnx`. Решение о CPU-дефолте требует замеров на целевом Intel Core
|
||||
i5, которого сейчас нет в доступе.
|
||||
- Не обновляем OpenVINO, OpenVINO GenAI и CTranslate2. Whisper medium на
|
||||
OpenVINO — то, с чем сравниваются новые модели; менять его движок
|
||||
одновременно значит потерять точку отсчёта. Верхние границы версий им при
|
||||
этом проставляются — см. «Границы версий»; это фиксация текущего состояния, а
|
||||
не обновление.
|
||||
- Не добавляем алиасы `large-v3-turbo`, диаризацию, чанкование по паузам и
|
||||
словарь замен терминов — отдельные пункты [backlog](../backlog.md).
|
||||
|
||||
## Ловушки
|
||||
|
||||
- **`compute_type` разрешается по устройству, а не по модели.**
|
||||
`DEVICE_DEFAULTS["onnx"]` даёт `int8` независимо от модели, а `int8`
|
||||
опубликован не для всех. Для модели без запрошенной квантизации проект должен
|
||||
взять доступную и сказать об этом; явно заданное пользователем значение
|
||||
остаётся ошибкой, а не тихой подменой. Явным считается и значение из
|
||||
`.transcriber.toml`, не только флаг CLI — подстановка допустима лишь там, где
|
||||
квантизацию выбрал сам проект. Это единственное изменение в каскаде
|
||||
конфигурации.
|
||||
- **Язык у multilingual-модели.** Проект по умолчанию форсирует `ru`, а интерес
|
||||
— как раз смешанная речь. Надо посмотреть, принимает ли модель подсказку языка
|
||||
или определяет сама, и описать правило в README.
|
||||
- **E2E меняет форму текста.** Пунктуация и нормализация могут повлиять на
|
||||
группировку абзацев в `formatter.py` и на предупреждения `quality.py`,
|
||||
написанные под поведение Whisper.
|
||||
- **VAD.** В 0.12 исправлены сегменты нулевой длины после VAD — проект
|
||||
оборачивает модель в `.with_vad()`, так что изменение касается нас напрямую.
|
||||
|
||||
## Как проверяем
|
||||
|
||||
Работа идёт в отдельной ветке (по конвенции репозитория — `feature/…`), в
|
||||
`master` вливается только после ручной проверки. Причина не в процессе ради
|
||||
процесса: переезд на 3.13 пересобирает окружение целиком, и откатывать это на
|
||||
основной ветке неприятно. Ветка позволяет держать рабочий `master` под рукой,
|
||||
пока новое окружение не подтвердилось.
|
||||
|
||||
Внутри ветки смена окружения и смена библиотеки проверяются раздельно: сначала
|
||||
Python и зависимости при неизменных моделях — поведение обязано остаться
|
||||
прежним, — и только потом `onnx-asr` 0.12 с новыми моделями. Иначе при первой же
|
||||
странности подозреваемых окажется десяток и разделить их будет нечем, CI тут не
|
||||
поможет.
|
||||
|
||||
Проект домашний, CI нет, репрезентативные записи приватны и на разных ноутбуках
|
||||
разные — автоматическая приёмка невозможна в принципе, уверенность держится на
|
||||
ручных прогонах. Что она покрывает и чего не покрывает:
|
||||
|
||||
- существующие тесты замоканы ([`AGENTS.md`](../../AGENTS.md)) и подтверждают
|
||||
CLI, конфиг и форматирование, но не работоспособность движка: поломку ORT или
|
||||
onnx-asr видно только на реальной записи;
|
||||
- ручные прогоны делаются на Windows и Linux, по всем трём путям — onnx,
|
||||
openvino, cuda;
|
||||
- **macOS не проверяется вовсе.** Колёса cp313 там опубликованы, но это
|
||||
наличие, а не работоспособность; при жалобе с Mac исходить из того, что путь
|
||||
не валидировался;
|
||||
- **скорость на Intel Core i5 не измерялась.** Замеры делаются на Ryzen 7
|
||||
8845H и записываются как наблюдение с указанием CPU — для решения о
|
||||
CPU-дефолте этого недостаточно;
|
||||
- **качество моделей друг относительно друга не измерялось.** Ручная проверка
|
||||
отвечает на вопрос «работает ли», а не «лучше ли»; сравнение — отдельная
|
||||
работа, см. [backlog](../backlog.md).
|
||||
|
||||
Пошаговый чеклист и практика прогона нужны только на время работ и живут в
|
||||
задаче трекера
|
||||
([#1](https://git.dementev.space/ddmitry/local-transcriber/issues/1)), а не в
|
||||
этом документе.
|
||||
|
||||
## Документация
|
||||
|
||||
README: три модели в таблицу ONNX-моделей. Рекомендация остаётся прежней —
|
||||
`gigaam-v3` для русского — пока нет данных, чтобы её менять; у новых моделей
|
||||
стоит оговорка, что скорость на слабых CPU не измерялась. Требование Python
|
||||
правится в [`docs/PRD.md`](../PRD.md) — раздел «Требования» и таблица
|
||||
технологий; в README версии Python не упоминаются.
|
||||
+9
-8
@@ -3,15 +3,16 @@ name = "local-transcriber"
|
||||
version = "0.1.0"
|
||||
description = "Local CLI tool for audio/video transcription using faster-whisper"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"typer",
|
||||
"rich",
|
||||
"faster-whisper>=1.2.1",
|
||||
"socksio>=1.0.0",
|
||||
"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
||||
"openvino-genai>=2025.0; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')",
|
||||
"tomli>=2.0; python_version < '3.11'",
|
||||
"typer>=0.24.1,<1",
|
||||
"rich>=14.3.3,<15",
|
||||
"faster-whisper>=1.2.1,<2",
|
||||
"socksio>=1.0.0,<2",
|
||||
"nvidia-cublas-cu12>=12.4,<13; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
||||
"openvino-genai>=2026.0.0.0,<2026.1; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')",
|
||||
"onnx-asr[cpu,hub]>=0.12,<0.13",
|
||||
"onnxruntime>=1.28,<2",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -25,6 +25,15 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend:
|
||||
ov_device=device, compute_type_explicit=compute_type_explicit
|
||||
)
|
||||
|
||||
if device == "onnx":
|
||||
try:
|
||||
from .onnx_asr import OnnxAsrBackend
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
"onnx-asr бэкенд недоступен. Установите: pip install onnx-asr[cpu,hub]"
|
||||
) from None
|
||||
return OnnxAsrBackend(compute_type_explicit=compute_type_explicit)
|
||||
|
||||
# cuda, cpu и всё остальное → faster-whisper
|
||||
from .faster_whisper import FasterWhisperBackend
|
||||
|
||||
|
||||
@@ -30,8 +30,12 @@ class Backend(Protocol):
|
||||
model_path: str,
|
||||
device: str,
|
||||
compute_type: str,
|
||||
cpu_threads: int = 0,
|
||||
) -> Any:
|
||||
"""Создаёт модель. Возвращает backend-специфичный объект."""
|
||||
"""Создаёт модель. Возвращает backend-специфичный объект.
|
||||
|
||||
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки).
|
||||
"""
|
||||
...
|
||||
|
||||
def transcribe(
|
||||
|
||||
@@ -84,10 +84,17 @@ class FasterWhisperBackend:
|
||||
model_path: str,
|
||||
device: str,
|
||||
compute_type: str,
|
||||
cpu_threads: int = 0,
|
||||
) -> Any:
|
||||
"""Создаёт WhisperModel."""
|
||||
"""Создаёт WhisperModel.
|
||||
|
||||
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки, обычно 4).
|
||||
"""
|
||||
try:
|
||||
return WhisperModel(model_path, device=device, compute_type=compute_type)
|
||||
return WhisperModel(
|
||||
model_path, device=device, compute_type=compute_type,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
except ImportError as exc:
|
||||
if _is_missing_socksio_error(exc):
|
||||
raise RuntimeError(
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Бэкенд транскрипции на основе onnx-asr (GigaAM, Parakeet, FastConformer)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from local_transcriber.types import Segment, TranscribeResult
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnnxModelSpec:
|
||||
"""Имя onnx-asr и опубликованные варианты квантизации модели."""
|
||||
|
||||
model_id: str
|
||||
quantizations: frozenset[str | None]
|
||||
|
||||
|
||||
_INT8_AND_FLOAT32 = frozenset({"int8", None})
|
||||
|
||||
MODEL_CATALOG: dict[str, OnnxModelSpec] = {
|
||||
"gigaam-v3": OnnxModelSpec("gigaam-v3-ctc", _INT8_AND_FLOAT32),
|
||||
"parakeet-v3": OnnxModelSpec(
|
||||
"nemo-parakeet-tdt-0.6b-v3",
|
||||
_INT8_AND_FLOAT32,
|
||||
),
|
||||
"gigaam-multilingual-ctc": OnnxModelSpec(
|
||||
"gigaam-multilingual-ctc",
|
||||
_INT8_AND_FLOAT32,
|
||||
),
|
||||
"gigaam-v3-e2e-ctc": OnnxModelSpec(
|
||||
"gigaam-v3-e2e-ctc",
|
||||
_INT8_AND_FLOAT32,
|
||||
),
|
||||
"gigaam-v3-e2e-rnnt": OnnxModelSpec(
|
||||
"gigaam-v3-e2e-rnnt",
|
||||
_INT8_AND_FLOAT32,
|
||||
),
|
||||
}
|
||||
|
||||
# Оставлено как совместимое представление публичного каталога алиасов.
|
||||
MODEL_ALIASES: dict[str, str] = {
|
||||
alias: spec.model_id for alias, spec in MODEL_CATALOG.items()
|
||||
}
|
||||
|
||||
SUPPORTED_ALIASES = ", ".join(MODEL_ALIASES)
|
||||
|
||||
# compute_type проекта → onnx-asr quantization (file suffix; None = unquantized).
|
||||
_QUANTIZATION_MAP: dict[str, str | None] = {
|
||||
"int8": "int8",
|
||||
"fp16": "fp16",
|
||||
"float16": "fp16",
|
||||
"float32": None,
|
||||
"fp32": None,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_quantization(compute_type: str) -> str | None:
|
||||
"""Маппит compute_type проекта в значение onnx-asr ``quantization``.
|
||||
|
||||
onnx-asr использует ``quantization`` как суффикс имени файла модели:
|
||||
``int8``/``fp16`` подгружают квантизованные веса, ``None`` — unquantized
|
||||
(float32). Передача ``"float32"`` строкой пытается найти несуществующий
|
||||
файл с суффиксом ``_float32`` и приводит к ошибке загрузки.
|
||||
"""
|
||||
if compute_type not in _QUANTIZATION_MAP:
|
||||
supported = ", ".join(sorted(_QUANTIZATION_MAP))
|
||||
raise ValueError(
|
||||
f"Неподдерживаемый compute_type '{compute_type}' для onnx-asr. "
|
||||
f"Допустимо: {supported}."
|
||||
)
|
||||
return _QUANTIZATION_MAP[compute_type]
|
||||
|
||||
|
||||
class OnnxAsrBackend:
|
||||
"""Бэкенд транскрипции через onnx-asr (ONNX Runtime)."""
|
||||
|
||||
def __init__(self, compute_type_explicit: bool = True):
|
||||
self._compute_type_explicit = compute_type_explicit
|
||||
self.actual_compute_type: str | None = None
|
||||
self._resolved_model_id: str | None = None
|
||||
self._vad: Any = None
|
||||
|
||||
def ensure_model_available(
|
||||
self,
|
||||
model_name: str,
|
||||
compute_type: str,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
) -> str:
|
||||
"""Resolves model alias and returns the onnx-asr model identifier.
|
||||
|
||||
onnx-asr downloads models automatically via load_model(),
|
||||
so this just validates the alias and returns the identifier string.
|
||||
"""
|
||||
spec = MODEL_CATALOG.get(model_name)
|
||||
quantization = _normalize_quantization(compute_type)
|
||||
if spec is not None and quantization not in spec.quantizations:
|
||||
if self._compute_type_explicit:
|
||||
available = _format_compute_types(spec.quantizations)
|
||||
raise ValueError(
|
||||
f"Модель '{model_name}' недоступна с compute_type='{compute_type}' "
|
||||
f"для onnx-asr. Доступные варианты: {available}"
|
||||
)
|
||||
|
||||
resolved_compute_type = _preferred_compute_type(spec.quantizations)
|
||||
_notify(
|
||||
on_status,
|
||||
f"Модель {model_name} недоступна с compute_type={compute_type}; "
|
||||
f"использую {resolved_compute_type}.",
|
||||
)
|
||||
else:
|
||||
resolved_compute_type = _compute_type_for_quantization(quantization)
|
||||
|
||||
self.actual_compute_type = resolved_compute_type
|
||||
self._resolved_model_id = self._resolve_model(model_name)
|
||||
return self._resolved_model_id
|
||||
|
||||
def create_model(
|
||||
self,
|
||||
model_path: str,
|
||||
device: str,
|
||||
compute_type: str,
|
||||
cpu_threads: int = 0,
|
||||
) -> Any:
|
||||
"""Creates onnx-asr model with VAD.
|
||||
|
||||
compute_type маппится в onnx-asr ``quantization`` — это суффикс файла
|
||||
модели; для unquantized (float32/fp32) нужно None, не строку.
|
||||
"""
|
||||
import onnx_asr
|
||||
|
||||
actual_compute_type = self.actual_compute_type or compute_type
|
||||
quantization = _normalize_quantization(actual_compute_type)
|
||||
|
||||
model = onnx_asr.load_model(
|
||||
model=model_path,
|
||||
quantization=quantization,
|
||||
)
|
||||
vad = onnx_asr.load_vad("silero")
|
||||
self._vad = vad
|
||||
return model.with_vad(vad)
|
||||
|
||||
def transcribe(
|
||||
self,
|
||||
model: Any,
|
||||
file_path: Path,
|
||||
language: str | None,
|
||||
on_segment: Callable[[Segment], None] | None = None,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
) -> TranscribeResult:
|
||||
"""Transcribes audio file using onnx-asr model with VAD.
|
||||
|
||||
model: result of create_model() — a SegmentResultsAsrAdapter.
|
||||
file_path: path to audio/video file (any format supported by faster-whisper decode).
|
||||
language: language code (e.g. "ru", "en") — only meaningful for multilingual models.
|
||||
"""
|
||||
from faster_whisper import decode_audio
|
||||
|
||||
_notify(on_status, "Загружаю аудио...")
|
||||
audio_array = decode_audio(str(file_path), sampling_rate=16000)
|
||||
duration = len(audio_array) / 16000.0
|
||||
|
||||
_notify(on_status, "Транскрибирую (onnx-asr)...")
|
||||
segments: list[Segment] = []
|
||||
detected_language = language or "unknown"
|
||||
|
||||
for vad_seg in model.recognize(
|
||||
audio_array, sample_rate=16000, language=language
|
||||
):
|
||||
start = max(0.0, vad_seg.start)
|
||||
end = max(0.0, vad_seg.end)
|
||||
if end <= start:
|
||||
continue
|
||||
seg = Segment(
|
||||
start=start,
|
||||
end=end,
|
||||
text=vad_seg.text,
|
||||
)
|
||||
if on_segment is not None:
|
||||
on_segment(seg)
|
||||
segments.append(seg)
|
||||
_notify(
|
||||
on_status,
|
||||
f"Транскрибирую (onnx-asr)... [{len(segments)} сегм.]",
|
||||
)
|
||||
|
||||
return TranscribeResult(
|
||||
segments=segments,
|
||||
language=detected_language,
|
||||
language_probability=1.0 if language else 0.0,
|
||||
duration=duration,
|
||||
device_used="", # оркестратор проставит
|
||||
)
|
||||
|
||||
def _resolve_model(self, model_name: str) -> str:
|
||||
"""Resolve alias to onnx-asr model name. Raw names pass through."""
|
||||
if model_name in MODEL_ALIASES:
|
||||
return MODEL_ALIASES[model_name]
|
||||
if "/" in model_name or model_name.count("-") >= 2:
|
||||
# Looks like a raw onnx-asr name — allow passthrough
|
||||
return model_name
|
||||
raise ValueError(
|
||||
f"Неподдерживаемая модель '{model_name}'. "
|
||||
f"Доступные алиасы: {SUPPORTED_ALIASES}. "
|
||||
f"Либо укажите полное имя модели onnx-asr."
|
||||
)
|
||||
|
||||
|
||||
def _format_compute_types(quantizations: frozenset[str | None]) -> str:
|
||||
values = [_compute_type_for_quantization(value) for value in quantizations]
|
||||
return ", ".join(sorted(values))
|
||||
|
||||
|
||||
def _compute_type_for_quantization(quantization: str | None) -> str:
|
||||
return "float32" if quantization is None else quantization
|
||||
|
||||
|
||||
def _preferred_compute_type(quantizations: frozenset[str | None]) -> str:
|
||||
for quantization in ("int8", None, "fp16"):
|
||||
if quantization in quantizations:
|
||||
return _compute_type_for_quantization(quantization)
|
||||
raise ValueError("Для ONNX-модели не указаны доступные квантизации")
|
||||
|
||||
|
||||
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
|
||||
if on_status is not None:
|
||||
on_status(message)
|
||||
@@ -106,8 +106,9 @@ class OpenVINOBackend:
|
||||
model_path: str,
|
||||
device: str,
|
||||
compute_type: str,
|
||||
cpu_threads: int = 0,
|
||||
) -> Any:
|
||||
"""Создаёт WhisperPipeline."""
|
||||
"""Создаёт WhisperPipeline. cpu_threads не используется (OpenVINO управляет сам)."""
|
||||
import openvino_genai as ov_genai
|
||||
|
||||
ov_dev = self._resolve_ov_device()
|
||||
|
||||
@@ -9,9 +9,23 @@ from rich.console import Console
|
||||
from rich.status import Status
|
||||
|
||||
from .config import apply_device_defaults, load_config, resolve_defaults
|
||||
from .formatter import format_transcript, write_transcript
|
||||
from .context_menu import install_menu as install_context_menu
|
||||
from .context_menu import uninstall_menu as uninstall_context_menu
|
||||
from .formatter import (
|
||||
format_duration,
|
||||
format_timestamp,
|
||||
format_transcript,
|
||||
write_transcript,
|
||||
)
|
||||
from .quality import (
|
||||
TAIL_GAP_WARN_S,
|
||||
RepetitionBlock,
|
||||
find_repetition_blocks,
|
||||
tail_gap,
|
||||
)
|
||||
from .transcriber import (
|
||||
Segment,
|
||||
TranscribeResult,
|
||||
_is_cuda_error,
|
||||
_transcribe_file,
|
||||
load_model,
|
||||
@@ -43,9 +57,62 @@ def _format_device_info(device_used: str) -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _format_repetition_blocks(
|
||||
blocks: list[RepetitionBlock],
|
||||
use_hours: bool,
|
||||
) -> str:
|
||||
"""Формирует краткое описание блоков повторов для консоли."""
|
||||
rendered = [
|
||||
f"[{format_timestamp(block.start, use_hours=use_hours)} - "
|
||||
f"{format_timestamp(block.end, use_hours=use_hours)}] ({block.count}×)"
|
||||
for block in blocks[:3]
|
||||
]
|
||||
summary = "; ".join(rendered)
|
||||
remaining = len(blocks) - 3
|
||||
if remaining > 0:
|
||||
summary = f"{summary} (+ ещё {remaining})"
|
||||
return summary
|
||||
|
||||
|
||||
def _print_quality_warnings(result: TranscribeResult, file_name: str | None = None) -> None:
|
||||
"""Печатает предупреждения о возможной потере содержания."""
|
||||
is_batch = file_name is not None
|
||||
use_hours = result.duration > 3600
|
||||
|
||||
gap = tail_gap(result)
|
||||
if gap > TAIL_GAP_WARN_S:
|
||||
covered = format_duration(result.segments[-1].end)
|
||||
total = format_duration(result.duration)
|
||||
message = (
|
||||
f"транскрипт покрывает {covered} из {total} — "
|
||||
"возможна потеря хвоста записи"
|
||||
)
|
||||
if is_batch:
|
||||
console.print(f" {file_name}: {message}", style="yellow")
|
||||
else:
|
||||
console.print(
|
||||
f"Внимание: {message}. Попробуйте другой --device.",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
blocks = find_repetition_blocks(result.segments)
|
||||
if blocks:
|
||||
message = (
|
||||
f"блоки повторов: {_format_repetition_blocks(blocks, use_hours)} "
|
||||
"— возможны галлюцинации модели"
|
||||
)
|
||||
if is_batch:
|
||||
console.print(f" {file_name}: {message}", style="yellow")
|
||||
else:
|
||||
console.print(
|
||||
f"Внимание: {message}. Попробуйте другой --device.",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
files: list[Path] = typer.Argument(..., help="Пути к аудио/видеофайлам"),
|
||||
files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"),
|
||||
model: str | None = typer.Option(
|
||||
None, "--model", "-m", show_default=False, help="Модель Whisper [по умолч.: medium]"
|
||||
),
|
||||
@@ -55,19 +122,60 @@ def main(
|
||||
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
|
||||
device: str | None = typer.Option(
|
||||
None, "--device", "-d", show_default=False,
|
||||
help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) [по умолч.: auto]"
|
||||
help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu|onnx) [по умолч.: auto]"
|
||||
),
|
||||
compute_type: str | None = typer.Option(
|
||||
None, "--compute-type", show_default=False,
|
||||
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]"
|
||||
),
|
||||
threads: int = typer.Option(
|
||||
0, "--threads", "-t", show_default=False, min=0,
|
||||
help="Потоки CPU (0 = дефолт библиотеки; рекомендуется = число физ. ядер)"
|
||||
),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
|
||||
force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"),
|
||||
install_menu: bool = typer.Option(False, "--install-menu", help="Установить пункт Transcribe в SendTo"),
|
||||
uninstall_menu: bool = typer.Option(False, "--uninstall-menu", help="Удалить пункт Transcribe из SendTo"),
|
||||
) -> None:
|
||||
"""Транскрибирует аудио/видеофайлы в markdown с таймкодами.
|
||||
|
||||
Каскад приоритетов параметров: CLI-флаги > .transcriber.toml > device-aware дефолты.
|
||||
"""
|
||||
files = [] if files is None else files
|
||||
|
||||
if install_menu or uninstall_menu:
|
||||
if install_menu and uninstall_menu:
|
||||
console.print("--install-menu и --uninstall-menu несовместимы.", style="red bold")
|
||||
raise SystemExit(2)
|
||||
if files:
|
||||
console.print("Флаги меню нельзя использовать вместе с файлами.", style="red bold")
|
||||
raise SystemExit(2)
|
||||
if sys.platform != "win32":
|
||||
console.print("Пункт меню SendTo доступен только на Windows.", style="red bold")
|
||||
raise SystemExit(1)
|
||||
|
||||
try:
|
||||
if install_menu:
|
||||
cmd_path = install_context_menu()
|
||||
console.print(f"Пункт меню установлен: \"{cmd_path}\"", style="green")
|
||||
else:
|
||||
cmd_path = uninstall_context_menu()
|
||||
if cmd_path is None:
|
||||
console.print("Пункт меню не был установлен.", style="yellow")
|
||||
else:
|
||||
console.print(f"Пункт меню удалён: \"{cmd_path}\"", style="green")
|
||||
except RuntimeError as exc:
|
||||
console.print(f"Ошибка: {exc}", style="red bold")
|
||||
raise SystemExit(1)
|
||||
return
|
||||
|
||||
if not files:
|
||||
console.print(
|
||||
"Укажите хотя бы один файл или используйте --install-menu/--uninstall-menu.",
|
||||
style="red bold",
|
||||
)
|
||||
raise SystemExit(2)
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
cli_values = {"model": model, "language": language, "device": device, "compute_type": compute_type}
|
||||
@@ -89,9 +197,9 @@ def main(
|
||||
raise SystemExit(1)
|
||||
|
||||
if is_batch:
|
||||
_run_batch(expanded, defaults, verbose, force, ct_explicit)
|
||||
_run_batch(expanded, defaults, verbose, force, ct_explicit, cpu_threads=threads)
|
||||
else:
|
||||
_run_single(expanded[0], defaults, output, verbose, ct_explicit)
|
||||
_run_single(expanded[0], defaults, output, verbose, ct_explicit, cpu_threads=threads)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\nПрервано пользователем.", style="yellow")
|
||||
raise SystemExit(130)
|
||||
@@ -129,6 +237,7 @@ def _run_single(
|
||||
output: Path | None,
|
||||
verbose: bool,
|
||||
compute_type_explicit: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
) -> None:
|
||||
"""Пайплайн одного файла: валидация → модель → транскрипция → запись."""
|
||||
start = time.monotonic()
|
||||
@@ -148,6 +257,7 @@ def _run_single(
|
||||
defaults["model"], resolved_device, defaults["compute_type"],
|
||||
on_status=lambda msg: console.print(msg), strict_device=strict,
|
||||
compute_type_explicit=compute_type_explicit,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
actual_ct = getattr(backend, "actual_compute_type", defaults["compute_type"]) or defaults["compute_type"]
|
||||
console.print(
|
||||
@@ -174,6 +284,7 @@ def _run_single(
|
||||
on_segment=on_segment if verbose else None,
|
||||
on_status=status.update,
|
||||
strict_device=strict,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
result = tfr.result
|
||||
@@ -211,6 +322,7 @@ def _run_single(
|
||||
elapsed = time.monotonic() - start
|
||||
console.print(f"Транскрипт сохранён: \"{output_path}\"", style="green")
|
||||
console.print(f" Сегментов: {len(result.segments)} Время: {elapsed:.1f}с")
|
||||
_print_quality_warnings(result)
|
||||
|
||||
|
||||
def _run_batch(
|
||||
@@ -219,6 +331,7 @@ def _run_batch(
|
||||
verbose: bool,
|
||||
force: bool,
|
||||
compute_type_explicit: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
) -> None:
|
||||
"""Трёхфазный батч-пайплайн: prescan → загрузка модели → транскрипция."""
|
||||
# Phase 1: Prescan — fail-fast + skip до загрузки модели (экономим ~2-5 сек)
|
||||
@@ -256,6 +369,7 @@ def _run_batch(
|
||||
defaults["model"], resolved_device, defaults["compute_type"],
|
||||
on_status=lambda msg: console.print(msg), strict_device=strict,
|
||||
compute_type_explicit=compute_type_explicit,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3":
|
||||
@@ -306,6 +420,7 @@ def _run_batch(
|
||||
on_segment=on_segment if verbose else None,
|
||||
on_status=status.update if not verbose else lambda msg: console.print(msg),
|
||||
strict_device=strict,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
if tfr.actual_device != actual_device:
|
||||
@@ -343,6 +458,7 @@ def _run_batch(
|
||||
style="green",
|
||||
)
|
||||
processed += 1
|
||||
_print_quality_warnings(result, file.name)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as exc:
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
"""Загрузка конфигурации из ``.transcriber.toml`` и каскад приоритетов."""
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
HARDCODED_DEFAULTS: dict[str, str] = {
|
||||
"model": "medium",
|
||||
"language": "ru",
|
||||
@@ -22,11 +17,20 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
|
||||
"openvino": {"model": "medium", "compute_type": "int8"},
|
||||
"openvino-gpu": {"model": "medium", "compute_type": "int8"},
|
||||
"openvino-cpu": {"model": "medium", "compute_type": "int8"},
|
||||
"onnx": {"model": "gigaam-v3", "compute_type": "int8"},
|
||||
}
|
||||
|
||||
# Одно место правды для допустимых ключей конфига
|
||||
_VALID_KEYS = set(HARDCODED_DEFAULTS)
|
||||
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu"}
|
||||
_VALID_DEVICES = {
|
||||
"auto",
|
||||
"cpu",
|
||||
"cuda",
|
||||
"openvino",
|
||||
"openvino-gpu",
|
||||
"openvino-cpu",
|
||||
"onnx",
|
||||
}
|
||||
|
||||
|
||||
def find_config_file() -> Path | None:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Установка пункта Transcribe в меню SendTo проводника Windows."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CMD_NAME = "Transcribe.cmd"
|
||||
CMD_ENCODING = "oem"
|
||||
|
||||
|
||||
def get_sendto_dir() -> Path:
|
||||
"""Возвращает путь к пользовательской папке SendTo."""
|
||||
appdata = os.environ.get("APPDATA")
|
||||
if appdata is None:
|
||||
raise RuntimeError("Переменная окружения APPDATA не задана.")
|
||||
return Path(appdata) / "Microsoft" / "Windows" / "SendTo"
|
||||
|
||||
|
||||
def get_transcribe_exe() -> Path:
|
||||
"""Возвращает путь к transcribe.exe рядом с текущим интерпретатором."""
|
||||
transcribe_exe = Path(sys.executable).parent / "transcribe.exe"
|
||||
if not transcribe_exe.exists():
|
||||
raise RuntimeError(
|
||||
f"Не найден transcribe.exe рядом с Python: {transcribe_exe}. "
|
||||
"Выполните uv sync и повторите установку пункта меню."
|
||||
)
|
||||
return transcribe_exe
|
||||
|
||||
|
||||
def install_menu() -> Path:
|
||||
"""Создаёт или обновляет Transcribe.cmd в папке SendTo."""
|
||||
sendto_dir = get_sendto_dir()
|
||||
transcribe_exe = get_transcribe_exe()
|
||||
cmd_path = sendto_dir / CMD_NAME
|
||||
content = f'@echo off\r\n"{transcribe_exe}" %*\r\npause\r\n'
|
||||
|
||||
try:
|
||||
encoded_content = content.encode(CMD_ENCODING)
|
||||
except UnicodeEncodeError as exc:
|
||||
raise RuntimeError(
|
||||
"Путь к transcribe.exe содержит символы, которые нельзя записать "
|
||||
"в OEM-кодировке cmd.exe. Установите проект в путь без таких символов "
|
||||
"и повторите --install-menu."
|
||||
) from exc
|
||||
|
||||
sendto_dir.mkdir(parents=True, exist_ok=True)
|
||||
cmd_path.write_bytes(encoded_content)
|
||||
return cmd_path
|
||||
|
||||
|
||||
def uninstall_menu() -> Path | None:
|
||||
"""Удаляет Transcribe.cmd из папки SendTo, если он существует."""
|
||||
cmd_path = get_sendto_dir() / CMD_NAME
|
||||
if not cmd_path.exists():
|
||||
return None
|
||||
cmd_path.unlink()
|
||||
return cmd_path
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from .quality import TAIL_GAP_WARN_S, find_repetition_blocks, tail_gap
|
||||
from .types import Segment, TranscribeResult
|
||||
|
||||
_PAUSE_THRESHOLD_S = 2.0 # пауза между сегментами для разбиения на абзацы
|
||||
@@ -63,7 +64,7 @@ def format_timestamp(seconds: float, use_hours: bool = False) -> str:
|
||||
return f"{minutes:02d}:{secs:02d}.{centiseconds:02d}"
|
||||
|
||||
|
||||
def _format_duration(seconds: float) -> str:
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""Человекочитаемая длительность для метаданных в шапке транскрипта."""
|
||||
total = int(seconds)
|
||||
h = total // 3600
|
||||
@@ -92,7 +93,20 @@ def format_transcript(
|
||||
lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append(f"- **Модель**: {model_name}")
|
||||
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:
|
||||
last_end = result.segments[-1].end
|
||||
lines.append(
|
||||
f"- **Внимание**: транскрипт покрывает {format_duration(last_end)} "
|
||||
f"из {format_duration(result.duration)} — возможна потеря хвоста записи"
|
||||
)
|
||||
for block in find_repetition_blocks(result.segments):
|
||||
start = format_timestamp(block.start, use_hours=use_hours)
|
||||
end = format_timestamp(block.end, use_hours=use_hours)
|
||||
lines.append(
|
||||
f"- **Внимание**: повторы в [{start} - {end}] ({block.count}×) "
|
||||
"— возможны галлюцинации модели"
|
||||
)
|
||||
lines.append(f"- **Устройство**: {device_info}")
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Эвристики качества транскрипта."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .types import Segment, TranscribeResult
|
||||
|
||||
TAIL_GAP_WARN_S = 120.0
|
||||
REPETITION_MIN_RUN = 4
|
||||
REPETITION_MIN_RUN_SHORT = 10
|
||||
REPETITION_MIN_LEN = 6
|
||||
|
||||
_PUNCTUATION_TO_REMOVE = ".,!?…:;—–-\"'«»()[]<>"
|
||||
_REMOVE_PUNCTUATION = str.maketrans("", "", _PUNCTUATION_TO_REMOVE)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepetitionBlock:
|
||||
start: float
|
||||
end: float
|
||||
count: int
|
||||
text: str
|
||||
|
||||
|
||||
def tail_gap(result: TranscribeResult) -> float:
|
||||
"""Возвращает непокрытый хвост записи в секундах."""
|
||||
if not result.segments:
|
||||
return 0.0
|
||||
return max(0.0, result.duration - result.segments[-1].end)
|
||||
|
||||
|
||||
def _normalize(text: str) -> str:
|
||||
"""Нормализует текст сегмента для поиска межсегментных повторов."""
|
||||
text = text.casefold()
|
||||
text = text.translate(_REMOVE_PUNCTUATION)
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
def find_repetition_blocks(segments: list[Segment]) -> list[RepetitionBlock]:
|
||||
"""Находит серии подряд идущих одинаковых сегментов."""
|
||||
blocks: list[RepetitionBlock] = []
|
||||
run_start = 0
|
||||
run_norm = ""
|
||||
|
||||
def append_run(run_end: int) -> None:
|
||||
count = run_end - run_start
|
||||
if not run_norm:
|
||||
return
|
||||
min_run = (
|
||||
REPETITION_MIN_RUN
|
||||
if len(run_norm) >= REPETITION_MIN_LEN
|
||||
else REPETITION_MIN_RUN_SHORT
|
||||
)
|
||||
if count >= min_run:
|
||||
blocks.append(
|
||||
RepetitionBlock(
|
||||
start=segments[run_start].start,
|
||||
end=segments[run_end - 1].end,
|
||||
count=count,
|
||||
text=segments[run_start].text,
|
||||
)
|
||||
)
|
||||
|
||||
for index, segment in enumerate(segments):
|
||||
norm = _normalize(segment.text)
|
||||
if index == 0:
|
||||
run_start = 0
|
||||
run_norm = norm
|
||||
continue
|
||||
if norm == run_norm:
|
||||
continue
|
||||
append_run(index)
|
||||
run_start = index
|
||||
run_norm = norm
|
||||
|
||||
if segments:
|
||||
append_run(len(segments))
|
||||
|
||||
return blocks
|
||||
@@ -22,11 +22,13 @@ def load_model(
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
strict_device: bool = False,
|
||||
compute_type_explicit: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
) -> tuple[Any, str, Any, str]:
|
||||
"""Загружает модель: ensure + create с fallback.
|
||||
|
||||
Возвращает (model, actual_device, backend, model_path).
|
||||
compute_type_explicit: True если пользователь явно указал --compute-type.
|
||||
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки).
|
||||
"""
|
||||
backend = get_backend(device, compute_type_explicit=compute_type_explicit)
|
||||
actual_device = device
|
||||
@@ -35,7 +37,7 @@ def load_model(
|
||||
|
||||
try:
|
||||
_notify_status(on_status, f"Инициализирую модель на {device}...")
|
||||
model = backend.create_model(model_path, device, compute_type)
|
||||
model = backend.create_model(model_path, device, compute_type, cpu_threads=cpu_threads)
|
||||
# Резолвим actual_device по реальному OpenVINO device
|
||||
ov_dev = getattr(backend, "actual_ov_device", None)
|
||||
if ov_dev == "GPU" and actual_device != "openvino-gpu":
|
||||
@@ -55,7 +57,7 @@ def load_model(
|
||||
backend = get_backend("cpu")
|
||||
model_path = backend.ensure_model_available(model_name, compute_type, on_status)
|
||||
_notify_status(on_status, "Инициализирую модель на cpu...")
|
||||
model = backend.create_model(model_path, "cpu", compute_type)
|
||||
model = backend.create_model(model_path, "cpu", compute_type, cpu_threads=cpu_threads)
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -74,6 +76,7 @@ def _transcribe_file(
|
||||
on_segment: Callable[[Segment], None] | None = None,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
strict_device: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
) -> TranscribeFileResult:
|
||||
"""Транскрибирует один файл. При mid-stream fallback перезагружает модель."""
|
||||
lang_arg = language if language and language != "auto" else None
|
||||
@@ -95,7 +98,7 @@ def _transcribe_file(
|
||||
backend = get_backend("cpu")
|
||||
model_path = backend.ensure_model_available(model_name, compute_type, on_status)
|
||||
_notify_status(on_status, "Инициализирую модель на cpu...")
|
||||
model = backend.create_model(model_path, "cpu", compute_type)
|
||||
model = backend.create_model(model_path, "cpu", compute_type, cpu_threads=cpu_threads)
|
||||
_notify_status(on_status, "Транскрибирую...")
|
||||
result = backend.transcribe(model, file_path, lang_arg, on_segment, on_status)
|
||||
result.device_used = actual_device
|
||||
@@ -120,16 +123,19 @@ def transcribe(
|
||||
on_segment: Callable[[Segment], None] | None = None,
|
||||
on_status: Callable[[str], None] | None = None,
|
||||
strict_device: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
) -> TranscribeResult:
|
||||
"""High-level API: загрузка модели + транскрипция за один вызов."""
|
||||
model, actual_device, backend, model_path = load_model(
|
||||
model_name, device, compute_type, on_status, strict_device,
|
||||
compute_type_explicit=True, # Python API — caller explicitly chose compute_type
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
tfr = _transcribe_file(
|
||||
model, actual_device, backend, model_path,
|
||||
file_path, model_name, compute_type,
|
||||
language, on_segment, on_status, strict_device,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
return tfr.result
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_resolve_repo_explicit_unsupported_pair_raises():
|
||||
"""Явный --compute-type с несуществующей парой → ошибка."""
|
||||
backend = OpenVINOBackend(compute_type_explicit=True)
|
||||
with pytest.raises(ValueError, match="недоступна с compute_type='fp16'"):
|
||||
backend._resolve_repo("medium", "fp16")
|
||||
backend._resolve_repo("small", "fp16")
|
||||
|
||||
|
||||
def test_resolve_repo_explicit_unknown_model_raises():
|
||||
|
||||
@@ -2,6 +2,7 @@ from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from local_transcriber.cli import _format_device_info, app
|
||||
@@ -862,3 +863,308 @@ def test_cli_openvino_alias_resolves_to_gpu(tmp_path):
|
||||
assert out.exit_code == 0
|
||||
# detect_device("openvino") resolved to "openvino-gpu", load_model receives it
|
||||
assert mock_load_model.call_args[0][1] == "openvino-gpu"
|
||||
|
||||
|
||||
# === --threads ===
|
||||
|
||||
|
||||
def test_cli_threads_passed_to_load_model(tmp_path):
|
||||
"""--threads передаётся в load_model как cpu_threads."""
|
||||
audio = tmp_path / "test.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
result = _make_result()
|
||||
model = _make_model()
|
||||
backend = _make_backend()
|
||||
tfr = _make_tfr(result=result, model=model, backend=backend)
|
||||
mock_load_model = MagicMock(return_value=(model, "cpu", backend, "/models/medium"))
|
||||
|
||||
with (
|
||||
patch("local_transcriber.cli.load_config", return_value={}),
|
||||
patch("local_transcriber.cli.validate_input_file", return_value=audio),
|
||||
patch("local_transcriber.cli.detect_device", return_value="cpu"),
|
||||
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), "--threads", "8"])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert mock_load_model.call_args.kwargs["cpu_threads"] == 8
|
||||
|
||||
|
||||
def test_cli_threads_default_zero(tmp_path):
|
||||
"""Без --threads load_model получает cpu_threads=0."""
|
||||
audio = tmp_path / "test.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
result = _make_result()
|
||||
model = _make_model()
|
||||
backend = _make_backend()
|
||||
tfr = _make_tfr(result=result, model=model, backend=backend)
|
||||
mock_load_model = MagicMock(return_value=(model, "cpu", backend, "/models/medium"))
|
||||
|
||||
with (
|
||||
patch("local_transcriber.cli.load_config", return_value={}),
|
||||
patch("local_transcriber.cli.validate_input_file", return_value=audio),
|
||||
patch("local_transcriber.cli.detect_device", return_value="cpu"),
|
||||
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_load_model.call_args.kwargs["cpu_threads"] == 0
|
||||
|
||||
|
||||
def test_cli_threads_negative_rejected(tmp_path):
|
||||
"""--threads с отрицательным значением отклоняется typer (min=0)."""
|
||||
audio = tmp_path / "test.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
out = runner.invoke(app, [str(audio), "--threads", "-1"])
|
||||
assert out.exit_code != 0
|
||||
|
||||
|
||||
# === SendTo context menu flags ===
|
||||
|
||||
|
||||
def test_cli_install_menu_success(tmp_path):
|
||||
cmd_path = tmp_path / "Transcribe.cmd"
|
||||
|
||||
with (
|
||||
patch("local_transcriber.cli.install_context_menu", return_value=cmd_path) as mock_install,
|
||||
patch("local_transcriber.cli.load_config") as mock_load_config,
|
||||
patch("local_transcriber.cli.sys") as mock_sys,
|
||||
):
|
||||
mock_sys.platform = "win32"
|
||||
out = runner.invoke(app, ["--install-menu"])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert "Пункт меню установлен" in out.output
|
||||
assert cmd_path.name in out.output
|
||||
mock_install.assert_called_once_with()
|
||||
mock_load_config.assert_not_called()
|
||||
|
||||
|
||||
def test_cli_uninstall_menu_success(tmp_path):
|
||||
cmd_path = tmp_path / "Transcribe.cmd"
|
||||
|
||||
with (
|
||||
patch("local_transcriber.cli.uninstall_context_menu", return_value=cmd_path) as mock_uninstall,
|
||||
patch("local_transcriber.cli.load_config") as mock_load_config,
|
||||
patch("local_transcriber.cli.sys") as mock_sys,
|
||||
):
|
||||
mock_sys.platform = "win32"
|
||||
out = runner.invoke(app, ["--uninstall-menu"])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert "Пункт меню удалён" in out.output
|
||||
assert cmd_path.name in out.output
|
||||
mock_uninstall.assert_called_once_with()
|
||||
mock_load_config.assert_not_called()
|
||||
|
||||
|
||||
def test_cli_uninstall_menu_missing_is_success():
|
||||
with (
|
||||
patch("local_transcriber.cli.uninstall_context_menu", return_value=None),
|
||||
patch("local_transcriber.cli.sys") as mock_sys,
|
||||
):
|
||||
mock_sys.platform = "win32"
|
||||
out = runner.invoke(app, ["--uninstall-menu"])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert "не был установлен" in out.output
|
||||
|
||||
|
||||
def test_cli_menu_flags_are_mutually_exclusive():
|
||||
out = runner.invoke(app, ["--install-menu", "--uninstall-menu"])
|
||||
|
||||
assert out.exit_code == 2
|
||||
assert "несовместимы" in out.output
|
||||
|
||||
|
||||
def test_cli_menu_flag_with_file_is_rejected(tmp_path):
|
||||
audio = tmp_path / "test.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
|
||||
out = runner.invoke(app, [str(audio), "--install-menu"])
|
||||
|
||||
assert out.exit_code == 2
|
||||
assert "нельзя использовать вместе с файлами" in out.output
|
||||
|
||||
|
||||
def test_cli_no_files_and_no_menu_flags_is_rejected():
|
||||
out = runner.invoke(app, [])
|
||||
|
||||
assert out.exit_code == 2
|
||||
assert "Укажите хотя бы один файл" in out.output
|
||||
|
||||
|
||||
def test_cli_menu_flags_available_only_on_windows():
|
||||
with patch("local_transcriber.cli.sys") as mock_sys:
|
||||
mock_sys.platform = "linux"
|
||||
out = runner.invoke(app, ["--install-menu"])
|
||||
|
||||
assert out.exit_code == 1
|
||||
assert "только на Windows" in out.output
|
||||
|
||||
|
||||
def test_cli_menu_runtime_error_has_no_verbose_hint():
|
||||
with (
|
||||
patch("local_transcriber.cli.install_context_menu", side_effect=RuntimeError("нет APPDATA")),
|
||||
patch("local_transcriber.cli.sys") as mock_sys,
|
||||
):
|
||||
mock_sys.platform = "win32"
|
||||
out = runner.invoke(app, ["--install-menu"])
|
||||
|
||||
assert out.exit_code == 1
|
||||
assert "нет APPDATA" in out.output
|
||||
assert "--verbose" not in out.output
|
||||
|
||||
|
||||
def test_cli_tail_gap_quality_warning_single(tmp_path):
|
||||
audio = tmp_path / "tail.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
result = _make_result(
|
||||
segments=[Segment(start=0.0, end=60.0, text="Фраза")],
|
||||
duration=600.0,
|
||||
)
|
||||
|
||||
patches = _single_patches(result=result, tmp_file=audio)
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patch("local_transcriber.cli.console", Console(stderr=True, width=1000)),
|
||||
):
|
||||
out = runner.invoke(app, [str(audio)])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert (
|
||||
"Внимание: транскрипт покрывает 01:00 из 10:00 — "
|
||||
"возможна потеря хвоста записи. Попробуйте другой --device."
|
||||
) in out.output
|
||||
|
||||
|
||||
def test_cli_repetition_quality_warning_single(tmp_path):
|
||||
audio = tmp_path / "repeat.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
result = _make_result(
|
||||
segments=[
|
||||
Segment(start=10.0, end=11.0, text="Повторяемая фраза"),
|
||||
Segment(start=11.0, end=12.0, text="повторяемая фраза"),
|
||||
Segment(start=12.0, end=13.0, text="повторяемая фраза"),
|
||||
Segment(start=13.0, end=14.0, text="повторяемая фраза"),
|
||||
],
|
||||
duration=60.0,
|
||||
)
|
||||
|
||||
patches = _single_patches(result=result, tmp_file=audio)
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patch("local_transcriber.cli.console", Console(stderr=True, width=1000)),
|
||||
):
|
||||
out = runner.invoke(app, [str(audio)])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert (
|
||||
"Внимание: блоки повторов: [00:10.00 - 00:14.00] (4×) — "
|
||||
"возможны галлюцинации модели. Попробуйте другой --device."
|
||||
) in out.output
|
||||
|
||||
|
||||
def test_cli_quality_warning_batch_includes_file_name(tmp_path):
|
||||
a = tmp_path / "a.mp3"
|
||||
b = tmp_path / "b.mp3"
|
||||
a.write_bytes(b"fake")
|
||||
b.write_bytes(b"fake")
|
||||
|
||||
result_warn = _make_result(
|
||||
segments=[Segment(start=0.0, end=60.0, text="Фраза")],
|
||||
duration=600.0,
|
||||
)
|
||||
result_ok = _make_result()
|
||||
model = _make_model()
|
||||
backend = _make_backend()
|
||||
tfr_warn = _make_tfr(result=result_warn, model=model, backend=backend)
|
||||
tfr_ok = _make_tfr(result=result_ok, model=model, backend=backend)
|
||||
|
||||
with (
|
||||
patch("local_transcriber.cli.load_config", return_value={}),
|
||||
patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p),
|
||||
patch("local_transcriber.cli.detect_device", return_value="cpu"),
|
||||
patch("local_transcriber.cli.load_model", return_value=(model, "cpu", backend, "/models/medium")),
|
||||
patch("local_transcriber.cli._transcribe_file", side_effect=[tfr_warn, tfr_ok]),
|
||||
patch("local_transcriber.cli.write_transcript"),
|
||||
patch("local_transcriber.cli.console", Console(stderr=True, width=1000)),
|
||||
):
|
||||
out = runner.invoke(app, [str(a), str(b)])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert (
|
||||
" a.mp3: транскрипт покрывает 01:00 из 10:00 — "
|
||||
"возможна потеря хвоста записи"
|
||||
) in out.output
|
||||
|
||||
|
||||
def test_cli_repetition_quality_warning_truncates_after_three_blocks(tmp_path):
|
||||
audio = tmp_path / "repeat-many.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
|
||||
def run(start, count, text):
|
||||
return [
|
||||
Segment(start=start + index, end=start + index + 1.0, text=text)
|
||||
for index in range(count)
|
||||
]
|
||||
|
||||
result = _make_result(
|
||||
segments=[
|
||||
*run(10.0, 6, "Первый повтор"),
|
||||
Segment(start=18.0, end=19.0, text="Разрыв один"),
|
||||
*run(20.0, 5, "Второй повтор"),
|
||||
Segment(start=28.0, end=29.0, text="Разрыв два"),
|
||||
*run(30.0, 4, "Третий повтор"),
|
||||
Segment(start=38.0, end=39.0, text="Разрыв три"),
|
||||
*run(40.0, 4, "Четвёртый повтор"),
|
||||
],
|
||||
duration=90.0,
|
||||
)
|
||||
|
||||
patches = _single_patches(result=result, tmp_file=audio)
|
||||
with (
|
||||
patches[0],
|
||||
patches[1],
|
||||
patches[2],
|
||||
patches[3],
|
||||
patches[4],
|
||||
patches[5],
|
||||
patch("local_transcriber.cli.console", Console(stderr=True, width=1000)),
|
||||
):
|
||||
out = runner.invoke(app, [str(audio)])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert (
|
||||
"Внимание: блоки повторов: [00:10.00 - 00:16.00] (6×); "
|
||||
"[00:20.00 - 00:25.00] (5×); [00:30.00 - 00:34.00] (4×) "
|
||||
"(+ ещё 1) — возможны галлюцинации модели. Попробуйте другой --device."
|
||||
) in out.output
|
||||
|
||||
|
||||
def test_cli_default_result_has_no_quality_warnings(tmp_path):
|
||||
audio = tmp_path / "normal.mp3"
|
||||
audio.write_bytes(b"fake")
|
||||
|
||||
patches = _single_patches(tmp_file=audio)
|
||||
with patches[0], patches[1], patches[2], patches[3], patches[4], patches[5]:
|
||||
out = runner.invoke(app, [str(audio)])
|
||||
|
||||
assert out.exit_code == 0
|
||||
assert "потеря хвоста" not in out.output
|
||||
assert "галлюцинации" not in out.output
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from local_transcriber import context_menu
|
||||
|
||||
|
||||
def _prepare_exe(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
scripts_dir = tmp_path / "venv" / "Scripts"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
python_exe = scripts_dir / "python.exe"
|
||||
transcribe_exe = scripts_dir / "transcribe.exe"
|
||||
python_exe.write_bytes(b"")
|
||||
transcribe_exe.write_bytes(b"")
|
||||
monkeypatch.setattr(context_menu.sys, "executable", str(python_exe))
|
||||
return transcribe_exe
|
||||
|
||||
|
||||
def test_install_menu_creates_expected_cmd(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr(context_menu, "CMD_ENCODING", "utf-8")
|
||||
transcribe_exe = _prepare_exe(tmp_path, monkeypatch)
|
||||
|
||||
cmd_path = context_menu.install_menu()
|
||||
|
||||
assert cmd_path.name == "Transcribe.cmd"
|
||||
assert cmd_path.exists()
|
||||
assert cmd_path.read_bytes() == (
|
||||
f'@echo off\r\n"{transcribe_exe}" %*\r\npause\r\n'.encode("utf-8")
|
||||
)
|
||||
assert b"chcp" not in cmd_path.read_bytes().lower()
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Кодировка oem доступна только на Windows")
|
||||
def test_install_menu_writes_real_oem_encoding_on_windows(tmp_path, monkeypatch):
|
||||
appdata = tmp_path / "AppData" / "Roaming"
|
||||
scripts_dir = tmp_path / "проект" / "Scripts"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
python_exe = scripts_dir / "python.exe"
|
||||
transcribe_exe = scripts_dir / "transcribe.exe"
|
||||
python_exe.write_bytes(b"")
|
||||
transcribe_exe.write_bytes(b"")
|
||||
monkeypatch.setenv("APPDATA", str(appdata))
|
||||
monkeypatch.setattr(context_menu.sys, "executable", str(python_exe))
|
||||
|
||||
cmd_path = context_menu.install_menu()
|
||||
|
||||
assert context_menu.CMD_ENCODING == "oem"
|
||||
assert cmd_path.read_bytes() == (
|
||||
f'@echo off\r\n"{transcribe_exe}" %*\r\npause\r\n'.encode("oem")
|
||||
)
|
||||
|
||||
|
||||
def test_install_menu_overwrites_existing_file(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr(context_menu, "CMD_ENCODING", "utf-8")
|
||||
_prepare_exe(tmp_path, monkeypatch)
|
||||
|
||||
cmd_path = context_menu.install_menu()
|
||||
cmd_path.write_text("old", encoding="utf-8")
|
||||
|
||||
second_path = context_menu.install_menu()
|
||||
|
||||
assert second_path == cmd_path
|
||||
assert "old" not in cmd_path.read_text(encoding="utf-8")
|
||||
assert "%*" in cmd_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_install_menu_creates_missing_sendto_dir(tmp_path, monkeypatch):
|
||||
appdata = tmp_path / "AppData" / "Roaming"
|
||||
monkeypatch.setenv("APPDATA", str(appdata))
|
||||
monkeypatch.setattr(context_menu, "CMD_ENCODING", "utf-8")
|
||||
_prepare_exe(tmp_path, monkeypatch)
|
||||
|
||||
cmd_path = context_menu.install_menu()
|
||||
|
||||
assert cmd_path.parent == appdata / "Microsoft" / "Windows" / "SendTo"
|
||||
assert cmd_path.parent.is_dir()
|
||||
|
||||
|
||||
def test_install_menu_oem_encoding_error_is_runtime_error(tmp_path, monkeypatch):
|
||||
appdata = tmp_path / "AppData" / "Roaming"
|
||||
cmd_path = appdata / "Microsoft" / "Windows" / "SendTo" / "Transcribe.cmd"
|
||||
monkeypatch.setenv("APPDATA", str(appdata))
|
||||
monkeypatch.setattr(context_menu, "CMD_ENCODING", "ascii")
|
||||
monkeypatch.setattr(
|
||||
context_menu,
|
||||
"get_transcribe_exe",
|
||||
lambda: tmp_path / "测试" / "transcribe.exe",
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="OEM"):
|
||||
context_menu.install_menu()
|
||||
|
||||
assert not cmd_path.exists()
|
||||
|
||||
|
||||
def test_uninstall_menu_removes_file_and_missing_is_not_error(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("APPDATA", str(tmp_path / "AppData" / "Roaming"))
|
||||
monkeypatch.setattr(context_menu, "CMD_ENCODING", "utf-8")
|
||||
_prepare_exe(tmp_path, monkeypatch)
|
||||
cmd_path = context_menu.install_menu()
|
||||
|
||||
removed_path = context_menu.uninstall_menu()
|
||||
missing_path = context_menu.uninstall_menu()
|
||||
|
||||
assert removed_path == cmd_path
|
||||
assert not cmd_path.exists()
|
||||
assert missing_path is None
|
||||
|
||||
|
||||
def test_get_sendto_dir_requires_appdata(monkeypatch):
|
||||
monkeypatch.delenv("APPDATA", raising=False)
|
||||
|
||||
with pytest.raises(RuntimeError, match="APPDATA"):
|
||||
context_menu.get_sendto_dir()
|
||||
|
||||
|
||||
def test_get_transcribe_exe_requires_existing_exe(tmp_path, monkeypatch):
|
||||
scripts_dir = tmp_path / "venv" / "Scripts"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
python_exe = scripts_dir / "python.exe"
|
||||
python_exe.write_bytes(b"")
|
||||
monkeypatch.setattr(context_menu.sys, "executable", str(python_exe))
|
||||
|
||||
with pytest.raises(RuntimeError, match="uv sync"):
|
||||
context_menu.get_transcribe_exe()
|
||||
@@ -172,3 +172,141 @@ def test_write_transcript(tmp_path):
|
||||
out = tmp_path / "output.md"
|
||||
write_transcript("# Test content\n", out)
|
||||
assert out.read_text(encoding="utf-8") == "# Test content\n"
|
||||
|
||||
|
||||
def test_format_transcript_tail_gap_warning():
|
||||
result = TranscribeResult(
|
||||
segments=[Segment(start=0.0, end=60.0, text=" Фраза.")],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=600.0,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="tail.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "возможна потеря хвоста" in content
|
||||
assert "транскрипт покрывает 01:00 из 10:00" in content
|
||||
|
||||
|
||||
def test_format_transcript_no_tail_gap_warning_for_small_gap():
|
||||
result = TranscribeResult(
|
||||
segments=[Segment(start=0.0, end=60.0, text=" Фраза.")],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=179.99,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="ok.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "потеря хвоста" not in content
|
||||
|
||||
|
||||
def test_format_transcript_no_tail_gap_warning_for_exact_threshold():
|
||||
result = TranscribeResult(
|
||||
segments=[Segment(start=0.0, end=60.0, text=" Фраза.")],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=180.0,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="ok.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "потеря хвоста" not in content
|
||||
|
||||
|
||||
def test_format_transcript_repetition_warning():
|
||||
result = TranscribeResult(
|
||||
segments=[
|
||||
Segment(start=10.0, end=11.0, text=" Повторяемая фраза."),
|
||||
Segment(start=11.0, end=12.0, text=" повторяемая фраза"),
|
||||
Segment(start=12.0, end=13.0, text=" «Повторяемая фраза»"),
|
||||
Segment(start=13.0, end=14.0, text=" повторяемая фраза…"),
|
||||
],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=60.0,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="repeat.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "повторы в [00:10.00 - 00:14.00] (4×)" in content
|
||||
assert "возможны галлюцинации" in content
|
||||
|
||||
|
||||
def test_format_transcript_repetition_warning_uses_hours():
|
||||
result = TranscribeResult(
|
||||
segments=[
|
||||
Segment(start=3600.0, end=3601.0, text=" Повтор."),
|
||||
Segment(start=3601.0, end=3602.0, text=" повтор"),
|
||||
Segment(start=3602.0, end=3603.0, text=" повтор"),
|
||||
Segment(start=3603.0, end=3604.0, text=" повтор"),
|
||||
],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=3700.0,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="long-repeat.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "повторы в [01:00:00.00 - 01:00:04.00] (4×)" in content
|
||||
|
||||
|
||||
def test_format_transcript_without_anomalies_has_no_warning_lines():
|
||||
result = TranscribeResult(
|
||||
segments=[Segment(start=0.0, end=60.0, text=" Обычная запись.")],
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=120.0,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
content = format_transcript(
|
||||
result,
|
||||
source_filename="ok.mp3",
|
||||
model_name="medium",
|
||||
device_info="CPU",
|
||||
language_mode="forced",
|
||||
transcription_date=datetime(2026, 1, 1, 0, 0, 0),
|
||||
)
|
||||
|
||||
assert "Внимание" not in content
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
"""Tests for onnx-asr backend."""
|
||||
|
||||
import pytest
|
||||
|
||||
from local_transcriber.backends.onnx_asr import OnnxAsrBackend
|
||||
from local_transcriber.types import Segment, TranscribeResult
|
||||
|
||||
|
||||
class FakeVadSegment:
|
||||
"""Mimics onnx-asr SegmentResult."""
|
||||
|
||||
def __init__(self, start, end, text):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.text = text
|
||||
|
||||
|
||||
class TestEnsureModelAvailable:
|
||||
def test_returns_model_id_for_gigaam(self):
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend.ensure_model_available("gigaam-v3", "int8")
|
||||
assert result == "gigaam-v3-ctc"
|
||||
|
||||
def test_returns_model_id_for_parakeet(self):
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend.ensure_model_available("parakeet-v3", "int8")
|
||||
assert result == "nemo-parakeet-tdt-0.6b-v3"
|
||||
|
||||
def test_stores_compute_type(self):
|
||||
backend = OnnxAsrBackend()
|
||||
backend.ensure_model_available("gigaam-v3", "float32")
|
||||
assert backend._resolved_model_id == "gigaam-v3-ctc"
|
||||
assert backend.actual_compute_type == "float32"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"gigaam-multilingual-ctc",
|
||||
"gigaam-v3-e2e-ctc",
|
||||
"gigaam-v3-e2e-rnnt",
|
||||
],
|
||||
)
|
||||
def test_explicit_unavailable_compute_type_is_rejected(self, model_name):
|
||||
backend = OnnxAsrBackend(compute_type_explicit=True)
|
||||
|
||||
with pytest.raises(ValueError, match="недоступна с compute_type='fp16'"):
|
||||
backend.ensure_model_available(model_name, "fp16")
|
||||
|
||||
def test_implicit_unavailable_compute_type_falls_back_and_reports(
|
||||
self, monkeypatch
|
||||
):
|
||||
quantizations = []
|
||||
statuses = []
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
def fake_load_model(*, model, quantization):
|
||||
quantizations.append(quantization)
|
||||
return FakeAsrAdapter()
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model: None)
|
||||
|
||||
backend = OnnxAsrBackend(compute_type_explicit=False)
|
||||
model_id = backend.ensure_model_available(
|
||||
"gigaam-v3-e2e-ctc",
|
||||
"fp16",
|
||||
on_status=statuses.append,
|
||||
)
|
||||
backend.create_model(model_id, "onnx", "fp16")
|
||||
|
||||
assert backend.actual_compute_type == "int8"
|
||||
assert quantizations == ["int8"]
|
||||
assert statuses == [
|
||||
"Модель gigaam-v3-e2e-ctc недоступна с compute_type=fp16; использую int8."
|
||||
]
|
||||
|
||||
|
||||
class TestCreateModel:
|
||||
def test_calls_load_model_with_correct_args(self, monkeypatch):
|
||||
"""Verify create_model passes correct args to onnx_asr.load_model."""
|
||||
calls = []
|
||||
|
||||
def fake_load_model(model=None, path=None, quantization=None,
|
||||
**kwargs):
|
||||
calls.append({
|
||||
"model": model, "path": path, "quantization": quantization,
|
||||
})
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.actual_compute_type = "int8"
|
||||
model = backend.create_model("gigaam-v3-ctc", "onnx", "int8")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["quantization"] == "int8"
|
||||
assert model is not None
|
||||
|
||||
def test_loads_silero_vad(self, monkeypatch):
|
||||
"""Verify Silero VAD is loaded and attached to model."""
|
||||
vad_calls = []
|
||||
|
||||
def fake_load_vad(model, **kwargs):
|
||||
vad_calls.append(model)
|
||||
return "fake_vad"
|
||||
|
||||
def fake_load_model(**kwargs):
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
self._vad = vad
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", fake_load_vad)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.create_model("gigaam-v3-ctc", "onnx", "int8")
|
||||
|
||||
assert vad_calls == ["silero"]
|
||||
|
||||
def test_fp16_compute_type(self, monkeypatch):
|
||||
"""Verify fp16 compute_type is passed through."""
|
||||
calls = []
|
||||
|
||||
def fake_load_model(model=None, quantization=None, **kwargs):
|
||||
calls.append(quantization)
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.create_model("parakeet-v3", "onnx", "fp16")
|
||||
|
||||
assert calls == ["fp16"]
|
||||
|
||||
def test_float32_maps_to_none(self, monkeypatch):
|
||||
"""compute_type='float32' маппится в quantization=None (unquantized).
|
||||
|
||||
onnx-asr использует quantization как суффикс файла; для float32 нужен None,
|
||||
строка "float32" приведёт к попытке загрузить несуществующий файл.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
def fake_load_model(model=None, quantization="MISSING", **kwargs):
|
||||
calls.append(quantization)
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.create_model("gigaam-v3-ctc", "onnx", "float32")
|
||||
|
||||
assert calls == [None]
|
||||
|
||||
def test_fp32_maps_to_none(self, monkeypatch):
|
||||
"""compute_type='fp32' тоже маппится в quantization=None."""
|
||||
calls = []
|
||||
|
||||
def fake_load_model(model=None, quantization="MISSING", **kwargs):
|
||||
calls.append(quantization)
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.create_model("gigaam-v3-ctc", "onnx", "fp32")
|
||||
|
||||
assert calls == [None]
|
||||
|
||||
def test_float16_alias_maps_to_fp16(self, monkeypatch):
|
||||
"""compute_type='float16' (CUDA-naming) маппится в onnx-asr 'fp16'."""
|
||||
calls = []
|
||||
|
||||
def fake_load_model(model=None, quantization=None, **kwargs):
|
||||
calls.append(quantization)
|
||||
return FakeAsrAdapter()
|
||||
|
||||
class FakeAsrAdapter:
|
||||
def with_vad(self, vad):
|
||||
return self
|
||||
|
||||
monkeypatch.setattr("onnx_asr.load_model", fake_load_model)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.create_model("gigaam-v3-ctc", "onnx", "float16")
|
||||
|
||||
assert calls == ["fp16"]
|
||||
|
||||
def test_unknown_compute_type_raises(self, monkeypatch):
|
||||
"""Неподдерживаемый compute_type → ValueError, не silent fallback."""
|
||||
monkeypatch.setattr("onnx_asr.load_model", lambda **kw: None)
|
||||
monkeypatch.setattr("onnx_asr.load_vad", lambda model, **kw: None)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
with pytest.raises(ValueError, match="Неподдерживаемый compute_type"):
|
||||
backend.create_model("gigaam-v3-ctc", "onnx", "int8_float32")
|
||||
|
||||
|
||||
class TestTranscribe:
|
||||
def test_transcribe_collects_segments(self, monkeypatch, tmp_path):
|
||||
"""Verify transcribe maps VAD segments to project Segments."""
|
||||
wav_file = tmp_path / "test.wav"
|
||||
wav_file.write_bytes(b"fake audio")
|
||||
|
||||
audio_samples = [0.0] * 16000 # 1 second of silence
|
||||
|
||||
def fake_decode_audio(path, sampling_rate=16000):
|
||||
import numpy as np
|
||||
return np.array(audio_samples, dtype=np.float32)
|
||||
|
||||
class FakeModel:
|
||||
def recognize(self, waveform, sample_rate, language=None):
|
||||
yield FakeVadSegment(0.0, 1.0, "hello")
|
||||
yield FakeVadSegment(1.0, 2.5, "world")
|
||||
|
||||
monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.actual_compute_type = "int8"
|
||||
result = backend.transcribe(
|
||||
FakeModel(), wav_file, language=None,
|
||||
)
|
||||
|
||||
assert isinstance(result, TranscribeResult)
|
||||
assert len(result.segments) == 2
|
||||
assert result.segments[0] == Segment(start=0.0, end=1.0, text="hello")
|
||||
assert result.segments[1] == Segment(start=1.0, end=2.5, text="world")
|
||||
assert result.duration == 1.0 # 16000 samples / 16000 Hz
|
||||
|
||||
def test_transcribe_calls_on_segment(self, monkeypatch, tmp_path):
|
||||
"""Verify on_segment callback is invoked per segment."""
|
||||
wav_file = tmp_path / "test.wav"
|
||||
wav_file.write_bytes(b"fake audio")
|
||||
|
||||
def fake_decode_audio(path, sampling_rate=16000):
|
||||
import numpy as np
|
||||
return np.array([0.0] * 16000, dtype=np.float32)
|
||||
|
||||
segments_captured = []
|
||||
|
||||
class FakeModel:
|
||||
def recognize(self, waveform, sample_rate, language=None):
|
||||
yield FakeVadSegment(0.0, 2.0, "one")
|
||||
yield FakeVadSegment(2.0, 4.0, "two")
|
||||
|
||||
monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.transcribe(
|
||||
FakeModel(), wav_file, language=None,
|
||||
on_segment=lambda s: segments_captured.append(s),
|
||||
)
|
||||
|
||||
assert len(segments_captured) == 2
|
||||
assert segments_captured[0].text == "one"
|
||||
assert segments_captured[1].text == "two"
|
||||
|
||||
def test_transcribe_passes_language(self, monkeypatch, tmp_path):
|
||||
"""Verify language is passed to recognize()."""
|
||||
wav_file = tmp_path / "test.wav"
|
||||
wav_file.write_bytes(b"fake audio")
|
||||
|
||||
def fake_decode_audio(path, sampling_rate=16000):
|
||||
import numpy as np
|
||||
return np.array([0.0] * 16000, dtype=np.float32)
|
||||
|
||||
lang_received = []
|
||||
|
||||
class FakeModel:
|
||||
def recognize(self, waveform, sample_rate, language=None):
|
||||
lang_received.append(language)
|
||||
yield FakeVadSegment(0.0, 1.0, "text")
|
||||
|
||||
monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
backend.transcribe(FakeModel(), wav_file, language="ru")
|
||||
|
||||
assert lang_received == ["ru"]
|
||||
|
||||
def test_transcribe_empty_audio(self, monkeypatch, tmp_path):
|
||||
"""Verify zero segments for silent audio."""
|
||||
wav_file = tmp_path / "test.wav"
|
||||
wav_file.write_bytes(b"fake audio")
|
||||
|
||||
def fake_decode_audio(path, sampling_rate=16000):
|
||||
import numpy as np
|
||||
return np.array([0.0] * 16000, dtype=np.float32)
|
||||
|
||||
class FakeModel:
|
||||
def recognize(self, waveform, sample_rate, language=None):
|
||||
# No segments yielded
|
||||
if False:
|
||||
yield
|
||||
|
||||
monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio)
|
||||
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend.transcribe(FakeModel(), wav_file, language=None)
|
||||
|
||||
assert len(result.segments) == 0
|
||||
assert result.language == "unknown"
|
||||
assert result.duration == 1.0
|
||||
|
||||
def test_transcribe_skips_zero_length_vad_segments(self, monkeypatch, tmp_path):
|
||||
wav_file = tmp_path / "test.wav"
|
||||
wav_file.write_bytes(b"fake audio")
|
||||
|
||||
def fake_decode_audio(path, sampling_rate=16000):
|
||||
import numpy as np
|
||||
|
||||
return np.array([0.0] * 16000, dtype=np.float32)
|
||||
|
||||
class FakeModel:
|
||||
def recognize(self, waveform, sample_rate, language=None):
|
||||
yield FakeVadSegment(0.5, 0.5, "нулевой")
|
||||
yield FakeVadSegment(0.75, 0.5, "обратный")
|
||||
yield FakeVadSegment(0.5, 1.0, "валидный")
|
||||
|
||||
monkeypatch.setattr("faster_whisper.decode_audio", fake_decode_audio)
|
||||
|
||||
result = OnnxAsrBackend().transcribe(FakeModel(), wav_file, language=None)
|
||||
|
||||
assert result.segments == [Segment(start=0.5, end=1.0, text="валидный")]
|
||||
|
||||
|
||||
class TestBackendRegistration:
|
||||
def test_get_backend_returns_onnx_backend(self):
|
||||
from local_transcriber.backends import get_backend
|
||||
backend = get_backend("onnx")
|
||||
assert isinstance(backend, OnnxAsrBackend)
|
||||
|
||||
def test_get_backend_preserves_implicit_compute_type(self):
|
||||
from local_transcriber.backends import get_backend
|
||||
|
||||
backend = get_backend("onnx", compute_type_explicit=False)
|
||||
backend.ensure_model_available("gigaam-v3-e2e-rnnt", "fp16")
|
||||
|
||||
assert backend.actual_compute_type == "int8"
|
||||
|
||||
|
||||
class TestModelAliases:
|
||||
def test_gigaam_v3_resolves(self):
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend._resolve_model("gigaam-v3")
|
||||
assert result == "gigaam-v3-ctc"
|
||||
|
||||
def test_parakeet_v3_resolves(self):
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend._resolve_model("parakeet-v3")
|
||||
assert result == "nemo-parakeet-tdt-0.6b-v3"
|
||||
|
||||
def test_raw_name_passes_through(self):
|
||||
backend = OnnxAsrBackend()
|
||||
result = backend._resolve_model("nemo-canary-1b-v2")
|
||||
assert result == "nemo-canary-1b-v2"
|
||||
|
||||
def test_unknown_alias_raises(self):
|
||||
backend = OnnxAsrBackend()
|
||||
with pytest.raises(ValueError, match="Неподдерживаемая модель"):
|
||||
backend._resolve_model("nonexistent-model")
|
||||
@@ -0,0 +1,125 @@
|
||||
import pytest
|
||||
|
||||
from local_transcriber.quality import (
|
||||
REPETITION_MIN_LEN,
|
||||
TAIL_GAP_WARN_S,
|
||||
_normalize,
|
||||
find_repetition_blocks,
|
||||
tail_gap,
|
||||
)
|
||||
from local_transcriber.types import Segment, TranscribeResult
|
||||
|
||||
|
||||
def _result(segments, duration):
|
||||
return TranscribeResult(
|
||||
segments=segments,
|
||||
language="ru",
|
||||
language_probability=0.95,
|
||||
duration=duration,
|
||||
device_used="cpu",
|
||||
)
|
||||
|
||||
|
||||
def _segments(texts, start=0.0):
|
||||
return [
|
||||
Segment(start=start + index, end=start + index + 1.0, text=text)
|
||||
for index, text in enumerate(texts)
|
||||
]
|
||||
|
||||
|
||||
def test_tail_gap_returns_positive_gap():
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=42.0)
|
||||
|
||||
assert tail_gap(result) == 32.0
|
||||
|
||||
|
||||
def test_tail_gap_empty_segments_returns_zero():
|
||||
assert tail_gap(_result([], duration=42.0)) == 0.0
|
||||
|
||||
|
||||
def test_tail_gap_negative_gap_returns_zero():
|
||||
result = _result([Segment(0.0, 43.0, "Текст")], duration=42.0)
|
||||
|
||||
assert tail_gap(result) == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gap", [119.99, 120.0])
|
||||
def test_tail_gap_boundary_does_not_warn(gap):
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=10.0 + gap)
|
||||
|
||||
assert tail_gap(result) <= TAIL_GAP_WARN_S
|
||||
|
||||
|
||||
def test_tail_gap_boundary_warns_above_threshold():
|
||||
result = _result([Segment(0.0, 10.0, "Текст")], duration=130.01)
|
||||
|
||||
assert tail_gap(result) > TAIL_GAP_WARN_S
|
||||
|
||||
|
||||
def test_normalize_removes_case_punctuation_and_collapses_spaces():
|
||||
assert _normalize(' «ПРИВЕТ…» — (мир) [тест] <да> ') == "привет мир тест да"
|
||||
assert _normalize("раз–два-три: да; нет!") == "раздватри да нет"
|
||||
|
||||
|
||||
def test_normalize_punctuation_only_returns_empty():
|
||||
assert _normalize('.,!?…:;—–-"\'«»()[]<> ') == ""
|
||||
|
||||
|
||||
def test_find_repetition_blocks_four_long_segments_with_normalization():
|
||||
segments = _segments(["Повтор!", "повтор", "«ПОВТОР»", "повтор…"])
|
||||
|
||||
blocks = find_repetition_blocks(segments)
|
||||
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0].start == 0.0
|
||||
assert blocks[0].end == 4.0
|
||||
assert blocks[0].count == 4
|
||||
assert blocks[0].text == "Повтор!"
|
||||
|
||||
|
||||
def test_find_repetition_blocks_three_long_segments_is_empty():
|
||||
assert find_repetition_blocks(_segments(["Повтор", "повтор", "повтор"])) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_length_boundary():
|
||||
assert find_repetition_blocks(_segments(["пять5"] * 4)) == []
|
||||
|
||||
blocks = find_repetition_blocks(_segments(["шесть6"] * 4))
|
||||
|
||||
assert len("шесть6") == REPETITION_MIN_LEN
|
||||
assert len(blocks) == 1
|
||||
|
||||
|
||||
def test_find_repetition_blocks_short_text_boundary():
|
||||
assert find_repetition_blocks(_segments(["Ага."] * 9)) == []
|
||||
|
||||
blocks = find_repetition_blocks(_segments(["Ага."] * 10))
|
||||
|
||||
assert len(blocks) == 1
|
||||
assert blocks[0].count == 10
|
||||
|
||||
|
||||
def test_find_repetition_blocks_empty_normalized_text_is_ignored():
|
||||
assert find_repetition_blocks(_segments([":", "", " "] * 10)) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_two_separate_runs():
|
||||
segments = [
|
||||
*_segments(["Первый повтор"] * 4),
|
||||
Segment(10.0, 11.0, "Разрыв"),
|
||||
*_segments(["Второй повтор"] * 4, start=20.0),
|
||||
]
|
||||
|
||||
blocks = find_repetition_blocks(segments)
|
||||
|
||||
assert len(blocks) == 2
|
||||
assert blocks[0].text == "Первый повтор"
|
||||
assert blocks[1].text == "Второй повтор"
|
||||
|
||||
|
||||
def test_find_repetition_blocks_empty_list_is_empty():
|
||||
assert find_repetition_blocks([]) == []
|
||||
|
||||
|
||||
def test_find_repetition_blocks_single_segment_is_empty():
|
||||
assert find_repetition_blocks([Segment(0.0, 1.0, "Повтор")]) == []
|
||||
@@ -321,6 +321,34 @@ def test_load_model_returns_backend_and_path(mock_get_backend):
|
||||
assert actual_device == "cpu"
|
||||
|
||||
|
||||
@patch("local_transcriber.transcriber.get_backend")
|
||||
def test_load_model_passes_cpu_threads_to_backend(mock_get_backend):
|
||||
backend = _make_backend(model_path="/mock/model/path")
|
||||
mock_get_backend.return_value = backend
|
||||
|
||||
load_model("tiny", "cpu", "int8", cpu_threads=8)
|
||||
|
||||
assert backend.create_model.call_args.kwargs["cpu_threads"] == 8
|
||||
|
||||
|
||||
@patch("local_transcriber.transcriber.get_backend")
|
||||
def test_load_model_fallback_preserves_cpu_threads(mock_get_backend):
|
||||
cuda_backend = _make_backend(create_model_error=RuntimeError("CUDA out of memory"))
|
||||
cpu_model = MagicMock()
|
||||
cpu_backend = _make_backend(model=cpu_model, model_path="/mock/cpu/model")
|
||||
|
||||
def backend_for_device(device, **kwargs):
|
||||
return cuda_backend if device == "cuda" else cpu_backend
|
||||
|
||||
mock_get_backend.side_effect = backend_for_device
|
||||
|
||||
with pytest.warns(UserWarning, match="Переключение на CPU"):
|
||||
load_model("tiny", "cuda", "int8", cpu_threads=6)
|
||||
|
||||
assert cuda_backend.create_model.call_args.kwargs["cpu_threads"] == 6
|
||||
assert cpu_backend.create_model.call_args.kwargs["cpu_threads"] == 6
|
||||
|
||||
|
||||
# === _transcribe_file() tests ===
|
||||
|
||||
|
||||
@@ -527,6 +555,36 @@ def test_transcribe_file_openvino_gpu_midstream_fallback(mock_get_backend):
|
||||
assert tfr.model_path == "/mock/cpu/model"
|
||||
|
||||
|
||||
@patch("local_transcriber.transcriber.get_backend")
|
||||
def test_transcribe_file_midstream_fallback_preserves_cpu_threads(mock_get_backend):
|
||||
ov_backend = _make_backend(
|
||||
transcribe_error=RuntimeError("OpenVINO inference error"),
|
||||
)
|
||||
cpu_backend = _make_backend(
|
||||
transcribe_result=_make_result(count=2, device_used="cpu"),
|
||||
model_path="/mock/cpu/model",
|
||||
)
|
||||
|
||||
def backend_for_device(device, **kwargs):
|
||||
return ov_backend if device.startswith("openvino") else cpu_backend
|
||||
|
||||
mock_get_backend.side_effect = backend_for_device
|
||||
|
||||
with pytest.warns(UserWarning, match="Переключение на CPU"):
|
||||
_transcribe_file(
|
||||
model=MagicMock(),
|
||||
actual_device="openvino-gpu",
|
||||
backend=ov_backend,
|
||||
model_path="/mock/ov/model",
|
||||
file_path=Path("test.mp3"),
|
||||
model_name="medium",
|
||||
compute_type="fp16",
|
||||
cpu_threads=6,
|
||||
)
|
||||
|
||||
assert cpu_backend.create_model.call_args.kwargs["cpu_threads"] == 6
|
||||
|
||||
|
||||
@patch("local_transcriber.transcriber.get_backend")
|
||||
def test_openvino_gpu_strict_device_no_fallback(mock_get_backend):
|
||||
"""strict_device=True + OpenVINO GPU ошибка → raise."""
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.11'",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
@@ -20,9 +16,7 @@ name = "anyio"
|
||||
version = "4.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||
wheels = [
|
||||
@@ -35,13 +29,6 @@ version = "17.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/eb/abca886df3a091bc406feb5ff71b4c4f426beaae6b71b9697264ce8c7211/av-17.0.0.tar.gz", hash = "sha256:c53685df73775a8763c375c7b2d62a6cb149d992a26a4b098204da42ade8c3df", size = 4410769, upload-time = "2026-03-14T14:38:45.868Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/4d/ea1ac272eeea83014daca1783679a9e9f894e1e68e5eb4f717dd8813da2a/av-17.0.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:4b21bcff4144acae658c0efb011fa8668c7a9638384f3ae7f5add33f35b907c6", size = 23407827, upload-time = "2026-03-14T14:37:47.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/1a/e433766470c57c9c1c8558021de4d2466b3403ed629e48722d39d12baa6c/av-17.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:17cd518fc88dc449ce9dcfd0b40e9b3530266927375a743efc80d510adfb188b", size = 18829899, upload-time = "2026-03-14T14:37:50.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/25/95ad714f950c188495ffbfef235d06a332123d6f266026a534801ffc2171/av-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9a8b7b63a92d8dc7cbe5000546e4684176124ddd49fdd9c12570e3aa6dadf11a", size = 35348062, upload-time = "2026-03-14T14:37:52.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/db/7f3f9e92f2ac8dba639ab01d69a33b723aa16b5e3e612dbfe667fbc02dcd/av-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8706ce9b5d8d087d093b46a9781e7532c4a9e13874bca1da468be78efc56cecc", size = 37684503, upload-time = "2026-03-14T14:37:55.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/53/3b356b14ba72354688c8d9777cf67b707769b6e14b63aaeb0cddeeac8d32/av-17.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a074835ce807434451086993fedfb3b223dacedb2119ab9d7a72480f2d77f32", size = 36547601, upload-time = "2026-03-14T14:37:58.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8d/f489cd6f9fe9c8b38dca00ecb39dc38836761767a4ec07dd95e62e124ac3/av-17.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8ef8e8f1a0cbb2e0ad49266015e2277801a916e2186ac9451b493ff6dfdec27", size = 38815129, upload-time = "2026-03-14T14:38:01.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/bd/e42536234e37caffd1a054de1a0e6abca226c5686e9672726a8d95511422/av-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a795e153ff31a6430e974b4e6ad0d0fab695b78e3f17812293a0a34cd03ee6a9", size = 28984602, upload-time = "2026-03-14T14:38:03.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/fb/55e3b5b5d1fc61466292f26fbcbabafa2642f378dc48875f8f554591e1a4/av-17.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ed4013fac77c309a4a68141dcf6148f1821bb1073a36d4289379762a6372f711", size = 23238424, upload-time = "2026-03-14T14:38:05.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/03/9ace1acc08bc9ae38c14bf3a4b1360e995e4d999d1d33c2cbd7c9e77582a/av-17.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:e44b6c83e9f3be9f79ee87d0b77a27cea9a9cd67bd630362c86b7e56a748dfbb", size = 18709043, upload-time = "2026-03-14T14:38:08.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c0/637721f3cd5bb8bd16105a1a08efd781fc12f449931bdb3a4d0cfd63fa55/av-17.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b440da6ac47da0629d509316f24bcd858f33158dbdd0f1b7293d71e99beb26de", size = 34018780, upload-time = "2026-03-14T14:38:10.45Z" },
|
||||
@@ -95,27 +82,11 @@ name = "ctranslate2"
|
||||
version = "4.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e0/b69c40c3d739b213a78d327071240590792071b4f890e34088b03b95bb1e/ctranslate2-4.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9017a355dd7c6d29dc3bca6e9fc74827306c61b702c66bb1f6b939655e7de3fa", size = 1255773, upload-time = "2026-02-04T06:11:04.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/29/e5c2fc1253e3fb9b2c86997f36524bba182a8ed77fb4f8fe8444a5649191/ctranslate2-4.7.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:6abcd0552285e7173475836f9d133e04dfc3e42ca8e6930f65eaa4b8b13a47fa", size = 11914945, upload-time = "2026-02-04T06:11:06.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/25/e7fe847d3f02c84d2e9c5e8312434fbeab5af3d8916b6c8e2bdbe860d052/ctranslate2-4.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8492cba605319e0d7f2760180957d5a2a435dfdebcef1a75d2ade740e6b9fb0b", size = 16547973, upload-time = "2026-02-04T06:11:09.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/75/074ed22bc340c2e26c09af6bf85859b586516e4e2d753b20189936d0dcf7/ctranslate2-4.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:688bd82482b5d057eff5bc1e727f11bb9a1277b7e4fce8ab01fd3bb70e69294b", size = 38636471, upload-time = "2026-02-04T06:11:12.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/b6/9baf8a565f6dcdbfbc9cfd179dd6214529838cda4e91e89b616045a670f0/ctranslate2-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3b39a5f4e3c87ac91976996458a64ba08a7cbf974dc0be4e6df83a9e040d4bd2", size = 18842389, upload-time = "2026-02-04T06:11:15.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/25/41920ccee68e91cb6fa0fc9e8078ab2b7839f2c668f750dc123144cb7c6e/ctranslate2-4.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f74200bab9996b14a57cf6f7cb27d0921ceedc4acc1e905598e3e85b4d75b1ec", size = 1256943, upload-time = "2026-02-04T06:11:17.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/22/bc81fcc9f10ba4da3ffd1a9adec15cfb73cb700b3bbe69c6c8b55d333316/ctranslate2-4.7.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:59b427eb3ac999a746315b03a63942fddd351f511db82ba1a66880d4dea98e25", size = 11916445, upload-time = "2026-02-04T06:11:19.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/a7/494a66bb02c7926331cadfff51d5ce81f5abfb1e8d05d7f2459082f31b48/ctranslate2-4.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95f0c1051c180669d2a83a44b44b518b2d1683de125f623bbc81ad5dd6f6141c", size = 16696997, upload-time = "2026-02-04T06:11:22.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/4e/b48f79fd36e5d3c7e12db383aa49814c340921a618ef7364bd0ced670644/ctranslate2-4.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed92d9ab0ac6bc7005942be83d68714c80adb0897ab17f98157294ee0374347", size = 38836379, upload-time = "2026-02-04T06:11:26.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/8c01ac52e1f26fc4dbe985a35222ae7cd365bbf7ee5db5fd5545d8926f91/ctranslate2-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:67d9ad9b69933fbfeee7dcec899b2cd9341d5dca4fdfb53e8ba8c109dc332ee1", size = 18843315, upload-time = "2026-02-04T06:11:29.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/0f/581de94b64c5f2327a736270bc7e7a5f8fe5cf1ed56a2203b52de4d8986a/ctranslate2-4.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c0cbd46a23b8dc37ccdbd9b447cb5f7fadc361c90e9df17d82ca84b1f019986", size = 1257089, upload-time = "2026-02-04T06:11:32.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e9/d55b0e436362f9fe26bd98fefd2dd5d81926121f1d7f799c805e6035bb26/ctranslate2-4.7.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5b141ddad1da5f84cf3c2a569a56227a37de649a555d376cbd9b80e8f0373dd8", size = 11918502, upload-time = "2026-02-04T06:11:33.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ce/9f29f0b0bb4280c2ebafb3ddb6cdff8ef1c2e185ee020c0ec0ecba7dc934/ctranslate2-4.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00a62544db4a3caaa58a3c50d39b25613c042b430053ae32384d94eb1d40990", size = 16859601, upload-time = "2026-02-04T06:11:36.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/86/428d270fd72117d19fb48ed3211aa8a3c8bd7577373252962cb634e0fd01/ctranslate2-4.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:722b93a89647974cbd182b4c7f87fefc7794fff7fc9cbd0303b6447905cc157e", size = 38995338, upload-time = "2026-02-04T06:11:42.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/f4/d23dbfb9c62cb642c114a30f05d753ba61d6ffbfd8a3a4012fe85a073bcb/ctranslate2-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f734dc3757118094663bdaaf713f5090c55c1927fb330a76bb8b84173940e8", size = 18844949, upload-time = "2026-02-04T06:11:45.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/6d/eb49ba05db286b4ea9d5d3fcf5f5cd0a9a5e218d46349618d5041001e303/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b2abf2929756e3ec6246057b56df379995661560a2d776af05f9d97f63afcf5", size = 1256960, upload-time = "2026-02-04T06:11:47.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/5a/b9cce7b00d89fc6fdeaf27587aa52d0597b465058563e93ff50910553bdd/ctranslate2-4.7.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:857ef3959d6b1c40dc227c715a36db33db2d097164996d6c75b6db8e30828f52", size = 11918645, upload-time = "2026-02-04T06:11:49.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/03/c0db0a5276599fb44ceafa2f2cb1afd5628808ec406fe036060a39693680/ctranslate2-4.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:393a9e7e989034660526a2c0e8bb65d1924f43d9a5c77d336494a353d16ba2a4", size = 16860452, upload-time = "2026-02-04T06:11:52.276Z" },
|
||||
@@ -133,18 +104,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/5c/9fa0ad6462b62efd0fb5ac1100eee47bc96ecc198ff4e237c731e5473616/ctranslate2-4.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dfb7657bdb7b8211c8f9ecb6f3b70bc0db0e0384d01a8b1808cb66fe7199df59", size = 19123451, upload-time = "2026-02-04T06:12:24.115Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faster-whisper"
|
||||
version = "1.2.1"
|
||||
@@ -301,10 +260,11 @@ source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "faster-whisper" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "onnx-asr", extra = ["cpu", "hub"] },
|
||||
{ name = "onnxruntime" },
|
||||
{ name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" },
|
||||
{ name = "rich" },
|
||||
{ name = "socksio" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
@@ -315,13 +275,14 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "faster-whisper", specifier = ">=1.2.1" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4" },
|
||||
{ name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2025.0" },
|
||||
{ name = "rich" },
|
||||
{ name = "socksio", specifier = ">=1.0.0" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" },
|
||||
{ name = "typer" },
|
||||
{ name = "faster-whisper", specifier = ">=1.2.1,<2" },
|
||||
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4,<13" },
|
||||
{ name = "onnx-asr", extras = ["cpu", "hub"], specifier = ">=0.12,<0.13" },
|
||||
{ name = "onnxruntime", specifier = ">=1.28,<2" },
|
||||
{ name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2026.0.0.0,<2026.1" },
|
||||
{ name = "rich", specifier = ">=14.3.3,<15" },
|
||||
{ name = "socksio", specifier = ">=1.0.0,<2" },
|
||||
{ name = "typer", specifier = ">=0.24.1,<1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
@@ -348,111 +309,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.11'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
|
||||
@@ -495,13 +357,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -512,43 +367,51 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/3c/aa88abe01f3be3d1f8f787d1d33dc83e76fec05945f9a28fbb41cfb99cd5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:453611eb21a7c1f2c2156ed9f3a45b691deda0440ec550860290dc901af5b4c2", size = 581242350, upload-time = "2025-06-05T20:04:51.979Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnx-asr"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/11/136f556cda02ecc172674dcea79232e9e84e74b951bbcd2e2722117f633b/onnx_asr-0.12.0.tar.gz", hash = "sha256:c1fcacddbced392f9f769ed5c6223f4ce739a6e4549346de33b9b520d4ead713", size = 45991, upload-time = "2026-07-15T00:09:58.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/60/2fa469a2ee674c35ab48821a1039762ae7b9d0b88188ac1012e779477f76/onnx_asr-0.12.0-py3-none-any.whl", hash = "sha256:5e7ceca454609819ea7833f61e2302e0c8f6ece4f8a78b66c5daba53cb51de4a", size = 3980006, upload-time = "2026-07-15T00:09:57.322Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cpu = [
|
||||
{ name = "onnxruntime" },
|
||||
]
|
||||
hub = [
|
||||
{ name = "huggingface-hub" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime"
|
||||
version = "1.24.3"
|
||||
version = "1.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "flatbuffers" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sympy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a4/b3240ea84b92a3efb83d49cc16c04a17ade1ab47a6a95c4866d15bf0ac35/onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8", size = 17344149, upload-time = "2026-03-05T16:35:13.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/4a/4b56757e51a56265e8c56764d9c36d7b435045e05e3b8a38bedfc5aedba3/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4", size = 15151571, upload-time = "2026-03-05T16:35:05.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/14/c6fb84980cec8f682a523fcac7c2bdd6b311e7f342c61ce48d3a9cb87fc6/onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400", size = 17238951, upload-time = "2026-03-05T17:18:12.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/14/447e1400165aca8caf35dabd46540eb943c92f3065927bb4d9bcbc91e221/onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b", size = 12903820, upload-time = "2026-03-05T17:18:57.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ec/6b2fa5702e4bbba7339ca5787a9d056fc564a16079f8833cc6ba4798da1c/onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53", size = 12594089, upload-time = "2026-03-05T17:18:47.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/dc/cd06cba3ddad92ceb17b914a8e8d49836c79e38936e26bde6e368b62c1fe/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36", size = 15162789, upload-time = "2026-03-05T16:35:08.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d6/413e98ab666c6fb9e8be7d1c6eb3bd403b0bea1b8d42db066dab98c7df07/onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa", size = 17240738, upload-time = "2026-03-05T17:18:15.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759, upload-time = "2026-07-25T01:21:53.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339, upload-time = "2026-07-25T01:21:43.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329, upload-time = "2026-07-25T01:22:04.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033, upload-time = "2026-07-25T01:22:29.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175, upload-time = "2026-07-25T01:22:19.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307, upload-time = "2026-07-25T01:21:45.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954, upload-time = "2026-07-25T01:22:06.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748, upload-time = "2026-07-25T01:21:56.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950, upload-time = "2026-07-25T01:21:48.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924, upload-time = "2026-07-25T01:22:09.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738, upload-time = "2026-07-25T01:22:31.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117, upload-time = "2026-07-25T01:22:22.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518, upload-time = "2026-07-25T01:21:51.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976, upload-time = "2026-07-25T01:22:12.474Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -556,17 +419,10 @@ name = "openvino"
|
||||
version = "2026.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "numpy" },
|
||||
{ name = "openvino-telemetry" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/7d/d7e428f469021c1ce309d1c000a6f186be414c08655a55250229b6b789c7/openvino-2026.0.0-20965-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c6a7d9bf433888b85c916a37b0c7c9b9747158760f6fc8a852299cd231fd20a", size = 53425782, upload-time = "2026-02-23T16:10:07.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b4/95a9c1de9259e83ac420f500278e30ccd52cd9979bb5473b27ec762f8a6e/openvino-2026.0.0-20965-cp310-cp310-win_amd64.whl", hash = "sha256:f7b28ad59dc2017ef7d2622628f8b31d052caa1565ce64d4f39937a2db044dc6", size = 69154088, upload-time = "2026-02-23T16:10:13.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/f1/21ebfb8f496fe1732de217c6cf676299c884b2e027217d1dec08a8030d65/openvino-2026.0.0-20965-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ba999f7fea57958a533a715afa976704ea6acf39dfd271a89ce78594e7aaa488", size = 53433492, upload-time = "2026-02-23T16:10:20.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e2/712d8ad00e577fbf1f0081132f199a1a6885bfafd6739cc18c560ec7eee2/openvino-2026.0.0-20965-cp311-cp311-win_amd64.whl", hash = "sha256:eee912bf4370a4f454c9bcf2cfe31eda85ba6f324c85137d225edffcbc50c456", size = 69161445, upload-time = "2026-02-23T16:10:26.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f8/bd25df6ebd42b2425a2d5fe504389e941adbee41356078a97357469d18cb/openvino-2026.0.0-20965-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ad9cbf77ed954b64b056cb91ebb95e5691da1c86238e610b565e5347e73a5c5", size = 53443036, upload-time = "2026-02-23T16:10:32.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ed/dd3886bdfd822bb1671c0f892a007a9e27a7fa1f97ea1a7c0c8e901a137c/openvino-2026.0.0-20965-cp312-cp312-win_amd64.whl", hash = "sha256:10af8a90373547c5dec2b53c60dac38a7a29df8662fe097a3c6c67750e7c88c6", size = 69160616, upload-time = "2026-02-23T16:10:39.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/3f/95b15760d7ae4b7dfefdbadeccae9604985d4335b221350e1bb04701a158/openvino-2026.0.0-20965-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:452902e4b8fba526e8740622cd5638c457d3ae44da7b39e6d4ef7919be0e95d4", size = 53444542, upload-time = "2026-02-23T16:10:46.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/26/82d326f3769c9e5c17d34ef10dd0aff4a94a3b550e1d2efe977e5754b84e/openvino-2026.0.0-20965-cp313-cp313-win_amd64.whl", hash = "sha256:14069e5af2ac8a77f6ea55d7020d34cc7d3538d49ebda91a18c00d4da830ab58", size = 69160606, upload-time = "2026-02-23T16:10:52.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/8e/52df01e8687f4711259729433226219c6839a0495988ddeb7e27af59aba8/openvino-2026.0.0-20965-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:79ce4b5d7f3a7f84de878752d97620b84e0c4a8ee550fb3bca9fc36fe5669450", size = 53449059, upload-time = "2026-02-23T16:10:59.54Z" },
|
||||
@@ -583,12 +439,6 @@ dependencies = [
|
||||
{ name = "openvino-tokenizers" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/23/39f4d63f1c301f2250ec903345f2775e0d18df5b637d8324501c7874750f/openvino_genai-2026.0.0.0-2050-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c583262256ec5864bc4fed79932d2774dbf2716acbebce36cc40b6b8c26c9509", size = 4920931, upload-time = "2026-02-23T16:17:15.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/d0/4160cee845e09789110a55b0b90d49dd457d5322ea03de11dd69014524ac/openvino_genai-2026.0.0.0-2050-cp310-cp310-win_amd64.whl", hash = "sha256:adb3a22584231463e269927f872797ff4a1176a6b2194516c5ed7ffb31218126", size = 2893230, upload-time = "2026-02-23T16:17:18.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/16/a4b6647ee0e77b136cc465f5222d3dc586c2cc8626d6a950509945e2317e/openvino_genai-2026.0.0.0-2050-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:deeb2d2e6d85af7e04e6d975c00b65e414bf9886baca2ac5c7ed2963d529ec4a", size = 4922353, upload-time = "2026-02-23T16:17:21.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/9b/4eed9a6543534857127b1ed2923c705025234a5c41286dd4dd5d7de95a27/openvino_genai-2026.0.0.0-2050-cp311-cp311-win_amd64.whl", hash = "sha256:b7e4c895da27b00dc6d87153b451b953f5803554e4409b2f5e8def714c71e06f", size = 2894286, upload-time = "2026-02-23T16:17:24.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/2d/963844c91c12c95249cd42056ffb1a94e62d209fab12a3e916e32f1fa9fb/openvino_genai-2026.0.0.0-2050-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d68c6dd226e39f6154d946410929ea1be1608ad769b5d2bf2aebe6ffd5d02ce4", size = 4930395, upload-time = "2026-02-23T16:17:28.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/0c/ed7e3d54c0ba171b23e91231adaf5b5c3f5343650959e144b5de8f9fcf2b/openvino_genai-2026.0.0.0-2050-cp312-cp312-win_amd64.whl", hash = "sha256:5cfd7c14a697c36af854743f86797b845fe423d0fab9a17d0c6fa5cd5b0cc606", size = 2900307, upload-time = "2026-02-23T16:17:30.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/98/fe5716b9dc2f29286b68b98a4c67bc33003e3b02cf4d92236345a2d7280e/openvino_genai-2026.0.0.0-2050-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:08f73852a455d63453e7f74f22b0aebd0c3579f8cb89133e66e438c09df26b71", size = 4930444, upload-time = "2026-02-23T16:17:33.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/80/bc87545dfbbd7f4a3bd1667a4c9b5f1d8e1b476fec34225692bca6a86590/openvino_genai-2026.0.0.0-2050-cp313-cp313-win_amd64.whl", hash = "sha256:ada030b34a9e2fe0c6c406c2af2ce7b6e8cb16654329a469055fdac8a3eba19d", size = 2900293, upload-time = "2026-02-23T16:17:37.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/09/18e678f2d0ac4ee993244d3cf0e6a80ec7013add2972153ad355d889d8cc/openvino_genai-2026.0.0.0-2050-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:77b73d1947d7f62a672857ef35d187074c6918de1db1d3f981452c2d67511c13", size = 4931196, upload-time = "2026-02-23T16:17:40.423Z" },
|
||||
@@ -666,12 +516,10 @@ version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
@@ -684,34 +532,6 @@ version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
@@ -782,18 +602,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.22.2"
|
||||
@@ -818,64 +626,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user