Author SHA1 Message Date
Dmitriy Dementiev 7f58a56913 feat(onnx): изменена модель по умолчанию на GigaAM RNN-T
- Зачем:
  - пользователям нужен читаемый транскрипт без обязательной LLM-обработки.
- Что:
  - моделью ONNX по умолчанию выбрана `gigaam-v3-e2e-rnnt`.
  - сохранён явный профиль `gigaam-v3` для более точного сырого текста.
  - обновлены тест, README, спецификация, ADR и benchmark.
- Проверка:
  - `uv run pytest -q` — 226 passed, 1 skipped.
  - `git diff --check`.
2026-08-11 16:47:09 +03:00
Dmitriy Dementiev a06dbe63a3 docs(benchmark): сравнены GigaAM Large и Parakeet
Зачем:
- зафиксировать сравнение моделей на одном и том же массиве записей
- сохранить воспроизводимый набор файлов для будущих прогонов

Что:
- добавлены метрики и качественный вывод по Parakeet v3
- записаны точные имена, размеры и SHA-256 видео и эталонов
- обновлена рекомендация в README

Проверка:
- git diff --check
2026-08-11 16:28:45 +03:00
Dmitriy Dementiev 8f85930616 feat(onnx): добавлена модель GigaAM Multilingual Large
- Зачем:
  - расширенный benchmark показал устойчивое улучшение multilingual large на трёх реальных записях.
- Что:
  - добавлен alias gigaam-multilingual-large-ctc с квантизациями int8 и float32.
  - обновлены README, спецификация, backlog и сравнительный benchmark.
- Проверка:
  - uv run pytest -q: 225 passed, 1 skipped.
  - uvx ruff check src/local_transcriber/backends/onnx_asr.py tests/test_onnx_asr.py.
2026-08-11 16:12:36 +03:00
Dmitriy Dementiev f0d7d06f93 docs(benchmark): расширено сравнение моделей на русских встречах
- Зачем:
  - выбор CPU-модели должен опираться на несколько реальных записей, а не на единичный прогон.
- Что:
  - добавлены скорость, WER и качественное сравнение пяти моделей на трёх встречах.
  - обновлены рекомендации README и открытый вопрос ADR-006 для E2E RNN-T.
- Проверка:
  - git diff --cached --check.
2026-08-11 15:45:28 +03:00
Dmitriy Dementiev 7392d72bca fix(onnx): исключены некорректные VAD-сегменты
- Зачем:
  - закрыты замечания независимого review по воспроизводимости и VAD-приёмке.
- Что:
  - рабочая версия Python точно зафиксирована на 3.13.13.
  - нулевые и обратные VAD-сегменты исключены из результата.
  - в README записаны наблюдаемые скорости новых моделей с указанием CPU.
- Проверка:
  - uv run pytest -q: 224 passed, 1 skipped.
  - ограниченная проверка Ruff и uv lock --check завершены успешно.
2026-08-11 13:21:45 +03:00
Dmitriy Dementiev 0af2dbdd17 feat(onnx): добавлен каталог моделей GigaAM
- Зачем:
  - добавлена локальная транскрипция смешанной речи и русского текста с пунктуацией.
- Что:
  - onnx-asr обновлён до 0.12 и зарегистрированы три модели GigaAM.
  - выбор compute_type учитывает опубликованные квантизации и явность настройки.
  - обновлены тесты, README и требования PRD.
- Проверка:
  - uv run pytest -q: 223 passed, 1 skipped.
  - выполнены smoke- и полные прогоны четырёх GigaAM-моделей.
2026-08-11 13:11:18 +03:00
Dmitriy Dementiev 3c519c6860 chore(runtime): обновлено окружение до Python 3.13
- Зачем:
  - подготовлена совместимая основа для ONNX Runtime 1.28 и onnx-asr 0.12.
- Что:
  - минимальная версия Python поднята до 3.13 и добавлен файл версии.
  - tomli заменён стандартным tomllib.
  - прямые runtime-зависимости ограничены сверху, ONNX Runtime объявлен явно.
- Проверка:
  - uv lock --check.
  - uv run pytest -q: 218 passed, 1 skipped.
  - выполнены реальные прогоны ONNX, OpenVINO CPU и FasterWhisper CPU.
2026-08-11 12:47:34 +03:00
Dmitriy DementievandClaude Opus 5 80b864077a docs(specs): спека onnx-каталога, Python 3.13 и границ версий
- Зачем:
  - смешанная русско-английская речь и отсутствие пунктуации у gigaam-v3-ctc
    закрываются моделями из onnx-asr 0.12, но обновление заблокировано пином.
- Что:
  - добавлена спека: три модели в каталог, Python 3.13 и ORT 1.28, верхние
    границы мажорных версий, ловушки и границы ручной приёмки.
  - в backlog добавлены ссылка на спеку из пункта про CPU-дефолт и новое
    направление «профили намерения вместо выбора модели».
- Проверка:
  - задача трекера #1 с порядком работ и критериями приёмки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:39:28 +03:00
Dmitriy DementievandClaude Opus 5 579cb87bc0 docs(context): убраны термины процедуры сравнительной оценки
- Зачем:
  - формальной процедуры оценки моделей не будет: проект домашний, CI нет,
    репрезентативные записи приватны, а тяжёлый пайплайн повышает шанс
    забросить работу.
- Что:
  - удалены «Кандидатная модель», «Сравнительная оценка», «Контрольная
    модель», «Интеграционный smoke-тест» и «Локальный оценочный корпус».
  - оставлены «Движок распознавания», «Поддерживаемая модель» и «Модель по
    умолчанию» — они держат различие между поддержкой и рекомендацией.
- Проверка:
  - git diff HEAD~1 -- CONTEXT.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 11:37:30 +03:00
Dmitriy Dementiev b302e3dbb2 docs(agents): добавлена конфигурация инженерных навыков
- Зачем:
  - инженерным навыкам нужна единая конфигурация трекера задач и доменных документов.
- Что:
  - задокументированы Gitea workflow через tea и стандартные triage-метки.
  - добавлены доменный словарь и правила работы с ADR, research и specs.
- Проверка:
  - git diff --cached --check.
  - tea --version: 0.15.1.
2026-08-11 11:34:18 +03:00
Dmitriy Dementiev f8c6a4fc7e docs(asr): зафиксировано исследование обновлений и CPU-дефолта
- Зачем:
  - нужен проверяемый путь от обновления ASR-стека к выбору безопасного CPU-дефолта.
- Что:
  - добавлено исследование актуальных движков и моделей с корректным именем файла.
  - зафиксированы дефект OpenVINO, кандидаты GigaAM и матрица бенчмарка на Intel Core i5.
- Проверка:
  - выполнена проверка git diff --cached --check.
2026-08-10 19:07:59 +03:00
Dmitriy DementievandClaude Fable 5 5a7fc0d613 docs(backlog): направления по структуре транскрипта из внешнего ревью
- Зачем:
  - внешнее сравнение с облачным Hypescribe показало: главный разрыв локальной транскрипции — не качество распознавания, а структура (спикеры 2/10 против 8/10); без диаризации MoM не собрать.
- Что:
  - новый раздел «Структура транскрипта»: диаризация как опциональный пост-процессинг (sherpa-onnx vs pyannote, нужен ADR), ручка нарезки абзацев в formatter, словарь замен терминов как запасной план после бенчмарка RNN-T.
  - зафиксировано вне ядра CLI: LLM-чистка и сопоставление имён спикеров.
- Проверка:
  - docs-only, тесты не затронуты.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:44:24 +03:00
Dmitriy DementievandClaude Fable 5 b1dfd9dcca feat(quality): предупреждения о возможной потере содержания транскрипта
- Зачем:
  - транскрипция может тихо терять содержание: блоки галлюцинированных повторов и обрыв распознавания до конца файла выглядят как законченный транскрипт (класс ошибок из ADR-006), и читатель об этом не узнаёт.
- Что:
  - новый модуль quality.py: детектор разрыва в хвосте (строго >120 с) и детектор блоков повторяющихся сегментов (серия >=4 для длинного текста, >=10 для короткого вроде «ага»).
  - предупреждения жёлтым в консоли (single и batch) и строками «Внимание» в шапке markdown-транскрипта; _format_duration переименована в публичную format_duration.
  - 27 новых тестов: границы порогов, нормализация текста, шапка formatter, точные строки CLI-сообщений с обрезкой «(+ ещё N)».
- Проверка:
  - uv run pytest — 218 passed, 1 skipped.
  - ручной прогон записи 23:01 (gigaam-v3/onnx) — транскрипт полный, предупреждений нет.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:21:07 +03:00
Dmitriy DementievandClaude Fable 5 650e7580e5 chore(repo): убран посторонний app.py, идея чанкования сохранена в backlog
- Зачем:
  - в дереве с апреля лежал некоммиченный app.py (референсный Parakeet-сервер, не часть CLI) и служебный каталог .qwen/ — мусор в git status.
- Что:
  - удалён app.py; его единственная ценная идея (чанкование длинных файлов по паузам через ffmpeg silencedetect) описана в docs/backlog.md с рабочими константами.
  - .qwen/ добавлен в .gitignore (по аналогии с .codex).
- Проверка:
  - git status — дерево чистое, untracked-файлов нет.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:49:45 +03:00
Dmitriy DementievandClaude Fable 5 df204e5ae4 chore: .scratch/ добавлен в .gitignore
- Зачем:
  - рабочие файлы конвейера (задачи-файлы) не должны попадать в дерево.
- Что:
  - .scratch/ в .gitignore.
- Проверка:
  - git status не показывает .scratch/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:33:02 +03:00
Dmitriy DementievandClaude Fable 5 9307486ceb docs: фича SendTo-меню отражена в README и AGENTS.md
- Зачем:
  - фича --install-menu была описана только в глубине README; верхний
    список возможностей и инструкция для AI-ассистентов о ней не знали.
- Что:
  - пункт «Из проводника Windows» в списке возможностей README со ссылкой
    на раздел; команды раздела приведены к tool-установке (без uv run).
  - строка context_menu.py в схеме архитектуры AGENTS.md.
- Проверка:
  - вычитка README, переход по якорю раздела.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:32:47 +03:00
Dmitriy DementievandClaude Fable 5 f18ad6ea2a feat(cli): добавлен пункт Transcribe в меню «Отправить» проводника
- Зачем:
  - запуск транскрипции правой кнопкой из проводника Windows без
    терминала, для пользователей без прав администратора.
- Что:
  - новый модуль context_menu.py: установка/удаление Transcribe.cmd
    в папке SendTo (OEM-кодировка, CRLF, без реестра).
  - флаги --install-menu / --uninstall-menu в cli.py с ранней
    валидацией до load_config; files стал необязательным аргументом.
  - раздел в README.md: установка, использование, ручное удаление
    через shell:sendto, известные ограничения.
- Проверка:
  - uv run pytest — 191 passed, 1 skipped.
  - uv run transcribe --install-menu и правый клик → Отправить →
    Transcribe на файле с кириллицей в имени.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:29:07 +03:00
ddadmin 966efc3bfe docs: добавлены AGENTS.md и CLAUDE.md для AI-ассистентов 2026-04-26 20:05:12 +03:00
ddadmin e41600510b add .codex to gitignore 2026-04-26 19:54:23 +03:00
ddadminandClaude Opus 4.7 0059e28f77 fix(deps): ограничить onnx-asr верхней границей <0.12.0
onnx-asr — pre-1.0 библиотека (текущая 0.11.x). Без верхней границы любой
minor-апдейт (0.12.x) или major (1.0.0) подгрузится автоматически и может
сломать совместимость API: для pre-1.0 SemVer не гарантирует обратную
совместимость даже на minor-bumps. Уже наблюдали это в первой итерации:
при smoke-тесте обнаружили расхождение API между ожидаемой и реальной
сигнатурой (cpu_preprocessing был removed, start_ts/end_ts → start/end).

Ограничиваем до compatible-release: >=0.11.0,<0.12.0. При появлении 0.12
обновим осознанно после проверки совместимости.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:34:57 +03:00
ddadminandClaude Opus 4.7 1c779be139 Merge branch 'experiment/parakeet-backend-opencode'
Эксперимент с DeepSeek V4 Pro: добавление onnx-asr бэкенда (Parakeet/GigaAM/
FastConformer) с независимой валидацией качества через agent-judge.

Реализация:
- backends/onnx_asr.py — третий pluggable backend через onnxruntime + Silero VAD.
- --device onnx с моделями gigaam-v3 (рекомендуется для русского) и parakeet-v3.
- _normalize_quantization() — маппинг compute_type → onnx-asr quantization
  (int8/fp16 pass-through, float16→fp16, float32/fp32→None, остальные→ValueError).
- DEVICE_DEFAULTS для onnx, _VALID_DEVICES расширен.

Документация:
- ADR-006 — итоги: GigaAM v3 как best-CPU choice для русских встреч, Parakeet
  непригоден для русского (воспроизведены три класса проблем из ADR-005), новый
  класс ошибок Whisper medium на длинных файлах с тихими фрагментами.
- backlog.md — устойчивый список будущих экспериментов (GigaAM v3 RNN-T и др.).

Контекст эксперимента:
- ADR-005 (Opus, master) отклонил Parakeet на двух 15-мин файлах с тихим
  микрофоном. ADR-006 повторяет на 22-81-мин файлах + добавляет GigaAM.
- Реализация прошла три фазы валидации: smoke-test (4 mock-API расхождения
  → fix), agent-judge на 9 транскриптах (подтверждение проблем Parakeet,
  обнаружение Whisper-галлюцинаций), adversarial-review через GPT-5.5
  (compute_type=float32 ломал onnx-загрузку → fix + 4 теста).

Не включено в merge: docs/superpowers/specs/ и plans/ от DeepSeek (stale
assumptions, итоговый канон в ADR-006; полные документы — в истории ветки).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:33:30 +03:00
ddadminandClaude Opus 4.7 9cfa437b33 fix(onnx-asr): корректный маппинг compute_type в onnx-asr quantization
При adversarial review (gpt-5.5) обнаружено: пользователь с config
compute_type = "float32" и --device onnx падал на старте.
HARDCODED_DEFAULTS["compute_type"] = "float32", apply_device_defaults не
заменяет значение если оно есть в config — старая проверка
(compute_type in ("int8", "fp16", "float32")) пропускала "float32"
дальше как onnx_asr quantization, что заставляло искать несуществующий
файл с суффиксом _float32.

- _normalize_quantization() — explicit маппинг в onnx-asr quantization.
- float32/fp32 → None (unquantized loading в onnx-asr — это None, не строка).
- float16 → fp16 (CUDA-naming → onnx-asr-naming).
- int8/fp16 → pass-through.
- Неизвестные compute_type (например, int8_float32 от CTranslate2) → ValueError
  вместо silent fallback на int8 — пользователь раньше получал не ту
  модель без предупреждения.

4 новых теста: float32→None, fp32→None, float16→fp16, unknown→raises.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:06:22 +03:00
ddadminandClaude Opus 4.7 0c3a67c4ea docs: добавлен backlog.md — устойчивый список будущих экспериментов
В проекте не было общего файла для будущих идей: docs/plan.md — историческая
летопись (всё [x]), docs/gpu.md — потенциальные направления привязаны к GPU/CPU
производительности, открытые вопросы из ADR легко теряются среди следующих ADR.

Создан docs/backlog.md как канонический хаб для будущих экспериментов
с обоснованием, цифрами и ссылками на источники. Первые записи:

- GigaAM v3 RNN-T — потенциальная замена CTC-дефолта (WER 2.6% vs 13.2%
  на сложных текстах, e2e_rnnt c пунктуацией, 70:30 vs Whisper-large-v3 LLM-judge)
- Canary 1B — multilingual + пунктуация на CPU
- Whisper medium галлюцинации на длинных файлах (chunk_length / pipeline-фильтры)
- Качественный pipeline для OpenVINO (давно зафиксировано в gpu.md)
- Включение onnx в --device auto после стабилизации

Все направления связаны ссылками на источники (ADR-006, gpu.md, публикации
SberDevices), чтобы при возврате к работе не пришлось воспроизводить контекст.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:48:36 +03:00
ddadminandClaude Opus 4.7 3b7eb603ae docs(adr): ADR-006 onnx-asr — независимая валидация через agent-judge
Переименовано из 005 в 006: на master уже есть ADR-005-parakeet-evaluation
от прошлого эксперимента (Opus, отклонение Parakeet), нумерация бы конфликтовала.

Качество замерено независимым agent-judge'ем по методологии ADR-005
на 9 транскриптах (3 файла × 3 backend), а не субъективными пометками
"Отлично/Хорошо" на одном файле, как в первой версии. Артефакты транскриптов
лежат в /mnt/c/ddmitry/Videos/OBS/ для верификации.

Подтверждены три класса проблем Parakeet из ADR-005 (Mm-hmm-редукция,
иноязычные вставки, искажение IT-терминов) на новом наборе файлов
с конкретными цитатами и таймкодами.

Обнаружен новый класс ошибок Whisper medium: галлюцинации на длинных
файлах с тихими фрагментами (до 17 минут потеряно на одном файле,
многоминутные повторы, приписывание несуществующих имён). Не описано
в ADR-005, потому что там были 15-минутные отрывки.

GigaAM v3 подтверждён как лучший backend для русских встреч на CPU
(summary utility 4/5 на всех файлах, единственный без потерь содержания).
Рекомендация в README уточнена: parakeet-v3 для русского не подходит,
openvino-cpu medium — только для встреч ≤30 мин с равномерной громкостью.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:35:04 +03:00
ddadmin 5990d85a58 chore: удалён скрипт сравнения (не пригодился)
- Зачем:
  - сравнение сделано вручную, скрипт не использовался.
- Что:
  - удалён scripts/compare_backends.py.
- Проверка:
  - uv run pytest -q (172 passed).
2026-04-25 23:17:30 +03:00
ddadmin 503f2a3732 docs(readme): уточнены рекомендации по onnx-моделям
- Зачем:
  - в README не было рекомендаций по выбору между gigaam-v3 и parakeet-v3.
- Что:
  - добавлена таблица ONNX-моделей с RTFx, языками и пунктуацией.
  - пример parakeet-v3 изменён на --language ru.
  - рекомендации: gigaam-v3 для скорости, parakeet-v3 для пунктуации.
- Проверка:
  - uv run pytest -q (172 passed).
2026-04-25 23:16:13 +03:00
ddadmin 854bf55145 docs: задокументировать onnx-бэкенд в README, CLI-help и ADR-005
- Зачем:
  - пользователь должен знать о --device onnx и поддерживаемых моделях.
- Что:
  - CLI help: добавлен onnx в список устройств.
  - README: секция примеров, таблица платформ, device-aware дефолты.
  - ADR-005: результаты parakeet-v3 --language ru, уточнённые рекомендации.
- Проверка:
  - uv run pytest -q (172 passed).
  - uv run transcribe --help показывает onnx.
2026-04-25 23:13:41 +03:00
ddadmin 53142fd341 docs(adr): ADR-005 — результаты эксперимента с onnx-asr бэкендом
- Зачем:
  - задокументировать результаты сравнения gigaam-v3 vs OpenVINO на CPU.
- Что:
  - таблица скорости по 3 файлам (3-5x ускорение).
  - сравнение качества русской речи с GPU faster-whisper.
  - решение: оставить бэкенд как экспериментальный (--device onnx).
- Проверка:
  - просмотр docs/adr/005-onnx-asr-backend.md.
2026-04-25 22:31:51 +03:00
ddadmin b1cbdc3e0b fix(onnx-asr): исправлены баги интеграции после smoke-теста
- Зачем:
  - при smoke-тесте выявлены расхождения между onnx-asr API и мок-тестами.
- Что:
  - убран deprecated cpu_preprocessing из load_model.
  - sample_rate передан как keyword-аргумент в recognize().
  - исправлены имена полей SegmentResult: start_ts→start, end_ts→end.
  - добавлен device "onnx" в DEVICE_DEFAULTS и _VALID_DEVICES.
- Проверка:
  - uv run pytest -q (172 passed).
  - uv run transcribe file.mp4 --device onnx --model gigaam-v3.
2026-04-25 21:48:54 +03:00
ddadmin 7d39ab908b test: добавить скрипт сравнения onnx-asr и OpenVINO бэкендов
- Зачем:
  - Экспериментальное сравнение скорости и качества транскрипции на реальных аудиофайлах.
- Что:
  - скрипт compare_backends.py запускает транскрипцию одного файла через gigaam-v3, parakeet-v3 и OpenVINO medium.
  - выводит время, количество сегментов и превью текста для каждого.
- Проверка:
  - python scripts/compare_backends.py /path/to/audio.mp3
2026-04-25 21:29:08 +03:00
ddadmin 79dbd170ce feat(onnx-asr): зарегистрировать бэкенд для --device onnx
- Зачем:
  - Бэкенд должен быть доступен через CLI по --device onnx.
- Что:
  - в backends/__init__.py добавлена ветка device == "onnx" с ленивым импортом OnnxAsrBackend.
  - при отсутствии onnx-asr выдаётся понятная ошибка.
  - добавлен тест TestBackendRegistration.
- Проверка:
  - uv run pytest -v (172 passed)
2026-04-25 21:25:28 +03:00
ddadmin 9e04dc8c25 feat(onnx-asr): реализовать transcribe с сегментацией через VAD
- Зачем:
  - Основной метод бэкенда — транскрипция аудиофайла в сегменты с временными метками.
- Что:
  - метод transcribe декодирует аудио через faster_whisper.decode_audio, затем вызывает model.recognize() с VAD-сегментацией.
  - каждый VAD-сегмент преобразуется в проектную структуру Segment.
  - поддержка колбэков on_segment, on_status.
  - написаны 4 теста: сбор сегментов, вызов on_segment, передача языка, обработка пустого аудио.
- Проверка:
  - uv run pytest tests/test_onnx_asr.py -v (14 passed)
2026-04-25 21:24:31 +03:00
ddadmin d9c9aefdb3 feat(onnx-asr): реализовать create_model с Silero VAD
- Зачем:
  - onnx-asr модель должна создаваться с квантизацией и VAD для разбивки аудио на сегменты.
- Что:
  - метод create_model вызывает onnx_asr.load_model с квантизацией и cpu_preprocessing=True.
  - подгружается Silero VAD, прикрепляется к модели через with_vad.
  - написаны 3 теста: проверка аргументов load_model, загрузка VAD, передача fp16.
- Проверка:
  - uv run pytest tests/test_onnx_asr.py -v
2026-04-25 21:22:02 +03:00
ddadmin f25a546754 feat(onnx-asr): реализовать ensure_model_available
- Зачем:
  - Бэкенд должен резолвить алиасы моделей и сохранять compute_type для дальнейшего использования.
- Что:
  - добавлен конструктор __init__ с полями actual_compute_type, _resolved_model_id, _vad.
  - метод ensure_model_available валидирует алиас и возвращает идентификатор модели onnx-asr.
  - написаны 3 теста на резолвинг и сохранение compute_type.
- Проверка:
  - uv run pytest tests/test_onnx_asr.py -v
2026-04-25 21:20:35 +03:00
ddadmin a3d0213cb3 feat(onnx-asr): добавить скелет бэкенда с разрешением алиасов моделей
- Зачем:
  - onnx-asr бэкенд должен распознавать короткие алиасы (gigaam-v3, parakeet-v3) и raw-имена (nemo-canary-1b-v2).
- Что:
  - создан файл backends/onnx_asr.py с MODEL_ALIASES и классом OnnxAsrBackend.
  - метод _resolve_model: преобразует алиасы, пропускает raw-имена, выдаёт ValueError для неизвестных.
  - написаны 4 теста на разрешение алиасов.
- Проверка:
  - uv run pytest tests/test_onnx_asr.py::TestModelAliases -v
2026-04-25 21:19:33 +03:00
ddadmin b5c1da24ce build(deps): добавлена зависимость onnx-asr[cpu,hub]>=0.11.0
- Зачем:
  - onnx-asr бэкенд для транскрипции (Parakeet/GigaAM) даёт 30-90x realtime на CPU.
- Что:
  - добавлен пакет onnx-asr[cpu,hub]>=0.11.0 в зависимости проекта.
  - uv.lock обновлён.
- Проверка:
  - uv run python -c "import onnx_asr; print(onnx_asr.__version__)"
2026-04-25 21:17:24 +03:00
ddadmin 82596187a4 docs(superpowers): добавлены spec и план для onnx-asr бэкенда
- Зачем:
  - задизайнить эксперимент с Parakeet/GigaAM через onnx-asr как третий бэкенд.
- Что:
  - spec: архитектура, модель-алиасы (gigaam-v3, parakeet-v3), API, регистрация.
  - plan: 8 задач TDD: зависимость → скелет → ensure → create → transcribe → регистрация.
- Проверка:
  - gh pr diff, ревью spec/plan.
2026-04-25 21:09:43 +03:00
ddadmin 0b5703a0e3 docs(adr): ADR-005 — Parakeet TDT оценён и отклонён для русских ИТ-встреч
- Зачем:
  - зафиксировать в master результат эксперимента с Parakeet-бэкендом:
    что проверяли, какие цифры получили, почему оставили Whisper как дефолт.
    Знания доступны, код — в экспериментальной ветке feature/parakeet-backend.
- Что:
  - docs/adr/005-parakeet-evaluation.md — полный контекст решения: гипотеза,
    методология, метрики скорости (Parakeet 2.7x быстрее, -35% RAM),
    качество (Summary utility 2-3/5 vs 4/5 у Whisper), систематические
    проблемы модели на тихой русской речи, VAD-тюнинг не помог, условия
    пересмотра решения в будущем (английский / live-captioning / русские
    specialized-модели через onnx-asr).
  - docs/gpu.md — короткая заметка под «Потенциальные направления развития»
    со ссылкой на ADR-005.
- Проверка:
  - cat docs/adr/005-parakeet-evaluation.md.
  - grep -A5 "Parakeet TDT — проверено" docs/gpu.md.
  - ветка feature/parakeet-backend сохранена и не удаляется.
2026-04-19 01:26:20 +03:00
ddadmin 2c1d798eeb Revert "docs(specs): дизайн-спек Parakeet backend"
This reverts commit cb6c1d7728.
2026-04-18 20:32:48 +03:00
ddadmin cb6c1d7728 docs(specs): дизайн-спек Parakeet backend
- Зачем:
  - зафиксировать дизайн экспериментального бэкенда до имплементации; гипотеза — Parakeet TDT v3 быстрее/качественнее Whisper на Intel/AMD встройках.
- Что:
  - добавлен docs/superpowers/specs/2026-04-18-parakeet-backend-design.md после 3 раундов adversarial-review.
  - зафиксированы: onnx-asr+Silero VAD как runtime, CPU EP в MVP, --device parakeet, int8/fp32 compute_type, no-fallback политика, long-audio gate с blocker-thresholds (peak RSS ≤4GB, первый status ≤10с, interval ≤30с, offline после первого запуска).
- Проверка:
  - cat docs/superpowers/specs/2026-04-18-parakeet-backend-design.md.
2026-04-18 20:26:56 +03:00
ddadminandClaude Opus 4.6 749ede090c docs(gpu): Quadro M3000M (CC 5.0) — CUDA не работает в CTranslate2 4.x
float16 требует CC ≥ 7.0, float32 падает с ошибкой — пребилды
CTranslate2 4.x не включают sm_50/sm_60. Рекомендация: --device cpu
или --device openvino.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:56:51 +03:00
ddadminandClaude Opus 4.6 59b74f166c docs(gpu): уточнить модель CPU — Intel i7-11800H
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:52:17 +03:00
ddadminandClaude Opus 4.6 857267763c feat(cli): --threads для управления CPU-потоками + бенчмарк CUDA compute_type
- Зачем:
  - CTranslate2 по умолчанию использует 4 потока; на многоядерных CPU (8+ ядер)
    это неоптимально — --threads 8 даёт +13% ускорения.
  - Не было данных по int8_float32/int8_float16 на NVIDIA GPU.

- Что:
  - --threads / -t: новый CLI-флаг, пробрасывается через load_model →
    backend.create_model(cpu_threads=...) → WhisperModel(cpu_threads=...).
  - Валидация min=0 на входе (typer), Backend протокол синхронизирован.
  - docs/gpu.md: результаты бенчмарка 6 комбинаций CUDA compute_type
    (medium/large-v3 × float16/int8_float32/int8_float16) на двух файлах
    (16 мин и 46 мин). Ключевой вывод: float16 — оптимальный дефолт;
    large-v3 ненадёжен на длинных записях.
  - README: --threads добавлен в таблицу опций.
  - Фикс теста: test_resolve_repo_explicit_unsupported_pair_raises обновлён
    под добавление medium fp16 модели.

- Проверка:
  - uv run pytest: 157 passed.
  - transcribe file.mp4 --device cpu --threads 8: 277с vs 320с (дефолт).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:49:16 +03:00
Dmitriy DementievandClaude Opus 4.6 8ae74d2748 docs(gpu): потенциальные направления — quality pipeline для OpenVINO, CUDA int8_float32
Зафиксированы два направления развития:
- temperature fallback и фильтры галлюцинаций для OpenVINO бэкенда
- тестирование int8_float32/int8_float16 на CUDA

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:00:31 +03:00
Dmitriy DementievandClaude Opus 4.6 248f260005 docs(gpu): CPU int8_float32, сравнение pipeline-ов, CUDA рекомендации
- Добавлена секция CPU бэкенда: int8_float32 как рекомендация, тюнинг потоков
- Объяснение разницы качества OpenVINO vs CTranslate2 (pipeline декодирования)
- Секция CUDA: int8_float32/int8_float16 как альтернатива для тестирования
- Обновлена таблица OpenVINO моделей (medium fp16)
- Обновлены таблицы скорости и качества

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:45:17 +03:00
Dmitriy DementievandClaude Opus 4.6 413cb3e6d8 feat(openvino): добавить medium fp16 модель как опцию
OpenVINO/whisper-medium-fp16-ov доступна через --compute-type fp16.
На CPU fp16 эмулируется программно (медленно), но на Intel Arc GPU
даёт аппаратное ускорение через XMX. Не меняет дефолты.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:48:29 +03:00
Dmitriy DementievandClaude Opus 4.6 9d41b893c3 docs(readme): добавить int8_float32 в таблицу compute_type
Бенчмарк показал: int8_float32 даёт 1.5x ускорение на CPU при сохранении
качества float32. Добавлен как рекомендуемый вариант для CPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:04:24 +03:00
40 changed files with 3235 additions and 836 deletions
+3
View File
@@ -4,3 +4,6 @@ __pycache__/
.mypy_cache/ .mypy_cache/
dist/ dist/
*.pyc *.pyc
.codex
.qwen/
.scratch/
+1
View File
@@ -0,0 +1 @@
3.13.13
+90
View File
@@ -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`.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+17
View File
@@ -0,0 +1,17 @@
# Локальная транскрипция
Контекст описывает язык проекта для локального распознавания аудио и видео.
## Language
**Движок распознавания**:
Программная среда, которая загружает и исполняет модели распознавания. Обновление движка само по себе не означает изменение выбранной модели или рекомендаций пользователю.
_Avoid_: Модель, ASR-модель
**Поддерживаемая модель**:
Модель, которую пользователь может выбрать явно и для которой проект обеспечивает работоспособный путь транскрипции. Этот статус не означает автоматический выбор или рекомендацию для большинства пользователей.
_Avoid_: Доступная модель, дефолт
**Модель по умолчанию**:
Поддерживаемая модель, которую проект выбирает без явного указания модели пользователем для определённого пути выполнения.
_Avoid_: Рекомендуемая модель, поддерживаемая модель
+91 -11
View File
@@ -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 с таймкодами** — удобен для суммаризации ИИ - **Markdown с таймкодами** — удобен для суммаризации ИИ
- **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы) - **Аудио и видео** — mp3, wav, mp4, mkv и [другие форматы](#поддерживаемые-форматы)
@@ -114,6 +115,18 @@ transcribe podcast.wav --model large-v3 --compute-type float16
# Максимальное качество на Intel GPU # Максимальное качество на Intel GPU
transcribe podcast.wav --model large-v3 --device openvino-gpu transcribe podcast.wav --model large-v3 --device openvino-gpu
# Максимальная скорость на CPU (русский)
transcribe meeting.mp4 --device onnx --model gigaam-v3
# CPU с пунктуацией и нормализацией русского текста
transcribe podcast.wav --device onnx --model gigaam-v3-e2e-ctc
# Смешанная русско-английская речь
transcribe meeting.wav --device onnx --model gigaam-multilingual-ctc
# Повышенная точность смешанной речи (медленнее, ~590 MB)
transcribe meeting.wav --device onnx --model gigaam-multilingual-large-ctc
# Сохранить в конкретный файл # Сохранить в конкретный файл
transcribe interview.m4a --output result.md transcribe interview.m4a --output result.md
``` ```
@@ -138,6 +151,31 @@ transcribe *.mp4 --force
- При ошибке в одном файле остальные продолжают обрабатываться - При ошибке в одном файле остальные продолжают обрабатываться
- `--output` несовместим с несколькими файлами - `--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 ### Опции CLI
| Опция | Сокращение | По умолчанию | Описание | | Опция | Сокращение | По умолчанию | Описание |
@@ -145,8 +183,9 @@ transcribe *.mp4 --force
| `--model` | `-m` | `medium` | Модель Whisper | | `--model` | `-m` | `medium` | Модель Whisper |
| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) | | `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) |
| `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу | | `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу |
| `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu) | | `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) |
| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU) | Тип вычислений | | `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений |
| `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) |
| `--force` | `-f` | — | Перезаписать существующие транскрипты | | `--force` | `-f` | — | Перезаписать существующие транскрипты |
| `--verbose` | `-v` | — | Подробный вывод | | `--verbose` | `-v` | — | Подробный вывод |
@@ -157,6 +196,7 @@ transcribe *.mp4 --force
| CPU | ✅ | ✅ | ✅ | | CPU | ✅ | ✅ | ✅ |
| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто | | OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто |
| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто | | OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто |
| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно |
| GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) | | GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
<details> <details>
@@ -209,18 +249,21 @@ language = "en"
Дефолты зависят от устройства: Дефолты зависят от устройства:
| Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | CPU | | Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU |
|----------|------|----------------|----------------|-----| |----------|------|----------------|----------------|------|-----|
| model | medium | medium | medium | medium | | model | medium | medium | medium | gigaam-v3-e2e-rnnt | medium |
| compute_type | float16 | int8 | int8 | float32 | | compute_type | float16 | int8 | int8 | int8 | float32 |
| language | ru | ru | ru | ru | | language | ru | ru | ru | ru | ru |
## Модели и GPU ## Модели и GPU
Рекомендации: Рекомендации:
- **По умолчанию:** `medium` — хороший баланс скорости и качества - **По умолчанию для ONNX:** `gigaam-v3-e2e-rnnt` — читаемый русский текст с
пунктуацией почти без потери скорости относительно сырого `gigaam-v3`
- **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` - **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16`
- **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu`
- **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после)
- **CPU с пунктуацией (русский):** `--device openvino-cpu --model medium` (5-6× RTF; для встреч ≤30 мин с равномерной громкостью — на длинных файлах с тихими фрагментами возможны галлюцинации)
- **Быстрый тест:** `tiny` — для проверки пайплайна - **Быстрый тест:** `tiny` — для проверки пайплайна
<details> <details>
@@ -234,6 +277,42 @@ language = "en"
| `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ | | `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ |
| `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ | | `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ |
#### ONNX-модели (`--device onnx`)
Другие архитектуры, не Whisper. Работают через onnxruntime на CPU:
| Модель | Размер (int8) | RTFx CPU | Языки | Пунктуация |
|--------|--------------|----------|-------|-----------|
| `gigaam-v3` | ~300 MB | 17-29× | ru | ❌ |
| `gigaam-multilingual-ctc` | ~300 MB | 10,0×* | ru, en, kk, ky, uz | ❌ |
| `gigaam-multilingual-large-ctc` | ~590 MB | 4,8×* | ru, en, kk, ky, uz | ❌ |
| `gigaam-v3-e2e-ctc` | ~300 MB | 11,9×* | ru | ✅ |
| `gigaam-v3-e2e-rnnt` | ~300 MB | 11,5×* | ru | ✅ |
| `parakeet-v3` | ~600 MB | 7,6×* | 25 языков | ✅ |
\* Наблюдение на AMD Ryzen 7 8845H, Windows, `int8`, три записи общей
длительностью 43:37. Это не приёмочный замер для целевого Intel Core i5.
Методика и качественное сравнение:
[benchmark GigaAM и Whisper](docs/benchmarks/2026-08-11-gigaam-model-comparison.md).
> **Рекомендация**: ONNX по умолчанию использует `gigaam-v3-e2e-rnnt` для
> готового читаемого русского текста. Для последующей машинной обработки можно
> явно выбрать более точный по словам `gigaam-v3` без пунктуации. Для смешанной
> речи с приоритетом качества используйте `gigaam-multilingual-large-ctc`: она примерно вдвое
> медленнее small-варианта, но приблизилась к monolingual GigaAM по WER.
> `parakeet-v3` в 1,58 раза быстрее Large и ставит пунктуацию, но на тех же
> трёх записях хуже по WER и вставляет ложные английские фразы в русскую речь;
> это подтверждает проблемы из [ADR-005](docs/adr/005-parakeet-evaluation.md).
Обе GigaAM Multilingual сами распознают русский, английский, казахский,
кыргызский и узбекский внутри одной записи. `onnx-asr` не передаёт этим моделям
подсказку языка, поэтому `--language` не управляет выбором языка.
Для моделей из таблицы опубликованы `int8` и `float32`. Если неявный
device-aware дефолт недоступен для выбранной модели, CLI сообщит о подстановке
доступного варианта. Явное значение из `--compute-type` или
`.transcriber.toml` вместо подстановки завершится ошибкой.
</details> </details>
<details> <details>
@@ -243,9 +322,10 @@ language = "en"
|-----|--------|----------|----------|--------------------| |-----|--------|----------|----------|--------------------|
| `float16` | CUDA | ~4.5-5 GB | Отлично | **По умолчанию для CUDA** | | `float16` | CUDA | ~4.5-5 GB | Отлично | **По умолчанию для CUDA** |
| `int8_float16` | CUDA | ~4.7 GB | Отлично | GPU от 6 GB, альтернатива float16 | | `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 (выбирается автоматически) | | `fp16` | OpenVINO | Низкое | Отлично | OpenVINO large-v3 (выбирается автоматически) |
| `float32` | CPU | Среднее | Отлично | **По умолчанию для CPU** | | `float32` | CPU / ONNX | Среднее | Отлично | **По умолчанию для CPU** |
**Важно:** `int8` на длинных записях может давать галлюцинации (повтор фраз, потеря контента). **Важно:** `int8` на длинных записях может давать галлюцинации (повтор фраз, потеря контента).
`float16`/`fp16` и `float32` значительно стабильнее на записях >20 минут. `float16`/`fp16` и `float32` значительно стабильнее на записях >20 минут.
+2 -2
View File
@@ -118,7 +118,7 @@ transcribe <путь_к_файлу> [опции]
### 4.2. Требования к окружению ### 4.2. Требования к окружению
- Python ≥ 3.10 - Python ≥ 3.13
- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов) - ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов)
- Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001) - Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001)
- Дисковое пространство для моделей: ~3 GB (large-v3) - Дисковое пространство для моделей: ~3 GB (large-v3)
@@ -134,7 +134,7 @@ transcribe <путь_к_файлу> [опции]
| Компонент | Технология | | Компонент | Технология |
|---------------------|-------------------------------------------------| |---------------------|-------------------------------------------------|
| Язык | Python 3.10+ | | Язык | Python 3.13+ |
| Управление проектом | uv (pyproject.toml) | | Управление проектом | uv (pyproject.toml) |
| Распознавание речи | faster-whisper (CTranslate2 backend) | | Распознавание речи | faster-whisper (CTranslate2 backend) |
| Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) | | Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) |
+102
View File
@@ -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, 15)
| Отрывок | 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-экономия и английский контент.
+143
View File
@@ -0,0 +1,143 @@
# ADR-006: GigaAM RNN-T как модель по умолчанию для ONNX-пути
**Статус**: Принято
**Дата**: 2026-04-25
**Обновлено**: 2026-08-11
## Контекст
Целевая аудитория `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 E2E RNN-T — модель по умолчанию для русских встреч на CPU.**
Бэкенд **не в auto-detect** — только при явном указании пользователем (политика experimental backend, как для openvino).
Модели:
- **`gigaam-v3-e2e-rnnt`** — модель по умолчанию: практически равна обычному
GigaAM по скорости, немного уступает по WER, но выдаёт готовую пунктуацию для
пользователей, которые читают транскрипт напрямую.
- **`gigaam-v3`** — явный профиль для последующей машинной обработки. 17-29×
RTF, summary utility 4/5 на всех протестированных файлах. Без пунктуации и
латиницы; ошибки локальны, чинятся LLM-нормализацией.
- **`parakeet-v3`** — multilingual (25 языков), формально доступен. **Не рекомендуется для русских встреч**: Mm-hmm-редукция и иноязычные вставки воспроизводятся систематически (см. выше). Уместен только для англоязычного контента.
Все перечисленные модели доступны в int8-квантизации.
## Последствия
- Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах).
- Пользователи ONNX без явного `--model` получают пунктуацию и нормализацию
RNN-T; более точный сырой CTC остаётся доступен как `--model gigaam-v3`.
- Whisper medium (`--device openvino-cpu`) **остаётся допустимым** для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с тихими участками он галлюцинирует целыми блоками — этот риск зафиксирован, но решение не выводит OpenVINO из списка дефолтов (часть пользователей всё ещё нуждается в пунктуации, и для коротких файлов галлюцинации не воспроизводятся).
- Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится.
- GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU).
- Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта.
## Открытые вопросы / следующие шаги
- **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю.
- **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-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте |
+47
View File
@@ -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, указать конфликт
явно и объяснить, почему решение стоит пересмотреть.
+66
View File
@@ -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.
+15
View File
@@ -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-роль, следует использовать соответствующую метку
из этой таблицы.
+267
View File
@@ -0,0 +1,267 @@
# Backlog — будущие эксперименты и направления
Список открытых направлений, которые имеют смысл, но не реализованы. Каждый пункт содержит обоснование и ссылку на источник (ADR / статья), чтобы при возврате не пришлось воспроизводить контекст с нуля.
Когда направление становится в работу — переносится в spec/план или соответствующий ADR. Когда отклоняется — остаётся в backlog с пометкой «отклонено» и причиной (для истории решений).
---
## ASR-бэкенды и модели
### Переоценка CPU-дефолта после обновления `onnx-asr` 0.12
**Промежуточный статус 2026-08-11:** короткая матрица на трёх записях завершена
и описана в [benchmark GigaAM и Whisper](benchmarks/2026-08-11-gigaam-model-comparison.md).
Multilingual Large добавлена как явный качественный профиль; решение о default
по-прежнему ждёт целевого Intel Core i5 и длинных негативных примеров.
**Проблема:** текущий 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#решение).
---
## Отклонённые направления
*(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)*
-123
View File
@@ -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 мин | 230 сек |
| Диаризация | нет | нет | нет | нет | есть (условная) |
## Ключевые выводы
### 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 или аналоги).
@@ -0,0 +1,217 @@
# Сравнение GigaAM v3, E2E и Whisper на русских встречах
**Дата:** 2026-08-11
**Статус:** завершённый benchmark на трёх записях
**Машина:** AMD Ryzen 7 8845H, Windows, CPU
**Квантизация ONNX:** int8
## Цель
Выбрать практический CPU-профиль на случай, когда OpenVINO недоступен или
нестабилен. Сравниваются скорость, близость к внешнему транскрипту, сохранность
смысла и терминов, пунктуация и пригодность результата для чтения.
Внешний транскрипт используется как неточный ориентир, а не ground truth: он
сам содержит ошибки, редактирует разговорную речь и добавляет диаризацию.
Поэтому WER — сравнительный сигнал внутри одного файла, а не абсолютная оценка.
## Набор данных
Три реальные записи общей длительностью 43:37:
| Запись | Длительность | Характер речи | Ориентир |
|---|---:|---|---|
| Уточнение задачи BDMA | 14:51 | 2 говорящих, русский с IT-терминами | внешний транскрипт |
| RSQM, внутренний дейлик | 14:04 | 5 говорящих, короткие реплики и задачи | внешний транскрипт с диаризацией |
| DataCat, обсуждение архитектуры | 14:42 | 3 говорящих, высокая плотность английских терминов | внешний транскрипт с диаризацией |
Сырые записи и внешние транскрипты не коммитятся в репозиторий.
### Манифест файлов
На тестовом ноутбуке все файлы находятся в `C:\Users\ddmitry\Videos\OBS`.
Размер и SHA-256 позволяют проверить, что в повторном прогоне используется та
же версия записи или ориентира.
| Набор | Роль | Имя файла | Размер, байт | SHA-256 |
|---|---|---|---:|---|
| BDMA | видео | `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4` | 19 216 865 | `51866D247FE3EDA134CDD884F707B1F1DB8855B492E8BD14D2B56B62476255ED` |
| BDMA | ориентир | `2026-07-29 T2 BDMA уточнение задачи от Ильи.md` | 18 000 | `9098548743A86F868DFCDD906BCDE34ED1CE5A935602CBE399F4BCEDA8685B3C` |
| RSQM | видео | `2026-02-18 RSQM дейлик внутренний.mp4` | 19 694 785 | `E72000BE43A5E04BFB670B0A16D85D8AEA51A02A285ED778FF5726767E97E9B0` |
| RSQM | ориентир | `2026-02-18 RSQM дейлик внутренний_transcript.md` | 16 128 | `260DF5D6964817434092005936127C0DC2C22682B7CB20FF07D14C5D81B8D287` |
| DataCat | видео | `2026-03-23 T2 DataCat - архитектура в контуре заказчика - первое обсуждение с Машей.mp4` | 22 584 108 | `ACFF960A68AAA6A305F7E7053BB0642B5DA265B68865ABD018E7C9A8980AC3F5` |
| DataCat | ориентир | `2026-03-23 T2 DataCat - архитектура в контуре заказчика - первое обсуждение с Машей-transcript-whsper.md` | 24 874 | `7943944A0A046EAA3A1384CAC47D36B381FBEF2FCFDF18089411619F00725456` |
## Запись 1: уточнение задачи BDMA
Файл `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4`, длительность 14:51,
преимущественно русская речь с IT- и продуктовыми терминами.
| Модель | Время | RTFx | WER к внешнему тексту | Пунктуация / 1000 слов |
|---|---:|---:|---:|---:|
| FasterWhisper medium CPU | 1133,9 с* | 0,79×* | **20,8%** | 313 |
| OpenVINO medium | 190,6 с | 4,67× | **23,9%** | 187 |
| GigaAM v3 | **72,6 с** | **12,27×** | 26,5% | 0 |
| GigaAM Multilingual CTC | 84,8 с | 10,51× | 27,9% | 0 |
| GigaAM Multilingual Large CTC | 192,0 с | 4,64× | 27,5% | 0 |
| Parakeet v3 | 119,2 с | 7,47× | 28,2% | 338 |
| GigaAM v3 E2E RNN-T | 76,1 с | 11,71× | 29,0% | 326 |
| GigaAM v3 E2E CTC | **72,6 с** | **12,27×** | 31,8% | 338 |
\* FasterWhisper включает около 263 секунд первой загрузки модели. Оценка
самого распознавания — около 871 секунды, то есть 1,02× RTFx. Нужен отдельный
cached-прогон для точного замера.
Все модели дошли до конца записи без предупреждений о потере хвоста.
## Расширенный прогон
| Запись | Модель | Время | RTFx | WER |
|---|---|---:|---:|---:|
| RSQM | GigaAM v3 | 75,1 с | 11,25× | **23,1%** |
| RSQM | Multilingual CTC | 82,7 с | 10,21× | 24,4% |
| RSQM | Multilingual Large CTC | 163,4 с | 5,17× | 23,8% |
| RSQM | Parakeet v3 | 103,7 с | 8,14× | 29,1% |
| RSQM | E2E RNN-T | 72,8 с | 11,60× | 25,4% |
| RSQM | E2E CTC | **71,1 с** | **11,88×** | 28,0% |
| RSQM | FasterWhisper medium | 686,1 с | 1,23× | **19,7%** |
| DataCat | GigaAM v3 | **74,8 с** | **11,79×** | 27,5% |
| DataCat | Multilingual CTC | 94,2 с | 9,36× | 27,9% |
| DataCat | Multilingual Large CTC | 189,6 с | 4,65× | 26,7% |
| DataCat | Parakeet v3 | 122,7 с | 7,19× | 27,9% |
| DataCat | E2E RNN-T | 78,0 с | 11,31× | **26,4%** |
| DataCat | E2E CTC | 76,0 с | 11,60× | 28,3% |
| DataCat | FasterWhisper medium | 954,5 с | 0,92× | **21,9%** |
Все модели обработали записи до конца. Разрыв между длительностью файла и
последним сегментом составил 0,3–5,9 секунды, предупреждений о потере хвоста или
повторах не было.
### Сводка по трём записям
WER ниже посчитан как micro-average: сумма ошибок по трём файлам делится на
суммарное число слов во внешних транскриптах.
| Модель | Суммарное время | RTFx | WER | Пунктуация / 1000 слов |
|---|---:|---:|---:|---:|
| FasterWhisper medium CPU | 2511,6 с* | 1,04× | **20,9%** | 269 |
| GigaAM v3 | 222,5 с | 11,76× | **26,0%** | 0 |
| GigaAM Multilingual Large CTC | 545,0 с | 4,80× | **26,2%** | 0 |
| GigaAM v3 E2E RNN-T | 226,9 с | 11,54× | 26,9% | 290 |
| GigaAM Multilingual CTC | 261,7 с | 10,00× | 26,9% | 0 |
| Parakeet v3 | 345,6 с | 7,57× | 28,3% | 301 |
| GigaAM v3 E2E CTC | **219,7 с** | **11,91×** | 29,3% | 305 |
\* Для BDMA использована оценка 871 секунд чистого inference: первый запуск
FasterWhisper включал ещё около 263 секунд загрузки. RSQM и DataCat измерены на
прогретом кеше — 686,1 и 954,5 секунды соответственно.
RNN-T оказалась лишь на 2% медленнее обычного GigaAM по суммарному времени.
Multilingual Small медленнее обычного GigaAM на 18%, а выигрыш E2E CTC
относительно RNN-T составляет только 3% и не компенсирует ухудшение текста.
Multilingual Large в 2,08 раза медленнее Small и в 2,45 раза медленнее обычного
GigaAM, но всё ещё в 4,6 раза быстрее FasterWhisper.
### GigaAM Multilingual Large против Parakeet v3
Large точнее Parakeet на каждой записи: 27,5% против 28,2% WER на BDMA,
23,8% против 29,1% на RSQM и 26,7% против 27,9% на DataCat. В сумме Large
получила 26,2% WER против 28,3%. Особенно заметна разница в пропусках:
103 удаления у Large против 264 у Parakeet.
Parakeet обработала набор за 345,6 секунды против 545,0 секунды у Large, то
есть была в 1,58 раза быстрее, и сразу расставила пунктуацию. Однако ручное
чтение выявило частые ложные переключения языка. В русских репликах RSQM
появляются `Yeah`, `No`, `Just die`, `What foo`; в DataCat — `Don't`,
`Skid of all`, `Talk no`, `Boy`. Это не единичная орфографическая ошибка, а
искажение обычной русской речи. Large в тех же местах сохраняет русский смысл,
хотя передаёт английские термины фонетически и не ставит пунктуацию.
На этом массиве Large предпочтительнее по качеству и сохранности содержания.
Parakeet интересна как более быстрый вариант с пунктуацией, но для русских
встреч требует обязательной ручной проверки и не рекомендуется как основной
профиль.
## Качественное чтение
Независимый reviewer сначала прочитал полные тексты без таблицы метрик, затем
сверил вывод с WER:
- FasterWhisper лучше сохраняет логику разговора и техническую лексику:
`Teradata`, `ClickHouse`, `ETL`, `legacy`, `MPP`, `SAP/BW/HANA`, `GUI`,
`Apex`. Значимых пропусков целых смысловых фрагментов не обнаружено.
- OpenVINO близок к FasterWhisper, но чаще склеивает слова и иногда локально
меняет смысл: около 00:59 «расчёт маржинальности называется» превращается в
«расчёт маржинальности не бывает».
- Обычный GigaAM v3 сохраняет почти весь смысл, числа и структуру обсуждения,
но фонетические ошибки и отсутствие пунктуации ухудшают чтение и поиск.
- E2E RNN-T читается лучше обычного GigaAM и устойчивее E2E CTC, но агрессивно
дробит живую речь. На DataCat она лучше обычного GigaAM по WER, однако на
BDMA и RSQM хуже на 2,3–2,5 процентного пункта.
- E2E CTC даёт больше обрывков, ложных границ и смешанных артефактов:
`иnженter`, `мetднные`, `оrкl`, `линейдgами`.
- Multilingual CTC не улучшила даже терминологически насыщенный DataCat и
оказалась на 15–26% медленнее обычного GigaAM на новых записях.
- Multilingual Large CTC улучшила Small на всех трёх записях и почти сравнялась
с monolingual GigaAM по совокупному WER: 26,2% против 26,0%. При ручном
чтении скрытой деградации не найдено, но пунктуации по-прежнему нет, а термины
передаются преимущественно фонетически (`файнбиай`, `найфай`, `терадата`).
- Parakeet v3 быстрее Large и выдаёт пунктуацию, но хуже по WER на всех трёх
записях, чаще пропускает слова и вставляет ложные английские фразы в русскую
речь. Для этого массива преимущество в читаемости не компенсирует искажения.
Длинных автономных галлюцинаций не найдено. Есть локальные потенциально
опасные искажения имён и терминов: `Иришка`, `Lutriness`, «расчёт
маржинальности не бывает», «расчёты по губам», «понюхаю дело».
На RSQM FasterWhisper сохраняет `edge-кейсы`, `Confluence` и `Backlog`; GigaAM
передаёт их фонетически и нестабильно (`еджкейсы`, `ичкейсы`). На DataCat
FasterWhisper лучше сохраняет `API`, `ETL`, `Open Platform`; обычный GigaAM
хорошо удерживает сам смысл архитектурного обсуждения и большинство названий
фонетически. RNN-T делает текст визуально аккуратнее, но название одного и того
же продукта может плавать между `Fine BI`, `Fine Bia`, `FNB`, а `API` — между
`API`, `AP`, `пи`.
## Рекомендация
- Для чтения человеком без дополнительного постпроцессинга —
`gigaam-v3-e2e-rnnt`: по трём записям всего на 2% медленнее обычного GigaAM,
но уже содержит пунктуацию и нормализацию. Это выбор цена/качество, а не
максимум дословной точности.
- Для последующей машинной обработки, поиска или собственной расстановки
пунктуации — `gigaam-v3`: лучший совокупный WER среди быстрых ONNX-вариантов
и более стабильный сырой текст.
- Для смешанной речи с приоритетом качества сырого текста —
`gigaam-multilingual-large-ctc`. Она устойчиво лучше Small и почти равна
monolingual GigaAM по WER, но требует 545 секунд против 262 секунд на том же
наборе. Small остаётся вариантом с приоритетом скорости.
- `parakeet-v3` в 1,58 раза быстрее Large и содержит пунктуацию, однако хуже
по WER на всех трёх записях и нестабилен на русской речи. Использовать его
стоит только там, где скорость и готовая пунктуация важнее сохранности текста.
- `gigaam-v3-e2e-ctc` преимуществ на этом наборе не показала.
- FasterWhisper medium остаётся выбором для важных записей с плотной
терминологией, если допустима обработка около realtime. По трём файлам она в
11 раз медленнее GigaAM ради снижения WER примерно на 5–6 процентных пунктов.
На CUDA соотношение необходимо измерять отдельно.
По итогам сравнения и обсуждения двух основных сценариев RNN-T выбрана моделью
по умолчанию для явного `--device onnx`: небольшая потеря дословной точности
принята ради читаемого результата с пунктуацией. Для LLM-пайплайнов и поиска,
где важнее сырой текст, `gigaam-v3` остаётся явным профилем.
## Воспроизводимость
Для каждого ONNX-профиля использовалась команда вида:
```powershell
uv run transcribe <recording.mp4> --device onnx --model <model> --compute-type int8
```
Для всех прогонов явно передавался язык `--language ru`.
Время измерялось на прогретом кеше модели. WER считался после удаления
метаданных и таймкодов, приведения к нижнему регистру, замены `ё` на `е` и
удаления пунктуации.
+181 -20
View File
@@ -37,7 +37,7 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
| tiny | OpenVINO/whisper-tiny-int8-ov | — | | tiny | OpenVINO/whisper-tiny-int8-ov | — |
| base | — | OpenVINO/whisper-base-fp16-ov | | base | — | OpenVINO/whisper-base-fp16-ov |
| small | OpenVINO/whisper-small-int8-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 | | large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov |
### Результаты тестирования OpenVINO ### Результаты тестирования OpenVINO
@@ -50,7 +50,7 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
|---|---|---|---| |---|---|---|---|
| Intel Ultra 7 255H | **122с** | **411с** | — | | Intel Ultra 7 255H | **122с** | **411с** | — |
| AMD Ryzen 7 8845H | 185с | 416с | 734с | | 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. **Ускорение vs CPU float32:** **3-6x** в зависимости от CPU.
@@ -77,6 +77,113 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM
- **medium** — для повседневного использования и обработки ИИ. Ключевые термины верные, единичные ляпы не влияют на смысл конспекта. Оптимальный баланс скорости и качества. - **medium** — для повседневного использования и обработки ИИ. Ключевые термины верные, единичные ляпы не влияют на смысл конспекта. Оптимальный баланс скорости и качества.
- **large-v3** — для важных записей, где нужна дословная точность. Лучшая пунктуация и связность. На OpenVINO (416с) быстрее, чем medium на чистом CPU (734с) — лучшее качество при выше скорости. - **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) ### Linux / WSL2 (x86_64)
@@ -98,45 +205,99 @@ winget install -e --id Nvidia.CUDA --version 12.9 # требует запус
## Совместимость GPU ## Совместимость GPU
| GPU | VRAM | medium float16 | large-v3 float16 | Рекомендация | | GPU | VRAM | CC | medium float16 | large-v3 float16 | Рекомендация |
|-----|------|---------------|-----------------|--------------| |-----|------|----|---------------|-----------------|--------------|
| RTX 3060 | 6 GB | ✅ | ✅ | medium float16 (дефолт) | | RTX 3060 | 6 GB | 8.6 | ✅ | ✅ | medium float16 (дефолт) |
| RTX 4050 | 6 GB | ✅ | ✅ | medium float16 | | RTX 4050 | 6 GB | 8.9 | ✅ | ✅ | medium float16 |
| Quadro M3000M | 4 GB | ✅ | ⚠️ tight | medium float16 или int8 | | 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): Замеры на RTX 3060 Laptop (6 GB) и Intel CPU (WSL2):
| Конфигурация | 16 мин файл | 42 мин файл | Отн. скорость | | Конфигурация | 16 мин файл | 46 мин файл | Отн. скорость |
|-------------|-------------|-------------|---------------| |-------------|-------------|-------------|---------------|
| GPU + medium float16 | ~35с | ~133с | ~19x реалтайм | | GPU + medium float16 | **53с** | **127с** | **~18-22x реалтайм** |
| GPU + large-v3 float16 | ~90с | ~350с | ~7x реалтайм | | 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 + small int8** | **93с** | **153с** | **~10-16x реалтайм** |
| **OpenVINO + medium int8** | **171-205с** | **413с** | **~4-6x реалтайм** | | **OpenVINO + medium int8** | **171-205с** | **413с** | **~4-6x реалтайм** |
| **OpenVINO + large-v3 fp16** | **416с** | — | **~2.3x реалтайм** | | **OpenVINO + large-v3 fp16** | **416с** | — | **~2.3x реалтайм** |
| **CPU + medium int8_float32** | **~460с*** | — | **~2x реалтайм** |
| CPU + medium float32 | 658с (11 мин) | ~26 мин | ~1.5x реалтайм | | CPU + medium float32 | 658с (11 мин) | ~26 мин | ~1.5x реалтайм |
| CPU + large-v3 int8 | 839с (14 мин) | ~37 мин | ~1:1 реалтайм | | 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). SQL, PostgreSQL, Greenplum, Airflow, ClickHouse, Docker, CDR, GTP, MAP).
| Конфигурация | Качество (длинная запись, 42 мин) | Проблемы | | Конфигурация | Качество (короткие, ≤16 мин) | Качество (длинные, >40 мин) | Проблемы |
|---|---|---| |---|---|---|---|
| large-v3 int8 GPU | Плохо | Галлюцинации (фразы ×25), потеря контента | | medium float16 GPU | **Отлично** | **Отлично** | Единичные ляпы в терминах ("email" вместо "ML") |
| large-v3 float16 GPU | Отлично | — | | medium int8_float32 GPU | **Отлично** | — | Качество ≈ float16, но на 20% медленнее |
| medium float16 GPU | Хорошо | Редкие мелкие ляпы в терминах | | medium int8_float16 GPU | Хорошо | — | 1-2 ошибки |
| medium float32 CPU | Хорошо | Сопоставимо с large-v3 int8, без галлюцинаций | | large-v3 float16 GPU | Хорошо | **Плохо** | Галлюцинации на длинных записях (мусор на 3+ языках) |
| large-v3 int8 CPU | Хорошо | Без галлюцинаций (на коротких файлах) | | 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 может ошибиться и выдать мусор 1. **Указание языка (`--language ru`) критично** — auto-detect может ошибиться и выдать мусор
2. **float16/float32 стабильнее int8** — особенно на записях >20 минут 2. **medium + float16 на GPU — лучший баланс** скорости и качества для любых записей
3. **medium + float16 на GPU — лучший баланс** скорости и качества для повседневного использования 3. **large-v3 ненадёжен на длинных записях (>20 мин)** — галлюцинации даже с float16;
4. **large-v3 + float16 на GPU** — для максимального качества важных записей 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 ## Troubleshooting
-351
View File
@@ -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, но это 600880с на 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
- **34x ускорение** vs CPU на Radeon 680M (RDNA2), 780M (RDNA3) должен быть не хуже
- Весь pipeline внутри: аудио → mel → encoder → decoder → beam search
- GGUF-модели компактнее ONNX
- Активная разработка, Vulkan-бэкенд зрелый
**Бенчмарки (чужие, похожее железо):**
| GPU | Модель | Результат |
|-----|--------|-----------|
| Radeon 680M (RDNA2) | medium | ~34x быстрее 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)).
+172
View File
@@ -0,0 +1,172 @@
# 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-multilingual-large-ctc` — смешанная речь
с англоязычными терминами; large добавлена после сравнительного benchmark по
явному решению пользователя;
- `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` при реализации.
## Последующее решение о модели по умолчанию
После сравнительного прогона на трёх реальных записях и обсуждения двух
основных потребителей транскрипта модель `gigaam-v3-e2e-rnnt` выбрана моделью
по умолчанию для явного `--device onnx`. Она всего на 2% медленнее сырого
`gigaam-v3`, но выдаёт пунктуацию для чтения человеком; `gigaam-v3` остаётся
явным профилем с приоритетом дословной точности и последующей обработки LLM.
Это решение не включает ONNX в auto-detect.
## Чего не делаем
- Не меняем auto-detect: ONNX остаётся только явным выбором пользователя.
- Не обновляем 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-дефолте этого недостаточно;
- **качество моделей измерено предварительно на трёх 14–15-минутных записях.**
Результат фиксирует WER, скорость и ручное чтение, но не заменяет приёмку на
целевом Intel Core i5 и длинных записях; см.
[benchmark](../benchmarks/2026-08-11-gigaam-model-comparison.md).
Пошаговый чеклист и практика прогона нужны только на время работ и живут в
задаче трекера
([#1](https://git.dementev.space/ddmitry/local-transcriber/issues/1)), а не в
этом документе.
## Документация
README: поддерживаемые модели добавляются в таблицу ONNX-моделей. Для готового
читаемого русского текста и как модель по умолчанию используется E2E RNN-T,
для русского без пунктуации и LLM-пайплайнов остаётся явный `gigaam-v3`, для
смешанной речи с приоритетом качества — multilingual large. Скорость на слабых
CPU не измерялась. Требование Python
правится в [`docs/PRD.md`](../PRD.md) — раздел «Требования» и таблица
технологий; в README версии Python не упоминаются.
+9 -8
View File
@@ -3,15 +3,16 @@ name = "local-transcriber"
version = "0.1.0" version = "0.1.0"
description = "Local CLI tool for audio/video transcription using faster-whisper" description = "Local CLI tool for audio/video transcription using faster-whisper"
license = "MIT" license = "MIT"
requires-python = ">=3.10" requires-python = ">=3.13"
dependencies = [ dependencies = [
"typer", "typer>=0.24.1,<1",
"rich", "rich>=14.3.3,<15",
"faster-whisper>=1.2.1", "faster-whisper>=1.2.1,<2",
"socksio>=1.0.0", "socksio>=1.0.0,<2",
"nvidia-cublas-cu12>=12.4; sys_platform == 'linux' and platform_machine == 'x86_64'", "nvidia-cublas-cu12>=12.4,<13; sys_platform == 'linux' and platform_machine == 'x86_64'",
"openvino-genai>=2025.0; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", "openvino-genai>=2026.0.0.0,<2026.1; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')",
"tomli>=2.0; python_version < '3.11'", "onnx-asr[cpu,hub]>=0.12,<0.13",
"onnxruntime>=1.28,<2",
] ]
[project.scripts] [project.scripts]
@@ -25,6 +25,15 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend:
ov_device=device, compute_type_explicit=compute_type_explicit ov_device=device, compute_type_explicit=compute_type_explicit
) )
if device == "onnx":
try:
from .onnx_asr import OnnxAsrBackend
except ImportError:
raise ValueError(
"onnx-asr бэкенд недоступен. Установите: pip install onnx-asr[cpu,hub]"
) from None
return OnnxAsrBackend(compute_type_explicit=compute_type_explicit)
# cuda, cpu и всё остальное → faster-whisper # cuda, cpu и всё остальное → faster-whisper
from .faster_whisper import FasterWhisperBackend from .faster_whisper import FasterWhisperBackend
+5 -1
View File
@@ -30,8 +30,12 @@ class Backend(Protocol):
model_path: str, model_path: str,
device: str, device: str,
compute_type: str, compute_type: str,
cpu_threads: int = 0,
) -> Any: ) -> Any:
"""Создаёт модель. Возвращает backend-специфичный объект.""" """Создаёт модель. Возвращает backend-специфичный объект.
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки).
"""
... ...
def transcribe( def transcribe(
@@ -84,10 +84,17 @@ class FasterWhisperBackend:
model_path: str, model_path: str,
device: str, device: str,
compute_type: str, compute_type: str,
cpu_threads: int = 0,
) -> Any: ) -> Any:
"""Создаёт WhisperModel.""" """Создаёт WhisperModel.
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки, обычно 4).
"""
try: 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: except ImportError as exc:
if _is_missing_socksio_error(exc): if _is_missing_socksio_error(exc):
raise RuntimeError( raise RuntimeError(
+233
View File
@@ -0,0 +1,233 @@
"""Бэкенд транскрипции на основе 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-multilingual-large-ctc": OnnxModelSpec(
"gigaam-multilingual-large-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)
+2 -1
View File
@@ -106,8 +106,9 @@ class OpenVINOBackend:
model_path: str, model_path: str,
device: str, device: str,
compute_type: str, compute_type: str,
cpu_threads: int = 0,
) -> Any: ) -> Any:
"""Создаёт WhisperPipeline.""" """Создаёт WhisperPipeline. cpu_threads не используется (OpenVINO управляет сам)."""
import openvino_genai as ov_genai import openvino_genai as ov_genai
ov_dev = self._resolve_ov_device() ov_dev = self._resolve_ov_device()
+121 -5
View File
@@ -9,9 +9,23 @@ from rich.console import Console
from rich.status import Status from rich.status import Status
from .config import apply_device_defaults, load_config, resolve_defaults 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 ( from .transcriber import (
Segment, Segment,
TranscribeResult,
_is_cuda_error, _is_cuda_error,
_transcribe_file, _transcribe_file,
load_model, load_model,
@@ -43,9 +57,62 @@ def _format_device_info(device_used: str) -> str:
return "CPU" 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() @app.command()
def main( def main(
files: list[Path] = typer.Argument(..., help="Пути к аудио/видеофайлам"), files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"),
model: str | None = typer.Option( model: str | None = typer.Option(
None, "--model", "-m", show_default=False, help="Модель Whisper [по умолч.: medium]" None, "--model", "-m", show_default=False, help="Модель Whisper [по умолч.: medium]"
), ),
@@ -55,19 +122,60 @@ def main(
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"), output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
device: str | None = typer.Option( device: str | None = typer.Option(
None, "--device", "-d", show_default=False, None, "--device", "-d", show_default=False,
help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) [по умолч.: auto]" help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu|onnx) [по умолч.: auto]"
), ),
compute_type: str | None = typer.Option( compute_type: str | None = typer.Option(
None, "--compute-type", show_default=False, None, "--compute-type", show_default=False,
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]" 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="Подробный вывод"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
force: bool = typer.Option(False, "--force", "-f", 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: ) -> None:
"""Транскрибирует аудио/видеофайлы в markdown с таймкодами. """Транскрибирует аудио/видеофайлы в markdown с таймкодами.
Каскад приоритетов параметров: CLI-флаги > .transcriber.toml > device-aware дефолты. Каскад приоритетов параметров: 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: try:
config = load_config() config = load_config()
cli_values = {"model": model, "language": language, "device": device, "compute_type": compute_type} cli_values = {"model": model, "language": language, "device": device, "compute_type": compute_type}
@@ -89,9 +197,9 @@ def main(
raise SystemExit(1) raise SystemExit(1)
if is_batch: if is_batch:
_run_batch(expanded, defaults, verbose, force, ct_explicit) _run_batch(expanded, defaults, verbose, force, ct_explicit, cpu_threads=threads)
else: else:
_run_single(expanded[0], defaults, output, verbose, ct_explicit) _run_single(expanded[0], defaults, output, verbose, ct_explicit, cpu_threads=threads)
except KeyboardInterrupt: except KeyboardInterrupt:
console.print("\nПрервано пользователем.", style="yellow") console.print("\nПрервано пользователем.", style="yellow")
raise SystemExit(130) raise SystemExit(130)
@@ -129,6 +237,7 @@ def _run_single(
output: Path | None, output: Path | None,
verbose: bool, verbose: bool,
compute_type_explicit: bool = False, compute_type_explicit: bool = False,
cpu_threads: int = 0,
) -> None: ) -> None:
"""Пайплайн одного файла: валидация → модель → транскрипция → запись.""" """Пайплайн одного файла: валидация → модель → транскрипция → запись."""
start = time.monotonic() start = time.monotonic()
@@ -148,6 +257,7 @@ def _run_single(
defaults["model"], resolved_device, defaults["compute_type"], defaults["model"], resolved_device, defaults["compute_type"],
on_status=lambda msg: console.print(msg), strict_device=strict, on_status=lambda msg: console.print(msg), strict_device=strict,
compute_type_explicit=compute_type_explicit, compute_type_explicit=compute_type_explicit,
cpu_threads=cpu_threads,
) )
actual_ct = getattr(backend, "actual_compute_type", defaults["compute_type"]) or defaults["compute_type"] actual_ct = getattr(backend, "actual_compute_type", defaults["compute_type"]) or defaults["compute_type"]
console.print( console.print(
@@ -174,6 +284,7 @@ def _run_single(
on_segment=on_segment if verbose else None, on_segment=on_segment if verbose else None,
on_status=status.update, on_status=status.update,
strict_device=strict, strict_device=strict,
cpu_threads=cpu_threads,
) )
result = tfr.result result = tfr.result
@@ -211,6 +322,7 @@ def _run_single(
elapsed = time.monotonic() - start elapsed = time.monotonic() - start
console.print(f"Транскрипт сохранён: \"{output_path}\"", style="green") console.print(f"Транскрипт сохранён: \"{output_path}\"", style="green")
console.print(f" Сегментов: {len(result.segments)} Время: {elapsed:.1f}с") console.print(f" Сегментов: {len(result.segments)} Время: {elapsed:.1f}с")
_print_quality_warnings(result)
def _run_batch( def _run_batch(
@@ -219,6 +331,7 @@ def _run_batch(
verbose: bool, verbose: bool,
force: bool, force: bool,
compute_type_explicit: bool = False, compute_type_explicit: bool = False,
cpu_threads: int = 0,
) -> None: ) -> None:
"""Трёхфазный батч-пайплайн: prescan → загрузка модели → транскрипция.""" """Трёхфазный батч-пайплайн: prescan → загрузка модели → транскрипция."""
# Phase 1: Prescan — fail-fast + skip до загрузки модели (экономим ~2-5 сек) # Phase 1: Prescan — fail-fast + skip до загрузки модели (экономим ~2-5 сек)
@@ -256,6 +369,7 @@ def _run_batch(
defaults["model"], resolved_device, defaults["compute_type"], defaults["model"], resolved_device, defaults["compute_type"],
on_status=lambda msg: console.print(msg), strict_device=strict, on_status=lambda msg: console.print(msg), strict_device=strict,
compute_type_explicit=compute_type_explicit, compute_type_explicit=compute_type_explicit,
cpu_threads=cpu_threads,
) )
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3": 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_segment=on_segment if verbose else None,
on_status=status.update if not verbose else lambda msg: console.print(msg), on_status=status.update if not verbose else lambda msg: console.print(msg),
strict_device=strict, strict_device=strict,
cpu_threads=cpu_threads,
) )
if tfr.actual_device != actual_device: if tfr.actual_device != actual_device:
@@ -343,6 +458,7 @@ def _run_batch(
style="green", style="green",
) )
processed += 1 processed += 1
_print_quality_warnings(result, file.name)
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except Exception as exc: except Exception as exc:
+11 -7
View File
@@ -1,14 +1,9 @@
"""Загрузка конфигурации из ``.transcriber.toml`` и каскад приоритетов.""" """Загрузка конфигурации из ``.transcriber.toml`` и каскад приоритетов."""
import sys import tomllib
import warnings import warnings
from pathlib import Path from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
HARDCODED_DEFAULTS: dict[str, str] = { HARDCODED_DEFAULTS: dict[str, str] = {
"model": "medium", "model": "medium",
"language": "ru", "language": "ru",
@@ -22,11 +17,20 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
"openvino": {"model": "medium", "compute_type": "int8"}, "openvino": {"model": "medium", "compute_type": "int8"},
"openvino-gpu": {"model": "medium", "compute_type": "int8"}, "openvino-gpu": {"model": "medium", "compute_type": "int8"},
"openvino-cpu": {"model": "medium", "compute_type": "int8"}, "openvino-cpu": {"model": "medium", "compute_type": "int8"},
"onnx": {"model": "gigaam-v3-e2e-rnnt", "compute_type": "int8"},
} }
# Одно место правды для допустимых ключей конфига # Одно место правды для допустимых ключей конфига
_VALID_KEYS = set(HARDCODED_DEFAULTS) _VALID_KEYS = set(HARDCODED_DEFAULTS)
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu"} _VALID_DEVICES = {
"auto",
"cpu",
"cuda",
"openvino",
"openvino-gpu",
"openvino-cpu",
"onnx",
}
def find_config_file() -> Path | None: def find_config_file() -> Path | None:
+57
View File
@@ -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
+16 -2
View File
@@ -4,6 +4,7 @@ from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from .quality import TAIL_GAP_WARN_S, find_repetition_blocks, tail_gap
from .types import Segment, TranscribeResult from .types import Segment, TranscribeResult
_PAUSE_THRESHOLD_S = 2.0 # пауза между сегментами для разбиения на абзацы _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}" return f"{minutes:02d}:{secs:02d}.{centiseconds:02d}"
def _format_duration(seconds: float) -> str: def format_duration(seconds: float) -> str:
"""Человекочитаемая длительность для метаданных в шапке транскрипта.""" """Человекочитаемая длительность для метаданных в шапке транскрипта."""
total = int(seconds) total = int(seconds)
h = total // 3600 h = total // 3600
@@ -92,7 +93,20 @@ def format_transcript(
lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}") lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}")
lines.append(f"- **Модель**: {model_name}") lines.append(f"- **Модель**: {model_name}")
lines.append(f"- **Язык**: {result.language} ({language_mode})") 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(f"- **Устройство**: {device_info}")
lines.append("") lines.append("")
lines.append("---") lines.append("---")
+78
View File
@@ -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
+9 -3
View File
@@ -22,11 +22,13 @@ def load_model(
on_status: Callable[[str], None] | None = None, on_status: Callable[[str], None] | None = None,
strict_device: bool = False, strict_device: bool = False,
compute_type_explicit: bool = False, compute_type_explicit: bool = False,
cpu_threads: int = 0,
) -> tuple[Any, str, Any, str]: ) -> tuple[Any, str, Any, str]:
"""Загружает модель: ensure + create с fallback. """Загружает модель: ensure + create с fallback.
Возвращает (model, actual_device, backend, model_path). Возвращает (model, actual_device, backend, model_path).
compute_type_explicit: True если пользователь явно указал --compute-type. compute_type_explicit: True если пользователь явно указал --compute-type.
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки).
""" """
backend = get_backend(device, compute_type_explicit=compute_type_explicit) backend = get_backend(device, compute_type_explicit=compute_type_explicit)
actual_device = device actual_device = device
@@ -35,7 +37,7 @@ def load_model(
try: try:
_notify_status(on_status, f"Инициализирую модель на {device}...") _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 # Резолвим actual_device по реальному OpenVINO device
ov_dev = getattr(backend, "actual_ov_device", None) ov_dev = getattr(backend, "actual_ov_device", None)
if ov_dev == "GPU" and actual_device != "openvino-gpu": if ov_dev == "GPU" and actual_device != "openvino-gpu":
@@ -55,7 +57,7 @@ def load_model(
backend = get_backend("cpu") backend = get_backend("cpu")
model_path = backend.ensure_model_available(model_name, compute_type, on_status) model_path = backend.ensure_model_available(model_name, compute_type, on_status)
_notify_status(on_status, "Инициализирую модель на cpu...") _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: else:
raise raise
@@ -74,6 +76,7 @@ def _transcribe_file(
on_segment: Callable[[Segment], None] | None = None, on_segment: Callable[[Segment], None] | None = None,
on_status: Callable[[str], None] | None = None, on_status: Callable[[str], None] | None = None,
strict_device: bool = False, strict_device: bool = False,
cpu_threads: int = 0,
) -> TranscribeFileResult: ) -> TranscribeFileResult:
"""Транскрибирует один файл. При mid-stream fallback перезагружает модель.""" """Транскрибирует один файл. При mid-stream fallback перезагружает модель."""
lang_arg = language if language and language != "auto" else None lang_arg = language if language and language != "auto" else None
@@ -95,7 +98,7 @@ def _transcribe_file(
backend = get_backend("cpu") backend = get_backend("cpu")
model_path = backend.ensure_model_available(model_name, compute_type, on_status) model_path = backend.ensure_model_available(model_name, compute_type, on_status)
_notify_status(on_status, "Инициализирую модель на cpu...") _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, "Транскрибирую...") _notify_status(on_status, "Транскрибирую...")
result = backend.transcribe(model, file_path, lang_arg, on_segment, on_status) result = backend.transcribe(model, file_path, lang_arg, on_segment, on_status)
result.device_used = actual_device result.device_used = actual_device
@@ -120,16 +123,19 @@ def transcribe(
on_segment: Callable[[Segment], None] | None = None, on_segment: Callable[[Segment], None] | None = None,
on_status: Callable[[str], None] | None = None, on_status: Callable[[str], None] | None = None,
strict_device: bool = False, strict_device: bool = False,
cpu_threads: int = 0,
) -> TranscribeResult: ) -> TranscribeResult:
"""High-level API: загрузка модели + транскрипция за один вызов.""" """High-level API: загрузка модели + транскрипция за один вызов."""
model, actual_device, backend, model_path = load_model( model, actual_device, backend, model_path = load_model(
model_name, device, compute_type, on_status, strict_device, model_name, device, compute_type, on_status, strict_device,
compute_type_explicit=True, # Python API — caller explicitly chose compute_type compute_type_explicit=True, # Python API — caller explicitly chose compute_type
cpu_threads=cpu_threads,
) )
tfr = _transcribe_file( tfr = _transcribe_file(
model, actual_device, backend, model_path, model, actual_device, backend, model_path,
file_path, model_name, compute_type, file_path, model_name, compute_type,
language, on_segment, on_status, strict_device, language, on_segment, on_status, strict_device,
cpu_threads=cpu_threads,
) )
return tfr.result return tfr.result
+1 -1
View File
@@ -31,7 +31,7 @@ def test_resolve_repo_explicit_unsupported_pair_raises():
"""Явный --compute-type с несуществующей парой → ошибка.""" """Явный --compute-type с несуществующей парой → ошибка."""
backend = OpenVINOBackend(compute_type_explicit=True) backend = OpenVINOBackend(compute_type_explicit=True)
with pytest.raises(ValueError, match="недоступна с compute_type='fp16'"): 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(): def test_resolve_repo_explicit_unknown_model_raises():
+306
View File
@@ -2,6 +2,7 @@ from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from rich.console import Console
from typer.testing import CliRunner from typer.testing import CliRunner
from local_transcriber.cli import _format_device_info, app from local_transcriber.cli import _format_device_info, app
@@ -862,3 +863,308 @@ def test_cli_openvino_alias_resolves_to_gpu(tmp_path):
assert out.exit_code == 0 assert out.exit_code == 0
# detect_device("openvino") resolved to "openvino-gpu", load_model receives it # detect_device("openvino") resolved to "openvino-gpu", load_model receives it
assert mock_load_model.call_args[0][1] == "openvino-gpu" 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
+15
View File
@@ -113,6 +113,21 @@ def test_apply_device_defaults_cpu():
assert result["compute_type"] == "float32" assert result["compute_type"] == "float32"
def test_apply_device_defaults_onnx_uses_readable_model():
defaults = {
"model": "medium",
"language": "ru",
"device": "auto",
"compute_type": "float32",
}
cli = {"model": None, "language": None, "device": None, "compute_type": None}
result = apply_device_defaults(defaults, "onnx", cli, {})
assert result["model"] == "gigaam-v3-e2e-rnnt"
assert result["compute_type"] == "int8"
def test_apply_device_defaults_cli_overrides(): def test_apply_device_defaults_cli_overrides():
defaults = {"model": "large-v3", "language": "ru", "device": "auto", "compute_type": "int8"} defaults = {"model": "large-v3", "language": "ru", "device": "auto", "compute_type": "int8"}
cli = {"model": "large-v3", "language": None, "device": None, "compute_type": "int8"} cli = {"model": "large-v3", "language": None, "device": None, "compute_type": "int8"}
+128
View File
@@ -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()
+138
View File
@@ -172,3 +172,141 @@ def test_write_transcript(tmp_path):
out = tmp_path / "output.md" out = tmp_path / "output.md"
write_transcript("# Test content\n", out) write_transcript("# Test content\n", out)
assert out.read_text(encoding="utf-8") == "# Test content\n" 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
+388
View File
@@ -0,0 +1,388 @@
"""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-multilingual-large-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")
+125
View File
@@ -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, "Повтор")]) == []
+58
View File
@@ -321,6 +321,34 @@ def test_load_model_returns_backend_and_path(mock_get_backend):
assert actual_device == "cpu" 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 === # === _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" 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") @patch("local_transcriber.transcriber.get_backend")
def test_openvino_gpu_strict_device_no_fallback(mock_get_backend): def test_openvino_gpu_strict_device_no_fallback(mock_get_backend):
"""strict_device=True + OpenVINO GPU ошибка → raise.""" """strict_device=True + OpenVINO GPU ошибка → raise."""
Generated
+49 -299
View File
@@ -1,10 +1,6 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.10" requires-python = ">=3.13"
resolution-markers = [
"python_full_version >= '3.11'",
"python_full_version < '3.11'",
]
[[package]] [[package]]
name = "annotated-doc" name = "annotated-doc"
@@ -20,9 +16,7 @@ name = "anyio"
version = "4.12.1" version = "4.12.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" }, { 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" } 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 = [ wheels = [
@@ -35,13 +29,6 @@ version = "17.0.0"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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" }, { 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" version = "4.7.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "setuptools" }, { name = "setuptools" },
] ]
wheels = [ 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/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/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" }, { 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" }, { 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]] [[package]]
name = "faster-whisper" name = "faster-whisper"
version = "1.2.1" version = "1.2.1"
@@ -301,10 +260,11 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "faster-whisper" }, { name = "faster-whisper" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "onnx-asr", extra = ["cpu", "hub"] },
{ name = "onnxruntime" },
{ name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" },
{ name = "rich" }, { name = "rich" },
{ name = "socksio" }, { name = "socksio" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typer" }, { name = "typer" },
] ]
@@ -315,13 +275,14 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "faster-whisper", specifier = ">=1.2.1" }, { name = "faster-whisper", specifier = ">=1.2.1,<2" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4,<13" },
{ name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2025.0" }, { name = "onnx-asr", extras = ["cpu", "hub"], specifier = ">=0.12,<0.13" },
{ name = "rich" }, { name = "onnxruntime", specifier = ">=1.28,<2" },
{ name = "socksio", specifier = ">=1.0.0" }, { 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 = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, { name = "rich", specifier = ">=14.3.3,<15" },
{ name = "typer" }, { name = "socksio", specifier = ">=1.0.0,<2" },
{ name = "typer", specifier = ">=0.24.1,<1" },
] ]
[package.metadata.requires-dev] [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" }, { 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]] [[package]]
name = "numpy" name = "numpy"
version = "2.4.3" version = "2.4.3"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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" }, { 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/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/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/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]] [[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" }, { 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]] [[package]]
name = "onnxruntime" name = "onnxruntime"
version = "1.24.3" version = "1.28.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "flatbuffers" }, { name = "flatbuffers" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "packaging" }, { name = "packaging" },
{ name = "protobuf" }, { name = "protobuf" },
{ name = "sympy" },
] ]
wheels = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
{ 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" },
] ]
[[package]] [[package]]
@@ -556,17 +419,10 @@ name = "openvino"
version = "2026.0.0" version = "2026.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "openvino-telemetry" }, { name = "openvino-telemetry" },
] ]
wheels = [ 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/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/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" }, { 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" }, { name = "openvino-tokenizers" },
] ]
wheels = [ 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/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/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" }, { 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" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" }, { name = "iniconfig" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pygments" }, { 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" } 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 = [ wheels = [
@@ -684,34 +532,6 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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" }, { 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" }, { 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]] [[package]]
name = "tokenizers" name = "tokenizers"
version = "0.22.2" 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/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/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/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]] [[package]]