Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
352296b84a | ||
|
|
26d4ca2f4a | ||
|
|
bbc2bdacfe | ||
|
|
99ddf77af3 | ||
|
|
b8092aade5 | ||
|
|
0fdeebb256 | ||
|
|
8c55eaa87f | ||
|
|
388de09c42 | ||
|
|
4c7f2920c1 |
@@ -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()` с поправкой на разные единицы измерения.
|
||||
|
||||
@@ -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} с) "
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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:00–12:00 | 25:59,9 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` |
|
||||
| `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4` | 2 | 00:00–05:00 | 14:50,9 | `51866D247FE3EDA134CDD884F707B1F1DB8855B492E8BD14D2B56B62476255ED` |
|
||||
| `2026-08-12 Созвон с Максом Мерлином по T2 Forecast и Yantar.mp4` | 2 | 00:00–05:00 | 20:22,2 | `4422F04E2771091A0648E5422D14A31DD7CA2C8EEF7ED9F4A5C63F43D8CA6400` |
|
||||
|
||||
Для первой записи число участников взято из согласованного MoM и не зависит от
|
||||
диаризации. Для двух остальных рядом с медиа лежат транскрипты Hypescribe с
|
||||
таймкодами и метками спикеров. Они получены другим инструментом и использованы
|
||||
как независимая грубая опорная разметка.
|
||||
|
||||
Hypescribe ставит метку только в начале реплики и не размечает точные границы,
|
||||
тишину и наложения голосов. Поэтому ниже считается не DER, а **mapped speaker
|
||||
purity**: лучший взаимно-однозначный маппинг кластеров на опорные метки по
|
||||
суммарному перекрытию. Метрика подходит для сравнения конфигураций на одной
|
||||
записи, но не является абсолютной оценкой диаризации.
|
||||
|
||||
Кластер считается содержательным, если в нём не меньше `max(5 с, 2% длины
|
||||
записи)` речи. Это только диагностический показатель: готовый CLI не должен
|
||||
молча отбрасывать малые кластеры без отдельного решения.
|
||||
|
||||
## Модели
|
||||
|
||||
Во всех прогонах использовалась одна сегментация
|
||||
`sherpa-onnx-pyannote-segmentation-3-0`.
|
||||
|
||||
| Роль | Модель | Языки обучения | Размер | SHA-256 |
|
||||
|---|---|---|---:|---|
|
||||
| выбранная | `wespeaker_en_voxceleb_resnet34_LM.onnx` | английский, VoxCeleb2 | 26 530 550 | `E9848563DA86F263117134DFD7AD63C92355B37DE492B55E325400C9D9C39012` |
|
||||
| многоязычная альтернатива | `3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx` | китайский + английский | 28 281 164 | `AA3CFC16963A10586A9393F5035D6D6B57E98D358B347F80C2A30BF4F00CEBA2` |
|
||||
| дополнительная разведка | `3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx` | китайский | 39 593 761 | `1A331345F04805BADBB495C775A6DDFFCDD1A732567D5EC8B3D5749E3C7A5E4B` |
|
||||
| сегментация | `model.onnx` из `sherpa-onnx-pyannote-segmentation-3-0` | — | 5 992 913 | `220AD67CA923BEF2FA91F2390C786097BF305BCEB5E261D4AF67B38E938E1079` |
|
||||
|
||||
WeSpeaker сам помечает VoxCeleb-модель как английскую и распространяет её под
|
||||
CC BY 4.0. Репозиторий 3D-Speaker и модель CAMPPlus на ModelScope используют
|
||||
Apache 2.0; в исходниках 3D-Speaker модель явно описана как обученная на
|
||||
большом китайско-английском корпусе. ONNX-файлы брались из официального релиза
|
||||
`k2-fsa/sherpa-onnx`, а не из сторонних зеркал.
|
||||
|
||||
Источники:
|
||||
|
||||
- [список и лицензирование моделей WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md);
|
||||
- [карточка `wespeaker-voxceleb-resnet34-LM`](https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet34-LM);
|
||||
- [исходники и лицензия 3D-Speaker](https://github.com/modelscope/3D-Speaker);
|
||||
- [официальный релиз ONNX-моделей sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models).
|
||||
|
||||
## Свип WeSpeaker
|
||||
|
||||
Порог сначала проверялся крупным шагом, затем уточнялся около переходов между
|
||||
числом кластеров. В ячейках — общее число кластеров; жирным выделено точное
|
||||
совпадение с известным числом участников.
|
||||
|
||||
| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 |
|
||||
|---:|---:|---:|---:|
|
||||
| 0,85 | **3** | **2** | 3 |
|
||||
| 0,87 | **3** | **2** | 3 |
|
||||
| 0,88 | **3** | **2** | 3 |
|
||||
| **0,89** | **3** | **2** | **2** |
|
||||
| 0,90 | 2 | **2** | **2** |
|
||||
| 0,95 | 2 | **2** | 1 |
|
||||
| явное `num_clusters` | **3** | **2** | **2** |
|
||||
|
||||
`0,89` — единственное проверенное значение, которое без знания числа
|
||||
участников дало правильное количество кластеров на всех трёх фрагментах. На
|
||||
двух записях с опорными метками purity составила 0,767 и 0,787. Явное число
|
||||
участников дало те же значения.
|
||||
|
||||
## Сравнение с 3D-Speaker
|
||||
|
||||
### CAMPPlus, китайский + английский
|
||||
|
||||
| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 |
|
||||
|---:|---:|---:|---:|
|
||||
| 0,85 | 7 | 7 | 7 |
|
||||
| 0,90 | 6 | 5 | 5 |
|
||||
| 0,95 | 5 | 5 | 5 |
|
||||
| 0,99 | 4 | 4 | 4 |
|
||||
| 1,00 | 4 | 4 | 4 |
|
||||
| 1,05 | **3** | 3 | 3 |
|
||||
| 1,10 | 2 | **2** | 3 |
|
||||
| явное `num_clusters` | **3** | **2** | **2** |
|
||||
|
||||
При `1,05` на двух записях остаётся по одному малому остаточному кластеру, а
|
||||
при `1,10` трёхсторонняя встреча уже склеивается до двух голосов. Общего
|
||||
автоматического порога нет.
|
||||
|
||||
При явном числе участников purity равна 0,801 на T2 BDMA и 0,911 на Yantar.
|
||||
Это лучше WeSpeaker на 0,034 и 0,124 соответственно. Однако на контрольной
|
||||
трёхсторонней записи один из трёх принудительных кластеров оказался меньше
|
||||
порога содержательности, поэтому улучшение по двум текстовым прокси нельзя
|
||||
обобщать на все записи.
|
||||
|
||||
### ERes2Net base, китайский
|
||||
|
||||
Эта модель проверялась дополнительно, но не считается выполнением требования
|
||||
о многоязычной альтернативе. Даже на пороге 0,99 она дала 5 / 4 / 7 кластеров
|
||||
вместо 3 / 2 / 2. При явном числе участников purity составила 0,688 и 0,907:
|
||||
результат неоднородный и автоматический режим заметно хуже выбранного.
|
||||
|
||||
## Полные прогоны выбранного кандидата
|
||||
|
||||
После свипа `WeSpeaker + 0,89` прогнан на всех трёх записях целиком.
|
||||
|
||||
| Запись | Кластеры | Содержательные | Речь по кластерам, с | Остаток сверх ожидаемых | Purity | Время | RTF |
|
||||
|---|---:|---:|---|---:|---:|---:|---:|
|
||||
| Data Test | 4 | 3 | 573,1 / 487,4 / 342,2 / 19,1 | 1,3% | — | 189,0 с | 0,121 |
|
||||
| T2 BDMA | 2 | 2 | 748,7 / 52,3 | 0% | 0,749 | 112,3 с | 0,126 |
|
||||
| Yantar | 2 | 2 | 733,2 / 259,8 | 0% | 0,906 | 151,4 с | 0,124 |
|
||||
|
||||
На полной контрольной записи остаётся ложный кластер на 19,1 с, но три
|
||||
содержательных кластера совпадают с известным составом. Повторный полный прогон
|
||||
на 0,9 дал тот же результат: JSON-массивы всех 340 интервалов на 0,89 и 0,9
|
||||
совпали в точности, включая границы и номера кластеров. Поэтому к кандидату
|
||||
0,89 непосредственно применима слуховая проверка отрезка 07:00–12:00,
|
||||
выполненная для результата из
|
||||
[разведочного замера](2026-08-12-diarization-feasibility.md): три основных
|
||||
голоса стабильны, остаточный кластер ложный, есть небольшие пропуски второго
|
||||
голоса, а наложения голосов определяются не полностью. Новая калибровка не
|
||||
устраняет эти ограничения сегментации.
|
||||
|
||||
На двух полных разговорах purity отличается от короткого фрагмента: 0,749
|
||||
против 0,767 и 0,906 против 0,787. Это подтверждает, что короткий свип годится
|
||||
для отсева конфигураций, а финальный кандидат надо проверять целиком.
|
||||
|
||||
## Производительность
|
||||
|
||||
Условия: AMD Ryzen 7 8845H, Windows 11 build 26200, Python 3.13.13,
|
||||
`sherpa-onnx` 1.13.5, `onnxruntime` 1.28.0, NumPy 2.4.3, 8 потоков CPU.
|
||||
Загрузка моделей и декодирование медиа не входят в измерение.
|
||||
|
||||
Средний RTF на коротких фрагментах:
|
||||
|
||||
| Модель | RTF | Относительно WeSpeaker |
|
||||
|---|---:|---:|
|
||||
| WeSpeaker ResNet34 LM | 0,118 | 1,00× |
|
||||
| CAMPPlus zh/en | 0,083 | 0,70× |
|
||||
| ERes2Net base zh | 0,152 | 1,29× |
|
||||
|
||||
CAMPPlus примерно на 30% быстрее WeSpeaker в этом эксперименте. Это плюс для
|
||||
варианта с известным числом участников. Производительность выбранной 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,0–2,3 с | 900–1035 МБ на тёплых прогонах |
|
||||
| Диаризация | 0,2 с | 366–469 МБ |
|
||||
|
||||
Первый ASR-запуск показал 23,6 секунды, но включал скачивание файлов модели,
|
||||
поэтому не считается чистым cold start. Peak RSS этого процесса достиг 1174 МБ.
|
||||
Пиковая память замерена отдельно для каждого последовательного прохода; для
|
||||
будущего параллельного режима значения нельзя механически считать измеренным
|
||||
общим пиком.
|
||||
|
||||
## Сопоставление с разведкой на Ryzen
|
||||
|
||||
На Ryzen 7 8845H для Data Test были получены 16,4× RTFx у ASR и 11,1× у
|
||||
диаризации. На старом Intel оба прохода медленнее примерно в 2,6 раза, а их
|
||||
соотношение почти не изменилось. Следовательно, слабое железо ухудшает абсолютное
|
||||
время, но не меняет основной архитектурный вывод: диаризация дороже ASR, а
|
||||
совмещение проходов потенциально полезно.
|
||||
|
||||
## Ограничения
|
||||
|
||||
- Core i7-6820HQ не моделирует производительность Core i5 11-го поколения;
|
||||
- влияние урезанного питания оценивается примерно в 30% только на глаз; прогон с
|
||||
полным питанием для сравнения не проводился;
|
||||
- медиана трёх прогонов снята только на одной полной записи, на двух других есть
|
||||
по одному подтверждающему прогону;
|
||||
- чистый cold start ASR без скачивания, но с холодным файловым кешем не измерен;
|
||||
- параллельный запуск ASR и диаризации не измерен;
|
||||
- результат отвечает только на стоимость выбранных моделей и конфигурации, а не
|
||||
на качество диаризации.
|
||||
@@ -0,0 +1,491 @@
|
||||
# Куда движутся ONNX Runtime и OpenVINO: жизненный цикл и переносимость моделей
|
||||
|
||||
**Дата:** 2026-08-12
|
||||
|
||||
**Статус:** исследование для карты диаризации. Не архитектурное решение и не
|
||||
основание для консолидации всех движков распознавания на ONNX Runtime.
|
||||
|
||||
## Вопрос и границы
|
||||
|
||||
Исследование отвечает на два связанных вопроса:
|
||||
|
||||
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): 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).
|
||||
Оно основано на первичных источниках: официальной документации, release notes,
|
||||
репозиториях владельцев и фактических метаданных PyPI на 2026-08-12.
|
||||
|
||||
Вне границ документа:
|
||||
|
||||
- решение о консолидации проекта на одном 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 | 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-артефакт можно исполнять обычным 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)).
|
||||
|
||||
### Лестница доказательства
|
||||
|
||||
Для каждой пары «модель × устройство × движок» используются пять уровней:
|
||||
|
||||
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 новые EP рекомендуется делать отдельными plugins. В 1.24–1.28 API
|
||||
получил prepacking, EP Context, zero-copy I/O, profiling и model packages
|
||||
([инструкция для нового EP](https://onnxruntime.ai/docs/execution-providers/add-execution-provider.html),
|
||||
[1.24.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.24.1),
|
||||
[1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0)). Это
|
||||
укрепляет ORT как общий движок, но одновременно отделяет lifecycle конкретного
|
||||
ускорителя от lifecycle ядра.
|
||||
|
||||
### Нативный OpenVINO и OpenVINO GenAI
|
||||
|
||||
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)).
|
||||
|
||||
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/)).
|
||||
|
||||
Whisper остаётся активным направлением:
|
||||
|
||||
- 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)).
|
||||
|
||||
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)).
|
||||
|
||||
Следствие: EP остаётся рабочим мостом для ONNX-моделей, но не является способом
|
||||
автоматически получить последние Whisper/NPU-возможности OpenVINO. Deprecated
|
||||
provider options, заменённые `load_config`, не означают deprecation самого EP.
|
||||
|
||||
## Практическая поставка по платформам
|
||||
|
||||
Срез сделан по фактическим wheel, а не только по classifiers.
|
||||
|
||||
| Платформа и устройство | Готовый 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 |
|
||||
|
||||
Обычный `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)).
|
||||
|
||||
### AMD
|
||||
|
||||
На 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)).
|
||||
|
||||
Под 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)).
|
||||
|
||||
Под Linux прежний ROCm EP удалён из ORT 1.23. Пакет `onnxruntime-rocm`
|
||||
1.22.2.post3 всё ещё имеет `cp313`, но закреплён на ветке до удаления EP.
|
||||
Активный `onnxruntime-migraphx` 1.27.1 публикует
|
||||
`cp313-manylinux_2_34_x86_64`; реальные ограничения теперь лежат в ROCm/GPU/OS
|
||||
и покрытии графа
|
||||
([ROCm PyPI JSON](https://pypi.org/pypi/onnxruntime-rocm/json),
|
||||
[MIGraphX PyPI JSON](https://pypi.org/pypi/onnxruntime-migraphx/json),
|
||||
[MIGraphX EP](https://onnxruntime.ai/docs/execution-providers/MIGraphX-ExecutionProvider.html)).
|
||||
|
||||
### Apple Silicon
|
||||
|
||||
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)).
|
||||
|
||||
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/)).
|
||||
|
||||
## Возможности текущих моделей
|
||||
|
||||
Таблица применяет одну и ту же лестницу доказательства к трем модельным путям.
|
||||
|
||||
| Модель и требуемый контракт | Переносимый 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
|
||||
|
||||
`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-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)).
|
||||
|
||||
### 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()
|
||||
Reference in New Issue
Block a user