Author SHA1 Message Date
Dmitriy Dementiev 352296b84a docs(prototype): добавлен макет транскрипта со спикерами
- Зачем:
  - требовалось выбрать компактный Markdown-формат реплик до реализации диаризации.
- Что:
  - добавлены три переключаемых варианта на реальном обезличенном фрагменте.
  - выбранный линейный вариант обновлён до формата `[MM:SS] Speaker N: текст`.
- Проверка:
  - файл открыт локально, варианты A–C переключаются стрелками и параметром `variant`.
2026-08-14 14:38:42 +03:00
Dmitriy Dementiev 26d4ca2f4a docs(context): добавлены термины диаризации
- Зачем:
  - решение о пословной привязке должно использовать единый язык во всех документах карты.
- Что:
  - определены сегмент распознавания и слово с временной привязкой.
  - реплика говорящего отделена от единицы запуска модели распознавания.
- Проверка:
  - git diff --check.
2026-08-14 14:16:01 +03:00
Dmitry Dementiev bbc2bdacfe docs(diarization): уточнены условия замера Intel
- Зачем:
  - результат benchmark нужно интерпретировать с учётом ограничения питания ноутбука.
- Что:
  - зафиксировано урезанное питание во время прогонов.
  - визуальная оценка потери производительности около 30% явно отделена от измеренных результатов.
- Проверка:
  - git diff --check.
2026-08-14 13:59:33 +03:00
Dmitry Dementiev 99ddf77af3 perf(diarization): добавлен замер на старом Intel
- Зачем:
  - требовалось оценить стоимость опциональной диаризации на доступном слабом 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.
2026-08-14 13:37:07 +03:00
Dmitriy Dementiev b8092aade5 docs(diarization): подтверждено смешение в ASR-сегментах
- Зачем:
  - требовалось проверить долю смешанных ASR-сегментов на трёх записях, включая разговоры на двоих.
- Что:
  - добавлен отчёт с долями сегментов и времени для трёх рабочих созвонов.
  - обвязка замеров переведена на откалиброванный порог 0,89.
  - исследовательский скрипт приведён к формату ruff.
- Проверка:
  - выполнены три полных прогона bench_conflict.py с WeSpeaker и порогом 0,89.
  - uv run ruff check и ruff format --check прошли успешно.
2026-08-14 11:44:02 +03:00
Dmitriy Dementiev 0fdeebb256 docs(diarization): добавлены отчёт и скрипт калибровки
- Зачем:
  - необходим устойчивый дефолт модели эмбеддингов и порога на русской речи.
- Что:
  - задокументирован выбор 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 11:18:17 +03:00
Dmitriy Dementiev 8c55eaa87f docs(context): добавлен термин «Опорная разметка»
- Зачем:
  - слуховая проверка (#12) показала, что разметку диаризации читают как
    истину, хотя она содержит ложный кластер, пропуски и неполные перекрытия.
- Что:
  - в глоссарий добавлен термин «Опорная разметка» с запретом на
    «эталонную», «истинную» и ground truth.
  - определение описывает роль разметки в измерении, а не результат
    конкретного прогона: выводы о пороге 0,9 остались в тикетах карты.
- Проверка:
  - git show --stat HEAD и чтение CONTEXT.md.
2026-08-12 18:08:26 +03:00
Dmitriy Dementiev 388de09c42 docs(research): переработана структура исследования
- Зачем:
  - выводы о жизненном цикле и возможностях моделей должны читаться как единое исследование.
- Что:
  - материал перестроен вокруг слоёв, платформ и уровней доказательства.
  - объединены выводы по Intel, AMD, Apple и браузерным путям.
  - подтверждённые возможности отделены от выводов и необходимых экспериментов.
- Проверка:
  - относительные ссылки и структура Markdown проверены.
  - git diff --cached --check выполнен успешно.
2026-08-12 16:25:54 +03:00
Dmitriy Dementiev 4c7f2920c1 docs(research): исследован жизненный цикл ORT и OpenVINO
- Зачем:
  - нужна фактическая опора для решений карты о диаризации на Intel-пути.
- Что:
  - исследованы жизненные циклы ORT, OpenVINO, DirectML и Windows ML.
  - сравнены пути AMD, Apple Silicon и браузерные EP для текущих моделей.
  - зафиксированы wheel-матрицы, fallback и необходимые model-specific тесты.
- Проверка:
  - git diff --cached --check.
2026-08-12 16:12:43 +03:00
11 changed files with 1583 additions and 251 deletions
+8 -6
View File
@@ -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-минутную запись, и время от настроек
кластеризации практически не зависит. Свип вести на коротком фрагменте.
@@ -60,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()` с поправкой на разные единицы измерения.
+15 -4
View File
@@ -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} с) "
+14 -4
View File
@@ -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
@@ -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()
+326
View File
@@ -0,0 +1,326 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PROTOTYPE — формат транскрипта со спикерами</title>
<style>
:root {
color-scheme: light;
--paper: #fbfaf7;
--ink: #25221f;
--muted: #746e67;
--line: #ddd7ce;
--accent: #9b3f2f;
--code: #f0ece5;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: #e9e4dc;
color: var(--ink);
font: 16px/1.58 system-ui, -apple-system, "Segoe UI", sans-serif;
}
main {
width: min(1180px, calc(100% - 32px));
margin: 28px auto 100px;
}
.prototype-note {
margin-bottom: 18px;
padding: 12px 16px;
border: 1px dashed #a59d92;
background: #fffdf8;
color: #5f5850;
}
.prototype-note strong { color: var(--accent); }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(340px, .72fr);
gap: 18px;
align-items: start;
}
.paper, .source-panel {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--paper);
box-shadow: 0 8px 28px rgb(49 40 31 / 9%);
}
.paper { padding: clamp(22px, 4vw, 54px); }
.source-panel { position: sticky; top: 18px; overflow: hidden; }
.source-panel h2 {
margin: 0;
padding: 13px 16px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 14px;
letter-spacing: .06em;
text-transform: uppercase;
}
pre {
max-height: calc(100vh - 120px);
margin: 0;
padding: 18px;
overflow: auto;
background: var(--code);
white-space: pre-wrap;
word-break: break-word;
font: 13px/1.55 ui-monospace, "Cascadia Code", Consolas, monospace;
}
h1 { margin: 0 0 22px; font: 700 clamp(26px, 3vw, 38px)/1.15 Georgia, serif; }
h2 { margin: 28px 0 12px; font-size: 20px; }
h3 { margin: 25px 0 7px; font-size: 17px; }
ul { padding-left: 21px; }
hr { margin: 28px 0; border: 0; border-top: 1px solid var(--line); }
.turn { margin: 18px 0; }
.time { color: var(--muted); font: 13px ui-monospace, "Cascadia Code", monospace; }
.speaker { color: #783427; }
blockquote {
margin: 8px 0 22px;
padding: 10px 16px;
border-left: 4px solid #b88772;
background: #f5f0e9;
}
blockquote p { margin: 0; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 9px 8px; border: 1px solid var(--line); vertical-align: top; text-align: left; }
th { background: #eee8de; }
.warning {
margin: 17px 0;
padding: 10px 13px;
border-left: 4px solid #c47b23;
background: #fff2dc;
}
.overlap { background: #f7e9e4; }
.switcher {
position: fixed;
left: 50%;
bottom: 22px;
z-index: 10;
display: flex;
align-items: center;
gap: 6px;
transform: translateX(-50%);
padding: 7px;
border: 1px solid rgb(255 255 255 / 30%);
border-radius: 999px;
background: #201d1a;
box-shadow: 0 8px 32px rgb(0 0 0 / 25%);
color: white;
}
.switcher button {
width: 38px;
height: 34px;
border: 0;
border-radius: 999px;
background: #39332e;
color: white;
cursor: pointer;
font-size: 20px;
}
.switcher button:hover { background: #554b43; }
#variant-label { min-width: 230px; text-align: center; font-size: 14px; }
@media (max-width: 820px) {
.layout { grid-template-columns: 1fr; }
.source-panel { position: static; }
pre { max-height: none; }
#variant-label { min-width: 190px; }
}
</style>
</head>
<body>
<!-- Три варианта markdown-транскрипта, переключаемые через ?variant=, в отдельном throwaway-прототипе. -->
<main>
<div class="prototype-note">
<strong>PROTOTYPE — не часть продукта.</strong>
Реальный фрагмент рабочей встречи слегка сокращён и обезличен; интервалы
и статистика взяты из уже выполненного замера. Слева — вид документа,
справа — буквальный Markdown для оценки последующей обработки ИИ.
</div>
<div class="layout">
<article class="paper" id="preview"></article>
<section class="source-panel">
<h2>Markdown-источник</h2>
<pre id="source"></pre>
</section>
</div>
</main>
<nav class="switcher" aria-label="Переключение вариантов">
<button id="previous" aria-label="Предыдущий вариант"></button>
<span id="variant-label"></span>
<button id="next" aria-label="Следующий вариант"></button>
</nav>
<script>
const variants = {
A: {
name: "Линейные реплики",
source: `# Транскрипт: пример-встречи.mp4
- **Дата транскрипции**: 2026-08-14 12:00:00
- **Модель**: gigaam-v3-e2e-rnnt
- **Язык**: ru (задан явно)
- **Длительность**: 25:59
- **Устройство**: ONNX (CPU)
- **Диаризация**: 4 голосовых кластера
- **Внимание**: Speaker 4 — малый кластер (00:19; 1,3% речи), возможна ошибка разделения
---
[08:50] Speaker 3: А показываем, получается, в их контуре, не в нашем?
[08:54] Speaker 2: В нашем, по-моему.
[08:55] Speaker 3: В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…
[09:07] Speaker 1: Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.
[11:34] Speaker 2: Результаты проверок пишутся туда же.
[11:39] Speaker 1: По сути, нам нужно переписать только ту часть, которая обрабатывает файл.
[11:43] Speaker 3: Да, а дальше переиспользовать существующую запись результатов.`,
preview: `
<h1>Транскрипт: пример-встречи.mp4</h1>
<ul>
<li><strong>Дата транскрипции</strong>: 2026-08-14 12:00:00</li>
<li><strong>Модель</strong>: gigaam-v3-e2e-rnnt</li>
<li><strong>Язык</strong>: ru (задан явно)</li>
<li><strong>Длительность</strong>: 25:59</li>
<li><strong>Устройство</strong>: ONNX (CPU)</li>
<li><strong>Диаризация</strong>: 4 голосовых кластера</li>
</ul>
<div class="warning"><strong>Внимание:</strong> Speaker 4 — малый кластер (00:19; 1,3% речи), возможна ошибка разделения.</div>
<hr>
<p class="turn"><span class="time">[08:50]</span> Speaker 3: А показываем, получается, в их контуре, не в нашем?</p>
<p class="turn"><span class="time">[08:54]</span> Speaker 2: В нашем, по-моему.</p>
<p class="turn"><span class="time">[08:55]</span> Speaker 3: В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…</p>
<p class="turn"><span class="time">[09:07]</span> Speaker 1: Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.</p>
<p class="turn"><span class="time">[11:34]</span> Speaker 2: Результаты проверок пишутся туда же.</p>
<p class="turn"><span class="time">[11:39]</span> Speaker 1: По сути, нам нужно переписать только ту часть, которая обрабатывает файл.</p>
<p class="turn overlap"><span class="time">[11:43]</span> Speaker 3: Да, а дальше переиспользовать существующую запись результатов.</p>`
},
B: {
name: "Сценарий",
source: `# Транскрипт: пример-встречи.mp4
> Диаризация нашла четыре голосовых кластера. Speaker 4 занимает 19 секунд и может быть ошибкой разделения.
---
### Speaker 3 · [08:50.75 - 08:54.73]
> А показываем, получается, в их контуре, не в нашем?
### Speaker 2 · [08:54.73 - 08:55.91]
> В нашем, по-моему.
### Speaker 3 · [08:55.81 - 09:07.96]
> В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…
### Speaker 1 · [09:07.96 - 09:18.80]
> Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.
## Перекрывающаяся речь · [11:43.57 - 11:44.82]
> **Speaker 1:** По сути, нам нужно переписать только ту часть, которая обрабатывает файл.
>
> **Speaker 3:** Да, а дальше переиспользовать существующую запись результатов.`,
preview: `
<h1>Транскрипт: пример-встречи.mp4</h1>
<blockquote><p>Диаризация нашла четыре голосовых кластера. Speaker 4 занимает 19 секунд и может быть ошибкой разделения.</p></blockquote>
<hr>
<h3>Speaker 3 · <span class="time">[08:50.75 - 08:54.73]</span></h3>
<blockquote><p>А показываем, получается, в их контуре, не в нашем?</p></blockquote>
<h3>Speaker 2 · <span class="time">[08:54.73 - 08:55.91]</span></h3>
<blockquote><p>В нашем, по-моему.</p></blockquote>
<h3>Speaker 3 · <span class="time">[08:55.81 - 09:07.96]</span></h3>
<blockquote><p>В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…</p></blockquote>
<h3>Speaker 1 · <span class="time">[09:07.96 - 09:18.80]</span></h3>
<blockquote><p>Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.</p></blockquote>
<h2>Перекрывающаяся речь · <span class="time">[11:43.57 - 11:44.82]</span></h2>
<blockquote class="overlap"><p><strong>Speaker 1:</strong> По сути, нам нужно переписать только ту часть, которая обрабатывает файл.<br><br><strong>Speaker 3:</strong> Да, а дальше переиспользовать существующую запись результатов.</p></blockquote>`
},
C: {
name: "Таблица событий",
source: `# Транскрипт: пример-встречи.mp4
| Speaker | Речь | Доля | Примечание |
|---|---:|---:|---|
| Speaker 1 | 09:33 | 40,3% | основной кластер |
| Speaker 2 | 08:07 | 34,3% | основной кластер |
| Speaker 3 | 05:42 | 24,1% | основной кластер |
| Speaker 4 | 00:19 | 1,3% | возможная ошибка разделения |
| Начало | Конец | Кто | Текст | Событие |
|---:|---:|---|---|---|
| 08:50.75 | 08:54.73 | S3 | А показываем, получается, в их контуре, не в нашем? | — |
| 08:54.73 | 08:55.91 | S2 | В нашем, по-моему. | — |
| 08:55.81 | 09:07.96 | S3 | В нашем. А, отлично. У нас просто есть тестовый контур… | — |
| 09:07.96 | 09:18.80 | S1 | Мы же у них не разворачиваемся. Мне проще накатывать обновления на наш контур. | — |
| 11:39.91 | 11:44.82 | S1 | Нам нужно переписать только ту часть, которая обрабатывает файл. | overlap:S3 |
| 11:43.57 | 11:46.47 | S3 | Да, а дальше переиспользовать существующую запись результатов. | overlap:S1 |`,
preview: `
<h1>Транскрипт: пример-встречи.mp4</h1>
<table>
<thead><tr><th>Speaker</th><th>Речь</th><th>Доля</th><th>Примечание</th></tr></thead>
<tbody>
<tr><td>Speaker 1</td><td>09:33</td><td>40,3%</td><td>основной кластер</td></tr>
<tr><td>Speaker 2</td><td>08:07</td><td>34,3%</td><td>основной кластер</td></tr>
<tr><td>Speaker 3</td><td>05:42</td><td>24,1%</td><td>основной кластер</td></tr>
<tr class="warning"><td>Speaker 4</td><td>00:19</td><td>1,3%</td><td>возможная ошибка разделения</td></tr>
</tbody>
</table>
<h2>События</h2>
<table>
<thead><tr><th>Начало</th><th>Конец</th><th>Кто</th><th>Текст</th><th>Событие</th></tr></thead>
<tbody>
<tr><td>08:50.75</td><td>08:54.73</td><td>S3</td><td>А показываем, получается, в их контуре, не в нашем?</td><td>—</td></tr>
<tr><td>08:54.73</td><td>08:55.91</td><td>S2</td><td>В нашем, по-моему.</td><td>—</td></tr>
<tr><td>08:55.81</td><td>09:07.96</td><td>S3</td><td>В нашем. А, отлично. У нас просто есть тестовый контур…</td><td>—</td></tr>
<tr><td>09:07.96</td><td>09:18.80</td><td>S1</td><td>Мы же у них не разворачиваемся. Мне проще накатывать обновления на наш контур.</td><td>—</td></tr>
<tr class="overlap"><td>11:39.91</td><td>11:44.82</td><td>S1</td><td>Нам нужно переписать только ту часть, которая обрабатывает файл.</td><td>overlap:S3</td></tr>
<tr class="overlap"><td>11:43.57</td><td>11:46.47</td><td>S3</td><td>Да, а дальше переиспользовать существующую запись результатов.</td><td>overlap:S1</td></tr>
</tbody>
</table>`
}
};
const keys = Object.keys(variants);
const params = new URLSearchParams(window.location.search);
let current = (params.get("variant") || "A").toUpperCase();
if (!variants[current]) current = "A";
function render() {
const variant = variants[current];
document.getElementById("preview").innerHTML = variant.preview;
document.getElementById("source").textContent = variant.source;
document.getElementById("variant-label").textContent = `${current}${variant.name}`;
document.title = `${current}${variant.name} · PROTOTYPE`;
}
function move(offset) {
const index = keys.indexOf(current);
current = keys[(index + offset + keys.length) % keys.length];
const url = new URL(window.location.href);
url.searchParams.set("variant", current);
window.history.replaceState({}, "", url);
render();
}
document.getElementById("previous").addEventListener("click", () => move(-1));
document.getElementById("next").addEventListener("click", () => move(1));
window.addEventListener("keydown", (event) => {
const target = event.target;
if (target.matches("input, textarea, [contenteditable]")) return;
if (event.key === "ArrowLeft") move(-1);
if (event.key === "ArrowRight") move(1);
});
render();
</script>
</body>
</html>
+16
View File
@@ -15,3 +15,19 @@ _Avoid_: Доступная модель, дефолт
**Модель по умолчанию**:
Поддерживаемая модель, которую проект выбирает без явного указания модели пользователем для определённого пути выполнения.
_Avoid_: Рекомендуемая модель, поддерживаемая модель
**Опорная разметка**:
Разметка, принятая за точку отсчёта при измерении чего-то другого. Опорной её делает роль в измерении, а не качество: она не выверена вручную и сама может содержать ошибки, поэтому посчитанная по ней величина осмысленна как порядок, но не как точное значение.
_Avoid_: Эталонная разметка, истинная разметка, ground truth
**Сегмент распознавания**:
Непрерывный временной фрагмент аудио, для которого движок возвращает связный текст с общим контекстом. Может содержать речь нескольких говорящих и не равен реплике говорящего.
_Avoid_: Реплика, фраза говорящего
**Слово с временной привязкой**:
Распознанное слово, положение которого известно на временной шкале записи. Минимальная единица, которой назначается говорящий.
_Avoid_: Токен, ASR-сегмент
**Реплика говорящего**:
Последовательность соседних слов с временной привязкой, назначенных одному говорящему. Это единица структуры готового транскрипта, а не запуска модели распознавания.
_Avoid_: Сегмент распознавания, ASR-сегмент
@@ -193,5 +193,7 @@ Silero VAD, то есть они проходят по тишине. В разг
- Проверить границы диаризации на слух или сверкой с внешним транскриптом:
совпадение числа говорящих не доказывает правильность интервалов.
- Сравнить эмбеддинги, обученные не только на английском, на русской речи.
- Измерить производительность на целевом Intel Core i5 11-го поколения.
- Измерить производительность на доступном слабом Intel baseline — выполнено в
[отдельном отчёте](2026-08-14-diarization-intel-i7.md); конкретный Core i5
11-го поколения недоступен.
- Измерить параллельный режим ASR и диаризации.
@@ -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).
@@ -0,0 +1,210 @@
# Калибровка модели эмбеддингов и порога диаризации
**Дата:** 2026-08-14
**Статус:** выбор конфигурации для проектирования. Производительность отдельно
проверена на [доступном старом Intel baseline](2026-08-14-diarization-intel-i7.md).
## Решение
Для автоматического определения числа участников использовать:
- эмбеддинги `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:0012:00 | 25:59,9 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` |
| `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4` | 2 | 00:0005:00 | 14:50,9 | `51866D247FE3EDA134CDD884F707B1F1DB8855B492E8BD14D2B56B62476255ED` |
| `2026-08-12 Созвон с Максом Мерлином по T2 Forecast и Yantar.mp4` | 2 | 00:0005: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 в этом эксперименте. Это плюс для
варианта с известным числом участников. Производительность выбранной WeSpeaker
отдельно проверена на доступном старом Intel Core i7.
## Воспроизводимость
Свип выполняется скриптом
[`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 i7; конкретный Core
i5 11-го поколения остаётся непроверенным, потому что такого устройства нет.
Для спецификации зафиксировать WeSpeaker + 0,89 как автоматический дефолт,
отдельную опцию явного числа участников и отсутствие автоматического
отбрасывания малых кластеров. CAMPPlus zh/en можно оставить кандидатом для
будущего режима с обязательным `num_clusters` после расширенной слуховой
проверки.
@@ -0,0 +1,96 @@
# Производительность диаризации на старом 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;
- прогоны последовательные, без намеренно запущенной конкурирующей нагрузки;
- модели после первого запуска находились в локальном кеше.
По наблюдению владельца, ограничение питания снижало производительность примерно
на 30%. Это визуальная оценка, а не результат отдельного A/B-замера, поэтому
фактические времена ниже не пересчитываются. Их следует читать как консервативный
результат именно в зафиксированном режиме питания.
Использованы те же три записи и те же 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,02,3 с | 9001035 МБ на тёплых прогонах |
| Диаризация | 0,2 с | 366469 МБ |
Первый 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-го поколения;
- влияние урезанного питания оценивается примерно в 30% только на глаз; прогон с
полным питанием для сравнения не проводился;
- медиана трёх прогонов снята только на одной полной записи, на двух других есть
по одному подтверждающему прогону;
- чистый cold start ASR без скачивания, но с холодным файловым кешем не измерен;
- параллельный запуск ASR и диаризации не измерен;
- результат отвечает только на стоимость выбранных моделей и конфигурации, а не
на качество диаризации.
@@ -1,289 +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.
## Краткий вывод
Вне границ документа:
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)).
- решение о консолидации проекта на одном runtime;
- выбор нового устройства или модели по умолчанию;
- обещание производительности без model-specific benchmark;
- разработка браузерной версии `local-transcriber`.
## Что именно является чем
## Краткий ответ
Слои нельзя сравнивать как взаимозаменяемые пакеты:
- **Переносимый фундамент проекта — 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.241.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)).
Под 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)).
## PyPI: версии, платформы и CPython 3.13
### Apple Silicon
Срез сделан по фактически опубликованным wheel, а не по classifiers страницы.
У всех трёх пакетов отсутствует source distribution, поэтому неподдержанная
комбинация платформы и Python не сможет штатно собраться через обычный
`pip install` без самостоятельной сборки из репозитория.
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)).
| Пакет | Последняя версия на 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)) |
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/)).
Практический вывод для 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/)).
## Возможности текущих моделей
## Что это меняет для карты диаризации
Таблица применяет одну и ту же лестницу доказательства к трем модельным путям.
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.
| Модель и требуемый контракт | Переносимый baseline | Intel accelerator | AMD accelerator | Apple accelerator | Browser |
|---|---|---|---|---|---|
| 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, но выбранная пара моделей не подтверждена |
## Рекомендация по жизненному циклу
### GigaAM E2E RNN-T
Ниже — **интерпретация источников для local-transcriber**, а не опубликованный
roadmap Microsoft или Intel. Она исходит из фактов о lifecycle, wheel-матрицах
и текущем контракте проекта; реальную пригодность каждого ускорителя должен
подтвердить проектный benchmark.
`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.
- Сохранять **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.
Таймкоды формирует `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)).
- Какой точный контракт 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?
### OpenVINO Whisper
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 не работает.
Проектный OpenVINO backend уже запрашивает timestamps, но сводит результат к
chunk-сегментам. Поэтому для диаризации нужно сначала определить и протянуть
word-level контракт через `Backend`/`TranscribeResult`.
### PyAnnote + WeSpeaker для диаризации
Локальная разведка доказала, что связка PyAnnote segmentation + WeSpeaker
embeddings создаёт интервалы на CPU ORT для одной записи. Это закрывает базовую
совместимость, но не upstream-гарантию пары и не переносимость на другие EP
([разведка](../benchmarks/2026-08-12-diarization-feasibility.md)).
Официальный 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)).
Итоговые сегменты создаёт sherpa после двух ONNX-моделей и clustering. Ускорение
одной сессии не означает ускорение pipeline; численные изменения эмбеддингов
могут изменить кластеры даже при совпадающем текстовом контракте
([C API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html)).
## Почему ONNX Runtime Web работает между браузерами
Браузерная переносимость появляется не из ONNX-файла отдельно, а из сочетания
трёх решений:
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)):
- 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)).
`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)).
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)).
Полезный для CLI вывод из браузерной архитектуры — не новый продукт, а строгая
политика capabilities:
- всегда сохранять переносимый CPU baseline;
- обнаруживать ускоритель во время запуска;
- различать наличие API, успешную сессию, размещение графа и сохранение
контракта;
- измерять end-to-end pipeline, а не отдельное имя provider.
## Что это меняет для `local-transcriber`
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-путь.
Для текущей карты это даёт два входа:
- [«Выбрать единицу привязки спикера к тексту»](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 и опциональных ускорителей?
@@ -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()