From 7e348e400f0a8ee96db8181ce87a2de8b0ea18d8 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 15:22:51 +0300 Subject: [PATCH 01/15] =?UTF-8?q?docs(diarization):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=80=D0=B0=D0=B7=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BD=20=D0=B1=D1=8D=D0=BA=D0=BB=D0=BE=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - схема из бэклога приписывала спикера целому ASR-сегменту, и до замера было неизвестно, насколько сильно это огрубляет результат. - Что: - добавлен разведочный замер sherpa-onnx на одной записи: 11,1x RTFx против 16,4x у ASR, свип порога кластеризации и доля загрязнённых сегментов. - пункт бэклога переписан: движок описан как практически закрытый вопрос, главной развилкой названа единица привязки спикера к тексту. - зафиксировано, что 27% сегментов содержат не менее секунды чужой речи и на них приходится больше половины времени транскрипта. - Проверка: - методика и условия замера воспроизводятся по разделам «Оборудование и условия» и «Контрольная запись» в docs/benchmarks/2026-08-12-diarization-feasibility.md. Co-Authored-By: Claude Opus 5 (1M context) --- docs/backlog.md | 57 +++-- .../2026-08-12-diarization-feasibility.md | 197 ++++++++++++++++++ 2 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 docs/benchmarks/2026-08-12-diarization-feasibility.md diff --git a/docs/backlog.md b/docs/backlog.md index f7d82a1..60dd751 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -181,27 +181,56 @@ LLM для чистки текста, сопоставление Speaker N с и ### Диаризация — разделение говорящих -**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): сегменты -уже несут таймкоды; диаризация даёт интервалы «кто когда говорил»; -merge по перекрытию интервалов; formatter ломает абзац на смене спикера -и подписывает `Speaker 1:`. Ставится как extra: -`uv sync --extra diarization`. +**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): диаризация +даёт интервалы «кто когда говорил», результат сводится с сегментами ASR, +formatter ломает абзац на смене спикера и подписывает `Speaker 1:`. +Ставится как extra: `uv sync --extra diarization`. **Почему:** Без спикеров MoM не собрать — это ключевой разрыв с облаком по внешнему ревью, и никакое качество распознавания его не компенсирует. Заодно естественно решает «разбивку на реплики» (приоритет №4). -**Варианты реализации (ключевое решение, нужен ADR):** +**Промежуточный статус 2026-08-12:** проведена разведка, описанная в +[разведочном замере диаризации](benchmarks/2026-08-12-diarization-feasibility.md). +Она закрыла вопрос о движке и открыла более важный вопрос о единице привязки. -- **sherpa-onnx** — диаризация целиком на onnxruntime (сегментация - pyannote в ONNX + спикер-эмбеддинги), без torch, в духе нашего - onnx-стека и «no cloud, no API keys». -- **pyannote.audio** — стандарт качества, но тянет torch и требует - HF-токен с принятием лицензии моделей — трение с духом проекта. +**Движок — вопрос практически закрыт.** `sherpa-onnx` ставится на Windows с +Python 3.13, содержит готовый `OfflineSpeakerDiarization`, не тянет torch и не +требует токена Hugging Face; модели сегментации и эмбеддингов весят около 33 МБ. +Скорость — 11,1× RTFx, то есть примерно полторы длительности ASR. Вариант +`pyannote.audio` остаётся отклонённым по прежней причине: torch и HF-токен с +принятием лицензии. Отдельный ADR имеет смысл заводить вместе с решением о +единице привязки, а не только про движок. -**Уточнить перед запуском:** качество обоих вариантов на русской речи -и перекрывающихся репликах; скорость на CPU (диаризация — второй проход -по всему аудио); лицензии моделей сегментации/эмбеддингов. +Замечание для будущих заходов: обе ML-части диаризации уже лежат в +`onnx-asr` 0.12 — `PyAnnoteVad` содержит полную локальную сегментацию pyannote +(powerset на трёх спикеров, склейка окон), а `WespeakerEmbeddings` даёт +эмбеддинги. Публичный API схлопывает сегментацию до речь/не-речь, `load_se` не +экспортирован, кластеризации нет. Собирать диаризацию самим на этих деталях — +экономия 33 МБ ценой опоры на приватный API; при разведке этот путь не +выбирался. + +**Единица привязки — настоящая развилка, решения нет.** Схема «мажоритарный +спикер на весь ASR-сегмент», записанная здесь раньше, замером не подтвердилась: +27% сегментов содержат не менее секунды чужой речи, и на них приходится больше +половины времени транскрипта. Причина — границы сегментов идут по тишине +(Silero VAD), а в ВКС собеседники отвечают встык. Варианты: + +- **пословная привязка** — `onnx-asr` отдаёт потокенные таймкоды + (`TimestampedResult`), сегмент режется на границе токена при смене + говорящего; ASR по-прежнему видит длинное аудио, контекст RNN-T и пунктуация + не страдают. Недоступно на OpenVINO GenAI — там потокенных таймкодов нет; +- **диаризация первым проходом**, ASR по интервалам говорящего — чистота + гарантирована, но короткие куски лишают RNN-T контекста и портят пунктуацию; +- **привязка к сегменту с честной пометкой** — оставить огрубление, но считать + чистоту и предупреждать в шапке, как уже делается для повторов и потери + хвоста. + +**Уточнить перед запуском:** воспроизводится ли доля 27% на других записях, +включая разговор на двоих; правильность границ диаризации на слух, а не только +совпадение числа говорящих; калибровка порога кластеризации (на пороге из +примеров получилось 29 спикеров вместо трёх); эмбеддинги, обученные не только +на английском; производительность на целевом Intel Core i5. --- diff --git a/docs/benchmarks/2026-08-12-diarization-feasibility.md b/docs/benchmarks/2026-08-12-diarization-feasibility.md new file mode 100644 index 0000000..084a8d0 --- /dev/null +++ b/docs/benchmarks/2026-08-12-diarization-feasibility.md @@ -0,0 +1,197 @@ +# Разведочный замер диаризации sherpa-onnx + +**Дата:** 2026-08-12 + +**Статус:** разведка на одной записи и одной нецелевой машине. Не приёмка. + +## Цель + +Ответить на два вопроса перед проектированием диаризации: + +1. Сколько времени диаризация добавляет к транскрипции. +2. Достаточно ли приписывать спикера целому ASR-сегменту по мажоритарному + перекрытию — то есть допустима ли схема из + [бэклога](../backlog.md#диаризация--разделение-говорящих) без пословной + привязки. + +Ни модель по умолчанию, ни код проекта в рамках замера не менялись. Все скрипты +выполнялись вне репозитория. + +## Ограничения замера + +Результаты ниже — разведка, а не основание для решения: + +- **одна запись** вместо трёх, принятых в [ADR-006](../adr/006-onnx-asr-backend.md); +- **нецелевая машина**: AMD Ryzen 7 8845H, тогда как приёмка производительности + по бэклогу требует Intel Core i5 11-го поколения; +- **границы диаризации не проверены на слух** — сверялось только число + говорящих и косвенный текстовый признак; +- **порог кластеризации подобран по этой же записи**, то есть на ней же и + проверен. + +## Оборудование и условия + +- ноутбук Lenovo 83D5 (та же машина, что в + [сравнении turbo](2026-08-12-openvino-large-v3-turbo-comparison.md#повторный-прогон-на-amd-ryzen-7-8845h)); +- AMD Ryzen 7 8845H, 8 ядер / 16 логических процессоров; +- 29,8 ГиБ LPDDR5X; +- Windows 11 Корпоративная, сборка 26200, схема питания «Сбалансированная»; +- Python 3.13.13, `onnx-asr` 0.12.0, `onnxruntime` 1.28.0, + `faster-whisper` 1.2.1, `sherpa-onnx` 1.13.5; +- прогоны последовательные, без конкурирующей нагрузки; +- модели предварительно скачаны; время загрузки моделей в замер не входит. + +## Контрольная запись + +| Файл | Длительность | Размер | SHA-256 | +|---|---|---|---| +| `2026-07-10 Data Test внутренний статус.mp4` | 26:00 | 34 217 769 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` | + +Запись выбрана потому, что рядом лежит согласованный MoM, из которого известен +состав: **три участника** — Маша, Дима, Роман. Это даёт независимую опорную +точку для проверки числа говорящих. + +## Модели диаризации + +| Роль | Модель | Размер | Источник | +|---|---|---|---| +| Сегментация | `sherpa-onnx-pyannote-segmentation-3-0` | 6,9 МБ | релизы `k2-fsa/sherpa-onnx` | +| Эмбеддинги | `wespeaker_en_voxceleb_resnet34_LM.onnx` | 26,5 МБ | релизы `k2-fsa/sherpa-onnx` | + +Обе загружаются в `onnxruntime`, torch и токен Hugging Face не требуются. +Эмбеддинги обучены на англоязычном VoxCeleb; их пригодность для русской речи в +этом замере не проверялась. + +## Стоимость по времени + +Параметры ASR — модель по умолчанию ONNX-пути, `gigaam-v3-e2e-rnnt` INT8, +язык задан явно. + +| Стадия | Время | RTFx | +|---|---:|---:| +| Декодирование аудио | 1,6 с | ~990× | +| ASR `gigaam-v3-e2e-rnnt` INT8 | 95,3 с | 16,4× | +| Диаризация, 8 потоков | 140,7 с | 11,1× | +| **Последовательно, итого** | **236 с** | **6,6×** | + +Диаризация дороже самой транскрипции и занимает около 60% общего времени. При +последовательном исполнении 26-минутная запись обрабатывается 3,9 минуты вместо +1,6; часовая — примерно 9 минут вместо 3,7. + +Масштабирование по потокам слабое: 4 потока дают 154 с, 8 потоков — 141 с, +выигрыш 9%. Закладываться на увеличение числа потоков не следует. + +Проходы ASR и диаризации независимы по данным, поэтому их можно совместить во +времени; тогда общее время стремится к максимуму из двух, а не к сумме. Прямой +замер параллельного режима не проводился. + +Пиковую память процесса снять не удалось из-за ошибки в измерительном скрипте. + +## Порог кластеризации + +Число говорящих подбиралось автоматически (`num_clusters=-1`); варьировался +порог `FastClusteringConfig.threshold`. + +| Конфигурация | Найдено спикеров | Из них с речью ≥30 с | Речь по спикерам, мин | +|---|---:|---:|---| +| порог 0,5 | 29 | 11 | 7,4 / 5,5 / 1,6 / 1,2 | +| порог 0,7 | 12 | 7 | 7,4 / 7,0 / 2,2 / 2,2 | +| **порог 0,9** | **4** | **3** | **9,6 / 8,1 / 5,7 / 0,3** | +| явное `k=5`, порог 0,5 | 4 | 3 | 9,6 / 8,1 / 5,7 / 0,3 | + +На пороге 0,9 число содержательных кластеров совпало с составом из MoM: три +говорящих с 9,6, 8,1 и 5,7 минуты речи плюс остаточный кластер на 0,3 минуты. +На пороге 0,5 получилось 29 говорящих вместо трёх — десятикратное +переразбиение. + +Время от порога не зависит (153–157 с во всех конфигурациях): кластеризация +стоит доли секунды, платится за сегментацию и эмбеддинги. + +Два следствия. Первое: порог кластеризации — основная ручка качества, и +значение по умолчанию из примеров `sherpa-onnx` для этого материала непригодно. +Второе: одного удачного совпадения на одной записи недостаточно, чтобы принять +0,9 за дефолт, а явное указание числа участников нужно как страховка. + +## Чистота ASR-сегментов + +Основной вопрос замера. Для каждого из 274 ASR-сегментов посчитано перекрытие +с интервалами каждого говорящего (диаризация на пороге 0,9, 340 интервалов). +Чистота — доля мажоритарного говорящего в суммарном перекрытии сегмента. + +| Категория | Сегментов | Доля | Времени | +|---|---:|---:|---:| +| Чистых, один говорящий | 164 | 59,9% | 8,3 мин | +| С поддакиванием, <1 с чужой речи | 30 | 10,9% | 2,4 мин | +| **С чужой репликой, ≥1 с** | **74** | **27,0%** | **11,2 мин** | +| Без спикера вообще | 6 | 2,2% | 0,0 мин | + +| Порог чистоты | Сегментов ниже порога | Доля | Времени | +|---|---:|---:|---:| +| < 0,95 | 100 | 36,5% | 13,0 мин | +| < 0,90 | 91 | 33,2% | 11,6 мин | +| < 0,80 | 72 | 26,3% | 9,1 мин | +| < 0,70 | 56 | 20,4% | 7,1 мин | + +**27% сегментов содержат не менее секунды чужой речи, и на них приходится +11,2 минуты из 21,9 — больше половины транскрипта.** У 20% сегментов +мажоритарный говорящий занимает менее двух третей сегмента. + +### Подтверждение из текста ASR + +Загрязнённые сегменты содержат диалог, и это видно независимо от диаризации — +`gigaam-v3-e2e` обучена с диалоговой пунктуацией и сама ставит тире на смене +реплики: + +``` +[533,8-551,1] 17,3 с, мажоритарный spk3, чистота 0,67 + «— В нашем, по-моему.— В нашем?— В нашем.— А, отлично.— Ну, у нас просто + есть некий дата-тест, который на самом деле...— Мы же у них не + разворачиваем.—» + +[432,9-448,6] 15,7 с, мажоритарный spk2, чистота 0,58 + «Дим, а вот то, что ты из образа вытаскивал, там есть чё-то на что + посмотреть?— Бэг, бэг там есть, пи» + +[694,2-706,5] 12,3 с, мажоритарный spk2, чистота 0,37 + spk2 = 5,4 с, spk1 = 4,9 с, spk3 = 4,3 с — три человека в одном сегменте +``` + +Акустическая диаризация и пунктуация ASR указывают на одно и то же, поэтому +доля 27% вряд ли объясняется только ошибками кластеризации. + +Причина загрязнения — в способе нарезки: границы сегментов на ONNX-пути даёт +Silero VAD, то есть они проходят по тишине. В разговоре по ВКС участники +отвечают встык, паузы длиной с порог VAD не возникает, и диалог попадает в один +сегмент. Предположение о том, что задержка канала связи сама создаёт паузу на +смене говорящего, этими данными не подтверждается. + +Приведённые сегменты — сырой выход ASR. `formatter.py` объединяет их в абзацы +длиной до 60 секунд, поэтому в готовом транскрипте загрязнение будет выше. + +## Выводы + +- Диаризация через `sherpa-onnx` работает на целевой платформе без torch и без + токена Hugging Face; связка сегментация + эмбеддинги весит около 33 МБ. +- Стоимость — 11,1× RTFx, примерно 1,5 длительности ASR. Последовательный + запуск даёт 2,5-кратное замедление, совмещение проходов может сократить + накладные расходы, но отдельно не измерялось. +- Автоматическая оценка числа говорящих с порогом из примеров даёт 29 спикеров + вместо трёх. Порог требует калибровки, а явное указание числа участников — + отдельной ручки. +- Привязка спикера к целому ASR-сегменту по мажоритарному перекрытию + огрубляет результат существенно: 27% сегментов и больше половины времени + транскрипта содержат чужую речь длиннее секунды. Схема из бэклога в этом виде + непригодна. +- `onnx-asr` отдаёт потокенные таймкоды (`TimestampedResult`), поэтому + пословная привязка на ONNX-пути достижима. У OpenVINO GenAI такого выхода + нет, что ограничивает диаризацию на `--device openvino-*`. + +## Что нужно проверить дальше + +- Повторить измерение чистоты сегментов ещё на двух-трёх записях, включая + разговор на двоих, и убедиться, что 27% — не свойство именно этой встречи. +- Проверить границы диаризации на слух или сверкой с внешним транскриптом: + совпадение числа говорящих не доказывает правильность интервалов. +- Сравнить эмбеддинги, обученные не только на английском, на русской речи. +- Измерить производительность на целевом Intel Core i5 11-го поколения. +- Измерить параллельный режим ASR и диаризации. -- 2.54.0 From a65cb74d88113fdd784e7cff1972a36600ec1c69 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 15:23:16 +0300 Subject: [PATCH 02/15] =?UTF-8?q?docs(agents):=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=B0=D1=80=D1=82=D1=8B=20wayfinder=20=D0=B2=20?= =?UTF-8?q?Gitea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - навык wayfinder ожидает раздел про операции карты в документе трекера, а в Gitea 1.27 нет подзадач, поэтому конвенции надо было зафиксировать явно. - Что: - описана принадлежность тикета карте через метку и ссылку в теле, поскольку родительских связей в API нет. - блокировки заведены на нативные зависимости Gitea, добавлены запросы фронтира. - зафиксированы три особенности tea api: путь без ведущего слэша, обязательные owner и repo в теле зависимости, нулевой код возврата при HTTP 404. - Проверка: - tea issues list --remote origin --labels wayfinder:map - tea api --remote origin repos/ddmitry/local-transcriber/issues/14/dependencies Co-Authored-By: Claude Opus 5 (1M context) --- docs/agents/issue-tracker.md | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md index 2c433a2..2c8013e 100644 --- a/docs/agents/issue-tracker.md +++ b/docs/agents/issue-tracker.md @@ -64,3 +64,55 @@ tea labels list --remote origin За пределами рабочего дерева явно указывать репозиторий `ddmitry/local-transcriber` и настроенный Gitea login. + +## Wayfinding operations + +Навык `wayfinder` ведёт карту как issue с меткой `wayfinder:map`, а её тикеты — +как отдельные issue с метками `wayfinder:research`, `wayfinder:prototype`, +`wayfinder:grilling` и `wayfinder:task`. + +### Принадлежность карте + +Gitea 1.27 не имеет подзадач в API: среди эндпоинтов `issues/{index}` есть +`dependencies` и `blocks`, но родительских связей нет. Поэтому принадлежность +тикета карте выражается двумя способами сразу: меткой `wayfinder:<тип>` и первой +строкой тела со ссылкой на карту. + +```markdown +Часть карты: [<заголовок карты>]() (#<номер>) +``` + +### Блокировки + +Блокировки — нативные зависимости Gitea, они отображаются в интерфейсе. Тикет +разблокирован, когда закрыты все блокирующие его тикеты. + +```powershell +tea api --remote origin -X POST ` + repos/ddmitry/local-transcriber/issues/<блокируемый>/dependencies ` + -d '{"index": <блокирующий>, "owner": "ddmitry", "repo": "local-transcriber"}' +``` + +### Запросы фронтира + +Фронтир — открытые, разблокированные и никому не назначенные тикеты карты. +Заявка на тикет — назначение его на себя до начала работы. + +```powershell +tea issues list --remote origin --labels wayfinder:map +tea issues list --remote origin --labels wayfinder:research,wayfinder:prototype,wayfinder:grilling,wayfinder:task +tea api --remote origin repos/ddmitry/local-transcriber/issues/<номер>/dependencies +``` + +### Особенности `tea api` + +Три вещи, на которых легко потерять время: + +- **Путь без ведущего слэша.** `repos/{owner}/{repo}/...` работает, + `/repos/...` возвращает `404 page not found`. Подстановка `{owner}` и `{repo}` + из контекста репозитория при этом не срабатывает — писать владельца и имя явно. +- **Тело зависимости требует `owner` и `repo`.** Только `{"index": N}` даёт + `repository does not exist [id: 0, uid: 0, owner_name: , name: ]`. +- **Код возврата не отражает HTTP-статус.** `tea api` завершается с нулевым + кодом даже на 404, поэтому скрипты должны запрашивать `-i` и разбирать строку + `HTTP/...` из stderr, иначе ошибки пройдут незамеченными. -- 2.54.0 From 87030e971817e06838020ffc83d512e134027e12 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 15:26:55 +0300 Subject: [PATCH 03/15] =?UTF-8?q?chore(git):=20=D0=BA=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=20.scratch=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D0=B8=D0=B3=D0=BD=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - рабочие материалы заходов (замеры, черновики, обвязка) должны попадать в историю вместе с веткой, а не жить только на машине разработчика. - Что: - строка .scratch/ удалена из .gitignore. - Проверка: - git check-ignore -v .scratch/ не должен ничего возвращать. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 14ed42f..12726ff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ __pycache__/ dist/ *.pyc .codex -.qwen/ -.scratch/ \ No newline at end of file +.qwen/ \ No newline at end of file -- 2.54.0 From 1cb6a36c9292b71280c8234521dc83f502b592f2 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 15:35:20 +0300 Subject: [PATCH 04/15] =?UTF-8?q?chore(diarization):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D0=B2=D1=8F?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20.scratch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - тикеты карты #10-#13 опираются на измерительную обвязку, которая до сих пор жила во временном каталоге сессии и исчезла бы вместе с ним. - Что: - перенесены четыре скрипта разведки: ASR, один прогон диаризации, свип порога кластеризации и подсчёт чистоты ASR-сегментов. - общая часть вынесена в common.py: пути от корня репозитория вместо захардкоженных, конфигурация диаризатора, проверка наличия моделей. - починен замер пиковой памяти: нужен экспорт K32GetProcessMemoryInfo из kernel32 и явные argtypes, иначе дескриптор процесса уезжает 32-битным. - модели и выход замеров исключены из истории локальным .gitignore. - Проверка: - export PYTHONIOENCODING=utf-8 - uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<запись>" 8 0.9 Co-Authored-By: Claude Opus 5 (1M context) --- .scratch/diarization/.gitignore | 6 + .scratch/diarization/README.md | 63 +++++++++++ .scratch/diarization/bench_asr.py | 49 ++++++++ .scratch/diarization/bench_conflict.py | 150 +++++++++++++++++++++++++ .scratch/diarization/bench_diar.py | 87 ++++++++++++++ .scratch/diarization/bench_sweep.py | 62 ++++++++++ .scratch/diarization/common.py | 133 ++++++++++++++++++++++ 7 files changed, 550 insertions(+) create mode 100644 .scratch/diarization/.gitignore create mode 100644 .scratch/diarization/README.md create mode 100644 .scratch/diarization/bench_asr.py create mode 100644 .scratch/diarization/bench_conflict.py create mode 100644 .scratch/diarization/bench_diar.py create mode 100644 .scratch/diarization/bench_sweep.py create mode 100644 .scratch/diarization/common.py diff --git a/.scratch/diarization/.gitignore b/.scratch/diarization/.gitignore new file mode 100644 index 0000000..7dfbded --- /dev/null +++ b/.scratch/diarization/.gitignore @@ -0,0 +1,6 @@ +# модели диаризации — 33 МБ, скачиваются по README +models/ + +# выход замеров +segments-*.tsv +conflict-*.json diff --git a/.scratch/diarization/README.md b/.scratch/diarization/README.md new file mode 100644 index 0000000..b046210 --- /dev/null +++ b/.scratch/diarization/README.md @@ -0,0 +1,63 @@ +# Обвязка замеров диаризации + +Исследовательские скрипты для карты +[Карта: диаризация спикеров в транскрипте](https://git.dementev.space/ddmitry/local-transcriber/issues/8) (#8). +Не часть пакета: они опираются на `sherpa-onnx`, которого нет в зависимостях +проекта, и живут в `.scratch/`, а не в `src/`. + +Результаты первого прогона описаны в +[разведочном замере](../../docs/benchmarks/2026-08-12-diarization-feasibility.md). + +## Модели + +Скачиваются один раз в `models/`, в git не попадают (см. `.gitignore` рядом). + +```bash +mkdir -p models && cd models +curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2 +tar xjf sherpa-onnx-pyannote-segmentation-3-0.tar.bz2 +curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/wespeaker_en_voxceleb_resnet34_LM.onnx +``` + +Сегментация — 6,9 МБ, эмбеддинги — 26,5 МБ. Опечатка `recongition` в URL +относится к самому релизу sherpa-onnx, это не ошибка набора. + +## Скрипты + +| Скрипт | Что делает | Тикеты | +|---|---|---| +| `bench_asr.py` | ASR тем же путём, что CLI: время, RTF, память | #13 | +| `bench_diar.py` | один прогон диаризации, сохраняет разметку в `segments-<порог>.tsv` | #12, #13 | +| `bench_sweep.py` | свип порога кластеризации и явного числа говорящих | #10 | +| `bench_conflict.py` | доля ASR-сегментов, внутри которых меняется говорящий | #11 | +| `common.py` | пути, конфигурация диаризатора, замер памяти | — | + +## Запуск + +Из корня репозитория. `PYTHONIOENCODING=utf-8` нужен, иначе вывод падает на +консоли cp1251. + +```bash +export PYTHONIOENCODING=utf-8 + +uv run python .scratch/diarization/bench_asr.py "<путь к записи>" +uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.9 +uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py "<путь>" +uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py "<путь>" +``` + +## Что стоит знать до запуска + +- **Порог кластеризации не откалиброван.** По умолчанию стоит 0,9 — значение из + разведки, подобранное на одной записи и на ней же проверенное. На пороге 0,5 + из примеров sherpa-onnx получалось 29 говорящих вместо трёх. Калибровка — это + тикет #10, до его закрытия любое значение считается временным. +- **Свип дорогой.** Каждая конфигурация — полный прогон сегментации и + эмбеддингов, около 2,5 минут на 26-минутную запись, и время от настроек + кластеризации практически не зависит. Свип вести на коротком фрагменте. +- **Чистота сегментов меряется относительно диаризации.** Если её границы + систематически смещены, метрика измеряет не то, что кажется. Проверка границ + на слух — тикет #12, и он намеренно идёт до калибровки. +- **Замер памяти чинился.** В разведке `psapi.GetProcessMemoryInfo` молча + возвращал ноль; `common.peak_rss_mb()` теперь зовёт `K32GetProcessMemoryInfo` + из kernel32 и проверяет код возврата. diff --git a/.scratch/diarization/bench_asr.py b/.scratch/diarization/bench_asr.py new file mode 100644 index 0000000..2d5225c --- /dev/null +++ b/.scratch/diarization/bench_asr.py @@ -0,0 +1,49 @@ +"""Замер ASR тем же путём, что использует CLI — для соотношения с диаризацией. + + uv run python .scratch/diarization/bench_asr.py <файл> [модель] + +sherpa-onnx здесь не нужен: скрипт зовёт бэкенд проекта напрямую. +""" + +from __future__ import annotations + +import sys +import time +from pathlib import Path + +from common import peak_rss_mb, use_project_sources + +use_project_sources() + +from local_transcriber.backends.onnx_asr import OnnxAsrBackend # noqa: E402 + +DEFAULT_MODEL = "gigaam-v3-e2e-rnnt" +COMPUTE_TYPE = "int8" + + +def main(audio_path: str, model_name: str) -> None: + backend = OnnxAsrBackend(compute_type_explicit=False) + + t0 = time.perf_counter() + model_path = backend.ensure_model_available(model_name, COMPUTE_TYPE) + model = backend.create_model(model_path, "onnx", COMPUTE_TYPE) + t_load = time.perf_counter() - t0 + + t0 = time.perf_counter() + result = backend.transcribe(model, Path(audio_path), "ru") + t_asr = time.perf_counter() - t0 + rss = peak_rss_mb() + + print(f"файл: {audio_path}") + print(f"модель: {model_name} ({COMPUTE_TYPE})") + print(f"длительность: {result.duration / 60:.1f} мин") + print(f"загрузка модели: {t_load:.1f} с") + print(f"ASR: {t_asr:.1f} с -> {result.duration / t_asr:.1f}x RTF") + print(f"пиковая память процесса: {rss:.0f} МБ" if rss else "память: снять не удалось") + print(f"сегментов: {len(result.segments)}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise SystemExit(__doc__) + main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else DEFAULT_MODEL) diff --git a/.scratch/diarization/bench_conflict.py b/.scratch/diarization/bench_conflict.py new file mode 100644 index 0000000..46664ca --- /dev/null +++ b/.scratch/diarization/bench_conflict.py @@ -0,0 +1,150 @@ +"""Чистота ASR-сегментов: как часто внутри одного сегмента меняется говорящий. + + uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py <файл> + +Прогоняет ASR и диаризацию по одному файлу и считает, какая доля ASR-сегментов +содержит чужую речь. Это мера того, насколько огрубляет привязка спикера к +целому сегменту по мажоритарному перекрытию. +""" + +from __future__ import annotations + +import json +import sys +import time +from collections import defaultdict +from pathlib import Path + +from common import ( + DEFAULT_THREADS, + DISCOVERY_THRESHOLD, + HERE, + load_audio, + make_diarizer, + use_project_sources, +) + +use_project_sources() + +ASR_MODEL = "gigaam-v3-e2e-rnnt" +COMPUTE_TYPE = "int8" + +# чужая речь короче порога — поддакивание, дольше — потерянная реплика +INTERJECTION_S = 1.0 + +PURITY_LEVELS = (0.95, 0.90, 0.80, 0.70) + + +def run_asr(audio_path: str): + from local_transcriber.backends.onnx_asr import OnnxAsrBackend + + backend = OnnxAsrBackend(compute_type_explicit=False) + path = backend.ensure_model_available(ASR_MODEL, COMPUTE_TYPE) + model = backend.create_model(path, "onnx", COMPUTE_TYPE) + t0 = time.perf_counter() + result = backend.transcribe(model, Path(audio_path), "ru") + print(f"ASR: {time.perf_counter() - t0:.0f} с, {len(result.segments)} сегм.") + return result + + +def run_diar(samples, threshold: float, threads: int): + diarizer = make_diarizer(threshold=threshold, threads=threads) + t0 = time.perf_counter() + segments = diarizer.process(samples).sort_by_start_time() + print(f"диаризация: {time.perf_counter() - t0:.0f} с, {len(segments)} интервалов") + return [(s.start, s.end, s.speaker) for s in segments] + + +def main(audio_path: str, threshold: float, threads: int) -> None: + samples = load_audio(audio_path) + asr = run_asr(audio_path) + diar = run_diar(samples, threshold, threads) + print(f"речи по диаризации: {sum(e - s for s, e, _ in diar) / 60:.1f} мин\n") + + rows = [] + for seg in asr.segments: + per_speaker: dict[int, float] = defaultdict(float) + for start, end, speaker in diar: + overlap = min(seg.end, end) - max(seg.start, start) + if overlap > 0: + per_speaker[speaker] += overlap + total = sum(per_speaker.values()) + if total <= 0: + rows.append((seg, None, 0.0, 0.0, {})) + continue + major = max(per_speaker, key=lambda k: per_speaker[k]) + rows.append( + (seg, major, per_speaker[major] / total, total - per_speaker[major], dict(per_speaker)) + ) + + n = len(rows) + unattributed = [r for r in rows if r[1] is None] + attributed = [r for r in rows if r[1] is not None] + lost = [r for r in attributed if r[3] >= INTERJECTION_S] + interjection = [r for r in attributed if 0 < r[3] < INTERJECTION_S] + clean = [r for r in attributed if r[3] == 0] + + def minutes(rs) -> float: + return sum(r[0].end - r[0].start for r in rs) / 60 + + print("=" * 64) + print(f"ASR-сегментов: {n} ({minutes(rows):.1f} мин)\n") + for label, group in ( + ("чистых (один говорящий)", clean), + (f"с поддакиванием (<{INTERJECTION_S:.0f} с чужой)", interjection), + (f"с чужой репликой (>={INTERJECTION_S:.0f} с)", lost), + ("без говорящего вообще", unattributed), + ): + print(f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин") + + print() + for level in PURITY_LEVELS: + bad = [r for r in attributed if r[2] < level] + print( + f" чистота мажоритарного < {level:.2f}: {len(bad):4d} сегм. " + f"({len(bad) / n * 100:.1f}%), {minutes(bad):.1f} мин" + ) + + print("\n" + "=" * 64) + print("ХУДШИЕ 12 СЕГМЕНТОВ (больше всего чужой речи внутри):") + for seg, major, purity, others, per_speaker in sorted(attributed, key=lambda r: -r[3])[:12]: + share = ", ".join( + f"spk{k}={v:.1f}с" for k, v in sorted(per_speaker.items(), key=lambda x: -x[1]) + ) + print( + f"\n [{seg.start:7.1f}-{seg.end:7.1f}] ({seg.end - seg.start:4.1f} с) " + f"мажор spk{major}, чистота {purity:.2f}, чужой {others:.1f} с" + ) + print(f" {share}") + print(f" «{seg.text.strip()[:150]}»") + + out = HERE / f"conflict-{Path(audio_path).stem[:40]}.json" + out.write_text( + json.dumps( + { + "file": Path(audio_path).name, + "threshold": threshold, + "asr_segments": n, + "clean": len(clean), + "interjection": len(interjection), + "lost_utterance": len(lost), + "unattributed": len(unattributed), + "minutes_lost_utterance": round(minutes(lost), 2), + "minutes_total": round(minutes(rows), 2), + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + print(f"\nсводка сохранена: {out.name}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise SystemExit(__doc__) + main( + sys.argv[1], + float(sys.argv[2]) if len(sys.argv) > 2 else DISCOVERY_THRESHOLD, + int(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_THREADS, + ) diff --git a/.scratch/diarization/bench_diar.py b/.scratch/diarization/bench_diar.py new file mode 100644 index 0000000..1ad3b94 --- /dev/null +++ b/.scratch/diarization/bench_diar.py @@ -0,0 +1,87 @@ +"""Один прогон диаризации: скорость, память, распределение по говорящим. + + uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py <файл> [потоки] + +Сохраняет разметку в ``segments-<порог>.tsv`` рядом со скриптом — она нужна +тикету про проверку границ на слух и скрипту bench_conflict.py. +""" + +from __future__ import annotations + +import sys +import time + +import numpy as np + +from common import ( + DEFAULT_THREADS, + DISCOVERY_THRESHOLD, + HERE, + SAMPLE_RATE, + load_audio, + make_diarizer, + peak_rss_mb, +) + + +def main(audio_path: str, threads: int, threshold: float) -> None: + print(f"файл: {audio_path}") + print(f"потоков: {threads}, порог кластеризации: {threshold}") + + t0 = time.perf_counter() + samples = load_audio(audio_path) + t_decode = time.perf_counter() - t0 + duration = len(samples) / SAMPLE_RATE + print(f"длительность: {duration / 60:.1f} мин ({duration:.0f} с)") + print(f"декодирование: {t_decode:.1f} с ({duration / t_decode:.0f}x RTF)") + + t0 = time.perf_counter() + diarizer = make_diarizer(threshold=threshold, threads=threads) + print(f"инициализация моделей: {time.perf_counter() - t0:.1f} с") + + progress = {"shown": 0.0} + t_start = time.perf_counter() + + def on_progress(processed: int, total: int, _arg=None) -> int: + pct = processed / total * 100 + if pct - progress["shown"] >= 20: + progress["shown"] = pct + print(f" ... {pct:.0f}% ({time.perf_counter() - t_start:.0f} с)", flush=True) + return 0 + + segments = diarizer.process(samples, callback=on_progress).sort_by_start_time() + t_diar = time.perf_counter() - t_start + + speakers = sorted({s.speaker for s in segments}) + speech = sum(s.end - s.start for s in segments) + rss = peak_rss_mb() + + print() + print(f"ДИАРИЗАЦИЯ: {t_diar:.1f} с -> {duration / t_diar:.1f}x RTF") + print(f"пиковая память процесса: {rss:.0f} МБ" if rss else "память: снять не удалось") + print(f"спикеров: {len(speakers)}, интервалов: {len(segments)}") + print(f"речи: {speech / 60:.1f} мин ({speech / duration * 100:.0f}% файла)") + print() + print("распределение по говорящим:") + for spk in speakers: + own = [s for s in segments if s.speaker == spk] + total = sum(s.end - s.start for s in own) + median = np.median([s.end - s.start for s in own]) + print(f" spk{spk:<3} {total / 60:6.1f} мин {len(own):4d} интерв. медиана {median:.1f} с") + + out = HERE / f"segments-{threshold}.tsv" + out.write_text( + "\n".join(f"{s.start:.3f}\t{s.end:.3f}\t{s.speaker}" for s in segments), + encoding="utf-8", + ) + print(f"\nразметка сохранена: {out.name}") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise SystemExit(__doc__) + main( + sys.argv[1], + int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_THREADS, + float(sys.argv[3]) if len(sys.argv) > 3 else DISCOVERY_THRESHOLD, + ) diff --git a/.scratch/diarization/bench_sweep.py b/.scratch/diarization/bench_sweep.py new file mode 100644 index 0000000..4943f07 --- /dev/null +++ b/.scratch/diarization/bench_sweep.py @@ -0,0 +1,62 @@ +"""Свип порога кластеризации и явного числа говорящих. + + uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py <файл> [потоки] + +Каждая конфигурация — полный прогон сегментации и эмбеддингов (около 2,5 минут +на 26-минутную запись), поэтому свип имеет смысл вести на коротком фрагменте, а +полные записи оставить для проверки финального кандидата. +""" + +from __future__ import annotations + +import sys +import time + +from common import DEFAULT_THREADS, SAMPLE_RATE, load_audio, make_diarizer + +# подпись, num_clusters, threshold +CONFIGS = [ + ("авто, порог 0.5", -1, 0.5), + ("авто, порог 0.7", -1, 0.7), + ("авто, порог 0.9", -1, 0.9), + ("явно k=5", 5, 0.5), +] + +# говорящий с речью короче порога считается остаточным кластером, не участником +MIN_SPEAKER_S = 30.0 + + +def main(audio_path: str, threads: int) -> None: + samples = load_audio(audio_path) + duration = len(samples) / SAMPLE_RATE + print(f"файл: {audio_path}") + print(f"длительность: {duration / 60:.1f} мин, потоков: {threads}\n") + + for label, num_clusters, threshold in CONFIGS: + diarizer = make_diarizer( + threshold=threshold, num_clusters=num_clusters, threads=threads + ) + t0 = time.perf_counter() + segments = diarizer.process(samples).sort_by_start_time() + elapsed = time.perf_counter() - t0 + + totals: dict[int, float] = {} + for seg in segments: + totals[seg.speaker] = totals.get(seg.speaker, 0.0) + (seg.end - seg.start) + real = [spk for spk, t in totals.items() if t >= MIN_SPEAKER_S] + top = sorted(totals.values(), reverse=True)[:8] + + print(f"--- {label}") + print( + f" {elapsed:.0f} с ({duration / elapsed:.1f}x RTF), " + f"говорящих: {len(totals)}, из них >= {MIN_SPEAKER_S:.0f} с речи: {len(real)}, " + f"интервалов: {len(segments)}" + ) + print(" топ по времени (мин): " + ", ".join(f"{t / 60:.1f}" for t in top)) + print() + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise SystemExit(__doc__) + main(sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_THREADS) diff --git a/.scratch/diarization/common.py b/.scratch/diarization/common.py new file mode 100644 index 0000000..56d4a35 --- /dev/null +++ b/.scratch/diarization/common.py @@ -0,0 +1,133 @@ +"""Общая обвязка для замеров диаризации. + +Скрипты в этом каталоге — исследовательские, не часть пакета. Они опираются на +``sherpa-onnx``, которого нет в зависимостях проекта, поэтому запускаются через +``uv run --with sherpa-onnx``. +""" + +from __future__ import annotations + +import ctypes +import ctypes.wintypes as wt +import sys +from pathlib import Path +from typing import Any + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[1] +MODELS = HERE / "models" + +SEGMENTATION = MODELS / "sherpa-onnx-pyannote-segmentation-3-0" / "model.onnx" +EMBEDDING = MODELS / "wespeaker_en_voxceleb_resnet34_LM.onnx" + +SAMPLE_RATE = 16_000 + +# Настройки разведки 2026-08-12. Порог 0.9 дал верное число говорящих на +# контрольной записи; на 0.5 из примеров sherpa-onnx получалось 29 вместо трёх. +DISCOVERY_THRESHOLD = 0.9 +DEFAULT_THREADS = 8 + + +def use_project_sources() -> None: + """Делает пакет проекта импортируемым без установки.""" + src = str(REPO_ROOT / "src") + if src not in sys.path: + sys.path.insert(0, src) + + +def require_models() -> None: + """Останавливает запуск с внятным сообщением, если модели не скачаны.""" + missing = [p for p in (SEGMENTATION, EMBEDDING) if not p.exists()] + if missing: + names = "\n ".join(str(p) for p in missing) + raise SystemExit( + f"Не найдены модели диаризации:\n {names}\n\n" + "Скачайте их по инструкции из README.md в этом каталоге." + ) + + +class _ProcessMemoryCounters(ctypes.Structure): + _fields_ = [ + ("cb", wt.DWORD), + ("PageFaultCount", wt.DWORD), + ("PeakWorkingSetSize", ctypes.c_size_t), + ("WorkingSetSize", ctypes.c_size_t), + ("QuotaPeakPagedPoolUsage", ctypes.c_size_t), + ("QuotaPagedPoolUsage", ctypes.c_size_t), + ("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t), + ("QuotaNonPagedPoolUsage", ctypes.c_size_t), + ("PagefileUsage", ctypes.c_size_t), + ("PeakPagefileUsage", ctypes.c_size_t), + ] + + +def peak_rss_mb() -> float | None: + """Пиковая рабочая память процесса в МБ; None, если снять не удалось. + + Два подвоха, на которых замер в разведке 2026-08-12 вернул ноль: + экспорт на современных Windows живёт в kernel32 как + ``K32GetProcessMemoryInfo``, а без явных ``restype``/``argtypes`` + псевдодескриптор процесса уезжает в вызов как 32-битное число и функция + молча не срабатывает. + """ + kernel32 = ctypes.windll.kernel32 + kernel32.GetCurrentProcess.restype = ctypes.c_void_p + handle = kernel32.GetCurrentProcess() + + pmc = _ProcessMemoryCounters() + pmc.cb = ctypes.sizeof(_ProcessMemoryCounters) + + for dll, name in ( + (kernel32, "K32GetProcessMemoryInfo"), + (ctypes.windll.psapi, "GetProcessMemoryInfo"), + ): + func = getattr(dll, name, None) + if func is None: + continue + func.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(_ProcessMemoryCounters), + wt.DWORD, + ] + func.restype = wt.BOOL + if func(handle, ctypes.byref(pmc), pmc.cb): + return pmc.PeakWorkingSetSize / 1024 / 1024 + return None + + +def load_audio(audio_path: str | Path): + """Декодирует файл в моно 16 кГц — тот же путь, что использует ONNX-бэкенд.""" + from faster_whisper import decode_audio + + return decode_audio(str(audio_path), sampling_rate=SAMPLE_RATE) + + +def make_diarizer( + threshold: float = DISCOVERY_THRESHOLD, + num_clusters: int = -1, + threads: int = DEFAULT_THREADS, +) -> Any: + """Собирает OfflineSpeakerDiarization с параметрами разведки.""" + import sherpa_onnx as so + + require_models() + config = so.OfflineSpeakerDiarizationConfig( + segmentation=so.OfflineSpeakerSegmentationModelConfig( + pyannote=so.OfflineSpeakerSegmentationPyannoteModelConfig( + model=str(SEGMENTATION) + ), + num_threads=threads, + provider="cpu", + ), + embedding=so.SpeakerEmbeddingExtractorConfig( + model=str(EMBEDDING), num_threads=threads, provider="cpu" + ), + clustering=so.FastClusteringConfig( + num_clusters=num_clusters, threshold=threshold + ), + min_duration_on=0.3, + min_duration_off=0.5, + ) + diarizer = so.OfflineSpeakerDiarization(config) + assert diarizer.sample_rate == SAMPLE_RATE, diarizer.sample_rate + return diarizer -- 2.54.0 From 4c7f2920c1a7729807557771d02377a8940c3303 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 15:55:06 +0300 Subject: [PATCH 05/15] =?UTF-8?q?docs(research):=20=D0=B8=D1=81=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=B6=D0=B8=D0=B7?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=86=D0=B8=D0=BA=D0=BB?= =?UTF-8?q?=20ORT=20=D0=B8=20OpenVINO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужна фактическая опора для решений карты о диаризации на Intel-пути. - Что: - исследованы жизненные циклы ORT, OpenVINO, DirectML и Windows ML. - сравнены пути AMD, Apple Silicon и браузерные EP для текущих моделей. - зафиксированы wheel-матрицы, fallback и необходимые model-specific тесты. - Проверка: - git diff --cached --check. --- ...6-08-12-onnx-runtime-openvino-lifecycle.md | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md diff --git a/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md b/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md new file mode 100644 index 0000000..bd8a29e --- /dev/null +++ b/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md @@ -0,0 +1,410 @@ +# Куда движутся ONNX Runtime и OpenVINO: сравнение жизненного цикла + +**Дата:** 2026-08-12 + +**Статус:** исследование для карты диаризации. Не архитектурное решение и не +основание для консолидации всех движков распознавания на ONNX Runtime. + +## Вопрос + +Насколько устойчивы ONNX Runtime, его DirectML и OpenVINO Execution Provider, +а также нативный стек OpenVINO/OpenVINO GenAI; какой из путей с большей +вероятностью сохранит Intel-ускорение и поддержку Whisper/NPU; что из этого +практически доступно проекту на CPython 3.13. + +Исследование опирается только на первичные источники: официальную документацию, +release notes, репозитории владельцев и метаданные PyPI. + +## Границы в контексте проекта + +Терминология следует [`CONTEXT.md`](../../CONTEXT.md): ONNX Runtime, OpenVINO и +OpenVINO GenAI здесь — **движки распознавания**, их обновление само по себе не +меняет поддерживаемую модель или модель по умолчанию. Архитектурная точка +отсчёта — отдельные pluggable backends из [ADR-003](../adr/003-pluggable-backends.md) +и принятый ONNX CPU-путь из [ADR-006](../adr/006-onnx-asr-backend.md). + +Исследование дополняет [срез обновлений движков](2026-08-10-engine-model-updates.md) +и отвечает на инфраструктурный вопрос, открытый +[разведкой диаризации](../benchmarks/2026-08-12-diarization-feasibility.md) и +[бэклогом](../backlog.md#диаризация--разделение-говорящих). Оно не пересматривает +качество моделей и не принимает решение о полной консолидации на ORT. + +## Краткий вывод + +Расширенный portability-срез не меняет исходный lifecycle-вывод, но уточняет его: ORT CPU остаётся наиболее ровным baseline на AMD Windows/Linux и Apple Silicon; Windows ML — уже production-поставка ORT для Windows с cp313, хотя vendor EP требуют bootstrap/registration; DirectML остаётся legacy; Linux AMD движется к MIGraphX; Apple accelerator-путь — CoreML Preview с обязательной проверкой partitioning. Наличие wheel/provider не подтверждает совместимость GigaAM или двух diarization graphs и тем более полный offload. + +Браузер показывает тот же устойчивый pattern: ONNX — artifact, ORT Web — отдельный runtime, WASM — portable baseline, WebGPU/WebNN — optional accelerators с operator subset и fallback. Это portability evidence, а не предложение browser product или консолидации всего проекта на ORT. + +1. **ONNX Runtime — активно развиваемый, production-стабильный движок, но не + все его EP имеют одинаковый жизненный цикл.** Версии 1.26, 1.27 и 1.28 + вышли 8 мая, 19 июня и 25 июля 2026 года, то есть три minor-релиза примерно + за одиннадцать недель; 1.28 продолжает развивать plugin EP API, ядро, + безопасность и аппаратные EP ([1.26.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0), + [1.27.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.27.0), + [1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). +2. **DirectML EP поддерживается, но переведён в sustained engineering.** Новая + функциональность Windows-пути перенесена в WinML; Microsoft рекомендует + WinML для новых Windows-развёртываний, а DirectML EP оставляет для legacy и + специальных сценариев ([официальная страница DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html), + [Windows-путь ORT](https://onnxruntime.ai/docs/get-started/with-windows.html)). + Поэтому `onnxruntime-directml` нельзя считать перспективным + кросс-вендорным GPU-дефолтом проекта, хотя пакет не заброшен. +3. **OpenVINO и OpenVINO GenAI — основной активно развиваемый Intel-стек.** В + 2026 году регулярные релизы вышли 23 февраля, 7 апреля, 28 мая и 4 августа; + OpenVINO публикует формальную release/LTS policy, где регулярная версия + поддерживается до следующей, а последняя версия года становится LTS с двумя + годами security updates ([release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html), + [release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)). +4. **Для Intel-ускорения более долгоживущая ставка — сам OpenVINO, а не + конкретная обвязка ORT OpenVINO EP.** EP остаётся активным мостом из ORT к + OpenVINO, но зависит сразу от двух release train и его готовые wheel заметно + отстают от обоих ядер. Нативный OpenVINO одновременно является runtime для + CPU/GPU/NPU, имеет собственную LTS policy и служит основанием OpenVINO GenAI + ([OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), + [GenAI как расширение runtime](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai.html)). + Это не означает, что проекту нужно переносить ONNX-путь на OpenVINO или + консолидироваться на ORT: ONNX-модели сохраняют переносимость, а выбор + движка распознавания остаётся отдельным решением по качеству и контракту. +5. **CPython 3.13 не блокирует ни один из трёх исследованных PyPI-пакетов на + целевой Windows x86-64**, но матрицы платформ радикально различаются: + `onnxruntime` кроссплатформенный, DirectML только Windows x86-64, + OpenVINO EP только Windows/Linux x86-64 + ([onnxruntime 1.28.0 files](https://pypi.org/project/onnxruntime/1.28.0/#files), + [DirectML 1.24.4 files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files), + [OpenVINO EP 1.24.1 files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)). + +## Что именно является чем + +Слои нельзя сравнивать как взаимозаменяемые пакеты: + +| Слой | Роль | Что фиксирует приложение | +|---|---|---| +| ONNX | Формат графа, операторов и типов данных; операторы исполняются внешней реализацией | Артефакт модели и его opset ([ONNX About](https://onnx.ai/about)) | +| ONNX Runtime | Движок выполнения ONNX-графа, который разбивает его между EP и CPU fallback | API сессии, версия ORT и набор EP ([архитектура ORT](https://onnxruntime.ai/docs/reference/high-level-design.html)) | +| OpenVINO | Intel runtime, компилятор и device plugins для CPU/GPU/NPU; умеет принимать в том числе ONNX-графы | API OpenVINO и поддерживаемые устройства/форматы ([поддержанные модели](https://docs.openvino.ai/2026/documentation/compatibility-and-support/supported-models.html)) | +| OpenVINO EP | Адаптер внутри ORT: получает поддержанные подграфы, переводит и компилирует их для OpenVINO | Одновременно контракты ORT, EP и совместимой версии OpenVINO ([EP architecture](https://onnxruntime.ai/docs/execution-providers/), [OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)) | +| OpenVINO GenAI | Высокоуровневые генеративные pipelines поверх OpenVINO runtime, включая Whisper и общий ASR API | Формат моделей OpenVINO IR, pipeline API и согласованные версии OpenVINO/Tokenizers/GenAI ([GenAI PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)) | + +Следствие: **модель в ONNX не означает ONNX Runtime**, а **OpenVINO EP не +является форматом модели**. Один ONNX-артефакт можно исполнять CPU EP в ORT, +передавать поддержанные подграфы OpenVINO EP либо загружать в OpenVINO +напрямую; однако покрытие операторов, квантование, fallback и производительность +у этих путей различаются ([ORT EP partitioning](https://onnxruntime.ai/docs/execution-providers/), +[OpenVINO EP support coverage](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), +[прямое чтение ONNX в OpenVINO](https://docs.openvino.ai/2026/openvino-workflow/model-preparation/convert-model-onnx.html)). + +## ONNX Runtime и execution providers + +### Ядро ORT + +Официальные страницы расходятся в обещанном cadence: servicing-документ всё +ещё говорит о full releases «примерно ежеквартально», тогда как roadmap — о +ежемесячных релизах и patch-релизах между ними. Формального LTS/EOL-окна в +публичной support policy нет. Поэтому для планирования надёжнее опираться на +фактические публикации и backward-compatibility policy, а не превращать +текущий почти месячный темп в гарантию +([releases and servicing](https://onnxruntime.ai/docs/reference/releases-servicing.html), +[roadmap](https://onnxruntime.ai/roadmap), +[support policy](https://github.com/microsoft/onnxruntime/blob/main/SUPPORT.md)). + +ORT 1.23 начал переход к независимо подключаемым plugin EP и прямо рекомендует +новые EP реализовывать как plugins, а не добавлять внутрь ядра. В 1.24–1.28 +plugin API последовательно получал prepacking, EP Context, zero-copy I/O, +profiling и model packages ([инструкция для нового EP](https://onnxruntime.ai/docs/execution-providers/add-execution-provider.html), +[релиз 1.24.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.24.1), +[релиз 1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). +Это сильный сигнал продолжения ORT как общего движка, но одновременно сигнал, +что жизненный цикл конкретного аппаратного backend всё больше принадлежит его +поставщику, а не ядру ORT. + +### DirectML EP + +Официальная формулировка однозначна: DirectML находится в **sustained +engineering**, поддержка продолжается, но feature development перешёл в WinML. +Документация также фиксирует DirectML 1.15.2 и покрытие только до ONNX opset 20; +модели с более высоким требованием официально не поддерживаются +([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)). + +Это согласуется с поставкой: последний `onnxruntime-directml` на дату среза — +1.24.4 от 17 марта, тогда как ядро ORT уже 1.28.0. При этом ORT 1.28 всё ещё +содержит исправление DML readback, то есть sustained engineering означает не +«удалён», а «исправления без прежнего темпа новых возможностей» +([DirectML на PyPI](https://pypi.org/project/onnxruntime-directml/), +[ORT 1.28, DML fix](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). + +Для проекта DirectML остаётся возможным Windows-only экспериментом на AMD, +Intel и NVIDIA GPU, но его стратегический successor — WinML, который требует +Windows-специфической интеграции. Это слабее текущего требования ADR-003 о +плаггируемых бэкендах и кроссплатформенном ONNX CPU-пути. + +### OpenVINO EP + +Официальная документация не объявляет OpenVINO EP deprecated или maintenance-only. +Наоборот, Intel публикует готовые пакеты, принимает issues/PR, заявляет CPU, +интегрированные и дискретные GPU и NPU, а ORT 1.26 и 1.28 содержат OpenVINO EP +development updates ([страница пакета](https://pypi.org/project/onnxruntime-openvino/), +[ORT 1.26](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0), +[ORT 1.28](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). + +Но готовая поставка имеет свой темп. Последний wheel `onnxruntime-openvino` +1.24.1 от 26 февраля 2026 года включает OpenVINO 2025.4.1 на Linux и требует +отдельной установки OpenVINO на Windows. Официальная таблица совместимости +покрывает только три версии OpenVINO: ORT-EP 1.22/2025.1, +1.23/2025.3 и 1.24.1/2025.4.1 +([PyPI](https://pypi.org/project/onnxruntime-openvino/1.24.1/), +[матрица совместимости](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)). +На дату среза нативный OpenVINO уже 2026.3, а ядро ORT — 1.28. Значит, EP +активен, но готовый Python-путь не является способом автоматически получить +самые новые возможности OpenVINO/NPU. + +Начиная с ORT 1.23 часть старых provider options OpenVINO EP deprecated в +пользу `load_config` с нативными OpenVINO properties. Это локальная миграция +конфигурации, а не deprecation самого EP +([deprecation notice](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)). + +## OpenVINO, OpenVINO GenAI, Whisper и Intel NPU + +OpenVINO имеет явно описанный цикл: несколько регулярных релизов в год, +поддержка каждого до следующего и ежегодный LTS. LTS получает security updates +два года либо до двух следующих LTS, а исправления новых bugs — один год; +preview-компоненты этой гарантией не покрываются +([release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)). + +OpenVINO GenAI — не конкурирующий runtime, а библиотека pipelines поверх +OpenVINO и OpenVINO Tokenizers. Их `major.minor.patch` должны совпадать: +разъезд может привести к ABI/import errors; PyPI wheel нельзя смешивать с C++ +archives другого ABI ([официальные правила совместимости](https://pypi.org/project/openvino-genai/2026.3.0.0/)). + +Whisper — активный, а не legacy use case OpenVINO GenAI: + +- OpenVINO 2026.0 добавил word-level timestamps в WhisperPipeline на CPU, GPU + и NPU; в 2026.3 результаты также содержат определённый/заданный язык, а NPU + отдаёт word timestamps по умолчанию + ([2026.0 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0), + [2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); +- OpenVINO 2026.3 ввёл общий `ASRPipeline` и поддержку Qwen3-ASR, то есть + speech API расширяется за пределы Whisper + ([2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); +- удалён только ранее deprecated **stateless decoder** Whisper; рекомендуемый + путь — stateful model, а не отказ от Whisper + ([deprecation section](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#deprecation-and-support)). + +NPU является первым классом устройств OpenVINO: NPU plugin доступен в +дистрибутивах, целевая аппаратная платформа начинается с Intel Core Ultra, +Compiler-In-Plugin появился preview в 2026.0 и стал предпочитаемым компилятором +в 2026.1. При этом NPU требует отдельный driver, поддерживает только static +shapes, а совместимость предкомпилированных blobs между версиями OpenVINO не +гарантируется ([NPU device](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html)). +WhisperPipeline официально работает на NPU с tiny/base/small/large без +специальных ограничений pipeline, но документация рекомендует актуальный NPU +driver и даёт workaround для memory failures +([Whisper on NPU](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai/inference-with-genai-on-npu.html#whisper-inference-on-npu)). + +Это не делает NPU актуальным для целевого Intel Core i5 11-го поколения: NPU +появился только в Core Ultra. Для текущей целевой машины долгоживущий +OpenVINO-путь означает прежде всего CPU/iGPU; NPU — будущий аппаратный профиль, +который нужно выбирать явно, тем более что OpenVINO AUTO пока исключает NPU из +дефолтного приоритета +([NPU hardware](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html), +[AUTO priority](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/auto-device-selection.html)). + +## PyPI: версии, платформы и CPython 3.13 + +Срез сделан по фактически опубликованным wheel, а не по classifiers страницы. +У всех трёх пакетов отсутствует source distribution, поэтому неподдержанная +комбинация платформы и Python не сможет штатно собраться через обычный +`pip install` без самостоятельной сборки из репозитория. + +| Пакет | Последняя версия на 2026-08-12 | Последняя публикация | wheel для CPython 3.13 | Платформы cp313 | +|---|---:|---:|---|---| +| `onnxruntime` | 1.28.0 | 2026-07-25 | Да | Windows x86-64 и ARM64; Linux x86-64 и ARM64 (glibc 2.27/2.28+); macOS 14+ ARM64 ([files](https://pypi.org/project/onnxruntime/1.28.0/#files)) | +| `onnxruntime-directml` | 1.24.4 | 2026-03-17 | Да | Только Windows x86-64; нет Linux, macOS и Windows ARM64 wheel ([files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files)) | +| `onnxruntime-openvino` | 1.24.1 | 2026-02-26 | Да | Windows x86-64 и Linux x86-64 с glibc 2.28+; нет ARM64 и macOS wheel ([files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)) | + +Практический вывод для ADR-003 сохраняется: обычный `onnxruntime` остаётся +широким zero-config CPU-путём. DirectML и OpenVINO EP нельзя подставить как одну +безусловную зависимость на всех платформах; они требуют platform markers и +отдельных проверок доступного provider. На Windows OpenVINO EP дополнительно +требует совместимый `openvino`, а на Linux wheel уже включает конкретный +OpenVINO 2025.4.1 +([OpenVINO EP installation](https://pypi.org/project/onnxruntime-openvino/1.24.1/)). + +## Windows ML: следующий Windows-слой над ORT + +### Что доступно сейчас, а что пока preview + +> Artifact caveat: Microsoft Learn ещё указывает Python 3.10–3.13, тогда как текущий `onnxruntime-windowsml` требует Python 3.11+ и уже имеет cp314. Для установки источником истины служит фактическая wheel matrix PyPI; cp313 подтверждён для x64 и ARM64 ([PyPI JSON](https://pypi.org/pypi/onnxruntime-windowsml/json)). + +**Подтверждено.** Windows ML — не новый формат модели и не замена ONNX Runtime: Microsoft описывает его как Windows-фреймворк локального инференса, **powered by ONNX Runtime**. Runtime содержит `onnxruntime.dll`, DirectML и Windows ML API; обычный инференс по-прежнему создаёт ORT `InferenceSession`. Windows ML добавляет обнаружение устройств, каталог совместимых vendor EP, их установку, регистрацию и обновление. DirectML остаётся встроенным legacy GPU EP; MIGraphX/VitisAI/OpenVINO/QNN/NvTensorRtRtx поставляются через каталог или вместе с приложением ([обзор](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview), [состав и deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app), [API](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/api-reference)). + +Для Python реальная поставка называется `onnxruntime-windowsml`, а не анонсированная ранее `onnxruntime-winml`. На 2026-08-12 последняя версия — `1.27.1.202607110137`, статус PyPI `Production/Stable`, `Requires-Python >=3.11`; есть `cp313` wheels для `win_amd64` и `win_arm64` ([PyPI](https://pypi.org/project/onnxruntime-windowsml/)). Это уже доступный продуктовый путь. Отдельная **ONNX Runtime GenAI Windows ML library 0.x** прямо обозначена как Preview; её нельзя переносить на статус обычного ONNX-инференса ([GenAI Preview](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/run-genai-onnx-models)). + +Python поддержан на 3.10–3.13, x64/ARM64, но только как framework-dependent unpackaged app: нужны Python с python.org/winget, Windows App SDK Runtime и bootstrap-пакеты. Self-contained deployment для Python не предусмотрен. Базовый runtime может работать на поддерживаемых Windows 10, но динамический каталог аппаратных EP требует Windows 11 24H2, build 26100+ ([getting started](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/get-started), [deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app), [поддерживаемые EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). + +**Существенный caveat для CLI.** Каталог скачивает и обновляет EP, однако Python-приложение должно перечислить подходящие EP, вызвать `ensure_ready_async()` и зарегистрировать библиотеку через `onnxruntime.register_execution_provider_library`. Microsoft отдельно предупреждает, что `EnsureAndRegisterCertifiedAsync()` не регистрирует EP в Python ORT environment ([инициализация EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/initialize-execution-providers), [выбор EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/select-execution-providers)). Поэтому: + +- `onnxruntime-windowsml` + встроенный DirectML — практически применимая замена ORT-дистрибутива для `onnx-asr`; upstream 0.12 документирует именно эту установку и тот же providers API ([installation](https://istupakov.github.io/onnx-asr/installation/)); +- vendor EP из каталога (например, MIGraphX) не появится в существующем `local-transcriber` без Windows App SDK bootstrap/registration glue; +- device policy (`MAX_PERFORMANCE`, `PREFER_NPU` и другие) — пожелание к выбору, а не доказательство полного offload конкретной модели. Нужны capability discovery, session profiling и проверка fallback. + +## AMD и Apple: практические пути без кастомной сборки + +### AMD CPU и GPU + +На AMD x86 CPU обычный `onnxruntime` использует portable CPU EP: это готовый baseline на Windows и Linux. OpenVINO 2026.3 официально перечисляет Intel и ARM/Apple CPU, но **не перечисляет AMD x86 в supported hardware**; наличие x86 wheel ещё не является обещанием поддержки AMD CPU ([OpenVINO system requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html)). Поэтому OpenVINO на AMD x86 — только экспериментальный путь, не поддерживаемая опора проекта. + +На AMD GPU под Windows есть два готовых пути: + +1. `onnxruntime-directml` 1.24.4 (`cp313-win_amd64`): DirectML официально поддерживает AMD GCN первого поколения и новее, но находится в sustained engineering, ограничен ONNX opset 20 и не гарантирует полный offload ([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)). +2. Windows ML 2.x: встроенный DirectML или загружаемый MIGraphX. Каталог MIGraphX доступен только на Windows 11 24H2+ и при совместимых GPU/driver; Microsoft отдельно отмечает, что текущий MIGraphX EP не поддерживает GenAI scenarios ([Windows ML EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). Для GigaAM RNN-T граница понятия GenAI в этой таблице не определена — нужен локальный тест, а не перенос ограничения по аналогии. + +На AMD GPU под Linux прежний ROCm EP удалён из source tree начиная с ORT 1.23; Microsoft рекомендует MIGraphX или VitisAI ([ORT 1.23](https://github.com/microsoft/onnxruntime/releases/tag/v1.23.0)). Старый `onnxruntime-rocm` всё ещё публикуется на PyPI (последний `1.22.2.post3`, включая `cp313`), но остаётся на ветке до удаления EP и потому не является долгоживущим направлением ([PyPI JSON](https://pypi.org/pypi/onnxruntime-rocm/json)). Активный `onnxruntime-migraphx` уже имеет версию `1.27.1` и `cp313-manylinux_2_34_x86_64`; это готовый wheel, хотя он требует совместимых ROCm/GPU/OS и отличается от core ORT 1.28 ([PyPI JSON](https://pypi.org/pypi/onnxruntime-migraphx/json), [MIGraphX EP](https://onnxruntime.ai/docs/execution-providers/MIGraphX-ExecutionProvider.html)). Следовательно, Python 3.13 больше не блокирует установку; реальными неизвестными остаются системный ROCm stack и совместимость конкретных графов. + +### Apple Silicon + +`onnxruntime` 1.28.0 публикует `cp313` wheel для macOS 14 ARM64; это готовый CPU baseline. Тот же macOS build доступен `onnx-asr`, который документирует `CPUExecutionProvider` и `CoreMLExecutionProvider` в обычном пакете ([ORT Python install](https://onnxruntime.ai/docs/get-started/with-python.html), [onnx-asr installation](https://istupakov.github.io/onnx-asr/installation/)). + +CoreML EP может задействовать CPU, GPU и Apple Neural Engine через `MLComputeUnits`. Он забирает поддерживаемые subgraphs, допускает динамические shapes, но предупреждает об их возможной цене; для `Loop`/`Scan`/`If` offload внутри тела по умолчанию выключен. Параметры `RequireStaticInputShapes`, `EnableOnSubgraphs` и `ProfileComputePlan` позволяют проверить фактическое размещение. Сам EP в общей таблице ORT всё ещё помечен Preview ([CoreML EP](https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html), [таблица EP](https://onnxruntime.ai/docs/execution-providers/)). Следовательно, «CoreML доступен» не означает «GigaAM целиком работает на ANE». + +OpenVINO 2026.3 и OpenVINO GenAI 2026.3 имеют `cp313-macosx_11_0_arm64` wheels и официально поддерживают Apple silicon, но на macOS OpenVINO выполняет inference только на CPU; GPU plugin поддерживает только Intel GPU, NPU plugin — Intel NPU ([system requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html), [openvino-genai PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)). Значит OpenVINO Whisper на Apple Silicon технически поставляется без сборки, но не использует GPU/ANE. Текущий marker проекта, исключающий OpenVINO extra на macOS, остаётся отдельным integration constraint. + +### Сводная матрица runtime/device + +| Платформа и устройство | Готовый runtime/EP | Статус на 2026-08-12 | `cp313` | Без кастомной сборки в текущем CLI | +|---|---|---|---|---| +| Windows, AMD CPU | ORT CPU EP | production baseline | да, `win_amd64` | да | +| Windows, AMD GPU | ORT DirectML | sustained engineering | да, `onnxruntime-directml` | пакет есть; нужен новый provider/device UX | +| Windows 11 24H2+, AMD GPU | Windows ML + DirectML/MIGraphX | Windows ML production; catalog EP зависит от driver/device | да, `onnxruntime-windowsml` | DirectML близко к готовому; MIGraphX требует bootstrap/registration | +| Linux, AMD CPU | ORT CPU EP | production baseline | да, `manylinux x86_64` | да | +| Linux, AMD GPU | ORT MIGraphX | active replacement for removed ROCm EP | да, `onnxruntime-migraphx 1.27.1` | wheel есть; нужны ROCm compatibility и model smoke-test | +| Apple Silicon, CPU | ORT CPU EP | production baseline | да, macOS 14 ARM64 | да | +| Apple Silicon, GPU/ANE | CoreML EP | preview; partial partitioning possible | да, в обычном ORT wheel | пакет есть; требуется provider integration и profiling | +| Apple Silicon, CPU | OpenVINO GenAI Whisper | production package; CPU-only на macOS | да | upstream да; текущий project extra/marker — нет | + +## Матрица моделей: что действительно переносится + +Легенда: **подтверждено** — есть прямое upstream-обещание/поставка; **вывод** — следует из одинакового ONNX/ORT контракта, но нет проверки данной модели; **эксперимент** — session/model compatibility и offload неизвестны. + +| Модель/контракт | ORT CPU (AMD Win/Linux, Apple) | AMD GPU Windows (DML/WinML) | AMD GPU Linux (MIGraphX) | Apple GPU/ANE (CoreML) | OpenVINO native | +|---|---|---|---|---|---| +| GigaAM v3 E2E RNN-T ONNX + token timestamps | **Подтверждено** upstream `onnx-asr` на x86/Arm CPU и готовыми cp313 wheels. `with_timestamps()` — API `onnx-asr` ([usage](https://istupakov.github.io/onnx-asr/usage/), [model card](https://huggingface.co/istupakov/gigaam-v3-onnx)) | `onnx-asr` заявляет DirectML/WebGPU support и документирует Windows ML package; **эксперимент** для конкретного E2E RNN-T: session creation, доля DML/MIGraphX, точность/timestamps | cp313 MIGraphX wheel есть; provider допустим как произвольная строка, но не first-class/tested; **эксперимент** для graph coverage и output | CoreML заявлен `onnx-asr`; **эксперимент** для dynamic encoder/decoder, control flow и доли ANE | OpenVINO EP package lagging; native IR не является тем же artifact; **эксперимент/конверсия** | +| OpenVINO GenAI Whisper + word timestamps | не тот runtime/model artifact | OpenVINO GPU не работает на AMD GPU; CPU path возможен лишь там, где CPU официально поддержан | то же | OpenVINO CPU на Apple silicon **подтверждён**; GPU/ANE нет | **Подтверждено** для Whisper tiny/base/small/medium/large-v3 и Distil-Whisper; word timestamps доступны CPU/GPU/NPU, stateful model обязателен ([ASR guide](https://openvinotoolkit.github.io/openvino.genai/docs/use-cases/speech-recognition/), [supported models](https://openvinotoolkit.github.io/openvino.genai/docs/supported-models/)) | +| sherpa-onnx pyannote segmentation + WeSpeaker embeddings | **Подтверждено локальной разведкой** на CPU ORT для одной записи; cp313 wheels есть на Windows/Linux/macOS ARM64 ([разведка](../benchmarks/2026-08-12-diarization-feasibility.md), [PyPI 1.13.5](https://pypi.org/project/sherpa-onnx/1.13.5/)) | sherpa имеет DirectML build option, но готовый Python wheel/provider и именно эти две модели на DML не подтверждены: **эксперимент/возможно rebuild** | готовая sherpa Python поставка с MIGraphX не подтверждена; **не готово** | upstream Python provider vocabulary обычно ограничивает `cpu,cuda,coreml`; наличие CoreML в wheel не доказывает поддержку diarization graphs: **эксперимент** | модели ONNX теоретически читаются OpenVINO, но полное/частичное покрытие и численная стабильность clustering inputs не подтверждены: **эксперимент** | + +Почему timestamps должны пережить смену EP — это **вывод**, а не готовая совместимость: `onnx-asr` сохраняет в Python preprocessing и greedy decoding, а EP исполняет ONNX encoder/decoder; одинаковые тензорные выходы должны дать тот же `TimestampedResult` ([описание архитектуры](https://github.com/istupakov/onnx-asr/tree/v0.12.0), [timestamps API](https://istupakov.github.io/onnx-asr/usage/)). Но mixed precision, unsupported ops/fallback и provider-specific graph transforms требуют golden-output проверки. Для diarization выходной контракт сегментов создаётся sherpa pipeline после двух ONNX-моделей и clustering ([C API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)); ускорение одной модели не должно считаться ускорением всего pipeline. + +Отдельный support gap: официальный recipe sherpa подтверждает PyAnnote +segmentation с 3D-Speaker или NeMo embeddings, но не с выбранным в разведке +WeSpeaker. Локальный CPU-прогон уже доказал, что эта пара создаёт интервалы на +одной записи; неизвестны upstream-гарантия контракта и переносимость на другие +EP, а не базовая совместимость CPU-пути +([разведка](../benchmarks/2026-08-12-diarization-feasibility.md), +[sherpa models](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html), +[WeSpeaker pretrained models](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md)). + +GigaAM timestamp caveat: upstream GigaAM `transcribe(..., word_timestamps=True)` возвращает слова со start/end, но его официальный ONNX helper экспортирует encoder/decoder/joint и проверяет только text parity; helper возвращает `List[str]` и теряет emission frames ([GigaAM repository](https://github.com/salute-developers/GigaAM), [ONNX parity test](https://github.com/salute-developers/GigaAM/blob/main/tests/test_onnx.py), [ONNX helper](https://github.com/salute-developers/GigaAM/blob/main/gigaam/onnx_utils.py)). Timestamped contract текущего проекта даёт именно `onnx-asr.with_timestamps()`, а не произвольный GigaAM ONNX export. Поэтому golden test должен сравнивать project/onnx-asr contract, не только распознанный текст. + +Минимальная будущая экспериментальная матрица без заявления производительности: + +1. Один фиксированный 5–10-минутный fixture с overlap и эталоном текущего CPU output. +2. Для GigaAM E2E RNN-T: ORT CPU против DML, WinML MIGraphX, CoreML и MIGraphX Linux; фиксировать session creation, provider assignment/profile, CPU fallback, transcript/token timestamps и численное расхождение. +3. Для diarization: отдельно pyannote segmentation и WeSpeaker embeddings, затем полный pipeline; фиксировать provider assignment каждой сессии, сегменты/число спикеров и стабильность embeddings/clustering. +4. Для OpenVINO Whisper: CPU на Apple и поддерживаемом Intel, GPU/NPU только на Intel; проверять word timestamps и project adapter отдельно. + +## Почему ONNX Runtime Web стал общим браузерным слоем + +ONNX остаётся переносимым serialized graph/IR, а `onnxruntime-web` — отдельным JavaScript/WebAssembly runtime. Общность браузеров даёт не ONNX-файл сам по себе, а единый ORT JS API поверх разных EP: `wasm` как default CPU baseline, `webgpu`, `webnn` и legacy `webgl`. ORT распределяет поддерживаемые nodes/subgraphs на accelerator, а неподдерживаемые может оставить WASM, если он указан вторым provider ([web overview](https://onnxruntime.ai/docs/tutorials/web/), [session options](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html)). Это та же архитектурная идея, что native ORT, но runtime binaries и provider kernels другие. + +На 2026-08-12 официальный browser matrix таков ([matrix](https://onnxruntime.ai/docs/get-started/with-javascript/web.html)): + +- WASM: Chrome/Edge, Safari, Firefox на основных desktop/mobile платформах; полный набор ONNX operators, CPU baseline; +- WebGPU: Chromium 113+ на Windows, Chromium на macOS/Android; в ORT Web всё ещё обозначен experimental, operator subset; +- WebNN: experimental и не включён по умолчанию; официальный matrix требует feature flag в Chrome/Edge Windows; unsupported ops fall back to WASM ([WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)); +- WebGL: maintenance mode, operator subset. + +Один ONNX artifact и близкий `InferenceSession` contract можно использовать native и web, но «один artifact» не означает одинаковую работоспособность. Для WebGPU опубликована отдельная operator table ([WebGPU operators](https://github.com/microsoft/onnxruntime/blob/main/js/web/docs/webgpu-operators.md)); accelerator EP поддерживают лишь subset, а WASM — все операторы. Большие модели ограничены браузером: около 2 GB для ArrayBuffer/Protobuf, 4 GB WebAssembly memory; external data нужно передавать URL/Blob явно ([large models](https://onnxruntime.ai/docs/tutorials/web/large-models.html)). WASM threading включается только при `crossOriginIsolated`; proxy worker не совместим с WebGPU и CSP-restricted environment ([env flags](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html)). Dynamic shapes и CPU fallback также исключают некоторые оптимизации WebGPU graph capture ([WebGPU guide](https://onnxruntime.ai/docs/tutorials/web/ep-webgpu.html)). + +`onnx-asr` 0.12 заявляет WebGPU support для **native Python package** и называет `onnxruntime-webgpu` beta; это не browser JavaScript port `onnx-asr` ([installation](https://istupakov.github.io/onnx-asr/installation/)). Для GigaAM E2E RNN-T browser compatibility не подтверждена: нужны JS preprocessing/decoder либо порт Python-логики, загрузка нескольких model artifacts, проверка WebGPU operator coverage и WASM fallback. У sherpa-onnx есть отдельная WebAssembly speaker-diarization сборка и JS example, но она однопоточная и не доказывает, что выбранные проектом pyannote + WeSpeaker models работают через ORT Web WebGPU ([sherpa JS diarization](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/javascript.html), [build option](https://github.com/k2-fsa/sherpa-onnx/blob/master/CMakeLists.txt)). + +Native WebGPU и browser WebGPU нельзя смешивать: native Python EP использует Dawn поверх D3D12/Vulkan/Metal и теперь поставляется plugin-пакетом `onnxruntime-ep-webgpu` (0.2.1, universal wheels), тогда как ORT Web использует browser JSEP/WASM path ([native WebGPU EP](https://onnxruntime.ai/docs/execution-providers/WebGPU-ExecutionProvider.html), [plugin PyPI JSON](https://pypi.org/pypi/onnxruntime-ep-webgpu/json)). + +Практический урок для CLI — не новый browser product, а более строгая portability-модель: + +- сохранять portable CPU baseline; +- считать accelerator опциональной capability, обнаруживаемой при запуске; +- различать «API/EP существует», «model session создалась», «graph offloaded» и «контракт/качество сохранены»; +- измерять долю fallback и end-to-end pipeline, а не обещать устройство по имени provider. + +## Что это меняет для карты диаризации + +1. Разведка `sherpa-onnx` на обычном CPU ORT не опирается на затухающий + компонент: ядро ORT активно и имеет самый широкий CPython/platform coverage. + Это поддерживает текущий вариант диаризации как optional post-processing, + но ничего не говорит о качестве DirectML/OpenVINO EP на конкретных двух + моделях. +2. Формулировка разведочного замера «у OpenVINO GenAI потокенных таймкодов нет» + требует уточнения. Upstream с 2026.0 предоставляет word-level timestamps; + сейчас их не экспортирует проектный OpenVINO backend. Следовательно, + невозможность пословной привязки на `--device openvino-*` — **интеграционный + пробел local-transcriber**, а не долгосрочное ограничение движка + ([OpenVINO 2026.0](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0)). +3. Не следует связывать UX диаризации с немедленным выбором EP. Сначала можно + определить пользовательский контракт — флаг, `num_speakers`, зависимость, + формат и честное поведение при отсутствии word timestamps. Ускорение + диаризации через OpenVINO EP или DirectML должно пройти отдельную + совместимость и benchmark на обеих моделях `sherpa-onnx`. +4. Не следует принимать решение о полной консолидации проекта на ORT. Нативный + OpenVINO GenAI развивается как ASR-платформа, в том числе по таймкодам и NPU, + а FasterWhisper сохраняет отдельные достоинства CUDA и языкового покрытия, + уже зафиксированные ADR-003/006. + +## Рекомендация по жизненному циклу + +Для portability evidence приоритеты такие: CPU baseline должен оставаться обязательным; accelerator — opt-in capability с явной диагностикой provider/device/fallback; platform wheel и provider name считаются только предпосылкой, пока model-specific smoke/golden test не подтвердил session creation, placement и выходной контракт. Windows ML, MIGraphX, CoreML и native WebGPU следует оценивать отдельными экспериментами, а не добавлять в UX как обещанные устройства заранее. + +Это не решение о консолидации backend-ов на ORT и не предложение browser-направления. + +Ниже — **интерпретация источников для local-transcriber**, а не опубликованный +roadmap Microsoft или Intel. Она исходит из фактов о lifecycle, wheel-матрицах +и текущем контракте проекта; реальную пригодность каждого ускорителя должен +подтвердить проектный benchmark. + +- Сохранять **ONNX-модели диаризации + обычный CPU ORT** как базовый переносимый + путь: это наименее связанный с одним вендором слой и единственная из трёх + поставок с wheel на Windows/Linux ARM64 и macOS ARM64. +- Рассматривать **OpenVINO EP как опциональное ускорение этих же ONNX-моделей на + Intel**, но не обещать его до проверки operator coverage, фактического + provider assignment, качества и скорости. Его wheel активен, однако отстаёт + от текущих ORT/OpenVINO и требует собственной матрицы версий. +- Рассматривать **нативный OpenVINO/OpenVINO GenAI как основной долгосрочный + Intel ASR-путь**, особенно для Whisper и будущего NPU. Для пословной + диаризации сначала проверить и протянуть уже существующие upstream word + timestamps через проектный `Backend` contract. +- Не закладывать новый DirectML backend проекта: текущий EP поддерживается, но + feature development официально ушёл в WinML. Если кросс-вендорное Windows GPU + ускорение станет отдельной целью, исследовать WinML как новый + Windows-специфический backend, а не считать `onnxruntime-directml` + долгоживущим default. + +## Новые вопросы карты + +- Нужен ли отдельный portability experiment: GigaAM E2E RNN-T и обе diarization-модели на DirectML, WinML MIGraphX, Linux MIGraphX и CoreML с node placement/profile и golden outputs? +- Насколько устойчива локально работающая пара PyAnnote + WeSpeaker между + платформами и EP, если upstream recipe её не фиксирует? +- Должен ли Windows UX показывать не только выбранный provider, но и фактический accelerator/fallback после capability discovery? +- Стоит ли поддерживать Windows ML bootstrap/catalog как отдельный integration layer или оставить низкофрикционный DirectML до появления подтверждённого выигрыша? +- Какой минимальный browser experiment проверит GigaAM preprocessing/decoder/timestamps и pyannote+embedding WASM/WebGPU, не превращая карту в browser roadmap? + +- Какой точный контракт word timestamps возвращают `WhisperPipeline` и новый + `ASRPipeline` 2026.3, и как без потери совместимости добавить их в проектный + `Backend`/`TranscribeResult`? +- Дают ли `sherpa-onnx` segmentation и embedding models полный offload в + OpenVINO EP, или часть графа уходит в CPU EP; меняются ли границы и + эмбеддинги численно? +- Есть ли выигрыш OpenVINO EP на целевом Intel Core i5 11-го поколения после + учёта второго runtime, загрузки модели и памяти, или CPU ORT уже оптимальнее? +- Нужен ли UX явного отказа/огрубления диаризации на backend без word + timestamps, либо backend contract должен сначала стать timestamp-aware? +- Следует ли разделить extra диаризации на переносимый CPU-вариант и + Intel-ускорение с platform marker, чтобы не ухудшить zero-config установку на + ARM/macOS? -- 2.54.0 From 388de09c423f9631f4893599e378378f43f6a21b Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 16:25:54 +0300 Subject: [PATCH 06/15] =?UTF-8?q?docs(research):=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D0=B0=20=D0=B8=D1=81?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - выводы о жизненном цикле и возможностях моделей должны читаться как единое исследование. - Что: - материал перестроен вокруг слоёв, платформ и уровней доказательства. - объединены выводы по Intel, AMD, Apple и браузерным путям. - подтверждённые возможности отделены от выводов и необходимых экспериментов. - Проверка: - относительные ссылки и структура Markdown проверены. - git diff --cached --check выполнен успешно. --- ...6-08-12-onnx-runtime-openvino-lifecycle.md | 749 ++++++++++-------- 1 file changed, 415 insertions(+), 334 deletions(-) diff --git a/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md b/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md index bd8a29e..1becf2f 100644 --- a/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md +++ b/docs/research/2026-08-12-onnx-runtime-openvino-lifecycle.md @@ -1,410 +1,491 @@ -# Куда движутся ONNX Runtime и OpenVINO: сравнение жизненного цикла +# Куда движутся ONNX Runtime и OpenVINO: жизненный цикл и переносимость моделей **Дата:** 2026-08-12 **Статус:** исследование для карты диаризации. Не архитектурное решение и не основание для консолидации всех движков распознавания на ONNX Runtime. -## Вопрос +## Вопрос и границы -Насколько устойчивы ONNX Runtime, его DirectML и OpenVINO Execution Provider, -а также нативный стек OpenVINO/OpenVINO GenAI; какой из путей с большей -вероятностью сохранит Intel-ускорение и поддержку Whisper/NPU; что из этого -практически доступно проекту на CPython 3.13. +Исследование отвечает на два связанных вопроса: -Исследование опирается только на первичные источники: официальную документацию, -release notes, репозитории владельцев и метаданные PyPI. +1. Насколько устойчивы ONNX Runtime (ORT), его аппаратные Execution Provider + (EP), Windows ML и нативный стек OpenVINO/OpenVINO GenAI? +2. Что эти пути практически дают текущим моделям проекта — GigaAM E2E RNN-T, + OpenVINO Whisper и связке диаризации PyAnnote + WeSpeaker — на Intel, AMD, + Apple Silicon и в браузере? -## Границы в контексте проекта - -Терминология следует [`CONTEXT.md`](../../CONTEXT.md): ONNX Runtime, OpenVINO и -OpenVINO GenAI здесь — **движки распознавания**, их обновление само по себе не -меняет поддерживаемую модель или модель по умолчанию. Архитектурная точка -отсчёта — отдельные pluggable backends из [ADR-003](../adr/003-pluggable-backends.md) -и принятый ONNX CPU-путь из [ADR-006](../adr/006-onnx-asr-backend.md). +Терминология следует [`CONTEXT.md`](../../CONTEXT.md): ORT, OpenVINO и OpenVINO +GenAI — **движки распознавания**. Обновление движка само по себе не меняет +поддерживаемую модель или модель по умолчанию. Архитектурная точка отсчёта — +независимые бэкенды из [ADR-003](../adr/003-pluggable-backends.md) и принятый +ONNX CPU-путь из [ADR-006](../adr/006-onnx-asr-backend.md). Исследование дополняет [срез обновлений движков](2026-08-10-engine-model-updates.md) -и отвечает на инфраструктурный вопрос, открытый -[разведкой диаризации](../benchmarks/2026-08-12-diarization-feasibility.md) и -[бэклогом](../backlog.md#диаризация--разделение-говорящих). Оно не пересматривает -качество моделей и не принимает решение о полной консолидации на ORT. +и [разведку диаризации](../benchmarks/2026-08-12-diarization-feasibility.md). +Оно основано на первичных источниках: официальной документации, release notes, +репозиториях владельцев и фактических метаданных PyPI на 2026-08-12. -## Краткий вывод +Вне границ документа: -Расширенный portability-срез не меняет исходный lifecycle-вывод, но уточняет его: ORT CPU остаётся наиболее ровным baseline на AMD Windows/Linux и Apple Silicon; Windows ML — уже production-поставка ORT для Windows с cp313, хотя vendor EP требуют bootstrap/registration; DirectML остаётся legacy; Linux AMD движется к MIGraphX; Apple accelerator-путь — CoreML Preview с обязательной проверкой partitioning. Наличие wheel/provider не подтверждает совместимость GigaAM или двух diarization graphs и тем более полный offload. +- решение о консолидации проекта на одном runtime; +- выбор нового устройства или модели по умолчанию; +- обещание производительности без model-specific benchmark; +- разработка браузерной версии `local-transcriber`. -Браузер показывает тот же устойчивый pattern: ONNX — artifact, ORT Web — отдельный runtime, WASM — portable baseline, WebGPU/WebNN — optional accelerators с operator subset и fallback. Это portability evidence, а не предложение browser product или консолидации всего проекта на ORT. +## Краткий ответ -1. **ONNX Runtime — активно развиваемый, production-стабильный движок, но не - все его EP имеют одинаковый жизненный цикл.** Версии 1.26, 1.27 и 1.28 - вышли 8 мая, 19 июня и 25 июля 2026 года, то есть три minor-релиза примерно - за одиннадцать недель; 1.28 продолжает развивать plugin EP API, ядро, - безопасность и аппаратные EP ([1.26.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0), - [1.27.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.27.0), - [1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). -2. **DirectML EP поддерживается, но переведён в sustained engineering.** Новая - функциональность Windows-пути перенесена в WinML; Microsoft рекомендует - WinML для новых Windows-развёртываний, а DirectML EP оставляет для legacy и - специальных сценариев ([официальная страница DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html), - [Windows-путь ORT](https://onnxruntime.ai/docs/get-started/with-windows.html)). - Поэтому `onnxruntime-directml` нельзя считать перспективным - кросс-вендорным GPU-дефолтом проекта, хотя пакет не заброшен. -3. **OpenVINO и OpenVINO GenAI — основной активно развиваемый Intel-стек.** В - 2026 году регулярные релизы вышли 23 февраля, 7 апреля, 28 мая и 4 августа; - OpenVINO публикует формальную release/LTS policy, где регулярная версия - поддерживается до следующей, а последняя версия года становится LTS с двумя - годами security updates ([release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html), - [release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)). -4. **Для Intel-ускорения более долгоживущая ставка — сам OpenVINO, а не - конкретная обвязка ORT OpenVINO EP.** EP остаётся активным мостом из ORT к - OpenVINO, но зависит сразу от двух release train и его готовые wheel заметно - отстают от обоих ядер. Нативный OpenVINO одновременно является runtime для - CPU/GPU/NPU, имеет собственную LTS policy и служит основанием OpenVINO GenAI - ([OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), - [GenAI как расширение runtime](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai.html)). - Это не означает, что проекту нужно переносить ONNX-путь на OpenVINO или - консолидироваться на ORT: ONNX-модели сохраняют переносимость, а выбор - движка распознавания остаётся отдельным решением по качеству и контракту. -5. **CPython 3.13 не блокирует ни один из трёх исследованных PyPI-пакетов на - целевой Windows x86-64**, но матрицы платформ радикально различаются: - `onnxruntime` кроссплатформенный, DirectML только Windows x86-64, - OpenVINO EP только Windows/Linux x86-64 - ([onnxruntime 1.28.0 files](https://pypi.org/project/onnxruntime/1.28.0/#files), - [DirectML 1.24.4 files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files), - [OpenVINO EP 1.24.1 files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)). +- **Переносимый фундамент проекта — ONNX-артефакт плюс ORT CPU EP.** ORT core + активно развивается и имеет наиболее широкую поставку для CPython 3.13: + Windows и Linux x86-64/ARM64, macOS ARM64. +- **Жизненный цикл ядра ORT не переносится автоматически на каждый EP.** + DirectML уже в sustained engineering, OpenVINO EP активен, но отстаёт от + ORT и OpenVINO, CoreML остаётся Preview, а удалённый ROCm EP сменяется + MIGraphX. +- **Долгосрочный Intel-путь — нативный OpenVINO/OpenVINO GenAI.** Он имеет + собственную release/LTS policy, развивает Whisper и NPU и уже предоставляет + word-level timestamps. OpenVINO EP полезен как мост для ONNX-моделей, но не + даёт автоматически последние возможности нативного стека. +- **Новый Windows-слой — Windows ML, а не DirectML.** Windows ML остаётся ORT, + но добавляет обнаружение устройств и управляемый каталог vendor EP. Для AMD + там доступен MIGraphX; Python-приложению всё равно нужны bootstrap, загрузка + и явная регистрация EP. +- **На AMD и Apple готовый пакет ещё не означает ускорение конкретной модели.** + Linux AMD имеет wheel MIGraphX для CPython 3.13, Apple Silicon — CoreML EP в + обычном ORT wheel; полный offload GigaAM и моделей диаризации не подтверждён + ни для одного из этих путей. +- **Браузеры используют ту же архитектурную идею:** ORT Web даёт единый API, + WASM — переносимый CPU baseline, WebGPU/WebNN — опциональные ускорители с + ограниченным набором операторов и fallback. Сам ONNX-файл не устраняет + различия preprocessing, decoding и доступных kernels. +- **Главная находка для карты диаризации:** OpenVINO GenAI уже умеет возвращать + пословные таймкоды. Их отсутствие в результате `local-transcriber` — пробел + проектного `Backend`/`TranscribeResult`, а не ограничение OpenVINO. -## Что именно является чем +Общий принцип: поддержка пути доказана только тогда, когда подтверждены +поставка, создание сессии, фактическое размещение графа, сохранение выходного +контракта и end-to-end стоимость. Наличие wheel или имени EP закрывает только +первый из этих пунктов. -Слои нельзя сравнивать как взаимозаменяемые пакеты: +## Как устроены исследуемые слои + +Сравниваемые названия относятся к разным уровням и не являются +взаимозаменяемыми пакетами. | Слой | Роль | Что фиксирует приложение | |---|---|---| -| ONNX | Формат графа, операторов и типов данных; операторы исполняются внешней реализацией | Артефакт модели и его opset ([ONNX About](https://onnx.ai/about)) | -| ONNX Runtime | Движок выполнения ONNX-графа, который разбивает его между EP и CPU fallback | API сессии, версия ORT и набор EP ([архитектура ORT](https://onnxruntime.ai/docs/reference/high-level-design.html)) | -| OpenVINO | Intel runtime, компилятор и device plugins для CPU/GPU/NPU; умеет принимать в том числе ONNX-графы | API OpenVINO и поддерживаемые устройства/форматы ([поддержанные модели](https://docs.openvino.ai/2026/documentation/compatibility-and-support/supported-models.html)) | -| OpenVINO EP | Адаптер внутри ORT: получает поддержанные подграфы, переводит и компилирует их для OpenVINO | Одновременно контракты ORT, EP и совместимой версии OpenVINO ([EP architecture](https://onnxruntime.ai/docs/execution-providers/), [OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)) | -| OpenVINO GenAI | Высокоуровневые генеративные pipelines поверх OpenVINO runtime, включая Whisper и общий ASR API | Формат моделей OpenVINO IR, pipeline API и согласованные версии OpenVINO/Tokenizers/GenAI ([GenAI PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)) | +| ONNX | Формат графа, операторов и типов данных | Артефакт модели и opset ([ONNX About](https://onnx.ai/about)) | +| ONNX Runtime | Движок, который загружает ONNX-граф и распределяет узлы между EP | API сессии, версия ORT и порядок EP ([архитектура ORT](https://onnxruntime.ai/docs/reference/high-level-design.html)) | +| Execution Provider | Адаптер ORT к CPU, GPU или NPU; получает только поддержанные узлы/подграфы | Аппаратный runtime, provider options и CPU fallback ([архитектура EP](https://onnxruntime.ai/docs/execution-providers/)) | +| Windows ML | Windows-поставка ORT с каталогом, установкой и обновлением vendor EP | Windows App SDK, deployment mode и политика выбора EP ([обзор](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview)) | +| OpenVINO | Runtime, компилятор и device plugins для CPU/GPU/NPU; читает в том числе ONNX | API OpenVINO, устройство и поддержанные форматы ([поддержанные модели](https://docs.openvino.ai/2026/documentation/compatibility-and-support/supported-models.html)) | +| OpenVINO GenAI | Высокоуровневые pipelines поверх OpenVINO, включая Whisper и общий ASR API | OpenVINO IR, pipeline API и согласованные версии компонентов ([GenAI PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)) | +| ORT Web | Отдельная JavaScript/WebAssembly-поставка ORT для браузера | JS API, WASM runtime и browser EP ([обзор ORT Web](https://onnxruntime.ai/docs/tutorials/web/)) | -Следствие: **модель в ONNX не означает ONNX Runtime**, а **OpenVINO EP не -является форматом модели**. Один ONNX-артефакт можно исполнять CPU EP в ORT, -передавать поддержанные подграфы OpenVINO EP либо загружать в OpenVINO -напрямую; однако покрытие операторов, квантование, fallback и производительность -у этих путей различаются ([ORT EP partitioning](https://onnxruntime.ai/docs/execution-providers/), -[OpenVINO EP support coverage](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), -[прямое чтение ONNX в OpenVINO](https://docs.openvino.ai/2026/openvino-workflow/model-preparation/convert-model-onnx.html)). +Один ONNX-артефакт можно исполнять обычным ORT CPU EP, передавать его +поддержанные подграфы OpenVINO EP или загружать напрямую в OpenVINO. Результат +различается по покрытию операторов, квантованию, fallback и производительности +([ORT partitioning](https://onnxruntime.ai/docs/execution-providers/), +[OpenVINO EP coverage](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), +[чтение ONNX в OpenVINO](https://docs.openvino.ai/2026/openvino-workflow/model-preparation/convert-model-onnx.html)). -## ONNX Runtime и execution providers +### Лестница доказательства -### Ядро ORT +Для каждой пары «модель × устройство × движок» используются пять уровней: -Официальные страницы расходятся в обещанном cadence: servicing-документ всё -ещё говорит о full releases «примерно ежеквартально», тогда как roadmap — о -ежемесячных релизах и patch-релизах между ними. Формального LTS/EOL-окна в -публичной support policy нет. Поэтому для планирования надёжнее опираться на -фактические публикации и backward-compatibility policy, а не превращать -текущий почти месячный темп в гарантию -([releases and servicing](https://onnxruntime.ai/docs/reference/releases-servicing.html), +1. **Поставка:** существует совместимый wheel/runtime. +2. **Загрузка:** все модельные сессии создаются без ошибки. +3. **Размещение:** profiler показывает, какие узлы действительно исполняет EP, + а какие ушли в CPU fallback. +4. **Контракт:** текст, таймкоды, сегменты и эмбеддинги остаются допустимыми. +5. **Пригодность:** end-to-end скорость, память и качество проходят проектную + приёмку. + +Ниже «подтверждено» означает прямое upstream-обещание или локальный результат; +«вывод» следует из архитектуры, но не проверен на конкретной модели; +«эксперимент» означает, что неизвестен хотя бы один уровень после поставки. + +## Жизненный цикл движков и аппаратных путей + +### ONNX Runtime core + +ORT core активно развивается. Версии 1.26, 1.27 и 1.28 вышли 8 мая, 19 июня и +25 июля 2026 года; в них продолжалось развитие plugin EP API, ядра, +безопасности и аппаратных провайдеров +([1.26.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0), +[1.27.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.27.0), +[1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). + +Официальные страницы расходятся в обещанном cadence: servicing-документ говорит +о full releases примерно раз в квартал, roadmap — о ежемесячных релизах и +промежуточных patch-релизах. Публичной LTS/EOL policy нет, поэтому текущий +почти месячный темп нельзя считать гарантией +([servicing](https://onnxruntime.ai/docs/reference/releases-servicing.html), [roadmap](https://onnxruntime.ai/roadmap), [support policy](https://github.com/microsoft/onnxruntime/blob/main/SUPPORT.md)). -ORT 1.23 начал переход к независимо подключаемым plugin EP и прямо рекомендует -новые EP реализовывать как plugins, а не добавлять внутрь ядра. В 1.24–1.28 -plugin API последовательно получал prepacking, EP Context, zero-copy I/O, -profiling и model packages ([инструкция для нового EP](https://onnxruntime.ai/docs/execution-providers/add-execution-provider.html), -[релиз 1.24.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.24.1), -[релиз 1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). -Это сильный сигнал продолжения ORT как общего движка, но одновременно сигнал, -что жизненный цикл конкретного аппаратного backend всё больше принадлежит его -поставщику, а не ядру ORT. +С ORT 1.23 новые EP рекомендуется делать отдельными plugins. В 1.24–1.28 API +получил prepacking, EP Context, zero-copy I/O, profiling и model packages +([инструкция для нового EP](https://onnxruntime.ai/docs/execution-providers/add-execution-provider.html), +[1.24.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.24.1), +[1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). Это +укрепляет ORT как общий движок, но одновременно отделяет lifecycle конкретного +ускорителя от lifecycle ядра. -### DirectML EP +### Нативный OpenVINO и OpenVINO GenAI -Официальная формулировка однозначна: DirectML находится в **sustained -engineering**, поддержка продолжается, но feature development перешёл в WinML. -Документация также фиксирует DirectML 1.15.2 и покрытие только до ONNX opset 20; -модели с более высоким требованием официально не поддерживаются -([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)). +OpenVINO публикует несколько регулярных релизов в год. Каждый поддерживается до +следующего, а последняя версия года становится LTS: security updates выходят +два года либо до двух следующих LTS, исправления новых bugs — один год. +Preview-компоненты этой гарантией не покрываются +([release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html), +[release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)). -Это согласуется с поставкой: последний `onnxruntime-directml` на дату среза — -1.24.4 от 17 марта, тогда как ядро ORT уже 1.28.0. При этом ORT 1.28 всё ещё -содержит исправление DML readback, то есть sustained engineering означает не -«удалён», а «исправления без прежнего темпа новых возможностей» -([DirectML на PyPI](https://pypi.org/project/onnxruntime-directml/), -[ORT 1.28, DML fix](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). +OpenVINO GenAI — pipeline-библиотека поверх OpenVINO и OpenVINO Tokenizers. +Их `major.minor.patch` должны совпадать; разъезд версий может привести к +ABI/import errors. PyPI wheel нельзя смешивать с C++ archive другого ABI +([правила совместимости](https://pypi.org/project/openvino-genai/2026.3.0.0/)). -Для проекта DirectML остаётся возможным Windows-only экспериментом на AMD, -Intel и NVIDIA GPU, но его стратегический successor — WinML, который требует -Windows-специфической интеграции. Это слабее текущего требования ADR-003 о -плаггируемых бэкендах и кроссплатформенном ONNX CPU-пути. +Whisper остаётся активным направлением: -### OpenVINO EP +- OpenVINO 2026.0 добавил word-level timestamps в `WhisperPipeline` на CPU, + GPU и NPU; 2026.3 добавил язык в результат + ([2026.0](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0), + [2026.3](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); +- OpenVINO 2026.3 ввёл общий `ASRPipeline` и Qwen3-ASR, расширив speech API за + пределы Whisper ([2026.3](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); +- удалён только ранее deprecated stateless Whisper decoder; рекомендуемый путь + использует stateful model + ([deprecations](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#deprecation-and-support)). -Официальная документация не объявляет OpenVINO EP deprecated или maintenance-only. -Наоборот, Intel публикует готовые пакеты, принимает issues/PR, заявляет CPU, -интегрированные и дискретные GPU и NPU, а ORT 1.26 и 1.28 содержат OpenVINO EP -development updates ([страница пакета](https://pypi.org/project/onnxruntime-openvino/), +NPU — полноценное устройство OpenVINO, но требует отдельного driver, работает +со static shapes, а совместимость compiled blobs между версиями не +гарантируется. `WhisperPipeline` поддерживает NPU, однако целевой Core i5 11-го +поколения NPU не имеет: для него OpenVINO означает CPU/iGPU +([NPU device](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html), +[Whisper on NPU](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai/inference-with-genai-on-npu.html#whisper-inference-on-npu), +[AUTO priority](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/auto-device-selection.html)). + +### Аппаратные пути ORT + +| Путь | Состояние на 2026-08-12 | Практическое следствие | +|---|---|---| +| CPU EP | Часть ORT core, production baseline | Самая широкая поставка; аппаратного ускорителя не обещает | +| DirectML EP | Sustained engineering; feature development перешёл в Windows ML | Поддерживается, но не подходит как новый долгосрочный GPU default ([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)) | +| Windows ML | Production-поставка ORT для Windows с управляемым каталогом EP | Стратегический Windows-слой, но требует platform-specific bootstrap ([deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app)) | +| OpenVINO EP | Активен; deprecated только часть старых provider options | Мост к Intel-ускорению, но готовый wheel отстаёт от ORT/OpenVINO ([OpenVINO EP](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)) | +| MIGraphX EP | Активный AMD-путь; прежний ROCm EP удалён из ORT 1.23 | Долгосрочнее ROCm EP, но зависит от ROCm/GPU/OS ([ORT 1.23](https://github.com/microsoft/onnxruntime/releases/tag/v1.23.0)) | +| CoreML EP | Preview | Доступен в macOS ORT wheel, но требует проверки partitioning ([CoreML EP](https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html)) | +| Native WebGPU EP | Новый plugin поверх Dawn/D3D12/Vulkan/Metal | Кросс-вендорный кандидат; browser WebGPU использует другой runtime path ([WebGPU EP](https://onnxruntime.ai/docs/execution-providers/WebGPU-ExecutionProvider.html)) | + +#### DirectML и Windows ML + +DirectML EP использует DirectML 1.15.2, поддерживает ONNX только до opset 20 и +не допускает parallel execution одной session. Последний +`onnxruntime-directml` на дату среза — 1.24.4, тогда как ORT core уже 1.28.0. +Исправления DML всё ещё входят в ORT, то есть sustained engineering означает +поддержку без прежнего feature cadence, а не удаление +([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html), +[PyPI](https://pypi.org/project/onnxruntime-directml/), +[ORT 1.28](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). + +Windows ML не меняет формат модели и не заменяет ORT: runtime содержит +`onnxruntime.dll`, DirectML и Windows ML API. Новый слой добавляет обнаружение +устройств, каталог vendor EP, их установку, регистрацию и обновление. DirectML +остаётся встроенным legacy EP; MIGraphX, VitisAI, OpenVINO, QNN и +NvTensorRtRtx поставляются через каталог или вместе с приложением +([обзор](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview), +[состав runtime](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app), +[каталог EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). + +Python-пакет называется `onnxruntime-windowsml`. Версия +`1.27.1.202607110137` имеет статус `Production/Stable`, требует Python 3.11+ и +публикует `cp313` wheels для Windows x86-64 и ARM64 +([PyPI](https://pypi.org/project/onnxruntime-windowsml/)). Отдельная ONNX +Runtime GenAI Windows ML library 0.x остаётся Preview; её статус не относится +к обычному ONNX-инференсу +([GenAI Preview](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/run-genai-onnx-models)). + +Для Python поддержан только framework-dependent unpackaged deployment: нужны +Windows App SDK Runtime и bootstrap packages. Динамический каталог аппаратных +EP требует Windows 11 24H2 build 26100+ +([get started](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/get-started), +[deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app)). +Приложение должно скачать выбранный EP через `ensure_ready_async()` и +зарегистрировать библиотеку в ORT; `EnsureAndRegisterCertifiedAsync()` не +регистрирует EP в Python environment +([инициализация EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/initialize-execution-providers)). + +#### OpenVINO EP + +OpenVINO EP не объявлен deprecated или maintenance-only. Intel продолжает +публиковать пакет, а ORT 1.26 и 1.28 содержат его изменения. Но последний +готовый `onnxruntime-openvino` 1.24.1 включает OpenVINO 2025.4.1 на Linux и +требует отдельный OpenVINO на Windows. Нативный OpenVINO уже достиг 2026.3, ORT +core — 1.28 +([PyPI](https://pypi.org/project/onnxruntime-openvino/1.24.1/), +[матрица совместимости](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html), [ORT 1.26](https://github.com/microsoft/onnxruntime/releases/tag/v1.26.0), [ORT 1.28](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). -Но готовая поставка имеет свой темп. Последний wheel `onnxruntime-openvino` -1.24.1 от 26 февраля 2026 года включает OpenVINO 2025.4.1 на Linux и требует -отдельной установки OpenVINO на Windows. Официальная таблица совместимости -покрывает только три версии OpenVINO: ORT-EP 1.22/2025.1, -1.23/2025.3 и 1.24.1/2025.4.1 -([PyPI](https://pypi.org/project/onnxruntime-openvino/1.24.1/), -[матрица совместимости](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)). -На дату среза нативный OpenVINO уже 2026.3, а ядро ORT — 1.28. Значит, EP -активен, но готовый Python-путь не является способом автоматически получить -самые новые возможности OpenVINO/NPU. +Следствие: EP остаётся рабочим мостом для ONNX-моделей, но не является способом +автоматически получить последние Whisper/NPU-возможности OpenVINO. Deprecated +provider options, заменённые `load_config`, не означают deprecation самого EP. -Начиная с ORT 1.23 часть старых provider options OpenVINO EP deprecated в -пользу `load_config` с нативными OpenVINO properties. Это локальная миграция -конфигурации, а не deprecation самого EP -([deprecation notice](https://onnxruntime.ai/docs/execution-providers/OpenVINO-ExecutionProvider.html)). +## Практическая поставка по платформам -## OpenVINO, OpenVINO GenAI, Whisper и Intel NPU +Срез сделан по фактическим wheel, а не только по classifiers. -OpenVINO имеет явно описанный цикл: несколько регулярных релизов в год, -поддержка каждого до следующего и ежегодный LTS. LTS получает security updates -два года либо до двух следующих LTS, а исправления новых bugs — один год; -preview-компоненты этой гарантией не покрываются -([release policy](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/release-policy.html)). +| Платформа и устройство | Готовый runtime/EP | Статус | `cp313` | Текущий CLI без новой интеграции | +|---|---|---|---|---| +| Intel x86 CPU | ORT CPU; OpenVINO CPU | Production | Да | Оба пути уже есть | +| Intel GPU/NPU | Нативный OpenVINO | Production; часть NPU-функций Preview | Да, Windows/Linux x86-64 | OpenVINO GPU есть; NPU потребует нового device profile | +| Windows, AMD CPU | ORT CPU | Production baseline | Да, `win_amd64` | Да | +| Windows, AMD GPU | DirectML | Sustained engineering | Да, `onnxruntime-directml` | Нужен новый provider/device UX | +| Windows 11 24H2+, AMD GPU | Windows ML + MIGraphX | Windows ML production; EP зависит от driver/device | Да, `onnxruntime-windowsml` | Нужны bootstrap и регистрация EP | +| Linux, AMD CPU | ORT CPU | Production baseline | Да, manylinux x86-64 | Да | +| Linux, AMD GPU | MIGraphX | Активная замена удалённого ROCm EP | Да, `onnxruntime-migraphx 1.27.1` | Нужны ROCm stack и provider integration | +| Apple Silicon, CPU | ORT CPU | Production baseline | Да, macOS 14 ARM64 | Да | +| Apple Silicon, GPU/ANE | CoreML EP | Preview | Да, в обычном ORT wheel | Нужны provider integration и profiling | +| Apple Silicon, CPU | OpenVINO GenAI Whisper | Production package; CPU-only на macOS | Да | Текущий dependency marker исключает macOS | -OpenVINO GenAI — не конкурирующий runtime, а библиотека pipelines поверх -OpenVINO и OpenVINO Tokenizers. Их `major.minor.patch` должны совпадать: -разъезд может привести к ABI/import errors; PyPI wheel нельзя смешивать с C++ -archives другого ABI ([официальные правила совместимости](https://pypi.org/project/openvino-genai/2026.3.0.0/)). +Обычный `onnxruntime` 1.28.0 поставляет `cp313` wheels для Windows x86-64 и +ARM64, Linux x86-64 и ARM64, macOS 14 ARM64 +([files](https://pypi.org/project/onnxruntime/1.28.0/#files)). Для сравнения, +`onnxruntime-directml` 1.24.4 ограничен Windows x86-64, а +`onnxruntime-openvino` 1.24.1 — Windows/Linux x86-64 +([DirectML files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files), +[OpenVINO EP files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)). -Whisper — активный, а не legacy use case OpenVINO GenAI: +### AMD -- OpenVINO 2026.0 добавил word-level timestamps в WhisperPipeline на CPU, GPU - и NPU; в 2026.3 результаты также содержат определённый/заданный язык, а NPU - отдаёт word timestamps по умолчанию - ([2026.0 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0), - [2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); -- OpenVINO 2026.3 ввёл общий `ASRPipeline` и поддержку Qwen3-ASR, то есть - speech API расширяется за пределы Whisper - ([2026.3 release notes](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-3-0)); -- удалён только ранее deprecated **stateless decoder** Whisper; рекомендуемый - путь — stateful model, а не отказ от Whisper - ([deprecation section](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#deprecation-and-support)). +На AMD x86 CPU поддерживаемая опора — ORT CPU EP. OpenVINO 2026.3 официально +перечисляет Intel и ARM/Apple CPU, но не AMD x86; наличие x86 wheel само по себе +не является обещанием поддержки AMD +([OpenVINO requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html)). -NPU является первым классом устройств OpenVINO: NPU plugin доступен в -дистрибутивах, целевая аппаратная платформа начинается с Intel Core Ultra, -Compiler-In-Plugin появился preview в 2026.0 и стал предпочитаемым компилятором -в 2026.1. При этом NPU требует отдельный driver, поддерживает только static -shapes, а совместимость предкомпилированных blobs между версиями OpenVINO не -гарантируется ([NPU device](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html)). -WhisperPipeline официально работает на NPU с tiny/base/small/large без -специальных ограничений pipeline, но документация рекомендует актуальный NPU -driver и даёт workaround для memory failures -([Whisper on NPU](https://docs.openvino.ai/2026/openvino-workflow-generative/inference-with-genai/inference-with-genai-on-npu.html#whisper-inference-on-npu)). +Под Windows DirectML поддерживает AMD GCN первого поколения и новее, но его +ограниченный lifecycle делает Windows ML + MIGraphX более перспективным путём. +Текущий Windows ML MIGraphX требует совместимый GPU/driver и не поддерживает +GenAI scenarios; применимость этой формулировки к GigaAM RNN-T не определена и +должна проверяться экспериментом +([Windows ML EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). -Это не делает NPU актуальным для целевого Intel Core i5 11-го поколения: NPU -появился только в Core Ultra. Для текущей целевой машины долгоживущий -OpenVINO-путь означает прежде всего CPU/iGPU; NPU — будущий аппаратный профиль, -который нужно выбирать явно, тем более что OpenVINO AUTO пока исключает NPU из -дефолтного приоритета -([NPU hardware](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/npu-device.html), -[AUTO priority](https://docs.openvino.ai/2026/openvino-workflow/running-inference/inference-devices-and-modes/auto-device-selection.html)). - -## PyPI: версии, платформы и CPython 3.13 - -Срез сделан по фактически опубликованным wheel, а не по classifiers страницы. -У всех трёх пакетов отсутствует source distribution, поэтому неподдержанная -комбинация платформы и Python не сможет штатно собраться через обычный -`pip install` без самостоятельной сборки из репозитория. - -| Пакет | Последняя версия на 2026-08-12 | Последняя публикация | wheel для CPython 3.13 | Платформы cp313 | -|---|---:|---:|---|---| -| `onnxruntime` | 1.28.0 | 2026-07-25 | Да | Windows x86-64 и ARM64; Linux x86-64 и ARM64 (glibc 2.27/2.28+); macOS 14+ ARM64 ([files](https://pypi.org/project/onnxruntime/1.28.0/#files)) | -| `onnxruntime-directml` | 1.24.4 | 2026-03-17 | Да | Только Windows x86-64; нет Linux, macOS и Windows ARM64 wheel ([files](https://pypi.org/project/onnxruntime-directml/1.24.4/#files)) | -| `onnxruntime-openvino` | 1.24.1 | 2026-02-26 | Да | Windows x86-64 и Linux x86-64 с glibc 2.28+; нет ARM64 и macOS wheel ([files](https://pypi.org/project/onnxruntime-openvino/1.24.1/#files)) | - -Практический вывод для ADR-003 сохраняется: обычный `onnxruntime` остаётся -широким zero-config CPU-путём. DirectML и OpenVINO EP нельзя подставить как одну -безусловную зависимость на всех платформах; они требуют platform markers и -отдельных проверок доступного provider. На Windows OpenVINO EP дополнительно -требует совместимый `openvino`, а на Linux wheel уже включает конкретный -OpenVINO 2025.4.1 -([OpenVINO EP installation](https://pypi.org/project/onnxruntime-openvino/1.24.1/)). - -## Windows ML: следующий Windows-слой над ORT - -### Что доступно сейчас, а что пока preview - -> Artifact caveat: Microsoft Learn ещё указывает Python 3.10–3.13, тогда как текущий `onnxruntime-windowsml` требует Python 3.11+ и уже имеет cp314. Для установки источником истины служит фактическая wheel matrix PyPI; cp313 подтверждён для x64 и ARM64 ([PyPI JSON](https://pypi.org/pypi/onnxruntime-windowsml/json)). - -**Подтверждено.** Windows ML — не новый формат модели и не замена ONNX Runtime: Microsoft описывает его как Windows-фреймворк локального инференса, **powered by ONNX Runtime**. Runtime содержит `onnxruntime.dll`, DirectML и Windows ML API; обычный инференс по-прежнему создаёт ORT `InferenceSession`. Windows ML добавляет обнаружение устройств, каталог совместимых vendor EP, их установку, регистрацию и обновление. DirectML остаётся встроенным legacy GPU EP; MIGraphX/VitisAI/OpenVINO/QNN/NvTensorRtRtx поставляются через каталог или вместе с приложением ([обзор](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview), [состав и deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app), [API](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/api-reference)). - -Для Python реальная поставка называется `onnxruntime-windowsml`, а не анонсированная ранее `onnxruntime-winml`. На 2026-08-12 последняя версия — `1.27.1.202607110137`, статус PyPI `Production/Stable`, `Requires-Python >=3.11`; есть `cp313` wheels для `win_amd64` и `win_arm64` ([PyPI](https://pypi.org/project/onnxruntime-windowsml/)). Это уже доступный продуктовый путь. Отдельная **ONNX Runtime GenAI Windows ML library 0.x** прямо обозначена как Preview; её нельзя переносить на статус обычного ONNX-инференса ([GenAI Preview](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/run-genai-onnx-models)). - -Python поддержан на 3.10–3.13, x64/ARM64, но только как framework-dependent unpackaged app: нужны Python с python.org/winget, Windows App SDK Runtime и bootstrap-пакеты. Self-contained deployment для Python не предусмотрен. Базовый runtime может работать на поддерживаемых Windows 10, но динамический каталог аппаратных EP требует Windows 11 24H2, build 26100+ ([getting started](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/get-started), [deployment](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/distributing-your-app), [поддерживаемые EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). - -**Существенный caveat для CLI.** Каталог скачивает и обновляет EP, однако Python-приложение должно перечислить подходящие EP, вызвать `ensure_ready_async()` и зарегистрировать библиотеку через `onnxruntime.register_execution_provider_library`. Microsoft отдельно предупреждает, что `EnsureAndRegisterCertifiedAsync()` не регистрирует EP в Python ORT environment ([инициализация EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/initialize-execution-providers), [выбор EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/select-execution-providers)). Поэтому: - -- `onnxruntime-windowsml` + встроенный DirectML — практически применимая замена ORT-дистрибутива для `onnx-asr`; upstream 0.12 документирует именно эту установку и тот же providers API ([installation](https://istupakov.github.io/onnx-asr/installation/)); -- vendor EP из каталога (например, MIGraphX) не появится в существующем `local-transcriber` без Windows App SDK bootstrap/registration glue; -- device policy (`MAX_PERFORMANCE`, `PREFER_NPU` и другие) — пожелание к выбору, а не доказательство полного offload конкретной модели. Нужны capability discovery, session profiling и проверка fallback. - -## AMD и Apple: практические пути без кастомной сборки - -### AMD CPU и GPU - -На AMD x86 CPU обычный `onnxruntime` использует portable CPU EP: это готовый baseline на Windows и Linux. OpenVINO 2026.3 официально перечисляет Intel и ARM/Apple CPU, но **не перечисляет AMD x86 в supported hardware**; наличие x86 wheel ещё не является обещанием поддержки AMD CPU ([OpenVINO system requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html)). Поэтому OpenVINO на AMD x86 — только экспериментальный путь, не поддерживаемая опора проекта. - -На AMD GPU под Windows есть два готовых пути: - -1. `onnxruntime-directml` 1.24.4 (`cp313-win_amd64`): DirectML официально поддерживает AMD GCN первого поколения и новее, но находится в sustained engineering, ограничен ONNX opset 20 и не гарантирует полный offload ([DirectML EP](https://onnxruntime.ai/docs/execution-providers/DirectML-ExecutionProvider.html)). -2. Windows ML 2.x: встроенный DirectML или загружаемый MIGraphX. Каталог MIGraphX доступен только на Windows 11 24H2+ и при совместимых GPU/driver; Microsoft отдельно отмечает, что текущий MIGraphX EP не поддерживает GenAI scenarios ([Windows ML EP](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/supported-execution-providers)). Для GigaAM RNN-T граница понятия GenAI в этой таблице не определена — нужен локальный тест, а не перенос ограничения по аналогии. - -На AMD GPU под Linux прежний ROCm EP удалён из source tree начиная с ORT 1.23; Microsoft рекомендует MIGraphX или VitisAI ([ORT 1.23](https://github.com/microsoft/onnxruntime/releases/tag/v1.23.0)). Старый `onnxruntime-rocm` всё ещё публикуется на PyPI (последний `1.22.2.post3`, включая `cp313`), но остаётся на ветке до удаления EP и потому не является долгоживущим направлением ([PyPI JSON](https://pypi.org/pypi/onnxruntime-rocm/json)). Активный `onnxruntime-migraphx` уже имеет версию `1.27.1` и `cp313-manylinux_2_34_x86_64`; это готовый wheel, хотя он требует совместимых ROCm/GPU/OS и отличается от core ORT 1.28 ([PyPI JSON](https://pypi.org/pypi/onnxruntime-migraphx/json), [MIGraphX EP](https://onnxruntime.ai/docs/execution-providers/MIGraphX-ExecutionProvider.html)). Следовательно, Python 3.13 больше не блокирует установку; реальными неизвестными остаются системный ROCm stack и совместимость конкретных графов. +Под Linux прежний ROCm EP удалён из ORT 1.23. Пакет `onnxruntime-rocm` +1.22.2.post3 всё ещё имеет `cp313`, но закреплён на ветке до удаления EP. +Активный `onnxruntime-migraphx` 1.27.1 публикует +`cp313-manylinux_2_34_x86_64`; реальные ограничения теперь лежат в ROCm/GPU/OS +и покрытии графа +([ROCm PyPI JSON](https://pypi.org/pypi/onnxruntime-rocm/json), +[MIGraphX PyPI JSON](https://pypi.org/pypi/onnxruntime-migraphx/json), +[MIGraphX EP](https://onnxruntime.ai/docs/execution-providers/MIGraphX-ExecutionProvider.html)). ### Apple Silicon -`onnxruntime` 1.28.0 публикует `cp313` wheel для macOS 14 ARM64; это готовый CPU baseline. Тот же macOS build доступен `onnx-asr`, который документирует `CPUExecutionProvider` и `CoreMLExecutionProvider` в обычном пакете ([ORT Python install](https://onnxruntime.ai/docs/get-started/with-python.html), [onnx-asr installation](https://istupakov.github.io/onnx-asr/installation/)). +ORT CPU — готовый baseline. `onnx-asr` документирует CoreML в обычном +`onnxruntime` package. CoreML EP может использовать CPU, GPU и Apple Neural +Engine через `MLComputeUnits`, но забирает только поддержанные подграфы. +Dynamic shapes могут быть дорогими; offload внутри `Loop`/`Scan`/`If` по +умолчанию выключен. `ProfileComputePlan` позволяет увидеть размещение +([onnx-asr installation](https://istupakov.github.io/onnx-asr/installation/), +[CoreML EP](https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html)). -CoreML EP может задействовать CPU, GPU и Apple Neural Engine через `MLComputeUnits`. Он забирает поддерживаемые subgraphs, допускает динамические shapes, но предупреждает об их возможной цене; для `Loop`/`Scan`/`If` offload внутри тела по умолчанию выключен. Параметры `RequireStaticInputShapes`, `EnableOnSubgraphs` и `ProfileComputePlan` позволяют проверить фактическое размещение. Сам EP в общей таблице ORT всё ещё помечен Preview ([CoreML EP](https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html), [таблица EP](https://onnxruntime.ai/docs/execution-providers/)). Следовательно, «CoreML доступен» не означает «GigaAM целиком работает на ANE». +OpenVINO/OpenVINO GenAI имеют `cp313-macosx_11_0_arm64` wheels и поддерживают +Apple Silicon, но на macOS исполняются только на CPU. GPU plugin рассчитан на +Intel GPU, NPU plugin — на Intel NPU +([OpenVINO requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html), +[OpenVINO GenAI files](https://pypi.org/project/openvino-genai/2026.3.0.0/)). -OpenVINO 2026.3 и OpenVINO GenAI 2026.3 имеют `cp313-macosx_11_0_arm64` wheels и официально поддерживают Apple silicon, но на macOS OpenVINO выполняет inference только на CPU; GPU plugin поддерживает только Intel GPU, NPU plugin — Intel NPU ([system requirements](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino/system-requirements.html), [openvino-genai PyPI](https://pypi.org/project/openvino-genai/2026.3.0.0/)). Значит OpenVINO Whisper на Apple Silicon технически поставляется без сборки, но не использует GPU/ANE. Текущий marker проекта, исключающий OpenVINO extra на macOS, остаётся отдельным integration constraint. +## Возможности текущих моделей -### Сводная матрица runtime/device +Таблица применяет одну и ту же лестницу доказательства к трем модельным путям. -| Платформа и устройство | Готовый runtime/EP | Статус на 2026-08-12 | `cp313` | Без кастомной сборки в текущем CLI | -|---|---|---|---|---| -| Windows, AMD CPU | ORT CPU EP | production baseline | да, `win_amd64` | да | -| Windows, AMD GPU | ORT DirectML | sustained engineering | да, `onnxruntime-directml` | пакет есть; нужен новый provider/device UX | -| Windows 11 24H2+, AMD GPU | Windows ML + DirectML/MIGraphX | Windows ML production; catalog EP зависит от driver/device | да, `onnxruntime-windowsml` | DirectML близко к готовому; MIGraphX требует bootstrap/registration | -| Linux, AMD CPU | ORT CPU EP | production baseline | да, `manylinux x86_64` | да | -| Linux, AMD GPU | ORT MIGraphX | active replacement for removed ROCm EP | да, `onnxruntime-migraphx 1.27.1` | wheel есть; нужны ROCm compatibility и model smoke-test | -| Apple Silicon, CPU | ORT CPU EP | production baseline | да, macOS 14 ARM64 | да | -| Apple Silicon, GPU/ANE | CoreML EP | preview; partial partitioning possible | да, в обычном ORT wheel | пакет есть; требуется provider integration и profiling | -| Apple Silicon, CPU | OpenVINO GenAI Whisper | production package; CPU-only на macOS | да | upstream да; текущий project extra/marker — нет | - -## Матрица моделей: что действительно переносится - -Легенда: **подтверждено** — есть прямое upstream-обещание/поставка; **вывод** — следует из одинакового ONNX/ORT контракта, но нет проверки данной модели; **эксперимент** — session/model compatibility и offload неизвестны. - -| Модель/контракт | ORT CPU (AMD Win/Linux, Apple) | AMD GPU Windows (DML/WinML) | AMD GPU Linux (MIGraphX) | Apple GPU/ANE (CoreML) | OpenVINO native | +| Модель и требуемый контракт | Переносимый baseline | Intel accelerator | AMD accelerator | Apple accelerator | Browser | |---|---|---|---|---|---| -| GigaAM v3 E2E RNN-T ONNX + token timestamps | **Подтверждено** upstream `onnx-asr` на x86/Arm CPU и готовыми cp313 wheels. `with_timestamps()` — API `onnx-asr` ([usage](https://istupakov.github.io/onnx-asr/usage/), [model card](https://huggingface.co/istupakov/gigaam-v3-onnx)) | `onnx-asr` заявляет DirectML/WebGPU support и документирует Windows ML package; **эксперимент** для конкретного E2E RNN-T: session creation, доля DML/MIGraphX, точность/timestamps | cp313 MIGraphX wheel есть; provider допустим как произвольная строка, но не first-class/tested; **эксперимент** для graph coverage и output | CoreML заявлен `onnx-asr`; **эксперимент** для dynamic encoder/decoder, control flow и доли ANE | OpenVINO EP package lagging; native IR не является тем же artifact; **эксперимент/конверсия** | -| OpenVINO GenAI Whisper + word timestamps | не тот runtime/model artifact | OpenVINO GPU не работает на AMD GPU; CPU path возможен лишь там, где CPU официально поддержан | то же | OpenVINO CPU на Apple silicon **подтверждён**; GPU/ANE нет | **Подтверждено** для Whisper tiny/base/small/medium/large-v3 и Distil-Whisper; word timestamps доступны CPU/GPU/NPU, stateful model обязателен ([ASR guide](https://openvinotoolkit.github.io/openvino.genai/docs/use-cases/speech-recognition/), [supported models](https://openvinotoolkit.github.io/openvino.genai/docs/supported-models/)) | -| sherpa-onnx pyannote segmentation + WeSpeaker embeddings | **Подтверждено локальной разведкой** на CPU ORT для одной записи; cp313 wheels есть на Windows/Linux/macOS ARM64 ([разведка](../benchmarks/2026-08-12-diarization-feasibility.md), [PyPI 1.13.5](https://pypi.org/project/sherpa-onnx/1.13.5/)) | sherpa имеет DirectML build option, но готовый Python wheel/provider и именно эти две модели на DML не подтверждены: **эксперимент/возможно rebuild** | готовая sherpa Python поставка с MIGraphX не подтверждена; **не готово** | upstream Python provider vocabulary обычно ограничивает `cpu,cuda,coreml`; наличие CoreML в wheel не доказывает поддержку diarization graphs: **эксперимент** | модели ONNX теоретически читаются OpenVINO, но полное/частичное покрытие и численная стабильность clustering inputs не подтверждены: **эксперимент** | +| GigaAM v3 E2E RNN-T: текст + token timestamps | **Подтверждено:** `onnx-asr` + ORT CPU на x86/ARM | OpenVINO EP или конверсия в IR — **эксперимент** | DirectML/WinML MIGraphX/Linux MIGraphX — **эксперимент** | CoreML — **эксперимент** | Нужен порт Python preprocessing/decoder и проверка kernels | +| OpenVINO GenAI Whisper: текст + word timestamps | Нативный OpenVINO CPU на поддержанных платформах | **Подтверждено:** OpenVINO CPU/GPU/NPU | AMD GPU не поддержан; AMD CPU не входит в official hardware | **Подтверждено:** только OpenVINO CPU | Это другой runtime/model artifact; не подтверждено | +| PyAnnote segmentation + WeSpeaker embeddings: интервалы + кластеры | **Подтверждено локально:** sherpa-onnx + ORT CPU на одной записи | OpenVINO EP/native — **эксперимент** | DML/MIGraphX — **эксперимент**, для sherpa может потребоваться rebuild | CoreML — **эксперимент** | sherpa имеет WASM demo, но выбранная пара моделей не подтверждена | -Почему timestamps должны пережить смену EP — это **вывод**, а не готовая совместимость: `onnx-asr` сохраняет в Python preprocessing и greedy decoding, а EP исполняет ONNX encoder/decoder; одинаковые тензорные выходы должны дать тот же `TimestampedResult` ([описание архитектуры](https://github.com/istupakov/onnx-asr/tree/v0.12.0), [timestamps API](https://istupakov.github.io/onnx-asr/usage/)). Но mixed precision, unsupported ops/fallback и provider-specific graph transforms требуют golden-output проверки. Для diarization выходной контракт сегментов создаётся sherpa pipeline после двух ONNX-моделей и clustering ([C API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)); ускорение одной модели не должно считаться ускорением всего pipeline. +### GigaAM E2E RNN-T -Отдельный support gap: официальный recipe sherpa подтверждает PyAnnote -segmentation с 3D-Speaker или NeMo embeddings, но не с выбранным в разведке -WeSpeaker. Локальный CPU-прогон уже доказал, что эта пара создаёт интервалы на -одной записи; неизвестны upstream-гарантия контракта и переносимость на другие -EP, а не базовая совместимость CPU-пути -([разведка](../benchmarks/2026-08-12-diarization-feasibility.md), -[sherpa models](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html), -[WeSpeaker pretrained models](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md)). +`onnx-asr` работает на x86/ARM CPU и перечисляет CoreML, DirectML, ROCm и +WebGPU. GigaAM создаёт обычные ORT-сессии encoder/decoder/joint, поэтому смена +EP архитектурно возможна +([onnx-asr](https://istupakov.github.io/onnx-asr/), +[installation](https://istupakov.github.io/onnx-asr/installation/), +[model card](https://huggingface.co/istupakov/gigaam-v3-onnx)). Но это не +доказывает operator coverage или полный offload конкретного E2E RNN-T. -GigaAM timestamp caveat: upstream GigaAM `transcribe(..., word_timestamps=True)` возвращает слова со start/end, но его официальный ONNX helper экспортирует encoder/decoder/joint и проверяет только text parity; helper возвращает `List[str]` и теряет emission frames ([GigaAM repository](https://github.com/salute-developers/GigaAM), [ONNX parity test](https://github.com/salute-developers/GigaAM/blob/main/tests/test_onnx.py), [ONNX helper](https://github.com/salute-developers/GigaAM/blob/main/gigaam/onnx_utils.py)). Timestamped contract текущего проекта даёт именно `onnx-asr.with_timestamps()`, а не произвольный GigaAM ONNX export. Поэтому golden test должен сравнивать project/onnx-asr contract, не только распознанный текст. +Таймкоды формирует `onnx-asr.with_timestamps()` из тензорных выходов модели. +Если EP сохраняет эти выходы, `TimestampedResult` должен сохраниться — это +**вывод**, который требует golden test. Mixed precision, graph transforms и +CPU fallback могут менять численные результаты +([timestamps API](https://istupakov.github.io/onnx-asr/usage/), +[архитектура пакета](https://github.com/istupakov/onnx-asr/tree/v0.12.0)). -Минимальная будущая экспериментальная матрица без заявления производительности: +Официальный ONNX helper исходного GigaAM проверяет только text parity и теряет +emission frames. Поэтому проверять нужно именно контракт `onnx-asr`, а не +произвольный GigaAM ONNX export +([GigaAM](https://github.com/salute-developers/GigaAM), +[ONNX parity test](https://github.com/salute-developers/GigaAM/blob/main/tests/test_onnx.py), +[ONNX helper](https://github.com/salute-developers/GigaAM/blob/main/gigaam/onnx_utils.py)). -1. Один фиксированный 5–10-минутный fixture с overlap и эталоном текущего CPU output. -2. Для GigaAM E2E RNN-T: ORT CPU против DML, WinML MIGraphX, CoreML и MIGraphX Linux; фиксировать session creation, provider assignment/profile, CPU fallback, transcript/token timestamps и численное расхождение. -3. Для diarization: отдельно pyannote segmentation и WeSpeaker embeddings, затем полный pipeline; фиксировать provider assignment каждой сессии, сегменты/число спикеров и стабильность embeddings/clustering. -4. Для OpenVINO Whisper: CPU на Apple и поддерживаемом Intel, GPU/NPU только на Intel; проверять word timestamps и project adapter отдельно. +### OpenVINO Whisper -## Почему ONNX Runtime Web стал общим браузерным слоем +OpenVINO GenAI подтверждает Whisper tiny/base/small/medium/large-v3 и +Distil-Whisper. Word timestamps доступны на CPU/GPU/NPU, stateful model +обязателен +([ASR guide](https://openvinotoolkit.github.io/openvino.genai/docs/use-cases/speech-recognition/), +[supported models](https://openvinotoolkit.github.io/openvino.genai/docs/supported-models/)). +Это наиболее доказанный accelerator-путь из рассматриваемых, но только для +поддержанного OpenVINO hardware. На Apple Silicon он остаётся CPU-путём; на AMD +GPU не работает. -ONNX остаётся переносимым serialized graph/IR, а `onnxruntime-web` — отдельным JavaScript/WebAssembly runtime. Общность браузеров даёт не ONNX-файл сам по себе, а единый ORT JS API поверх разных EP: `wasm` как default CPU baseline, `webgpu`, `webnn` и legacy `webgl`. ORT распределяет поддерживаемые nodes/subgraphs на accelerator, а неподдерживаемые может оставить WASM, если он указан вторым provider ([web overview](https://onnxruntime.ai/docs/tutorials/web/), [session options](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html)). Это та же архитектурная идея, что native ORT, но runtime binaries и provider kernels другие. +Проектный OpenVINO backend уже запрашивает timestamps, но сводит результат к +chunk-сегментам. Поэтому для диаризации нужно сначала определить и протянуть +word-level контракт через `Backend`/`TranscribeResult`. -На 2026-08-12 официальный browser matrix таков ([matrix](https://onnxruntime.ai/docs/get-started/with-javascript/web.html)): +### PyAnnote + WeSpeaker для диаризации -- WASM: Chrome/Edge, Safari, Firefox на основных desktop/mobile платформах; полный набор ONNX operators, CPU baseline; -- WebGPU: Chromium 113+ на Windows, Chromium на macOS/Android; в ORT Web всё ещё обозначен experimental, operator subset; -- WebNN: experimental и не включён по умолчанию; официальный matrix требует feature flag в Chrome/Edge Windows; unsupported ops fall back to WASM ([WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)); -- WebGL: maintenance mode, operator subset. +Локальная разведка доказала, что связка PyAnnote segmentation + WeSpeaker +embeddings создаёт интервалы на CPU ORT для одной записи. Это закрывает базовую +совместимость, но не upstream-гарантию пары и не переносимость на другие EP +([разведка](../benchmarks/2026-08-12-diarization-feasibility.md)). -Один ONNX artifact и близкий `InferenceSession` contract можно использовать native и web, но «один artifact» не означает одинаковую работоспособность. Для WebGPU опубликована отдельная operator table ([WebGPU operators](https://github.com/microsoft/onnxruntime/blob/main/js/web/docs/webgpu-operators.md)); accelerator EP поддерживают лишь subset, а WASM — все операторы. Большие модели ограничены браузером: около 2 GB для ArrayBuffer/Protobuf, 4 GB WebAssembly memory; external data нужно передавать URL/Blob явно ([large models](https://onnxruntime.ai/docs/tutorials/web/large-models.html)). WASM threading включается только при `crossOriginIsolated`; proxy worker не совместим с WebGPU и CSP-restricted environment ([env flags](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html)). Dynamic shapes и CPU fallback также исключают некоторые оптимизации WebGPU graph capture ([WebGPU guide](https://onnxruntime.ai/docs/tutorials/web/ep-webgpu.html)). +Официальный sherpa recipe перечисляет PyAnnote с 3D-Speaker или NeMo +embeddings, а WeSpeaker публикует собственные ONNX-модели. Поэтому на новом EP +нужно отдельно проверять обе сессии и полный pipeline +([sherpa models](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html), +[WeSpeaker models](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md)). -`onnx-asr` 0.12 заявляет WebGPU support для **native Python package** и называет `onnxruntime-webgpu` beta; это не browser JavaScript port `onnx-asr` ([installation](https://istupakov.github.io/onnx-asr/installation/)). Для GigaAM E2E RNN-T browser compatibility не подтверждена: нужны JS preprocessing/decoder либо порт Python-логики, загрузка нескольких model artifacts, проверка WebGPU operator coverage и WASM fallback. У sherpa-onnx есть отдельная WebAssembly speaker-diarization сборка и JS example, но она однопоточная и не доказывает, что выбранные проектом pyannote + WeSpeaker models работают через ORT Web WebGPU ([sherpa JS diarization](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/javascript.html), [build option](https://github.com/k2-fsa/sherpa-onnx/blob/master/CMakeLists.txt)). +Итоговые сегменты создаёт sherpa после двух ONNX-моделей и clustering. Ускорение +одной сессии не означает ускорение pipeline; численные изменения эмбеддингов +могут изменить кластеры даже при совпадающем текстовом контракте +([C API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)). -Native WebGPU и browser WebGPU нельзя смешивать: native Python EP использует Dawn поверх D3D12/Vulkan/Metal и теперь поставляется plugin-пакетом `onnxruntime-ep-webgpu` (0.2.1, universal wheels), тогда как ORT Web использует browser JSEP/WASM path ([native WebGPU EP](https://onnxruntime.ai/docs/execution-providers/WebGPU-ExecutionProvider.html), [plugin PyPI JSON](https://pypi.org/pypi/onnxruntime-ep-webgpu/json)). +## Почему ONNX Runtime Web работает между браузерами -Практический урок для CLI — не новый browser product, а более строгая portability-модель: +Браузерная переносимость появляется не из ONNX-файла отдельно, а из сочетания +трёх решений: -- сохранять portable CPU baseline; -- считать accelerator опциональной capability, обнаруживаемой при запуске; -- различать «API/EP существует», «model session создалась», «graph offloaded» и «контракт/качество сохранены»; -- измерять долю fallback и end-to-end pipeline, а не обещать устройство по имени provider. +1. ONNX задаёт общий сериализованный граф. +2. ORT Web даёт один JavaScript `InferenceSession` API. +3. WASM служит широким CPU baseline, а WebGPU/WebNN подключаются как + ускорители с fallback на WASM. -## Что это меняет для карты диаризации +На 2026-08-12 официальный browser matrix выглядит так +([матрица](https://onnxruntime.ai/docs/get-started/with-javascript/web.html)): -1. Разведка `sherpa-onnx` на обычном CPU ORT не опирается на затухающий - компонент: ядро ORT активно и имеет самый широкий CPython/platform coverage. - Это поддерживает текущий вариант диаризации как optional post-processing, - но ничего не говорит о качестве DirectML/OpenVINO EP на конкретных двух - моделях. -2. Формулировка разведочного замера «у OpenVINO GenAI потокенных таймкодов нет» - требует уточнения. Upstream с 2026.0 предоставляет word-level timestamps; - сейчас их не экспортирует проектный OpenVINO backend. Следовательно, - невозможность пословной привязки на `--device openvino-*` — **интеграционный - пробел local-transcriber**, а не долгосрочное ограничение движка - ([OpenVINO 2026.0](https://docs.openvino.ai/2026/about-openvino/release-notes-openvino.html#openvino-2026-0-0)). -3. Не следует связывать UX диаризации с немедленным выбором EP. Сначала можно - определить пользовательский контракт — флаг, `num_speakers`, зависимость, - формат и честное поведение при отсутствии word timestamps. Ускорение - диаризации через OpenVINO EP или DirectML должно пройти отдельную - совместимость и benchmark на обеих моделях `sherpa-onnx`. -4. Не следует принимать решение о полной консолидации проекта на ORT. Нативный - OpenVINO GenAI развивается как ASR-платформа, в том числе по таймкодам и NPU, - а FasterWhisper сохраняет отдельные достоинства CUDA и языкового покрытия, - уже зафиксированные ADR-003/006. +- WASM работает в Chrome/Edge, Safari и Firefox на основных desktop/mobile + платформах и имеет наиболее полное покрытие операторов; +- WebGPU поддерживается Chromium на Windows/macOS/Android, остаётся experimental + в ORT Web и имеет собственный operator subset; +- WebNN experimental и в официальной матрице требует feature flag в + Chrome/Edge Windows; неподдержанные узлы могут уйти в WASM + ([WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)); +- WebGL находится в maintenance mode. -## Рекомендация по жизненному циклу +Один model artifact не означает одинаковую работоспособность. WebGPU имеет +отдельную [таблицу операторов](https://github.com/microsoft/onnxruntime/blob/main/js/web/docs/webgpu-operators.md), +а preprocessing и decoding остаются кодом приложения. Большие модели упираются +примерно в 2 GB для ArrayBuffer/Protobuf и 4 GB WebAssembly memory; external +data нужно загружать отдельно +([large models](https://onnxruntime.ai/docs/tutorials/web/large-models.html)). +WASM threading требует `crossOriginIsolated`; proxy worker несовместим с +WebGPU, а dynamic shapes и CPU fallback ограничивают graph capture +([environment flags](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html), +[WebGPU guide](https://onnxruntime.ai/docs/tutorials/web/ep-webgpu.html)). -Для portability evidence приоритеты такие: CPU baseline должен оставаться обязательным; accelerator — opt-in capability с явной диагностикой provider/device/fallback; platform wheel и provider name считаются только предпосылкой, пока model-specific smoke/golden test не подтвердил session creation, placement и выходной контракт. Windows ML, MIGraphX, CoreML и native WebGPU следует оценивать отдельными экспериментами, а не добавлять в UX как обещанные устройства заранее. +`onnx-asr` заявляет WebGPU для **native Python package**. Это не browser port: +GigaAM потребует JavaScript preprocessing/decoder, загрузки нескольких +артефактов и проверки kernels. У sherpa-onnx есть отдельная однопоточная WASM +speaker-diarization demo, но она не доказывает работу проектной пары PyAnnote + +WeSpeaker через ORT Web WebGPU +([onnx-asr installation](https://istupakov.github.io/onnx-asr/installation/), +[sherpa JS diarization](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/javascript.html)). -Это не решение о консолидации backend-ов на ORT и не предложение browser-направления. +Native WebGPU EP также не равен browser WebGPU: Python plugin использует Dawn +поверх D3D12/Vulkan/Metal, ORT Web — browser JSEP/WASM path +([native WebGPU EP](https://onnxruntime.ai/docs/execution-providers/WebGPU-ExecutionProvider.html), +[plugin PyPI JSON](https://pypi.org/pypi/onnxruntime-ep-webgpu/json)). -Ниже — **интерпретация источников для local-transcriber**, а не опубликованный -roadmap Microsoft или Intel. Она исходит из фактов о lifecycle, wheel-матрицах -и текущем контракте проекта; реальную пригодность каждого ускорителя должен -подтвердить проектный benchmark. +Полезный для CLI вывод из браузерной архитектуры — не новый продукт, а строгая +политика capabilities: -- Сохранять **ONNX-модели диаризации + обычный CPU ORT** как базовый переносимый - путь: это наименее связанный с одним вендором слой и единственная из трёх - поставок с wheel на Windows/Linux ARM64 и macOS ARM64. -- Рассматривать **OpenVINO EP как опциональное ускорение этих же ONNX-моделей на - Intel**, но не обещать его до проверки operator coverage, фактического - provider assignment, качества и скорости. Его wheel активен, однако отстаёт - от текущих ORT/OpenVINO и требует собственной матрицы версий. -- Рассматривать **нативный OpenVINO/OpenVINO GenAI как основной долгосрочный - Intel ASR-путь**, особенно для Whisper и будущего NPU. Для пословной - диаризации сначала проверить и протянуть уже существующие upstream word - timestamps через проектный `Backend` contract. -- Не закладывать новый DirectML backend проекта: текущий EP поддерживается, но - feature development официально ушёл в WinML. Если кросс-вендорное Windows GPU - ускорение станет отдельной целью, исследовать WinML как новый - Windows-специфический backend, а не считать `onnxruntime-directml` - долгоживущим default. +- всегда сохранять переносимый CPU baseline; +- обнаруживать ускоритель во время запуска; +- различать наличие API, успешную сессию, размещение графа и сохранение + контракта; +- измерять end-to-end pipeline, а не отдельное имя provider. -## Новые вопросы карты +## Что это меняет для `local-transcriber` -- Нужен ли отдельный portability experiment: GigaAM E2E RNN-T и обе diarization-модели на DirectML, WinML MIGraphX, Linux MIGraphX и CoreML с node placement/profile и golden outputs? -- Насколько устойчива локально работающая пара PyAnnote + WeSpeaker между - платформами и EP, если upstream recipe её не фиксирует? -- Должен ли Windows UX показывать не только выбранный provider, но и фактический accelerator/fallback после capability discovery? -- Стоит ли поддерживать Windows ML bootstrap/catalog как отдельный integration layer или оставить низкофрикционный DirectML до появления подтверждённого выигрыша? -- Какой минимальный browser experiment проверит GigaAM preprocessing/decoder/timestamps и pyannote+embedding WASM/WebGPU, не превращая карту в browser roadmap? +1. **CPU ORT остаётся переносимым baseline.** Он не зависит от затухающего EP и + обеспечивает самый широкий CPython/platform coverage для GigaAM и + диаризации. +2. **Нативный OpenVINO остаётся отдельным долгосрочным Intel ASR-путём.** Его + не следует заменять OpenVINO EP только ради единого ORT API: EP отстаёт и не + даёт автоматически pipeline-возможности OpenVINO GenAI. +3. **Пословная диаризация на `openvino-*` технически достижима.** Upstream уже + возвращает word timestamps; карта должна решить контракт и fallback, а не + считать отсутствие таймкодов свойством движка. +4. **AMD/Apple acceleration нельзя добавлять по факту наличия wheel.** Сначала + нужны model-specific smoke/profile/golden tests; только затем device UX и + dependency markers. +5. **Диаризацию не нужно связывать с немедленным выбором аппаратного EP.** + Переносимый CPU-вариант может быть специфицирован независимо; ускорение двух + моделей — отдельная работа. +6. **Консолидация на ORT из исследования не следует.** FasterWhisper сохраняет + CUDA и языковое покрытие, нативный OpenVINO — актуальный Intel ASR API, ORT — + переносимый ONNX-путь. -- Какой точный контракт word timestamps возвращают `WhisperPipeline` и новый - `ASRPipeline` 2026.3, и как без потери совместимости добавить их в проектный - `Backend`/`TranscribeResult`? -- Дают ли `sherpa-onnx` segmentation и embedding models полный offload в - OpenVINO EP, или часть графа уходит в CPU EP; меняются ли границы и - эмбеддинги численно? -- Есть ли выигрыш OpenVINO EP на целевом Intel Core i5 11-го поколения после - учёта второго runtime, загрузки модели и памяти, или CPU ORT уже оптимальнее? -- Нужен ли UX явного отказа/огрубления диаризации на backend без word - timestamps, либо backend contract должен сначала стать timestamp-aware? -- Следует ли разделить extra диаризации на переносимый CPU-вариант и - Intel-ускорение с platform marker, чтобы не ухудшить zero-config установку на - ARM/macOS? +Для текущей карты это даёт два входа: + +- [«Выбрать единицу привязки спикера к тексту»](https://git.dementev.space/ddmitry/local-transcriber/issues/14) + должен назвать timestamp-aware изменение `Backend`/`TranscribeResult`; +- [«UX диаризации: флаг, число участников, зависимость и поведение на OpenVINO»](https://git.dementev.space/ddmitry/local-transcriber/issues/16) + должен определить поведение там, где конкретный backend/model не отдаёт + нужных таймкодов. + +Реализация и приёмка WinML, MIGraphX, CoreML и browser-путей остаются за +пунктом назначения карты. + +## Минимальная экспериментальная матрица + +Будущий platform experiment должен использовать один 5–10-минутный fixture с +перекрывающейся речью и зафиксированным CPU output. + +| Объект | Сравнение | Что фиксировать | +|---|---|---| +| GigaAM E2E RNN-T | ORT CPU против DirectML, WinML MIGraphX, Linux MIGraphX, CoreML | Создание всех сессий, node placement, CPU fallback, текст, token timestamps, численное расхождение | +| PyAnnote segmentation | Те же EP отдельно от остального pipeline | Placement, интервалы и расхождение выходных тензоров | +| WeSpeaker embeddings | Те же EP отдельно | Placement, cosine drift и влияние на clustering | +| Полная диаризация | CPU baseline против каждого прошедшего EP | Число спикеров, границы, стабильность кластеров, время и память | +| OpenVINO Whisper | Intel CPU/GPU/NPU и Apple CPU | Word timestamps, проектный adapter, время и память | +| Browser, только если станет целью | WASM baseline против WebGPU/WebNN | Размер артефактов, kernels/fallback, preprocessing/decoder и память | + +Путь можно предлагать пользователю только после прохождения всех пяти уровней +доказательства; выигрыш отдельной ONNX-сессии не считается выигрышем +end-to-end транскрипции или диаризации. + +## Открытые вопросы после исследования + +### Внутри карты диаризации + +- Какой точный word/token timestamp contract нужен formatter и объединению с + интервалами спикеров? +- Как представить capability backend/model и какое fallback-поведение выбрать, + если пословных таймкодов нет? + +### Отдельные будущие работы + +- Проходят ли GigaAM E2E RNN-T, PyAnnote и WeSpeaker все пять уровней на + DirectML, Windows ML MIGraphX, Linux MIGraphX и CoreML? +- Насколько устойчива локально работающая пара PyAnnote + WeSpeaker между EP, + если upstream recipe её не фиксирует? +- Окупает ли Windows ML bootstrap/catalog преимущество над низкофрикционным, + но legacy DirectML? +- Даёт ли OpenVINO EP выигрыш моделям диаризации на целевом Intel Core i5 после + учёта fallback, загрузки и памяти? +- Нужен ли отдельный browser experiment, или браузерная ветка остаётся только + архитектурным примером переносимого baseline и опциональных ускорителей? -- 2.54.0 From 8c55eaa87f46b0b832b535101e240ea734466120 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 18:08:26 +0300 Subject: [PATCH 07/15] =?UTF-8?q?docs(context):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=82=D0=B5=D1=80=D0=BC=D0=B8=D0=BD?= =?UTF-8?q?=20=C2=AB=D0=9E=D0=BF=D0=BE=D1=80=D0=BD=D0=B0=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=BC=D0=B5=D1=82=D0=BA=D0=B0=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - слуховая проверка (#12) показала, что разметку диаризации читают как истину, хотя она содержит ложный кластер, пропуски и неполные перекрытия. - Что: - в глоссарий добавлен термин «Опорная разметка» с запретом на «эталонную», «истинную» и ground truth. - определение описывает роль разметки в измерении, а не результат конкретного прогона: выводы о пороге 0,9 остались в тикетах карты. - Проверка: - git show --stat HEAD и чтение CONTEXT.md. --- CONTEXT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index 5065abf..2fb7177 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -15,3 +15,7 @@ _Avoid_: Доступная модель, дефолт **Модель по умолчанию**: Поддерживаемая модель, которую проект выбирает без явного указания модели пользователем для определённого пути выполнения. _Avoid_: Рекомендуемая модель, поддерживаемая модель + +**Опорная разметка**: +Разметка, принятая за точку отсчёта при измерении чего-то другого. Опорной её делает роль в измерении, а не качество: она не выверена вручную и сама может содержать ошибки, поэтому посчитанная по ней величина осмысленна как порядок, но не как точное значение. +_Avoid_: Эталонная разметка, истинная разметка, ground truth -- 2.54.0 From 0fdeebb256f50525fe767058888634b826080b65 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 11:18:17 +0300 Subject: [PATCH 08/15] =?UTF-8?q?docs(diarization):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=82=D1=87=D1=91?= =?UTF-8?q?=D1=82=20=D0=B8=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B1=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - необходим устойчивый дефолт модели эмбеддингов и порога на русской речи. - Что: - задокументирован выбор WeSpeaker ResNet34 LM с порогом 0,89. - добавлен возобновляемый скрипт свипа моделей и параметров диаризации. - Проверка: - uvx --cache-dir .uv-cache ruff check scripts/benchmarks/diarization_calibration.py. - uv run --cache-dir .uv-cache pytest: 243 passed, 1 skipped. --- .../2026-08-14-diarization-calibration.md | 209 +++++++++++ scripts/benchmarks/diarization_calibration.py | 355 ++++++++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 docs/benchmarks/2026-08-14-diarization-calibration.md create mode 100644 scripts/benchmarks/diarization_calibration.py diff --git a/docs/benchmarks/2026-08-14-diarization-calibration.md b/docs/benchmarks/2026-08-14-diarization-calibration.md new file mode 100644 index 0000000..d5870a3 --- /dev/null +++ b/docs/benchmarks/2026-08-14-diarization-calibration.md @@ -0,0 +1,209 @@ +# Калибровка модели эмбеддингов и порога диаризации + +**Дата:** 2026-08-14 + +**Статус:** выбор конфигурации для проектирования. Не приёмка +производительности на целевом Intel Core i5 11-го поколения. + +## Решение + +Для автоматического определения числа участников использовать: + +- эмбеддинги `wespeaker_en_voxceleb_resnet34_LM.onnx`; +- `FastClusteringConfig.threshold=0.89`; +- `num_clusters=-1` по умолчанию. + +Если число участников известно, передавать его через `num_clusters`: это +устраняет остаточные кластеры и служит страховкой от особенностей записи. На +трёх проверенных фрагментах явное число участников не ухудшило прокси-метрику +качества WeSpeaker. + +Двуязычная `3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx` +быстрее и при известном числе участников лучше на одной из двух записей с +таймкодами, но для автоматического режима не нашлось общего порога без лишних +кластеров или склейки реальных голосов. Поэтому она не выбрана по умолчанию. + +## Что проверялось + +Свип выполнялся на трёх русскоязычных рабочих созвонах с известным составом: + +| Запись | Участников | Короткий фрагмент | Полный прогон кандидата | SHA-256 | +|---|---:|---:|---:|---| +| `2026-07-10 Data Test внутренний статус.mp4` | 3 | 07:00–12:00 | 25:59,9 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` | +| `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4` | 2 | 00:00–05:00 | 14:50,9 | `51866D247FE3EDA134CDD884F707B1F1DB8855B492E8BD14D2B56B62476255ED` | +| `2026-08-12 Созвон с Максом Мерлином по T2 Forecast и Yantar.mp4` | 2 | 00:00–05:00 | 20:22,2 | `4422F04E2771091A0648E5422D14A31DD7CA2C8EEF7ED9F4A5C63F43D8CA6400` | + +Для первой записи число участников взято из согласованного MoM и не зависит от +диаризации. Для двух остальных рядом с медиа лежат транскрипты Hypescribe с +таймкодами и метками спикеров. Они получены другим инструментом и использованы +как независимая грубая опорная разметка. + +Hypescribe ставит метку только в начале реплики и не размечает точные границы, +тишину и наложения голосов. Поэтому ниже считается не DER, а **mapped speaker +purity**: лучший взаимно-однозначный маппинг кластеров на опорные метки по +суммарному перекрытию. Метрика подходит для сравнения конфигураций на одной +записи, но не является абсолютной оценкой диаризации. + +Кластер считается содержательным, если в нём не меньше `max(5 с, 2% длины +записи)` речи. Это только диагностический показатель: готовый CLI не должен +молча отбрасывать малые кластеры без отдельного решения. + +## Модели + +Во всех прогонах использовалась одна сегментация +`sherpa-onnx-pyannote-segmentation-3-0`. + +| Роль | Модель | Языки обучения | Размер | SHA-256 | +|---|---|---|---:|---| +| выбранная | `wespeaker_en_voxceleb_resnet34_LM.onnx` | английский, VoxCeleb2 | 26 530 550 | `E9848563DA86F263117134DFD7AD63C92355B37DE492B55E325400C9D9C39012` | +| многоязычная альтернатива | `3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx` | китайский + английский | 28 281 164 | `AA3CFC16963A10586A9393F5035D6D6B57E98D358B347F80C2A30BF4F00CEBA2` | +| дополнительная разведка | `3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx` | китайский | 39 593 761 | `1A331345F04805BADBB495C775A6DDFFCDD1A732567D5EC8B3D5749E3C7A5E4B` | +| сегментация | `model.onnx` из `sherpa-onnx-pyannote-segmentation-3-0` | — | 5 992 913 | `220AD67CA923BEF2FA91F2390C786097BF305BCEB5E261D4AF67B38E938E1079` | + +WeSpeaker сам помечает VoxCeleb-модель как английскую и распространяет её под +CC BY 4.0. Репозиторий 3D-Speaker и модель CAMPPlus на ModelScope используют +Apache 2.0; в исходниках 3D-Speaker модель явно описана как обученная на +большом китайско-английском корпусе. ONNX-файлы брались из официального релиза +`k2-fsa/sherpa-onnx`, а не из сторонних зеркал. + +Источники: + +- [список и лицензирование моделей WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md); +- [карточка `wespeaker-voxceleb-resnet34-LM`](https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet34-LM); +- [исходники и лицензия 3D-Speaker](https://github.com/modelscope/3D-Speaker); +- [официальный релиз ONNX-моделей sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models). + +## Свип WeSpeaker + +Порог сначала проверялся крупным шагом, затем уточнялся около переходов между +числом кластеров. В ячейках — общее число кластеров; жирным выделено точное +совпадение с известным числом участников. + +| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 | +|---:|---:|---:|---:| +| 0,85 | **3** | **2** | 3 | +| 0,87 | **3** | **2** | 3 | +| 0,88 | **3** | **2** | 3 | +| **0,89** | **3** | **2** | **2** | +| 0,90 | 2 | **2** | **2** | +| 0,95 | 2 | **2** | 1 | +| явное `num_clusters` | **3** | **2** | **2** | + +`0,89` — единственное проверенное значение, которое без знания числа +участников дало правильное количество кластеров на всех трёх фрагментах. На +двух записях с опорными метками purity составила 0,767 и 0,787. Явное число +участников дало те же значения. + +## Сравнение с 3D-Speaker + +### CAMPPlus, китайский + английский + +| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 | +|---:|---:|---:|---:| +| 0,85 | 7 | 7 | 7 | +| 0,90 | 6 | 5 | 5 | +| 0,95 | 5 | 5 | 5 | +| 0,99 | 4 | 4 | 4 | +| 1,00 | 4 | 4 | 4 | +| 1,05 | **3** | 3 | 3 | +| 1,10 | 2 | **2** | 3 | +| явное `num_clusters` | **3** | **2** | **2** | + +При `1,05` на двух записях остаётся по одному малому остаточному кластеру, а +при `1,10` трёхсторонняя встреча уже склеивается до двух голосов. Общего +автоматического порога нет. + +При явном числе участников purity равна 0,801 на T2 BDMA и 0,911 на Yantar. +Это лучше WeSpeaker на 0,034 и 0,124 соответственно. Однако на контрольной +трёхсторонней записи один из трёх принудительных кластеров оказался меньше +порога содержательности, поэтому улучшение по двум текстовым прокси нельзя +обобщать на все записи. + +### ERes2Net base, китайский + +Эта модель проверялась дополнительно, но не считается выполнением требования +о многоязычной альтернативе. Даже на пороге 0,99 она дала 5 / 4 / 7 кластеров +вместо 3 / 2 / 2. При явном числе участников purity составила 0,688 и 0,907: +результат неоднородный и автоматический режим заметно хуже выбранного. + +## Полные прогоны выбранного кандидата + +После свипа `WeSpeaker + 0,89` прогнан на всех трёх записях целиком. + +| Запись | Кластеры | Содержательные | Речь по кластерам, с | Остаток сверх ожидаемых | Purity | Время | RTF | +|---|---:|---:|---|---:|---:|---:|---:| +| Data Test | 4 | 3 | 573,1 / 487,4 / 342,2 / 19,1 | 1,3% | — | 189,0 с | 0,121 | +| T2 BDMA | 2 | 2 | 748,7 / 52,3 | 0% | 0,749 | 112,3 с | 0,126 | +| Yantar | 2 | 2 | 733,2 / 259,8 | 0% | 0,906 | 151,4 с | 0,124 | + +На полной контрольной записи остаётся ложный кластер на 19,1 с, но три +содержательных кластера совпадают с известным составом. Повторный полный прогон +на 0,9 дал тот же результат: JSON-массивы всех 340 интервалов на 0,89 и 0,9 +совпали в точности, включая границы и номера кластеров. Поэтому к кандидату +0,89 непосредственно применима слуховая проверка отрезка 07:00–12:00, +выполненная для результата из +[разведочного замера](2026-08-12-diarization-feasibility.md): три основных +голоса стабильны, остаточный кластер ложный, есть небольшие пропуски второго +голоса, а наложения голосов определяются не полностью. Новая калибровка не +устраняет эти ограничения сегментации. + +На двух полных разговорах purity отличается от короткого фрагмента: 0,749 +против 0,767 и 0,906 против 0,787. Это подтверждает, что короткий свип годится +для отсева конфигураций, а финальный кандидат надо проверять целиком. + +## Производительность + +Условия: AMD Ryzen 7 8845H, Windows 11 build 26200, Python 3.13.13, +`sherpa-onnx` 1.13.5, `onnxruntime` 1.28.0, NumPy 2.4.3, 8 потоков CPU. +Загрузка моделей и декодирование медиа не входят в измерение. + +Средний RTF на коротких фрагментах: + +| Модель | RTF | Относительно WeSpeaker | +|---|---:|---:| +| WeSpeaker ResNet34 LM | 0,118 | 1,00× | +| CAMPPlus zh/en | 0,083 | 0,70× | +| ERes2Net base zh | 0,152 | 1,29× | + +CAMPPlus примерно на 30% быстрее WeSpeaker в этом эксперименте. Это плюс для +варианта с известным числом участников, но замер на AMD не заменяет приёмку на +целевом Intel Core i5 11-го поколения. + +## Воспроизводимость + +Свип выполняется скриптом +[`scripts/benchmarks/diarization_calibration.py`](../../scripts/benchmarks/diarization_calibration.py). +Он принимает JSON-манифест с путями к моделям и записям, декодирует указанные +фрагменты через ffmpeg, последовательно сохраняет каждый результат и может +возобновить прерванный прогон. + +Пример: + +```powershell +uv run python scripts/benchmarks/diarization_calibration.py ` + --manifest diarization-calibration.json ` + --output diarization-calibration-results.json ` + --work-dir .scratch/diarization-calibration ` + --threads 8 +``` + +Сырые JSON содержат локальные пути к конфиденциальным рабочим записям и сами +интервалы диаризации, поэтому в репозиторий не добавляются. Для проверки +артефактов выше приведены SHA-256 медиа и моделей. + +## Ограничения и следующий шаг + +- Три записи принадлежат одному типу русскоязычных рабочих созвонов; это не + репрезентативная выборка для всех микрофонов, шумов и акцентов. +- Опорные метки двух записей грубые и не дают посчитать DER. +- На слух проверен только фрагмент 07:00–12:00 контрольной записи; перед + выпуском нужен слуховой контроль плотного диалога на финальной сборке. +- Калибровка выбирает эмбеддинги и кластеризацию, но не решает ошибки границ и + неполное распознавание наложений голосов. +- Производительность должна отдельно приниматься на целевом Intel Core i5. + +Для спецификации зафиксировать WeSpeaker + 0,89 как автоматический дефолт, +отдельную опцию явного числа участников и отсутствие автоматического +отбрасывания малых кластеров. CAMPPlus zh/en можно оставить кандидатом для +будущего режима с обязательным `num_clusters` после расширенной слуховой +проверки. diff --git a/scripts/benchmarks/diarization_calibration.py b/scripts/benchmarks/diarization_calibration.py new file mode 100644 index 0000000..150af33 --- /dev/null +++ b/scripts/benchmarks/diarization_calibration.py @@ -0,0 +1,355 @@ +"""Воспроизводимый свип параметров офлайн-диаризации sherpa-onnx.""" + +from __future__ import annotations + +import argparse +import hashlib +import itertools +import json +import re +import subprocess +import time +import wave +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +import sherpa_onnx + +TURN_RE = re.compile(r"^\*\*\[(\d{2}):(\d{2})(?::(\d{2}))?\] Speaker (\d+):\*\*") + + +@dataclass(frozen=True) +class Recording: + name: str + path: Path + start: float + duration: float + expected_speakers: int + reference: Path | None + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--work-dir", type=Path, required=True) + parser.add_argument("--threads", type=int, default=8) + return parser.parse_args() + + +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest().upper() + + +def decode_clip(recording: Recording, work_dir: Path) -> Path: + output = work_dir / f"{recording.name}.wav" + if output.exists(): + return output + + command = [ + "ffmpeg", + "-hide_banner", + "-loglevel", + "error", + "-y", + "-ss", + str(recording.start), + "-t", + str(recording.duration), + "-i", + str(recording.path), + "-vn", + "-ac", + "1", + "-ar", + "16000", + "-c:a", + "pcm_s16le", + str(output), + ] + subprocess.run(command, check=True) + return output + + +def read_wav(path: Path) -> np.ndarray: + with wave.open(str(path), "rb") as source: + if source.getnchannels() != 1 or source.getsampwidth() != 2: + raise ValueError(f"Ожидался mono PCM16 WAV: {path}") + if source.getframerate() != 16000: + raise ValueError(f"Ожидалась частота 16 кГц: {path}") + samples = np.frombuffer(source.readframes(source.getnframes()), np.int16) + return samples.astype(np.float32) / 32768.0 + + +def timestamp_seconds(match: re.Match[str]) -> float: + first, second, third = match.group(1), match.group(2), match.group(3) + if third is None: + return int(first) * 60 + int(second) + return int(first) * 3600 + int(second) * 60 + int(third) + + +def read_reference_turns(recording: Recording) -> list[dict[str, Any]]: + if recording.reference is None: + return [] + + starts: list[tuple[float, str]] = [] + for line in recording.reference.read_text(encoding="utf-8").splitlines(): + match = TURN_RE.match(line) + if match: + starts.append((timestamp_seconds(match), match.group(4))) + + clip_end = recording.start + recording.duration + turns: list[dict[str, Any]] = [] + for index, (start, speaker) in enumerate(starts): + end = starts[index + 1][0] if index + 1 < len(starts) else clip_end + overlap_start = max(start, recording.start) + overlap_end = min(end, clip_end) + if overlap_end > overlap_start: + turns.append( + { + "speaker": speaker, + "start": overlap_start - recording.start, + "end": overlap_end - recording.start, + } + ) + return turns + + +def interval_overlap(left: dict[str, Any], right: dict[str, Any]) -> float: + return max(0.0, min(left["end"], right["end"]) - max(left["start"], right["start"])) + + +def best_mapping( + segments: list[dict[str, Any]], + reference_turns: list[dict[str, Any]], +) -> dict[str, Any] | None: + if not reference_turns or not segments: + return None + + predicted = sorted({str(segment["speaker"]) for segment in segments}) + reference = sorted({str(turn["speaker"]) for turn in reference_turns}) + overlap: defaultdict[tuple[str, str], float] = defaultdict(float) + total = 0.0 + for segment in segments: + predicted_speaker = str(segment["speaker"]) + for turn in reference_turns: + value = interval_overlap(segment, turn) + if value: + reference_speaker = str(turn["speaker"]) + overlap[(predicted_speaker, reference_speaker)] += value + total += value + + best_score = -1.0 + best_pairs: list[tuple[str, str]] = [] + if len(predicted) >= len(reference): + for candidate in itertools.permutations(predicted, len(reference)): + pairs = list(zip(candidate, reference, strict=True)) + score = sum(overlap[pair] for pair in pairs) + if score > best_score: + best_score, best_pairs = score, pairs + else: + for candidate in itertools.permutations(reference, len(predicted)): + pairs = list(zip(predicted, candidate, strict=True)) + score = sum(overlap[pair] for pair in pairs) + if score > best_score: + best_score, best_pairs = score, pairs + + return { + "mapped_speaker_purity": best_score / total if total else None, + "mapped_overlap_seconds": best_score, + "total_overlap_seconds": total, + "mapping": {predicted: reference for predicted, reference in best_pairs}, + } + + +def make_config( + segmentation_model: Path, + embedding_model: Path, + threshold: float, + num_clusters: int, + threads: int, +) -> sherpa_onnx.OfflineSpeakerDiarizationConfig: + pyannote = sherpa_onnx.OfflineSpeakerSegmentationPyannoteModelConfig( + model=str(segmentation_model) + ) + segmentation = sherpa_onnx.OfflineSpeakerSegmentationModelConfig( + pyannote=pyannote, + num_threads=threads, + ) + embedding = sherpa_onnx.SpeakerEmbeddingExtractorConfig( + model=str(embedding_model), + num_threads=threads, + ) + clustering = sherpa_onnx.FastClusteringConfig( + num_clusters=num_clusters, + threshold=threshold, + ) + return sherpa_onnx.OfflineSpeakerDiarizationConfig( + segmentation=segmentation, + embedding=embedding, + clustering=clustering, + ) + + +def summarize_segments( + segments: list[dict[str, Any]], + recording: Recording, +) -> dict[str, Any]: + durations: defaultdict[str, float] = defaultdict(float) + for segment in segments: + durations[str(segment["speaker"])] += segment["end"] - segment["start"] + + ordered = sorted(durations.items(), key=lambda item: item[1], reverse=True) + total = sum(durations.values()) + residual = sum(duration for _, duration in ordered[recording.expected_speakers :]) + substantial_threshold = max(5.0, recording.duration * 0.02) + return { + "clusters": len(ordered), + "substantial_clusters": sum( + duration >= substantial_threshold for _, duration in ordered + ), + "substantial_threshold_seconds": substantial_threshold, + "cluster_durations_seconds": dict(ordered), + "speaker_time_seconds": total, + "residual_seconds_after_expected": residual, + "residual_share_after_expected": residual / total if total else None, + } + + +def save_output(path: Path, output: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text( + json.dumps(output, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + temporary.replace(path) + + +def manifest_shape(manifest: dict[str, Any]) -> dict[str, Any]: + """Отделить параметры эксперимента от машинно-зависимых путей.""" + return { + "models": [item["name"] for item in manifest["models"]], + "recordings": [ + { + key: item[key] + for key in ("name", "start", "duration", "expected_speakers") + } + for item in manifest["recordings"] + ], + "runs": manifest["runs"], + } + + +def main() -> None: + args = parse_args() + manifest = json.loads(args.manifest.read_text(encoding="utf-8")) + args.work_dir.mkdir(parents=True, exist_ok=True) + + recordings = [ + Recording( + name=item["name"], + path=Path(item["path"]), + start=float(item["start"]), + duration=float(item["duration"]), + expected_speakers=int(item["expected_speakers"]), + reference=Path(item["reference"]) if item.get("reference") else None, + ) + for item in manifest["recordings"] + ] + if args.output.exists(): + output = json.loads(args.output.read_text(encoding="utf-8")) + if ( + manifest_shape(output["manifest"]) != manifest_shape(manifest) + or output["threads"] != args.threads + ): + raise ValueError("Существующий output создан с другим manifest/threads") + output["manifest"] = manifest + else: + output = { + "manifest": manifest, + "sherpa_onnx_version": sherpa_onnx.__version__, + "threads": args.threads, + "results": [], + } + completed = { + (item["recording"], item["model"], item["run"]) for item in output["results"] + } + + segmentation_model = Path(manifest["segmentation_model"]) + for recording in recordings: + print(f"Декодирование {recording.name}", flush=True) + wav_path = decode_clip(recording, args.work_dir) + samples = read_wav(wav_path) + reference_turns = read_reference_turns(recording) + source_hash = file_sha256(recording.path) + + for model in manifest["models"]: + embedding_model = Path(model["path"]) + for run in manifest["runs"]: + run_key = (recording.name, model["name"], run["name"]) + if run_key in completed: + print(f"Пропуск готового прогона: {run_key}", flush=True) + continue + num_clusters = run["num_clusters"] + if num_clusters == "expected": + num_clusters = recording.expected_speakers + threshold = float(run["threshold"]) + print( + f"{recording.name}: {model['name']} / {run['name']}", + flush=True, + ) + config = make_config( + segmentation_model=segmentation_model, + embedding_model=embedding_model, + threshold=threshold, + num_clusters=int(num_clusters), + threads=args.threads, + ) + diarizer = sherpa_onnx.OfflineSpeakerDiarization(config) + started = time.perf_counter() + result = diarizer.process(samples) + elapsed = time.perf_counter() - started + segments = [ + { + "speaker": int(segment.speaker), + "start": float(segment.start), + "end": float(segment.end), + } + for segment in result.sort_by_start_time() + ] + item = { + "recording": recording.name, + "source": recording.path.name, + "source_sha256": source_hash, + "clip_start": recording.start, + "clip_duration": recording.duration, + "expected_speakers": recording.expected_speakers, + "reference": recording.reference.name + if recording.reference + else None, + "model": model["name"], + "model_file": embedding_model.name, + "run": run["name"], + "threshold": threshold, + "num_clusters": int(num_clusters), + "elapsed_seconds": elapsed, + "rtf": elapsed / recording.duration, + "summary": summarize_segments(segments, recording), + "reference_mapping": best_mapping(segments, reference_turns), + "segments": segments, + } + output["results"].append(item) + completed.add(run_key) + save_output(args.output, output) + + +if __name__ == "__main__": + main() -- 2.54.0 From b8092aade51f942eb0b410fed212740cc96198d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 11:44:02 +0300 Subject: [PATCH 09/15] =?UTF-8?q?docs(diarization):=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BC=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=20ASR-?= =?UTF-8?q?=D1=81=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - требовалось проверить долю смешанных ASR-сегментов на трёх записях, включая разговоры на двоих. - Что: - добавлен отчёт с долями сегментов и времени для трёх рабочих созвонов. - обвязка замеров переведена на откалиброванный порог 0,89. - исследовательский скрипт приведён к формату ruff. - Проверка: - выполнены три полных прогона bench_conflict.py с WeSpeaker и порогом 0,89. - uv run ruff check и ruff format --check прошли успешно. --- .scratch/diarization/README.md | 11 +- .scratch/diarization/bench_conflict.py | 19 +++- .scratch/diarization/common.py | 6 +- .../2026-08-14-asr-segment-speaker-mixing.md | 102 ++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md diff --git a/.scratch/diarization/README.md b/.scratch/diarization/README.md index b046210..a24d99c 100644 --- a/.scratch/diarization/README.md +++ b/.scratch/diarization/README.md @@ -41,17 +41,18 @@ curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-rec export PYTHONIOENCODING=utf-8 uv run python .scratch/diarization/bench_asr.py "<путь к записи>" -uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.9 +uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.89 uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py "<путь>" uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py "<путь>" ``` ## Что стоит знать до запуска -- **Порог кластеризации не откалиброван.** По умолчанию стоит 0,9 — значение из - разведки, подобранное на одной записи и на ней же проверенное. На пороге 0,5 - из примеров sherpa-onnx получалось 29 говорящих вместо трёх. Калибровка — это - тикет #10, до его закрытия любое значение считается временным. +- **Порог кластеризации откалиброван.** По умолчанию стоит 0,89 — единственное + проверенное значение, которое без знания числа участников дало правильные + 3 / 2 / 2 кластера на трёх калибровочных фрагментах. Решение и ограничения + описаны в + [отчёте о калибровке](../../docs/benchmarks/2026-08-14-diarization-calibration.md). - **Свип дорогой.** Каждая конфигурация — полный прогон сегментации и эмбеддингов, около 2,5 минут на 26-минутную запись, и время от настроек кластеризации практически не зависит. Свип вести на коротком фрагменте. diff --git a/.scratch/diarization/bench_conflict.py b/.scratch/diarization/bench_conflict.py index 46664ca..79c7555 100644 --- a/.scratch/diarization/bench_conflict.py +++ b/.scratch/diarization/bench_conflict.py @@ -74,7 +74,13 @@ def main(audio_path: str, threshold: float, threads: int) -> None: continue major = max(per_speaker, key=lambda k: per_speaker[k]) rows.append( - (seg, major, per_speaker[major] / total, total - per_speaker[major], dict(per_speaker)) + ( + seg, + major, + per_speaker[major] / total, + total - per_speaker[major], + dict(per_speaker), + ) ) n = len(rows) @@ -95,7 +101,9 @@ def main(audio_path: str, threshold: float, threads: int) -> None: (f"с чужой репликой (>={INTERJECTION_S:.0f} с)", lost), ("без говорящего вообще", unattributed), ): - print(f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин") + print( + f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин" + ) print() for level in PURITY_LEVELS: @@ -107,9 +115,12 @@ def main(audio_path: str, threshold: float, threads: int) -> None: print("\n" + "=" * 64) print("ХУДШИЕ 12 СЕГМЕНТОВ (больше всего чужой речи внутри):") - for seg, major, purity, others, per_speaker in sorted(attributed, key=lambda r: -r[3])[:12]: + for seg, major, purity, others, per_speaker in sorted( + attributed, key=lambda r: -r[3] + )[:12]: share = ", ".join( - f"spk{k}={v:.1f}с" for k, v in sorted(per_speaker.items(), key=lambda x: -x[1]) + f"spk{k}={v:.1f}с" + for k, v in sorted(per_speaker.items(), key=lambda x: -x[1]) ) print( f"\n [{seg.start:7.1f}-{seg.end:7.1f}] ({seg.end - seg.start:4.1f} с) " diff --git a/.scratch/diarization/common.py b/.scratch/diarization/common.py index 56d4a35..2302295 100644 --- a/.scratch/diarization/common.py +++ b/.scratch/diarization/common.py @@ -22,9 +22,9 @@ EMBEDDING = MODELS / "wespeaker_en_voxceleb_resnet34_LM.onnx" SAMPLE_RATE = 16_000 -# Настройки разведки 2026-08-12. Порог 0.9 дал верное число говорящих на -# контрольной записи; на 0.5 из примеров sherpa-onnx получалось 29 вместо трёх. -DISCOVERY_THRESHOLD = 0.9 +# Конфигурация, выбранная калибровкой 2026-08-14 на трёх записях. +# На 0.5 из примеров sherpa-onnx получалось 29 говорящих вместо трёх. +DISCOVERY_THRESHOLD = 0.89 DEFAULT_THREADS = 8 diff --git a/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md b/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md new file mode 100644 index 0000000..52c61da --- /dev/null +++ b/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md @@ -0,0 +1,102 @@ +# Смешение говорящих внутри ASR-сегментов + +**Дата:** 2026-08-14 + +**Статус:** проверка на трёх русскоязычных рабочих созвонах. Не оценка DER и +не решение о единице привязки спикера к тексту. + +## Вопрос + +Воспроизводится ли смешение говорящих внутри ASR-сегментов на других записях, +или результат разведки — особенность одной встречи на троих? + +В [разведочном замере](2026-08-12-diarization-feasibility.md) 27% сегментов +контрольной записи содержали не меньше секунды чужой речи. На них приходилось +больше половины времени ASR-сегментов. Проверка повторена на тех же трёх +записях, на которых калибровалась диаризация, включая два разговора на двоих. + +## Метод + +ASR выполнялся через модель по умолчанию ONNX-пути +`gigaam-v3-e2e-rnnt` INT8 с языком `ru`. Диаризация выполнялась через +`sherpa-onnx` 1.13.5 с выбранной в +[калибровке](2026-08-14-diarization-calibration.md) конфигурацией: + +- сегментация `sherpa-onnx-pyannote-segmentation-3-0`; +- эмбеддинги `wespeaker_en_voxceleb_resnet34_LM.onnx`; +- `FastClusteringConfig.threshold=0.89`; +- автоматическое определение числа кластеров (`num_clusters=-1`). + +Для каждого ASR-сегмента считалось перекрытие с интервалами каждого кластера. +Кластер с максимальным перекрытием считался мажоритарным, а сумма перекрытий +остальных кластеров — чужой речью. Сегменты разделены на четыре категории: + +- **чистый** — перекрытие только с одним кластером; +- **с поддакиванием** — меньше 1 секунды чужой речи; +- **с чужой репликой** — не меньше 1 секунды чужой речи; +- **без спикера** — нет перекрытия с интервалами диаризации. + +Время категории — сумма длительностей попавших в неё ASR-сегментов. Это не +сумма времени речи: интервалы разных кластеров могут перекрываться при +наложенной речи. Метрика отвечает на узкий вопрос, насколько огрубляет текст +одна метка спикера на весь ASR-сегмент. Она не измеряет точность диаризации. + +## Результаты + +| Запись | Участников | ASR-сегментов | Чистые | Поддакивание <1 с | Чужая реплика ≥1 с | Без спикера | +|---|---:|---:|---:|---:|---:|---:| +| Data Test | 3 | 274 | 164 (59,9%) | 30 (10,9%) | **74 (27,0%)** | 6 (2,2%) | +| T2 BDMA | 2 | 223 | 167 (74,9%) | 34 (15,2%) | **14 (6,3%)** | 8 (3,6%) | +| Yantar | 2 | 339 | 275 (81,1%) | 20 (5,9%) | **24 (7,1%)** | 20 (5,9%) | + +| Запись | Время всех ASR-сегментов | Время сегментов с чужой репликой ≥1 с | Доля времени | +|---|---:|---:|---:| +| Data Test | 21,89 мин | **11,20 мин** | **51,2%** | +| T2 BDMA | 12,56 мин | **1,53 мин** | **12,2%** | +| Yantar | 15,93 мин | **2,67 мин** | **16,8%** | + +На двух разговорах на двоих вместе чужая реплика не меньше секунды встречается +в 38 из 562 сегментов (6,8%) и затрагивает 4,20 из 28,49 минуты (14,7%). По +сравнению со встречей на троих это в четыре раза меньше по доле сегментов и +примерно в 3,5 раза меньше по доле времени. + +## Вывод + +Смешение говорящих внутри ASR-сегмента — общее свойство проверенного материала, +а не аномалия одной записи: оно воспроизвелось на обоих разговорах на двоих. +Однако тяжесть сильно зависит от характера разговора. Значение 27% сегментов и +51% времени не переносится на двухсторонние созвоны: там получено 6–7% +сегментов и 12–17% времени. + +Мажоритарная метка на весь ASR-сегмент поэтому остаётся заметным огрублением +даже на разговорах на двоих, а на плотной встрече втроём теряет реплики в +массовом масштабе. Эти данные не выбирают единицу привязки сами по себе, но +исключают предположение, что сегментная привязка безопасна для всех обычных +созвонов без пословной привязки или явной пометки качества. + +## Ограничения + +- Все три записи — русскоязычные рабочие созвоны одного пользователя; другие + микрофоны, шумы и стили разговора не представлены. +- Диаризация служит опорной разметкой и сама содержит ошибки. На контрольной + записи есть ложный остаточный кластер, редкие пропуски и неполная разметка + перекрывающейся речи. +- На двухсторонних записях один голос заметно доминирует по времени. Баланс + реплик может влиять на долю смешанных сегментов. +- Порог 1 секунда разделяет короткие вставки и потенциально потерянные реплики, + но не доказывает смысловую важность каждого фрагмента. + +## Воспроизводимость + +Расчёт выполняется скриптом +[`bench_conflict.py`](../../.scratch/diarization/bench_conflict.py): + +```powershell +$env:PYTHONIOENCODING = "utf-8" +uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py ` + "<путь к записи>" 0.89 8 +``` + +Медиа и сырые JSON не добавлены в репозиторий: они содержат локальные пути и +относятся к рабочим созвонам. SHA-256 всех трёх исходных файлов зафиксированы в +[отчёте о калибровке](2026-08-14-diarization-calibration.md). -- 2.54.0 From 99ddf77af3763571890c7dd7a4d3edc31aec945e Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Fri, 14 Aug 2026 13:37:07 +0300 Subject: [PATCH 10/15] =?UTF-8?q?perf(diarization):=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D1=80=D0=BE=D0=BC=20Intel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - требовалось оценить стоимость опциональной диаризации на доступном слабом Intel baseline. - Что: - зафиксированы ASR, RTFx и peak RSS на трёх контрольных записях. - замер peak RSS адаптирован для Linux и macOS. - недоступный Core i5 явно заменён Core i7-6820HQ с сохранением ограничения применимости. - Проверка: - uv run ruff check .scratch/diarization/common.py. - uv run pytest -q: 243 passed, 1 skipped. --- .scratch/diarization/README.md | 3 +- .scratch/diarization/common.py | 12 ++- .../2026-08-12-diarization-feasibility.md | 4 +- .../2026-08-14-diarization-calibration.md | 11 +-- .../2026-08-14-diarization-intel-i7.md | 88 +++++++++++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 docs/benchmarks/2026-08-14-diarization-intel-i7.md diff --git a/.scratch/diarization/README.md b/.scratch/diarization/README.md index a24d99c..ce16722 100644 --- a/.scratch/diarization/README.md +++ b/.scratch/diarization/README.md @@ -61,4 +61,5 @@ uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py "<пут на слух — тикет #12, и он намеренно идёт до калибровки. - **Замер памяти чинился.** В разведке `psapi.GetProcessMemoryInfo` молча возвращал ноль; `common.peak_rss_mb()` теперь зовёт `K32GetProcessMemoryInfo` - из kernel32 и проверяет код возврата. + из kernel32 и проверяет код возврата. На Linux и macOS используется + `resource.getrusage()` с поправкой на разные единицы измерения. diff --git a/.scratch/diarization/common.py b/.scratch/diarization/common.py index 2302295..3ff7220 100644 --- a/.scratch/diarization/common.py +++ b/.scratch/diarization/common.py @@ -64,12 +64,22 @@ class _ProcessMemoryCounters(ctypes.Structure): def peak_rss_mb() -> float | None: """Пиковая рабочая память процесса в МБ; None, если снять не удалось. - Два подвоха, на которых замер в разведке 2026-08-12 вернул ноль: + На Linux ``ru_maxrss`` измеряется в КиБ, на macOS — в байтах. В Windows + используются системные счётчики процесса. + + Два подвоха, на которых замер в разведке 2026-08-12 вернул ноль в Windows: экспорт на современных Windows живёт в kernel32 как ``K32GetProcessMemoryInfo``, а без явных ``restype``/``argtypes`` псевдодескриптор процесса уезжает в вызов как 32-битное число и функция молча не срабатывает. """ + if sys.platform != "win32": + import resource + + peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + divisor = 1024 * 1024 if sys.platform == "darwin" else 1024 + return peak / divisor + kernel32 = ctypes.windll.kernel32 kernel32.GetCurrentProcess.restype = ctypes.c_void_p handle = kernel32.GetCurrentProcess() diff --git a/docs/benchmarks/2026-08-12-diarization-feasibility.md b/docs/benchmarks/2026-08-12-diarization-feasibility.md index 084a8d0..21b35dd 100644 --- a/docs/benchmarks/2026-08-12-diarization-feasibility.md +++ b/docs/benchmarks/2026-08-12-diarization-feasibility.md @@ -193,5 +193,7 @@ Silero VAD, то есть они проходят по тишине. В разг - Проверить границы диаризации на слух или сверкой с внешним транскриптом: совпадение числа говорящих не доказывает правильность интервалов. - Сравнить эмбеддинги, обученные не только на английском, на русской речи. -- Измерить производительность на целевом Intel Core i5 11-го поколения. +- Измерить производительность на доступном слабом Intel baseline — выполнено в + [отдельном отчёте](2026-08-14-diarization-intel-i7.md); конкретный Core i5 + 11-го поколения недоступен. - Измерить параллельный режим ASR и диаризации. diff --git a/docs/benchmarks/2026-08-14-diarization-calibration.md b/docs/benchmarks/2026-08-14-diarization-calibration.md index d5870a3..b42b951 100644 --- a/docs/benchmarks/2026-08-14-diarization-calibration.md +++ b/docs/benchmarks/2026-08-14-diarization-calibration.md @@ -2,8 +2,8 @@ **Дата:** 2026-08-14 -**Статус:** выбор конфигурации для проектирования. Не приёмка -производительности на целевом Intel Core i5 11-го поколения. +**Статус:** выбор конфигурации для проектирования. Производительность отдельно +проверена на [доступном старом Intel baseline](2026-08-14-diarization-intel-i7.md). ## Решение @@ -166,8 +166,8 @@ Apache 2.0; в исходниках 3D-Speaker модель явно описа | ERes2Net base zh | 0,152 | 1,29× | CAMPPlus примерно на 30% быстрее WeSpeaker в этом эксперименте. Это плюс для -варианта с известным числом участников, но замер на AMD не заменяет приёмку на -целевом Intel Core i5 11-го поколения. +варианта с известным числом участников. Производительность выбранной WeSpeaker +отдельно проверена на доступном старом Intel Core i7. ## Воспроизводимость @@ -200,7 +200,8 @@ uv run python scripts/benchmarks/diarization_calibration.py ` выпуском нужен слуховой контроль плотного диалога на финальной сборке. - Калибровка выбирает эмбеддинги и кластеризацию, но не решает ошибки границ и неполное распознавание наложений голосов. -- Производительность должна отдельно приниматься на целевом Intel Core i5. +- Производительность принята на доступном старом Intel Core i7; конкретный Core + i5 11-го поколения остаётся непроверенным, потому что такого устройства нет. Для спецификации зафиксировать WeSpeaker + 0,89 как автоматический дефолт, отдельную опцию явного числа участников и отсутствие автоматического diff --git a/docs/benchmarks/2026-08-14-diarization-intel-i7.md b/docs/benchmarks/2026-08-14-diarization-intel-i7.md new file mode 100644 index 0000000..4ca15bc --- /dev/null +++ b/docs/benchmarks/2026-08-14-diarization-intel-i7.md @@ -0,0 +1,88 @@ +# Производительность диаризации на старом Intel Core i7 + +**Дата:** 2026-08-14 + +**Статус:** приёмка на доступном слабом Intel baseline. Не эквивалент замеру на +Intel Core i5 11-го поколения. + +## Решение + +Диаризация проходит по стоимости как **опциональная функция**. На доступном +ноутбуке обработка остаётся заметно быстрее реального времени: час записи +занимает около 23 минут при последовательном запуске ASR и диаризации. + +Цена функции существенная: диаризация медленнее ASR и увеличивает полное время +примерно в 2,4 раза. Поэтому включать её без явного запроса пользователя нельзя. +Теоретическое совмещение независимых проходов уменьшило бы время часа записи до +примерно 13,6 минуты, но параллельный режим здесь не измерялся. + +Запланированный Intel Core i5 11-го поколения недоступен и в обозримом будущем +не появится. Вместо бессрочного блокирующего требования принят доступный старый +Intel Core i7 как практический слабый baseline. Результат не переносится на +конкретный SKU i5 и не является сравнением микроархитектур. + +## Оборудование и условия + +- HP ZBook 17 G3, BIOS N81 01.61; +- Intel Core i7-6820HQ, 4 ядра / 8 логических процессоров, 2,7–3,6 ГГц; +- 29 ГиБ доступной RAM; +- Ubuntu, Linux 7.0.0-29-generic x86_64; +- питание от сети, профиль `balanced`, governor `powersave`; +- Python 3.13.13, `onnx-asr` 0.12.0, `onnxruntime` 1.28.0, + `faster-whisper` 1.2.1, `sherpa-onnx` 1.13.5, NumPy 2.4.3; +- 8 потоков CPU, порог кластеризации 0,89; +- прогоны последовательные, без намеренно запущенной конкурирующей нагрузки; +- модели после первого запуска находились в локальном кеше. + +Использованы те же три записи и те же SHA-256, что в +[отчёте о калибровке](2026-08-14-diarization-calibration.md). Модель ASR — +`gigaam-v3-e2e-rnnt` INT8; диаризация — Pyannote segmentation 3.0 и WeSpeaker +ResNet34 LM. + +## Результаты + +Для самой длинной записи сделано три прогона каждого прохода. Для двух +остальных — по одному подтверждающему прогону: разброс трёх повторов был мал, +а коэффициенты на записях другой длины подтвердили линейное масштабирование. + +| Запись | Длительность | ASR | RTFx ASR | Диаризация | RTFx диаризации | +|---|---:|---:|---:|---:|---:| +| Data Test | 26:00 | **257,1 с** (медиана: 261,2 / 257,1 / 253,7) | 6,1× | **352,6 с** (медиана: 352,6 / 349,8 / 358,8) | 4,4× | +| T2 BDMA | 14:51 | 149,0 с | 6,0× | 203,7 с | 4,4× | +| Yantar | 20:22 | 185,3 с | 6,6× | 276,0 с | 4,4× | +| **Взвешенно, три записи** | **61:13** | **591,4 с** | **6,2×** | **832,3 с** | **4,4×** | + +Последовательная обработка трёх записей занимает около 1424 секунд, или +23,7 минуты, при общей длительности 61,2 минуты: **2,58× realtime**. В пересчёте +на час это около 9,7 минуты ASR и 13,6 минуты диаризации, всего **23,3 минуты**. + +## Инициализация и память + +| Проход | Инициализация из локального кеша | Peak RSS | +|---|---:|---:| +| ASR | 2,0–2,3 с | 900–1035 МБ на тёплых прогонах | +| Диаризация | 0,2 с | 366–469 МБ | + +Первый ASR-запуск показал 23,6 секунды, но включал скачивание файлов модели, +поэтому не считается чистым cold start. Peak RSS этого процесса достиг 1174 МБ. +Пиковая память замерена отдельно для каждого последовательного прохода; для +будущего параллельного режима значения нельзя механически считать измеренным +общим пиком. + +## Сопоставление с разведкой на Ryzen + +На Ryzen 7 8845H для Data Test были получены 16,4× RTFx у ASR и 11,1× у +диаризации. На старом Intel оба прохода медленнее примерно в 2,6 раза, а их +соотношение почти не изменилось. Следовательно, слабое железо ухудшает абсолютное +время, но не меняет основной архитектурный вывод: диаризация дороже ASR, а +совмещение проходов потенциально полезно. + +## Ограничения + +- Core i7-6820HQ не моделирует производительность Core i5 11-го поколения; +- медиана трёх прогонов снята только на одной полной записи, на двух других есть + по одному подтверждающему прогону; +- чистый cold start ASR без скачивания, но с холодным файловым кешем не измерен; +- параллельный запуск ASR и диаризации не измерен; +- результат отвечает только на стоимость выбранных моделей и конфигурации, а не + на качество диаризации. -- 2.54.0 From bbc2bdacfeeb03a4c42e2e59545ab866ce32d55c Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Fri, 14 Aug 2026 13:59:33 +0300 Subject: [PATCH 11/15] =?UTF-8?q?docs(diarization):=20=D1=83=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=B5=D0=BD=D1=8B=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80=D0=B0=20Intel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - результат benchmark нужно интерпретировать с учётом ограничения питания ноутбука. - Что: - зафиксировано урезанное питание во время прогонов. - визуальная оценка потери производительности около 30% явно отделена от измеренных результатов. - Проверка: - git diff --check. --- docs/benchmarks/2026-08-14-diarization-intel-i7.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/benchmarks/2026-08-14-diarization-intel-i7.md b/docs/benchmarks/2026-08-14-diarization-intel-i7.md index 4ca15bc..67c2ed2 100644 --- a/docs/benchmarks/2026-08-14-diarization-intel-i7.md +++ b/docs/benchmarks/2026-08-14-diarization-intel-i7.md @@ -27,13 +27,19 @@ Intel Core i7 как практический слабый baseline. Резул - Intel Core i7-6820HQ, 4 ядра / 8 логических процессоров, 2,7–3,6 ГГц; - 29 ГиБ доступной RAM; - Ubuntu, Linux 7.0.0-29-generic x86_64; -- питание от сети, профиль `balanced`, governor `powersave`; +- питание от сети, профиль `balanced`, governor `powersave`; доступная мощность + была урезана, и ноутбук ограничивал производительность; - Python 3.13.13, `onnx-asr` 0.12.0, `onnxruntime` 1.28.0, `faster-whisper` 1.2.1, `sherpa-onnx` 1.13.5, NumPy 2.4.3; - 8 потоков CPU, порог кластеризации 0,89; - прогоны последовательные, без намеренно запущенной конкурирующей нагрузки; - модели после первого запуска находились в локальном кеше. +По наблюдению владельца, ограничение питания снижало производительность примерно +на 30%. Это визуальная оценка, а не результат отдельного A/B-замера, поэтому +фактические времена ниже не пересчитываются. Их следует читать как консервативный +результат именно в зафиксированном режиме питания. + Использованы те же три записи и те же SHA-256, что в [отчёте о калибровке](2026-08-14-diarization-calibration.md). Модель ASR — `gigaam-v3-e2e-rnnt` INT8; диаризация — Pyannote segmentation 3.0 и WeSpeaker @@ -80,6 +86,8 @@ ResNet34 LM. ## Ограничения - Core i7-6820HQ не моделирует производительность Core i5 11-го поколения; +- влияние урезанного питания оценивается примерно в 30% только на глаз; прогон с + полным питанием для сравнения не проводился; - медиана трёх прогонов снята только на одной полной записи, на двух других есть по одному подтверждающему прогону; - чистый cold start ASR без скачивания, но с холодным файловым кешем не измерен; -- 2.54.0 From 26d4ca2f4a7fb0c322332a299ecbc4cc5eb09621 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 14:16:01 +0300 Subject: [PATCH 12/15] =?UTF-8?q?docs(context):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=80=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B4=D0=B8=D0=B0=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - решение о пословной привязке должно использовать единый язык во всех документах карты. - Что: - определены сегмент распознавания и слово с временной привязкой. - реплика говорящего отделена от единицы запуска модели распознавания. - Проверка: - git diff --check. --- CONTEXT.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index 2fb7177..1c44474 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -19,3 +19,15 @@ _Avoid_: Рекомендуемая модель, поддерживаемая **Опорная разметка**: Разметка, принятая за точку отсчёта при измерении чего-то другого. Опорной её делает роль в измерении, а не качество: она не выверена вручную и сама может содержать ошибки, поэтому посчитанная по ней величина осмысленна как порядок, но не как точное значение. _Avoid_: Эталонная разметка, истинная разметка, ground truth + +**Сегмент распознавания**: +Непрерывный временной фрагмент аудио, для которого движок возвращает связный текст с общим контекстом. Может содержать речь нескольких говорящих и не равен реплике говорящего. +_Avoid_: Реплика, фраза говорящего + +**Слово с временной привязкой**: +Распознанное слово, положение которого известно на временной шкале записи. Минимальная единица, которой назначается говорящий. +_Avoid_: Токен, ASR-сегмент + +**Реплика говорящего**: +Последовательность соседних слов с временной привязкой, назначенных одному говорящему. Это единица структуры готового транскрипта, а не запуска модели распознавания. +_Avoid_: Сегмент распознавания, ASR-сегмент -- 2.54.0 From b260dab0476afce6d72129d72b4fcb1abbdfbc3c Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 15:18:59 +0300 Subject: [PATCH 13/15] =?UTF-8?q?docs(domain):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B3=D0=BE=D0=B2=D0=BE=D1=80=D1=8F=D1=89?= =?UTF-8?q?=D0=B8=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - потребовалось отделить временный результат диаризации от итоговых реплик транскрипта. - Что: - добавлено определение разметки говорящих и перечислены нежелательные синонимы. - уточнено, что разметка не содержит текст, имена участников и голосовые эмбеддинги. - Проверка: - выполнена команда git diff --cached --check. --- CONTEXT.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index 1c44474..6891e37 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -28,6 +28,10 @@ _Avoid_: Реплика, фраза говорящего Распознанное слово, положение которого известно на временной шкале записи. Минимальная единица, которой назначается говорящий. _Avoid_: Токен, ASR-сегмент +**Разметка говорящих**: +Упорядоченный набор временных интервалов речи, каждому из которых назначена анонимная метка говорящего. Не содержит распознанного текста, имени участника или голосового эмбеддинга. +_Avoid_: Результат диаризации, сегменты говорящих + **Реплика говорящего**: Последовательность соседних слов с временной привязкой, назначенных одному говорящему. Это единица структуры готового транскрипта, а не запуска модели распознавания. _Avoid_: Сегмент распознавания, ASR-сегмент -- 2.54.0 From b8333ba58341b738a0964dae8b727080fd00a8a0 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 15:55:25 +0300 Subject: [PATCH 14/15] =?UTF-8?q?docs(diarization):=20=D0=B7=D0=B0=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?ADR=20=D0=B8=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - решения карты должны стать устойчивой основой для реализации диаризации. - Что: - добавлены ADR-007 и спецификация пословной диаризации. - дополнен доменный словарь и удалены закрытые направления из backlog. - Проверка: - git diff --cached --check. --- CONTEXT.md | 4 + .../adr/007-word-level-speaker-diarization.md | 97 +++++++++ docs/backlog.md | 69 ------ docs/specs/2026-08-14-speaker-diarization.md | 201 ++++++++++++++++++ 4 files changed, 302 insertions(+), 69 deletions(-) create mode 100644 docs/adr/007-word-level-speaker-diarization.md create mode 100644 docs/specs/2026-08-14-speaker-diarization.md diff --git a/CONTEXT.md b/CONTEXT.md index 6891e37..7ac5ae0 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -32,6 +32,10 @@ _Avoid_: Токен, ASR-сегмент Упорядоченный набор временных интервалов речи, каждому из которых назначена анонимная метка говорящего. Не содержит распознанного текста, имени участника или голосового эмбеддинга. _Avoid_: Результат диаризации, сегменты говорящих +**Голосовой кластер**: +Анонимная группа интервалов разметки говорящих, которые диаризатор относит к одному голосу. Не равен подтверждённому участнику встречи: ложный или малый кластер сохраняет отдельную метку. +_Avoid_: Участник, человек + **Реплика говорящего**: Последовательность соседних слов с временной привязкой, назначенных одному говорящему. Это единица структуры готового транскрипта, а не запуска модели распознавания. _Avoid_: Сегмент распознавания, ASR-сегмент diff --git a/docs/adr/007-word-level-speaker-diarization.md b/docs/adr/007-word-level-speaker-diarization.md new file mode 100644 index 0000000..0d94fef --- /dev/null +++ b/docs/adr/007-word-level-speaker-diarization.md @@ -0,0 +1,97 @@ +# ADR-007: Пословная диаризация через sherpa-onnx + +**Статус**: Принято +**Дата**: 2026-08-14 + +## Контекст + +Разделение говорящих — главный структурный разрыв между локальным транскриптом +и облачными сервисами в сценарии подготовки конспектов и протоколов встреч. +Диаризация при этом не является ещё одним движком распознавания: она независимо +строит [разметку говорящих](../../CONTEXT.md#language), которую затем нужно +свести с результатом ASR. + +Привязка одного говорящего ко всему сегменту распознавания оказалась слишком +грубой. На трёх русскоязычных рабочих созвонах чужая реплика не короче секунды +встретилась в 6–7% сегментов разговоров на двоих и в 27% сегментов встречи +втроём. Сегменты распознавания проходят по тишине, а не по смене говорящего, +поэтому сохранить контекст RNN-T и получить реплики можно только через более +мелкую единицу сведения. + +## Эксперимент + +Локальная связка `sherpa-onnx` с сегментацией Pyannote 3.0 и эмбеддингами +WeSpeaker ResNet34 LM проверена на трёх записях с известным составом. Для +автоматического определения числа голосовых кластеров выбран порог 0,89: это +единственное проверенное значение, которое на трёх контрольных фрагментах дало +3 / 2 / 2 кластера. На полной встрече втроём остался ложный кластер длительностью +19,1 секунды; поэтому малые кластеры нельзя молча отбрасывать, а разметку нельзя +считать эталоном точных границ и перекрывающейся речи. + +На доступном слабом Intel baseline, Core i7-6820HQ с урезанным питанием, +последовательные ASR и диаризация обработали час записи примерно за 23 минуты. +Диаризация увеличивает полное время примерно в 2,4 раза, но остаётся быстрее +реального времени и приемлема как явно включаемая функция. Конкретный Core i5 +11-го поколения не проверен, поскольку такого устройства нет. + +Исходные данные и ограничения зафиксированы в отчётах о +[калибровке](../benchmarks/2026-08-14-diarization-calibration.md), +[смешении говорящих](../benchmarks/2026-08-14-asr-segment-speaker-mixing.md) и +[производительности на Intel](../benchmarks/2026-08-14-diarization-intel-i7.md). + +## Решение + +Диаризацию реализуем как явно включаемый пост-процессинг через `sherpa-onnx`. +Первая версия использует Pyannote segmentation 3.0, WeSpeaker ResNet34 LM, +порог кластеризации 0,89 и автоматическое число кластеров; известное число +участников можно передать явно. + +Говорящий назначается [слову с временной +привязкой](../../CONTEXT.md#language), а не сегменту распознавания. Каждый +ASR-бэкенд приводит свой результат к общему набору слов с положением на +временной шкале. Проходы ASR и диаризации независимо получают одно аудио, после +чего отдельная операция сводит слова с интервалами разметки говорящих и +объединяет соседние слова одного говорящего в реплики. Распознавание по-прежнему +выполняется на полных сегментах и сохраняет контекст модели. + +Первая реализация последовательна на всех устройствах: ASR, диаризация, +сведение, Markdown. В батче один диаризатор создаётся после prescan, +переиспользуется для всех файлов и освобождается вместе с командой. Разметка +говорящих живёт только в памяти текущего запуска; постоянного кеша результата +нет. + +Грубого fallback на целый сегмент и автоматического переключения устройства +нет. Отсутствие пословных таймкодов или ошибка инициализации диаризатора +останавливают запуск до ASR. Ошибка диаризации конкретного файла после успешного +ASR не уничтожает полезный результат: сохраняется обычный транскрипт с явным +предупреждением и ненулевым статусом, а батч продолжает остальные файлы. +Подробная матрица поведения находится в +[спецификации](../specs/2026-08-14-speaker-diarization.md). + +## Последствия + +- Общий контракт результата распознавания расширяется каноническими словами с + временной привязкой; сегменты распознавания сохраняются для совместимости и + контроля качества. +- FasterWhisper, ONNX-ASR и OpenVINO должны экспортировать один и тот же + пословный контракт. OpenVINO GenAI 2026.x уже предоставляет нужные таймкоды, + поэтому ограничение находится в адаптере проекта, а не в движке. +- `sherpa-onnx` становится обычной runtime-зависимостью, а две модели + диаризации скачиваются и кешируются лениво при первом запросе. +- Выход остаётся линейным Markdown с анонимными метками `Speaker N`. + Сопоставление голосов с именами и специальная запись перекрывающейся речи не + входят в ядро CLI. +- Последовательный режим задаёт корректный baseline. Параллельный запуск и + автоматическое включение на мощных устройствах требуют отдельных измерений + после стабилизации. + +## Отклонённые альтернативы + +| Альтернатива | Почему отклонена | +|---|---| +| Не делать диаризацию | Оставляет главный продуктовый разрыв, хотя измеренная стоимость допустима для явной функции | +| Мажоритарный говорящий на весь сегмент распознавания | Теряет чужие реплики на всех трёх проверенных записях | +| Сначала диаризация, затем ASR коротких интервалов | Лишает RNN-T длинного контекста и ухудшает согласование и пунктуацию | +| `pyannote.audio` | Тянет PyTorch и требует Hugging Face token с принятием лицензии | +| Сборка поверх приватных деталей `onnx-asr` | Экономит небольшую отдельную зависимость ценой нестабильного внутреннего API и собственной кластеризации | +| Параллельные проходы в первой версии | Нет прямого benchmark и измеренного общего пика памяти; сначала нужен корректный последовательный baseline | diff --git a/docs/backlog.md b/docs/backlog.md index 60dd751..ed6450b 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -179,75 +179,6 @@ openvino-cpu, запись 25:59) с облачным сервисом Hypescrib LLM для чистки текста, сопоставление Speaker N с именами — это работа поверх готового транскрипта. -### Диаризация — разделение говорящих - -**Что:** Опциональный пост-процессинг (не четвёртый бэкенд): диаризация -даёт интервалы «кто когда говорил», результат сводится с сегментами ASR, -formatter ломает абзац на смене спикера и подписывает `Speaker 1:`. -Ставится как extra: `uv sync --extra diarization`. - -**Почему:** Без спикеров MoM не собрать — это ключевой разрыв с облаком -по внешнему ревью, и никакое качество распознавания его не компенсирует. -Заодно естественно решает «разбивку на реплики» (приоритет №4). - -**Промежуточный статус 2026-08-12:** проведена разведка, описанная в -[разведочном замере диаризации](benchmarks/2026-08-12-diarization-feasibility.md). -Она закрыла вопрос о движке и открыла более важный вопрос о единице привязки. - -**Движок — вопрос практически закрыт.** `sherpa-onnx` ставится на Windows с -Python 3.13, содержит готовый `OfflineSpeakerDiarization`, не тянет torch и не -требует токена Hugging Face; модели сегментации и эмбеддингов весят около 33 МБ. -Скорость — 11,1× RTFx, то есть примерно полторы длительности ASR. Вариант -`pyannote.audio` остаётся отклонённым по прежней причине: torch и HF-токен с -принятием лицензии. Отдельный ADR имеет смысл заводить вместе с решением о -единице привязки, а не только про движок. - -Замечание для будущих заходов: обе ML-части диаризации уже лежат в -`onnx-asr` 0.12 — `PyAnnoteVad` содержит полную локальную сегментацию pyannote -(powerset на трёх спикеров, склейка окон), а `WespeakerEmbeddings` даёт -эмбеддинги. Публичный API схлопывает сегментацию до речь/не-речь, `load_se` не -экспортирован, кластеризации нет. Собирать диаризацию самим на этих деталях — -экономия 33 МБ ценой опоры на приватный API; при разведке этот путь не -выбирался. - -**Единица привязки — настоящая развилка, решения нет.** Схема «мажоритарный -спикер на весь ASR-сегмент», записанная здесь раньше, замером не подтвердилась: -27% сегментов содержат не менее секунды чужой речи, и на них приходится больше -половины времени транскрипта. Причина — границы сегментов идут по тишине -(Silero VAD), а в ВКС собеседники отвечают встык. Варианты: - -- **пословная привязка** — `onnx-asr` отдаёт потокенные таймкоды - (`TimestampedResult`), сегмент режется на границе токена при смене - говорящего; ASR по-прежнему видит длинное аудио, контекст RNN-T и пунктуация - не страдают. Недоступно на OpenVINO GenAI — там потокенных таймкодов нет; -- **диаризация первым проходом**, ASR по интервалам говорящего — чистота - гарантирована, но короткие куски лишают RNN-T контекста и портят пунктуацию; -- **привязка к сегменту с честной пометкой** — оставить огрубление, но считать - чистоту и предупреждать в шапке, как уже делается для повторов и потери - хвоста. - -**Уточнить перед запуском:** воспроизводится ли доля 27% на других записях, -включая разговор на двоих; правильность границ диаризации на слух, а не только -совпадение числа говорящих; калибровка порога кластеризации (на пороге из -примеров получилось 29 спикеров вместо трёх); эмбеддинги, обученные не только -на английском; производительность на целевом Intel Core i5. - ---- - -### Ручка нарезки абзацев в formatter - -**Что:** «Минутные простыни» в транскрипте — не свойство модели, а наши -константы группировки `_PAUSE_THRESHOLD_S = 2.0` / `_MAX_PARAGRAPH_S = -60.0` в `formatter.py` (сырых сегментов много: 23-минутная запись — 360 -сегментов, ~4 с на реплику). Вынести в опцию/конфиг или уменьшить -дефолт. - -**Почему откладывается:** при диаризации абзацы будут ломаться по смене -спикера естественно — сначала решить с диаризацией, чтобы не делать -ручку, которая устареет. - ---- - ### Словарь замен технических терминов — запасной план **Что:** Пост-обработка текста сегментов словарём замен по границам слов diff --git a/docs/specs/2026-08-14-speaker-diarization.md b/docs/specs/2026-08-14-speaker-diarization.md new file mode 100644 index 0000000..3171d99 --- /dev/null +++ b/docs/specs/2026-08-14-speaker-diarization.md @@ -0,0 +1,201 @@ +# Диаризация говорящих в транскрипте + +## Проблема + +Текущий транскрипт знает только сегменты распознавания. Их границы проходят по +тишине и не совпадают со сменой говорящего, поэтому один сегмент может содержать +несколько реплик. Назначение одной метки всему сегменту искажает структуру +диалога и делает транскрипт слабым сырьём для конспекта или протокола встречи. + +[ADR-007](../adr/007-word-level-speaker-diarization.md) выбирает явную +пословную диаризацию через `sherpa-onnx`. Эта спецификация фиксирует форму первой +реализации, не меняя принятые решения. + +## Цели + +- По явному запросу строить реплики говорящих, сохраняя текст и длинный контекст + ASR. +- Поддержать один контракт слов с временной привязкой на FasterWhisper, + ONNX-ASR и OpenVINO. +- Сохранить предсказуемый single- и batch-режим при отсутствии речи, ошибках + диаризации и малых голосовых кластерах. +- Выдать компактный Markdown, удобный и человеку, и последующей обработке LLM. + +## Не входит + +- Автоматическое включение диаризации без флага и `--no-diarize`. +- Параллельный запуск ASR и диаризации. +- Сопоставление `Speaker N` с именами участников. +- Постоянный кеш разметки говорящих, голосовые эмбеддинги и диагностические + файлы. +- Отдельный синтаксис для перекрывающейся речи. +- Изменение устройства ASR или диаризации ради восстановления функции. + +## Пользовательский интерфейс + +- `--diarize` включает диаризацию. По умолчанию она выключена; ключ в + `.transcriber.toml` в первой версии не добавляется. +- `--speakers N`, где `N >= 1`, задаёт известное число участников и сам включает + диаризацию. Без него число кластеров определяется автоматически. +- `--threads N` остаётся единым бюджетом активного CPU-прохода. Значение целиком + получает сначала ASR, затем диаризация; `0` оставляет настройки библиотек. +- `--verbose` показывает в консоли прогресс, число кластеров и интервалов, + длительность прохода и предупреждения, но не создаёт дополнительные файлы. +- `--force` пересчитывает и ASR, и диаризацию. Без него готовый транскрипт, как и + сейчас, пропускается целиком. + +`sherpa-onnx` входит в обычные runtime-зависимости. Модели +`sherpa-onnx-pyannote-segmentation-3-0` и +`wespeaker_en_voxceleb_resnet34_LM.onnx` скачиваются и кешируются лениво при +первом запросе диаризации. Отдельного installation extra нет. + +## Контракты данных + +Результат ASR сохраняет существующие сегменты распознавания и дополнительно +содержит упорядоченные канонические слова. Для каждого слова известны текст, +начало и конец на временной шкале исходной записи. Backend-специфичные токены и +слова нормализуются в адаптере бэкенда; их обратная сборка должна сохранять +распознанный текст с точностью до нормализации пробелов. + +Результат диаризации — отдельная упорядоченная разметка говорящих: временные +интервалы с анонимным идентификатором голосового кластера. ASR-бэкенд не знает о +кластерах, а диаризатор не знает о распознанном тексте. + +Операция сведения назначает слову кластер с наибольшим временным перекрытием. +Если пересечения нет либо наибольшее перекрытие не единственно, слово получает +неизвестного говорящего. Соседние слова одного говорящего объединяются в +реплику; порядок слов и исходная временная шкала не меняются. + +Все три ASR-пути обязаны предоставлять пословный контракт до включения +диаризации: + +| Путь | Источник временных привязок | +|---|---| +| FasterWhisper | word timestamps CTranslate2 | +| ONNX-ASR | timestamped result модели | +| OpenVINO | word-level timestamps `WhisperPipeline` | + +Грубая подстановка метки на весь сегмент распознавания запрещена. + +## Пайплайн и время жизни + +После prescan и только при наличии файлов для обработки загружаются ASR-модель +и один batch-owned диаризатор. До первого ASR проверяются доступность пословных +таймкодов, модели диаризации и возможность создать диаризатор. Ошибка этого +этапа останавливает весь запуск без частичных транскриптов. + +Каждый файл обрабатывается последовательно: + +1. ASR; +2. диаризация, если ASR нашёл речь; +3. сведение слов с разметкой говорящих; +4. форматирование и запись Markdown. + +Один диаризатор последовательно переиспользуется для всех файлов батча. Данные +конкретной записи не становятся состоянием следующей. Объект освобождается при +завершении команды и не переносится через `TranscribeFileResult`. + +Разметка говорящих хранится только до сведения. Единственный постоянный +продуктовый артефакт — Markdown-транскрипт; локальный кеш файлов моделей живёт +по существующим правилам загрузчиков. + +## Конфигурация диаризации + +Автоматический режим использует: + +- Pyannote segmentation 3.0; +- WeSpeaker ResNet34 LM; +- `FastClusteringConfig.threshold = 0.89`; +- автоматическое число кластеров. + +`--speakers N` передаёт явное число кластеров вместо автоматического. Малый +кластер — кластер с речью короче максимума из 5 секунд и 2% длительности записи. +Он не отбрасывается и получает обычный номер, но вызывает предупреждение. + +## Формат Markdown + +При двух и более найденных кластерах тело состоит из линейных реплик: + +```markdown +[09:07] Speaker 1: Мы же у них не разворачиваемся… + +[09:18] Speaker 2: Мне гораздо проще накатывать обновления… +``` + +- Печатается только начало реплики, округлённое до целой секунды. Для записей + длиннее часа используется `[HH:MM:SS]`, иначе `[MM:SS]`. +- Метка `Speaker N` не получает Markdown-выделение. +- Нумерация начинается заново для каждого файла; номера назначаются по порядку + первого появления кластера в словах транскрипта. +- Смена говорящего всегда начинает новую реплику. +- Речь одного говорящего дополнительно разбивается по паузе не меньше 2 секунд + и максимальной длительности реплики 60 секунд. +- Слова без назначенного кластера группируются под `Speaker ?`. +- Перекрывающаяся речь остаётся в хронологическом порядке без особого + синтаксиса. +- В шапку добавляется число голосовых кластеров и предупреждения. Таблица + длительности по кластерам не выводится. + +Без успешной разметки нескольких говорящих сохраняется нынешний формат +обычного транскрипта с диапазонами времени. + +## Деградация и статус команды + +| Ситуация | Артефакт | Консоль и шапка | Статус | +|---|---|---|---| +| Диаризация не запрошена | Обычный транскрипт | Без новых сообщений | Текущий | +| ASR не нашёл речь | Текущий пустой Markdown | Речь не обнаружена; диаризация не запускалась | 0 | +| Найдено не меньше двух кластеров | Транскрипт с `Speaker N` | Число кластеров и предупреждения | 0, если нет иной ошибки | +| Найден один кластер | Обычный транскрипт без `Speaker 1` | Причина в консоли и шапке | Ненулевой | +| Есть слова без пересечения | Транскрипт с `Speaker ?` | Число таких слов | 0 | +| Есть малый кластер | Транскрипт со всеми кластерами | Длительность малого кластера | 0 | +| Разметка пуста при непустом ASR | Обычный транскрипт | Явное предупреждение | Ненулевой | +| Ошибка диаризации конкретного файла | Обычный транскрипт | Явное предупреждение | Ненулевой | +| Нет пословных таймкодов или не инициализировался диаризатор | Файлы не обрабатываются | Понятная ошибка до ASR | Ненулевой | + +В батче деградированный файл записывается, учитывается как неуспешная +диаризация, а остальные файлы продолжают обрабатываться. Итоговый статус батча +ненулевой, если хотя бы один файл деградировал или завершился ошибкой. + +## Критерии приёмки + +### Автоматические проверки + +- Адаптер каждого ASR-бэкенда возвращает монотонные слова с временной + привязкой; сборка слов сохраняет текст сегментов с точностью до пробелов. +- Сведение покрывает смену говорящего, отсутствие пересечения, равное + перекрытие, пунктуацию на границе реплик и хронологический порядок. +- Форматтер проверяется для обычных, часовых, неизвестных и малых кластеров, + `Speaker ?`, паузы 2 секунды и предела 60 секунд. +- CLI проверяет несовместимые и граничные значения, не запускает ASR при ошибке + preflight и соблюдает всю матрицу деградации в single- и batch-режимах. +- Батч создаёт диаризатор ровно один раз, пропускает его для пустого ASR, + переиспользует между файлами и не пишет промежуточный кеш. +- `--threads`, `--verbose` и `--force` сохраняют описанную семантику. + +Все автоматические тесты мокают движки и не скачивают реальные модели. + +### Ручная проверка + +Финальная сборка прогоняется на трёх записях из отчётов карты: + +- текст до и после сведения совпадает с точностью до переносов и пробелов; +- на плотном диалоге вручную проверяются устойчивость «голос → кластер», смены + говорящего, пропуски, малый остаточный кластер и перекрывающаяся речь; +- автоматическая конфигурация воспроизводит наблюдённую форму результата: + три основных и один малый остаточный кластер на Data Test, по два кластера на + T2 BDMA и Yantar; +- последовательный прогон на доступном Intel baseline остаётся быстрее + реального времени; фактические wall time и peak RSS записываются рядом с + результатом проверки. + +## Документация + +README должен описать новые CLI-флаги, ленивую загрузку моделей, ожидаемую +стоимость, формат `Speaker N`, предупреждения и batch-поведение. Направления +«Диаризация» и «Ручка нарезки абзацев» удаляются из backlog: первое перешло в +эту спецификацию, второе закрыто разрывом реплики на смене говорящего. + +После стабилизации отдельно рассматриваются +[параллельный запуск](https://git.dementev.space/ddmitry/local-transcriber/issues/22) +и [hardware-aware default](https://git.dementev.space/ddmitry/local-transcriber/issues/23). -- 2.54.0 From b8347004da998489382d8d2add8a3dc49b7629b9 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 17:09:27 +0300 Subject: [PATCH 15/15] =?UTF-8?q?docs(diarization):=20=D1=83=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20=D1=81=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - спецификация не должна оставлять неоднозначности перед реализацией диаризации. - Что: - уточнены сведение слов, таймкоды и предупреждение о малом кластере. - очищены терминология и граница между ADR и спецификацией. - сохранён результат сравнения WeSpeaker и CAMPPlus. - Проверка: - git diff --cached --check и проверка относительных ссылок. --- CONTEXT.md | 2 +- .../adr/007-word-level-speaker-diarization.md | 26 +++++++------- docs/specs/2026-08-14-speaker-diarization.md | 34 +++++++++++-------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 7ac5ae0..9e227e3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -33,7 +33,7 @@ _Avoid_: Токен, ASR-сегмент _Avoid_: Результат диаризации, сегменты говорящих **Голосовой кластер**: -Анонимная группа интервалов разметки говорящих, которые диаризатор относит к одному голосу. Не равен подтверждённому участнику встречи: ложный или малый кластер сохраняет отдельную метку. +Анонимная группа интервалов разметки говорящих, которые диаризатор относит к одному голосу. Не обязательно соответствует реальному участнику встречи: диаризация может создать ложный или малый кластер. _Avoid_: Участник, человек **Реплика говорящего**: diff --git a/docs/adr/007-word-level-speaker-diarization.md b/docs/adr/007-word-level-speaker-diarization.md index 0d94fef..6e2d2dc 100644 --- a/docs/adr/007-word-level-speaker-diarization.md +++ b/docs/adr/007-word-level-speaker-diarization.md @@ -28,6 +28,12 @@ WeSpeaker ResNet34 LM проверена на трёх записях с изв 19,1 секунды; поэтому малые кластеры нельзя молча отбрасывать, а разметку нельзя считать эталоном точных границ и перекрывающейся речи. +Двуязычная CAMPPlus zh/en оказалась примерно на 30% быстрее и при известном +числе участников улучшила прокси-метрику на двух записях, но для неё не нашлось +общего автоматического порога без лишних кластеров или склейки реальных голосов. +Поэтому она остаётся кандидатом только для будущего режима с обязательным +явным числом участников, а не для первой версии. + На доступном слабом Intel baseline, Core i7-6820HQ с урезанным питанием, последовательные ASR и диаризация обработали час записи примерно за 23 минуты. Диаризация увеличивает полное время примерно в 2,4 раза, но остаётся быстрее @@ -54,18 +60,14 @@ ASR-бэкенд приводит свой результат к общему н объединяет соседние слова одного говорящего в реплики. Распознавание по-прежнему выполняется на полных сегментах и сохраняет контекст модели. -Первая реализация последовательна на всех устройствах: ASR, диаризация, -сведение, Markdown. В батче один диаризатор создаётся после prescan, -переиспользуется для всех файлов и освобождается вместе с командой. Разметка -говорящих живёт только в памяти текущего запуска; постоянного кеша результата -нет. - -Грубого fallback на целый сегмент и автоматического переключения устройства -нет. Отсутствие пословных таймкодов или ошибка инициализации диаризатора -останавливают запуск до ASR. Ошибка диаризации конкретного файла после успешного -ASR не уничтожает полезный результат: сохраняется обычный транскрипт с явным -предупреждением и ненулевым статусом, а батч продолжает остальные файлы. -Подробная матрица поведения находится в +Диаризация не вводит грубый fallback на целый сегмент и не переключает +устройство ASR ради получения пословных таймкодов. Существующий GPU→CPU fallback +распознавания сохраняется и завершается до диаризации. Отсутствие пословных +таймкодов или ошибка инициализации диаризатора останавливают запуск до ASR. +Ошибка диаризации конкретного файла после успешного ASR не уничтожает полезный +результат: сохраняется обычный транскрипт с явным предупреждением и ненулевым +статусом, а батч продолжает остальные файлы. Порядок первой реализации, время +жизни диаризатора, кеш и подробная матрица поведения находятся в [спецификации](../specs/2026-08-14-speaker-diarization.md). ## Последствия diff --git a/docs/specs/2026-08-14-speaker-diarization.md b/docs/specs/2026-08-14-speaker-diarization.md index 3171d99..832b349 100644 --- a/docs/specs/2026-08-14-speaker-diarization.md +++ b/docs/specs/2026-08-14-speaker-diarization.md @@ -57,13 +57,15 @@ слова нормализуются в адаптере бэкенда; их обратная сборка должна сохранять распознанный текст с точностью до нормализации пробелов. -Результат диаризации — отдельная упорядоченная разметка говорящих: временные -интервалы с анонимным идентификатором голосового кластера. ASR-бэкенд не знает о -кластерах, а диаризатор не знает о распознанном тексте. +Разметка говорящих хранится отдельно от результата ASR: это упорядоченные +временные интервалы с анонимным идентификатором голосового кластера. ASR-бэкенд +не знает о кластерах, а диаризатор не знает о распознанном тексте. -Операция сведения назначает слову кластер с наибольшим временным перекрытием. -Если пересечения нет либо наибольшее перекрытие не единственно, слово получает -неизвестного говорящего. Соседние слова одного говорящего объединяются в +Операция сведения суммирует временное перекрытие слова с интервалами каждого +кластера и назначает кластер с единственным наибольшим ненулевым перекрытием. +Если пересечения нет либо несколько кластеров делят наибольшее значение, слово +получает неизвестного говорящего: порядок кластеров не используется как +искусственная развязка ничьей. Соседние слова одного говорящего объединяются в реплику; порядок слов и исходная временная шкала не меняются. Все три ASR-пути обязаны предоставлять пословный контракт до включения @@ -105,12 +107,14 @@ - Pyannote segmentation 3.0; - WeSpeaker ResNet34 LM; -- `FastClusteringConfig.threshold = 0.89`; +- порог кластеризации 0,89; - автоматическое число кластеров. -`--speakers N` передаёт явное число кластеров вместо автоматического. Малый -кластер — кластер с речью короче максимума из 5 секунд и 2% длительности записи. -Он не отбрасывается и получает обычный номер, но вызывает предупреждение. +`--speakers N` передаёт явное число кластеров вместо автоматического. Для +предупреждения используется диагностическая граница из калибровки: малым +считается кластер с речью короче максимума из 5 секунд и 2% длительности записи. +Граница влияет только на предупреждение — кластер не отбрасывается, получает +обычный номер и не меняет статус команды. ## Формат Markdown @@ -122,8 +126,9 @@ [09:18] Speaker 2: Мне гораздо проще накатывать обновления… ``` -- Печатается только начало реплики, округлённое до целой секунды. Для записей - длиннее часа используется `[HH:MM:SS]`, иначе `[MM:SS]`. +- Печатается только начало реплики; доли секунды отбрасываются, а не округляются + (`09:07.96` → `[09:07]`). Для записей длиннее часа используется + `[HH:MM:SS]`, иначе `[MM:SS]`. - Метка `Speaker N` не получает Markdown-выделение. - Нумерация начинается заново для каждого файла; номера назначаются по порядку первого появления кластера в словах транскрипта. @@ -164,9 +169,10 @@ - Адаптер каждого ASR-бэкенда возвращает монотонные слова с временной привязкой; сборка слов сохраняет текст сегментов с точностью до пробелов. - Сведение покрывает смену говорящего, отсутствие пересечения, равное - перекрытие, пунктуацию на границе реплик и хронологический порядок. + наибольшее перекрытие с результатом `Speaker ?`, пунктуацию на границе реплик + и хронологический порядок. - Форматтер проверяется для обычных, часовых, неизвестных и малых кластеров, - `Speaker ?`, паузы 2 секунды и предела 60 секунд. + `Speaker ?`, отбрасывания долей таймкода, паузы 2 секунды и предела 60 секунд. - CLI проверяет несовместимые и граничные значения, не запускает ASR при ошибке preflight и соблюдает всю матрицу деградации в single- и batch-режимах. - Батч создаёт диаризатор ровно один раз, пропускает его для пустого ASR, -- 2.54.0