docs(diarization): спроектирована end-to-end карта

- Зачем:
  - локальные оптимизации каскада не достигли 30%-го порога ускорения;
  - старые закрытые хранилища и капсулы не должны стать будущей зависимостью.
- Что:
  - собран единый реестр результатов задач #41 и #42;
  - опубликовано разложение Ryzen baseline и выбрана архитектурная гипотеза;
  - подготовлены самодостаточная спецификация и ready-for-agent задача #46.
- Проверка:
  - пройдены self-review и две независимые линии ревью;
  - проверены 44 строки реестра, 42 арифметических инварианта и ссылки;
  - выполнены ai-text-lint, privacy scan и post-lint guard.

Refs: #45
This commit is contained in:
Dmitry Dementiev committed 2026-09-01 16:26:58 +03:00
1 parent 312153f4bc
commit 89475b9b97
3 files changed
+927 -11

No files matched your search

@@ -202,7 +202,7 @@ Ryzen-manifest отдельно связывается с handoff и входа
NumPy 2.4.3, ONNX 1.22.0, kaldi-native-fbank 1.22.3, psutil 7.2.2;
- питание от сети, стабильный Windows-профиль `balanced`, EPP AC 25%,
максимальное состояние процессора 100%, boost mode `aggressive`;
- WSL не предоставляет governor, а доступные интерфейсы не показывают
- WSL не предоставляет governor; доступные интерфейсы также не показывают
температурный датчик или аппаратные лимиты мощности. Температура
записана как недоступная; это ограничивает тепловой guard;
- нативные WSL-интерфейсы не отдали supply, platform profile и throttle count,
@@ -248,8 +248,11 @@ E1 и C2 сохранили все обязательные и диагност
## Что уже не нужно прогонять заново
Полный replay этапов 0-5 для следующей гипотезы не нужен. Результаты закреплены
публичными агрегатами, хешами и закрытой капсулой.
Полный replay этапов 0-5 для следующей гипотезы не нужен. Долговечное
доказательство составляют публичные агрегаты, хеши и
[карта архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md).
Закрытые хранилища задач #41/#42 и старый материал для прослушивания C1
подлежат удалению и не являются будущей зависимостью.
| Результат | Что можно переиспользовать | Когда повтор действительно нужен |
|---|---|---|
@@ -258,7 +261,7 @@ E1 и C2 сохранили все обязательные и диагност
| Q1 | Этот S8S8 QDQ-граф на Intel медленнее FP32 и дает `2 / 2 / 2` | Новая калибровка, формат квантизации, модель или runtime |
| E2/E3 | Больше worker и отдельные session не окупились при прежнем бюджете потоков | Изменилась топология session, runtime или бюджет ядер |
| E1/C2 на Ryzen | Для точного рецепта shared-session медиана равна 23,57%, качество и память проходят | Изменилась реализация механизма, runtime, машина или режим питания |
| C1 | Замер скорости уже есть; открыт только диагностический слуховой gate | Сначала прослушать подготовленные места; новый CPU-замер нужен лишь после положительного решения |
| C1 | Исторический замер скорости остается доказательством незакрытого gate; старый материал для прослушивания не сохраняется | Для повторного открытия заново выполнить минимальную пару W0/C1 и подготовить новый материал для прослушивания |
Есть три разных вида проверки, и они не заменяют друг друга:
@@ -274,10 +277,12 @@ E1 и C2 сохранили все обязательные и диагност
- C1 не получила слухового решения. Из автоматических метрик нельзя вывести,
допустимы ли два дополнительных слова без говорящего.
- На Ryzen не опубликовано разложение wall time на segmentation, embeddings,
clustering и orchestration. По общему времени нельзя надежно назвать следующий
доминирующий компонент.
- Полные записи не запускались: C2 не достигла порога, а C1 не дошла до Ryzen.
- Для восьми принятых последовательных W0 опубликовано
[суммарное разложение](../research/2026-09-01-end-to-end-cpu-diarization-map.md#измеренное-разложение-текущего-ryzen-baseline)
на segmentation, embeddings, clustering и orchestration residual. Для
shared-session C2 schema-compatible native total и orchestration residual
остаются недоступны.
- Полные записи не запускались: C2 не достигла порога; C1 не дошла до Ryzen.
- Температурный датчик в WSL и Windows недоступен. Внешняя проверка питания и
throttling снижает риск дрейфа, но не заменяет измерение температуры во время
каждой ячейки.
@@ -310,9 +315,12 @@ E1 и C2 сохранили все обязательные и диагност
диагностическим слуховым gate: сокращение окон и цены эмбеддинга уперлось в
качество раньше итогового порога.
4. По правилу спецификации следующий шаг - отдельная карта end-to-end
диаризации, заведенная как
[задача #45](https://git.dementev.space/ddmitry/local-transcriber/issues/45).
Ее гипотезы и замеры относятся к отдельному эксперименту.
диаризации: [карта архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md)
и [контракт Streaming Sortformer](../specs/2026-09-01-streaming-sortformer-cpu-experiment.md).
Карта подготовлена в
[задаче #45](https://git.dementev.space/ddmitry/local-transcriber/issues/45),
а контракт исполняет
[задача #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46).
5. Полные записи и Ryzen-слуховая приемка не требуются без победителя.
6. Production-профиль, CLI, конфигурационный контракт и модель по умолчанию
остаются без изменений.
@@ -0,0 +1,436 @@
# Карта end-to-end архитектур CPU-диаризации
**Дата:** 2026-09-01
**Статус:** выбран один кандидат для свежего минимального A/B - Streaming
Sortformer 4spk v2 Q8_0 через закрепленный NeMo-Speech.cpp. Это единственная
ветвь, где полная замена каскада опирается на готовые Q8_0 веса и закрепляемый
локальный CPU runtime. Production-профиль, CLI, конфигурационный контракт и
модель по умолчанию не меняются.
Документ сохраняет обезличенные агрегаты задач #41 и #42, потому что после
удаления закрытых хранилищ их нельзя будет получить повторно без нового
вычисления. Здесь нет локальных путей, идентификаторов записей, текста,
временных интервалов, manifest-содержимого или сырых строк результата.
Непрозрачные SHA-256 уже опубликованных результатов оставлены как устойчивые
ссылки на исторические байты. Они не создают будущую зависимость от этих байтов.
## Источники реестра
- [Intel-отчет](../benchmarks/2026-08-31-cpu-diarization-intel.md);
- [Ryzen-отчет](../benchmarks/2026-08-31-cpu-diarization-ryzen.md);
- [спецификация прежнего эксперимента](../specs/2026-08-30-cpu-diarization-experiment.md);
- [ADR-007](../adr/007-word-level-speaker-diarization.md).
## Как читать реестр
`Ускорение` ниже означает сокращение wall time:
`100 x (wall_baseline - wall_candidate) / wall_baseline`.
Отрицательное значение означает замедление. Intel W/M/Q/E - одиночные
разведочные прогоны трех 300-секундных фрагментов, если явно не указана соседняя
пара. Ryzen-строки - принятые соседние A/B. ASR, загрузка моделей и декодирование
не входят в wall.
Обязательный автоматический gate: `3 / 2 / 2` голосовых кластера, неизменный
текст, повторяемые параметры/метки/финализация, конечные эмбеддинги и отсутствие
потерянных или продублированных входов. `51 / 13 / 9` слов без говорящего и
`mapped speaker purity = 0,76655 / 0,78742` - диагностика совпадающего baseline,
а не ручная истина. Рост диагностического показателя требует прослушивания, но
сам по себе не превращает вариант в проигравший.
## Переносимые рецепты
Все Intel-рецепты используют четыре физических ядра; Ryzen-рецепты - восемь.
Везде `inter_op_threads=1`, batch 1, если строка не говорит обратного.
Код измерительного стенда закреплен source commit
`6345cb6449d906767b6ed33e695f2ca4eeaf3008`; sherpa-onnx 1.13.6 - upstream
commit `1cb484af9b1ab05cc36f89e03dd7d76322f2e911` плюс опубликованный ниже патч.
Последовательные W0-рецепты используют CPU provider для segmentation и
embeddings, `debug=True` для обеих конфигураций,
`min_duration_on=0.3` и `min_duration_off=0.5`. Debug-режим входит в
измерительный рецепт: из его журнала получены native stage times.
| Recipe ID | Точный переносимый рецепт |
|---|---|
| `I-B135` | `sherpa-onnx/core 1.13.5`; Pyannote Segmentation 3.0 FP32; WeSpeaker ResNet34 LM FP32; `window_shift_ratio=0.1`; последовательный инференс; один worker/session; `intra_op=4`; `FastClustering`, auto-N, threshold `0.89`. |
| `I-B136` / `I-W0` / `I-M0` / `I-Q0` / `I-E0` / `I-C0` | Тот же профиль, но `sherpa-onnx/core 1.13.6`; это совпадающий Intel baseline. |
| `I-W(r)` | `I-B136`, меняется только `window_shift_ratio=r`. |
| `I-M2` / `I-C1` | `I-B136`, но CAMPPlus вместо WeSpeaker; NME автоматически выбрал `p=111/54/104`, затем N=`3/2/2` и `FastClustering(N)`; sequential. |
| `I-M3` | `I-B136`, но TitaNet small вместо WeSpeaker; NME автоматически выбрал `p=90/65/110`, затем N=`3/2/2` и `FastClustering(N)`; sequential. |
| `I-Q1` | `I-B136`, но WeSpeaker S8S8 QDQ вместо FP32; sequential. |
| `I-E1` / `I-C2` | `I-B136`, но два outer worker используют одну shared ONNX Runtime session и общий `intra_op=4`. |
| `I-E2` | `I-B136`, но четыре outer worker, одна shared session, `intra_op=1`. |
| `I-E3` | `I-B136`, но четыре outer worker и четыре отдельные session, у каждой `intra_op=1`; отрицательный контроль. |
| `R-B` | `sherpa-onnx/core 1.13.6`; Pyannote FP32; WeSpeaker FP32; shift `0.1`; `FastClustering(auto, 0.89)`; sequential; один worker/session; `intra_op=8`. |
| `R-C2` | `R-B`, но два outer worker, одна shared session, общий `intra_op=8`. |
Закрепленные SHA-256 моделей: Pyannote FP32
`220ad67ca923bef2fa91f2390c786097bf305bceb5e261d4af67b38e938e1079`,
WeSpeaker FP32
`e9848563da86f263117134dfd7ad63c92355b37de492b55e325400c9d9c39012`,
WeSpeaker QDQ
`9a3f8ae64d1083b64a6276f1e1138b17b7511a9a8d731b632945efc84098588b`,
CAMPPlus
`aa3cfc16963a10586a9393f5035d6d6b57e98d358b347f80c2a30bf4f00ceba2`,
TitaNet small
`ad4a1802485d8b34c722d2a9d04249662f2ece5d28a7a039063ca22f515a789e`.
Экспериментальные `Prepare/Finalize` и shared worker требовали патч
`5f12df858cd525c8a0cfe8ff95b647c3b0e915a13d45621acc4ac42660014c49`:
API не было ни в 1.13.6, ни в проверенном upstream master. Ryzen-wheel был
пересобран из того же upstream commit и патча из-за несовместимости GLIBC;
его опубликованный SHA-256 -
`baaaa2532606f1e1411e5c84655afa70107ad4b1676420953f5e67233c46a484`.
## Реестр: baselines и проверки механизмов
| Stable ID | Recipe / измененная ось | Машина / стадия | Wall / ускорение / RSS | Обязательное качество | Решение и точная причина | Публичное доказательство |
|---|---|---|---|---|---|---|
| `41-V135` | `I-B135`; version baseline | Intel / version baseline | `190,104 с`; baseline; `365,7 МиБ` в соответствующем сыром агрегате | `3/2/2`, текст и диагностика baseline | Принят как контроль версии; одиночный запуск не доказывает причинное ускорение/замедление версии | `docs/benchmarks/2026-08-31-cpu-diarization-intel.md`, "Зачем понадобился 1.13.6" |
| `41-V136` | `I-B136`; меняется только runtime 1.13.5->1.13.6 | Intel / version baseline | `157,892 с`; `+16,94%` к `41-V135`; `356,7 МиБ` | `3/2/2`; канонические интервалы, назначения слов и диагностика совпали | Baseline 1.13.6 принят; разница времени диагностическая, без повторов | тот же раздел; SHA `eb7e3d508836e1eefd679a851271eaa6823d688fef39d2ae03978e7fa7006bba` |
| `41-S-WINDOW` | 1.13.6 принимает shift `0.10/0.15/0.20/0.25/0.50`; `0.10` сохраняет поведение | Intel / smoke | end-to-end wall/RSS: not measured | Smoke пройден | Допустить W0-W4 | `docs/specs/2026-08-30-cpu-diarization-experiment.md`, этап 0 |
| `41-S-PF` | Локальный `Prepare -> cluster(copy) -> Finalize` против штатного `process()` | Intel / smoke | wall/ускорение/RSS: not measured | Строки, метки и интервалы совпали; пропусков/дублей нет | Использовать патч в стенде | Intel-отчет, этап 0 |
| `41-S-MANIFEST` | Строгая schema v3 и стабильные ID ячеек | Intel / smoke | не применимо | Валидация капсулы: 199 файлов, QDQ доступен, предупреждений нет | Использовать; это механизм воспроизводимости, не измерительная ячейка | Ryzen-отчет, handoff |
| `41-S-CEIG` | CAMPPlus prepared-матрицы; eigengap | Intel / counter smoke | counter wall по трем матрицам `0,107 / 1,575 / 0,067 с`; end-to-end ускорение/RSS: not measured | N=`6/1/7`, не `3/2/2` | Исключить счетчик для CAMPPlus: обязательный count gate провален | Новый aggregate задачи #45 из frozen raw; previous result SHA not published |
| `41-S-CNME` | Те же CAMPPlus-матрицы; NME + `FastClustering(N)` | Intel / counter smoke | counter wall `3,536 / 1,488 / 1,148 с`; end-to-end ускорение/RSS: not measured | N=`3/2/2`, повторяемые метки, 0 потерянных строк | Выбрать для M2/C1 | Intel-отчет, индекс доказательств |
| `41-S-TEIG` | TitaNet prepared-матрицы; eigengap | Intel / counter smoke | counter wall `0,126 / 0,073 / 0,101 с`; end-to-end ускорение/RSS: not measured | N=`7/2/4`, не `3/2/2` | Исключить счетчик для TitaNet: count gate провален | Новый aggregate задачи #45 из frozen raw; previous result SHA not published |
| `41-S-TNME` | Те же TitaNet-матрицы; NME + `FastClustering(N)` | Intel / counter smoke | counter wall `1,084 / 0,780 / 1,112 с`; end-to-end ускорение/RSS: not measured | N=`3/2/2`, повторяемые метки, 0 потерянных строк | Выбрать для M3 | Intel-отчет, индекс доказательств |
| `41-S-QDQ` | WeSpeaker S8S8 QDQ, 48 независимых calibration-интервалов | Intel / artifact smoke | wall/ускорение/RSS: not measured | interface/opset/metadata, checker, dynamic shapes и загрузка sherpa пройдены | Допустить Q1; техническая корректность не означает качество/скорость | Intel-отчет, этап 0; model SHA выше |
| `41-S-SHARED` | Один и два worker, одна session, одинаковые prepared-входы | Intel / worker smoke | wall/ускорение/RSS: not measured | строки, метки и интервалы совпали; гонок/NaN/потерь нет | Допустить E1/E2 | Ryzen-отчет, этап 0 |
| `41-S-TB1` | TitaNet batch 1 | Intel / embedding smoke | embedding-only `75,923 с` суммарно; ускорение/RSS: not measured | штатные эмбеддинги воспроизведены, max abs drift <= `3,4e-6` | Контроль batch | Ryzen-отчет, этап 3; batch SHA `f12bfdb5cadf80c62871c0725eac744a99825cb5d06b70fcf11e4e538ff6df72` |
| `41-S-TB4` | TitaNet batch 4 | Intel / embedding smoke | embedding-only `98,596 с`, на `29,86%` медленнее batch 1; RSS: not measured | `3/2/2`, но метки всех трех матриц изменились; drift `0,277-0,317` | Остановить до CPU-ячейки: медленнее и нарушена идентичность меток | Ryzen-отчет, этап 3; тот же batch SHA |
Counter wall в строках `41-S-CEIG` и `41-S-TEIG` впервые опубликован в задаче
#45 как безопасный aggregate frozen raw evidence. Прежний result SHA не был
опубликован; после удаления хранилища эти числа нельзя повторно сверить с
исходными байтами.
Примечание к `41-V135`: публичный Intel-индекс печатает SHA
`c53c1f970ee4db094779bc32b42f978c37c2cb74d814504ccfac87f09e802ddd`,
но этот хеш связывает другой сырой агрегат (`148,806 с`), а не опубликованные
`190,104 с`. Сырой результат с `190,104 с` имеет неопубликованный хеш. Поэтому
для этой строки устойчивым публичным доказательством остается только якорь
публичного отчета; после удаления хранилища байтовая проверка числа невозможна.
## Реестр: Intel W/M/Q/E/C
| Stable ID | Recipe / измененная ось | Wall / ускорение / RSS | Обязательное качество и диагностика | Решение и точная причина | Публичное доказательство |
|---|---|---|---|---|---|
| `41-W0` | `I-W(0.10)`, совпадающий baseline | `148,220 с`; baseline; `361,2 МиБ` в том же raw result | pass `3/2/2`; слова без говорящего `51/13/9` | Принять shift `0.10` | SHA `32d513cd0e81fdca0e37484f86b5c4312d3573608493972a282302dd891cdf0e` |
| `41-W1` | `I-W(0.15)`; меняется только shift | `100,799 с`; `+31,99%`; `362,6 МиБ` | fail `2/2/2`; `50/17/9` | Исключить: потерян кластер на первом фрагменте | SHA `be8935b5a81e2aadaeb38540e615c0cd3ae5ac31d6e0949856fd7d16d31a78ea` |
| `41-W2` | `I-W(0.20)` | `76,127 с`; `+48,64%`; `363,6 МиБ` | fail `2/2/2`; `52/13/9` | Исключить: потерян кластер | SHA `f95433d516772d40c4071da0d0745b981314ee7a7063fb4a43518e94bc17848e` |
| `41-W3` | `I-W(0.25)` | `61,369 с`; `+58,60%`; `358,4 МиБ` | fail `2/2/2`; `59/16/11` | Исключить: потерян кластер | SHA `14cac4f1d8cd38ed5b4a20189438d2b1e109ba267c5feaa36f0928fbec848580` |
| `41-W4` | `I-W(0.50)` | `31,291 с`; `+78,89%`; `360,7 МиБ` | fail `1/2/2`; `0/19/10` | Исключить: потеряны два кластера суммарно относительно `3/2/2` | SHA `6fafefce00e90590ab2d834fb560413cd7780e20cbd1f631d9954ec8b97a540e` |
| `41-M0` | `I-M0`; baseline модельной оси | Переиспользован `41-W0`: `148,220 с`; `361,2 МиБ`; отдельное ускорение: not measured | Качество baseline прошло | Точка сравнения; не новая гипотеза | Ryzen-отчет, этап 2; SHA W0 выше |
| `41-M2` | `I-M2`; меняются embedding model и estimator N | `94,550 с`; `+36,21%`; `358,6 МиБ` | Обязательный gate пройден: `3/2/2`; `53/13/9` | Самая быстрая модельная ветвь; собрать C1, но диагностический рост требует слухового gate | SHA `3debc8ae5cf1bd13d806dbfe8e4cfa137d6705622a721f4a42f1a22c030ec277` |
| `41-M3` | `I-M3`; меняются embedding model и estimator N | `107,205 с`; `+27,67%`; `511,9 МиБ` | Обязательный gate пройден: `3/2/2`; `52/13/9` | Не выбирать: медленнее и тяжелее M2; обязательный gate при этом пройден | SHA `531a842234e35e29f582ff8a51406fb9dd6211e017b3162d80cca21d4732d316` |
| `41-Q0` | `I-Q0`; baseline quantization-оси | Переиспользован `41-W0`; отдельный raw aggregate: not published | Качество baseline прошло | Точка сравнения; не новая гипотеза | Старая спецификация, QDQ |
| `41-Q1` | `I-Q1`; FP32->S8S8 QDQ | `177,537 с`; `-19,78%` (замедление); `367,7 МиБ` | `fail`: `2/2/2`; `51/13/8` | Исключить: медленнее и потерян кластер | Intel-отчет, этапы 2-3; result SHA not published |
| `41-Q-U8-SKIP` | Условный U8U8; меняется формат квантизации; Intel / этап 3 | wall/ускорение/RSS: not measured | Качество: not measured | Ветка не открылась: S8S8 не достиг 10%-го порога продолжения и провалил обязательное качество | Старая спецификация, этап 3; result SHA not published |
| `41-E0a` | `I-E0`; companion baseline для E1 | Отдельные wall/ускорение/RSS: not published; отдельный result blob: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка; самостоятельного решения нет | Intel-отчет, таблица ID ячеек |
| `41-E1` | `I-E1`; topology 1 worker->2 shared workers | `139,219 с`; `+6,07%`; `398,6 МиБ` | Качество прошло: `3/2/2`; метки/интервалы/диагностика совпали | Ниже Intel-порога продолжения, но допущен как заранее оговоренная ISA-гипотеза для собственного Ryzen A/B | SHA `f5ac41d29eeff1f7cfbb93497a0025aef40de0838e933e0f9baed30a236d6b85` |
| `41-E0b` | `I-E0`; companion baseline для E2 | Отдельные wall/ускорение/RSS: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка | Intel-отчет, таблица ID ячеек |
| `41-E2` | `I-E2`; 4 shared workers, `intra_op=1` | `152,167 с`; `-2,66%`; `551,2 МиБ` | pass `3/2/2`, точное совпадение результата | Исключить: медленнее и больше RSS | SHA `f2664d354ede1e4bf2c42b974513cdc845ff601c73f30ebae51456764fd8dcdc` |
| `41-E0c` | `I-E0`; companion baseline для E3 | Отдельные wall/ускорение/RSS: not published; сравнительный W0=`148,220 с` | Качество baseline прошло | Служебная baseline-ячейка | Intel-отчет, таблица ID ячеек |
| `41-E3` | `I-E3`; shared->4 separate session | `159,038 с` составная оценка; `-7,30%`; `732,3 МиБ` process-wide | `pass`: `3/2/2`; метки совпали | Исключить: отрицательный контроль подтвердил рост времени и памяти; wall не равноправен штатному `process()` | Intel-отчет, этапы 2-3; result SHA not published |
| `41-C0` | `I-C0`; рабочий shift + baseline stack | wall/ускорение/RSS: not measured | по определению совпадает с прошедшим W0 | Явно пропущена как дубликат baseline | Intel-отчет, этап 1/4 |
| `41-C1` | `I-C1`; финальная проверка сильнейшей Intel-ветви | `98,347 с`; `+33,65%` к одиночному W0; `399,5 МиБ` | Обязательный gate пройден: `3/2/2`, purity `0,80137/0,91079`; слова `53/13/9` (`+2` на первом фрагменте) | **Неразрешенный слуховой gate: ни победитель, ни проигравший.** Не передавать до прослушивания подготовленных мест | SHA `4f96e6f7363e0edb79d4034a07f4d3b6b0003a820fcf6bceff5426bb3c2caff4` |
| `41-W0C2-SWAP` | `I-B136`; первая попытка guarded baseline; Intel / финальная пара | wall/ускорение/RSS: not published | Качество: not published | Отклонить: runtime guard обнаружил swap-in | Intel-отчет, раздел "Условия"; result SHA not published |
| `41-W0C2` | `I-B136`; соседний guarded baseline | `146,978 с`; baseline; `376,4 МиБ` | Качество прошло; диагностика baseline | Допустимая база сравнения только для `41-C2` | SHA `e572dd057ec87514aca4617465c48ce361677ae19279efcadbbde404afc85b5d` |
| `41-C2` | `I-C2`; baseline->2 shared workers | `140,632 с`; `+4,32%`; `406,0 МиБ` | Обязательная и диагностическая проверка точно совпала с `41-W0C2` | Intel-порог не достигнут; передать как работоспособную аппаратную ISA-гипотезу по явному правилу спецификации | SHA `d687074cc582e2ac379ed3375362f9480778feca02c7492a4c4be4fcc1a4f15a` |
`M1` не был ячейкой согласованной матрицы: нумерация spec - M0, M2, M3.
В публичной сводке для W0/M0 указан RSS `362,6 МиБ`, однако опубликованный
SHA W0 связывает wall `148,220 с` с RSS `361,2 МиБ`. В реестре выше сохранена
согласованная пара из одного raw result; публичные `362,6 МиБ` нельзя привязать
к тому же опубликованному SHA.
## Реестр: Ryzen, этапы 5-6
Для каждой A/B-строки recipe baseline - `R-B`, candidate - `R-C2`; меняется
только topology embedding-инференса. Все ячейки прошли обязательный gate и
сохранили baseline-диагностику (`3/2/2`, `51/13/9`, та же purity, те же числа
окон/embedding jobs, без потерь или дублей).
| Stable ID | Машина / стадия / порядок | Wall baseline -> candidate / ускорение | RSS baseline -> candidate | Решение и точная причина | Published result SHA-256 |
|---|---|---|---|---|---|
| `42-W0` | Ryzen / matching baseline | `110,998 с`; candidate: not measured | `370,4 МиБ`; candidate: not measured | Baseline принят | `f50684784392cacc2d99c8aee51d01f0a67dfee3da6ea3ba5f3db749a3d5287a` |
| `42-E1-AB` | Ryzen / isolated mechanism / W0->E1 | `111,557 -> 84,723 с`; `24,05%` | `367,3 -> 412,2 МиБ` | Механизм прошел качество, память и 10%-й порог продолжения | `24d5edae117e0619e432ef3f3019e8c66cf3516b87b80b64b8591390db25bf48` |
| `42-C2-P` | Ryzen / primary / C2->W0 | `111,423 -> 85,909 с`; `22,90%` | `369,6 -> 410,3 МиБ` | Прошел primary-порог; допустить пять циклов | `142db6d06efc8d558b38e18f47f47e3e35e7df00751da2583339f99add8d75c4` |
| `42-C2-F1` | Ryzen / final 1 / W0->C2 | `111,354 -> 83,529 с`; `24,99%` | `369,2 -> 413,4 МиБ` | Принятый цикл; качество pass | `4b5095c9e64ff0ef75bbc58516c662bedbbdb0bedf131f66cc1031c9a0ad8c7f` |
| `42-C2-F2` | Ryzen / final 2 / C2->W0 | `108,899 -> 83,907 с`; `22,95%` | `373,0 -> 418,1 МиБ` | Принятый цикл; качество pass | `256a17b2d08510aad03d43f5c626c1a4269c9a25271ce492dc3305c4afca1888` |
| `42-C2-F3` | Ryzen / final 3 / W0->C2 | `110,928 -> 84,968 с`; `23,40%` | `373,8 -> 410,9 МиБ` | Принятый цикл; качество pass | `b5d37c7a8b05b065c61bc9d5f1c0ab639a80a821730e505179388d90c4239caa` |
| `42-C2-F4` | Ryzen / final 4 / C2->W0 | `114,985 -> 83,342 с`; `27,52%` | `376,6 -> 411,1 МиБ` | Принятый цикл; качество pass | `639cc9f13511a59c7240c6fb6ec6bf1c31ec26350330e3ca0c64fb721b768736` |
| `42-C2-F5` | Ryzen / final 5 / W0->C2 | `113,233 -> 86,548 с`; `23,57%` | `358,6 -> 413,4 МиБ` | Принятый цикл; качество pass | `4ff2863a93fc22a037de5c292391b6b46de1b539cf397026b349ea5134cf1051` |
| `42-STAGE6-SKIP` | Ryzen / этап 6 / `R-B` и `R-C2` на трех полных записях | wall/ускорение: not measured | RSS: not measured | Этап пропущен: медиана финальных циклов `23,57%` ниже порога `30%`; качество: not measured | Ryzen-отчет, "Итог"; result SHA not published |
Итог C2: медиана сокращения `23,57%`, MAD `0,62 п. п.`; до порога `go`
`30%` не хватило `6,43 п. п.`. Качество и память прошли, скорость - нет;
решение всей воронки `new map`. Stage 6 (полные записи и пятичастная слуховая
приемка) не запускался, потому что победителя нет.
## Измеренное разложение текущего Ryzen baseline
### Семантика полей
В непатченном последовательном baseline outer `wall_seconds` измеряется вокруг
одного `diarizer.process(samples)`. Native debug отдельно дает
`segmentation_seconds`, `embedding_seconds`, `clustering_seconds` и
`engine_total_seconds`. Поэтому для каждого из трех фрагментов, а затем для их
суммы, допустимо вычислить:
`orchestration = measured outer wall - native engine total`.
Это остаток границы Python/native плюс progress callback и измерительный
обвес; схема не позволяет разложить его дальше и не доказывает, что весь остаток
принадлежит production-коду. Native stage и native total напечатаны с точностью
до `0,001 с`; таблица округляет итоговые суммы до `0,001 с`. Разность использует
сохраненные числа, до отображения не округляется.
У патченного shared-session runner `engine_total_seconds` намеренно записан
равным outer wall всего `Prepare + counter + clustering + Finalize`. Это не
отдельный native total. Поэтому для E1/C2 нельзя честно вычислить orchestration
как `outer - native`: соответствующая величина **недоступна**, а ноль был бы
артефактом схемы.
### Сумма трех записей в каждой принятой baseline-ячейке
| Baseline ID | Outer wall, с | Segmentation, с | Embeddings, с | Clustering, с | Native engine total, с | Orchestration, с |
|---|---:|---:|---:|---:|---:|---:|
| `42-W0` | 110,998 | 18,233 | 82,172 | 0,015 | 100,444 | 10,554 |
| `42-E1-AB/W0` | 111,557 | 17,888 | 82,952 | 0,015 | 100,871 | 10,686 |
| `42-C2-P/W0` | 111,423 | 19,685 | 83,635 | 0,014 | 103,357 | 8,066 |
| `42-C2-F1/W0` | 111,354 | 18,614 | 84,596 | 0,015 | 103,240 | 8,114 |
| `42-C2-F2/W0` | 108,899 | 21,071 | 79,911 | 0,012 | 101,010 | 7,889 |
| `42-C2-F3/W0` | 110,928 | 21,204 | 81,722 | 0,016 | 102,960 | 7,968 |
| `42-C2-F4/W0` | 114,985 | 19,594 | 84,715 | 0,013 | 104,342 | 10,643 |
| `42-C2-F5/W0` | 113,233 | 21,625 | 82,884 | 0,013 | 104,540 | 8,693 |
| **Суммарно: 8 ячеек / 24 записи** | **893,377** | **157,914** | **662,587** | **0,113** | **820,764** | **72,613** |
Stage-суммы отличаются от native total pooled на `0,150 с` (`0,017%` outer
wall): это ожидаемый остаток тысячного округления native debug, а не оцененная
пятая стадия.
Медианы по восьми matching baselines, рассчитанные из показанных значений без
промежуточного округления: outer `111,388 с`, segmentation `19,6395 с`,
embeddings `82,918 с`, clustering `0,0145 с`, native total
`103,100 с`, orchestration `8,404 с`.
Наблюдаемый spread (max-min, процент относительно min):
| Величина | Диапазон, с | Spread |
|---|---:|---:|
| Outer wall | `108,899-114,985` | `6,087 с` / `5,59%` |
| Segmentation | `17,888-21,625` | `3,737 с` / `20,89%` |
| Embeddings | `79,911-84,715` | `4,804 с` / `6,01%` |
| Clustering | `0,012-0,016` | `0,004 с`; относительный процент неинформативен при гранулярности `0,001 с` |
| Native total | `100,444-104,540` | `4,096 с` / `4,08%` |
| Orchestration residual | `7,889-10,686` | `2,797 с` / `35,45%` |
Это объясняет, почему Intel-разница W0/C2 `4,32%` неотличима от наблюдавшегося
там разброса, а Ryzen-финал опирался на парные циклы, а не на один baseline.
## Доли, проверенные механизмы и Amdahl ceiling
Доли ниже - pooled stage time / pooled outer wall восьми matching Ryzen
baseline. Amdahl ceiling предполагает невозможное идеальное удаление ровно
одного компонента при неизменности остальных:
`max speedup = 1 / (1 - share)`.
| Компонент | Измеренная доля current Ryzen wall | Уже проверенные сокращения | Что осталось после эксперимента | Теоретический ceiling |
|---|---:|---|---|---:|
| Segmentation | `17,676%` | W1-W4 уменьшали число окон `291->195/146/117/59` на каждом фрагменте и вместе число embedding jobs; wall падал до `31,291 с`, но уже W1 потерял обязательный кластер | Без новой segmentation-модели/логики окон дальнейшее увеличение shift закрыто quality gate | максимум `1,2147x` (`17,676%` сокращения wall) при полном устранении стадии |
| Embeddings | `74,167%` | CAMPPlus+NME (`36,21%` Intel, слуховой gate C1 открыт), TitaNet+NME (`27,67%`, тяжелее M2), QDQ (медленнее и fail), shared session (Ryzen median `23,57%`), 4 workers/separate sessions (хуже), TitaNet batch 4 (медленнее и другие метки) | Это доминирующая измеренная стадия; локальные замены прежнего каскада не дали принятого 30%-победителя | максимум `3,8709x` (`74,167%` wall) при полном устранении стадии |
| Clustering | `0,013%` | Eigengap быстро, но неверный N; NME дал правильный N для CAMPPlus/TitaNet, однако его отдельный counter wall измеряется секундами и не является baseline native clustering time | Ускорять baseline `FastClustering` бессмысленно по Amdahl; менять алгоритм можно только ради качества/архитектуры | максимум `1,00013x` (`0,013%` wall) |
| Orchestration residual | `8,128%` | `Prepare/Finalize` и shared worker прошли correctness smoke; отдельного эксперимента на сокращение остатка не было | Нельзя отделить project orchestration от callback/RSS/harness; candidate-side доля недоступна из текущей схемы | максимум `1,0885x` (`8,128%` wall) при полном устранении остатка |
Сумма четырех долей `99,983%`; недостающие `0,017%` - только эффект
округления native stage logs. Нельзя складывать наблюденные ускорения разных
ячеек как независимые: W-ветвь одновременно сокращала segmentation и число
embedding jobs, а shared-session меняла исполнение embedding внутри патченного
runner.
## Открытые gates и неизмеренные факты
1. **C1 остается неразрешенным слуховым gate - ни победитель, ни проигравший.**
Обязательная автоматическая проверка прошла, скорость на Intel превысила
30%, purity выросла, но два
дополнительных слова без говорящего требуют человеческого прослушивания.
Автоматические метрики не разрешают этот выбор.
2. Полные записи не измерялись ни для C1, ни для C2. C1 не дошла до Ryzen, C2
не прошла финальный порог скорости.
3. Ryzen C1 wall/RSS/quality неизвестны полностью; переносить Intel `33,65%` на
другую ISA нельзя.
4. Для shared-session E1/C2 нет schema-compatible native total и, следовательно,
нет candidate-side orchestration share. Доступные segmentation/embedding/
clustering поля не закрывают остаток `counter + Finalize + Python` отдельными
сопоставимыми стадиями.
5. Orchestration baseline - измеренный residual, но его production/harness
состав не измерен.
6. Температура Ryzen во время ячеек недоступна. Внешний power/throttle
guard прошел, но не заменяет сенсор внутри WSL.
7. Intel W/M/Q/E разведка - по одному прогону, ранние ячейки не чередовались;
их проценты не являются оценкой дисперсии.
8. End-to-end архитектуры не входили в матрицу. Измерены только локальные
изменения прежнего каскада.
9. `M1` отсутствует по дизайну, C0 пропущена как точный baseline-дубликат,
stage 6 пропущен по правилу "нет победителя"; это известные пропуски, а не
потерянные результаты.
10. Отдельные агрегаты Intel companion-ячеек M0/Q0/E0 не опубликованы. Их можно
использовать только как recipe-alias W0, а не выдавать за новые замеры.
## Что станет невосстановимым после удаления замороженных хранилищ
Долговечная карта должна сохранить все таблицы и формулы выше. После удаления
обоих хранилищ останутся публичные рецепты, агрегаты, решения и непрозрачные
хеши, но сами хеши уже нельзя будет повторно сверить с исходными байтами.
Без нового model run или повторной подготовки будут потеряны:
- все raw per-recording outer/stage/CPU/RSS значения за пределами опубликованных
агрегатов, включая точное восьмиклеточное Ryzen-разложение;
- сырые интервалы разметки говорящих, назначения слов, диагностические records,
ASR-sidecars и опорные материалы;
- prepared embedding-матрицы, row mappings, промежуточные результаты счетчиков,
worker requests и native logs, которыми проверялись строки/метки/финализация;
- runtime-guard свидетельства и полное состояние планировщика/manifest, включая
непубличные идентификаторы ячеек и порядок внутренних повторов;
- подготовленные C1 listening excerpts и plan. Если gate не закрыть до удаления,
для его будущего решения придется заново построить минимальный baseline/C1
diff и новые excerpts из исходных записей;
- calibration provenance и байты QDQ-графа, локальные wheel/patch artifacts,
если они отдельно не сохранены; опубликованный SHA доказывает идентичность
только при наличии байтов;
- возможность расследовать расхождения публичного 1.13.5 hash/wall и W0 RSS по
исходным файлам.
Исходные записи, по условию задачи #45, останутся лишь удобными входами для нового
минимального среза. Они не восстанавливают старые sidecars, prepared matrices,
слуховой plan, environment state или старые timing samples без повторного
вычисления.
## Граница выводов
Исторические строки выше описывают прежний каскад и прежние контрольные
фрагменты. Они отвечают на вопрос, какие локальные изменения уже проверены.
Новый эксперимент создает из сохраненных исходных записей собственные
фрагменты, ASR-sidecar, опорную разметку, baseline-диагностику и закрытое
хранилище результата. Числа `3 / 2 / 2`, `51 / 13 / 9` и прежняя purity
остаются характеристиками старых контролей. В новый gate переходят правила
точного совпадения с заново проверенным числом голосовых кластеров,
неизменности текста и безопасной памяти.
C1 после удаления старого слухового материала остается исторически
неразрешенной. Для ее повторного открытия требуется свежая минимальная пара
W0/C1 и заново подготовленный материал для прослушивания. Карта и следующий
эксперимент эту работу не выполняют.
## Класс архитектур и формула полной замены
Текущий путь строит разметку говорящих каскадом `segmentation -> embeddings ->
clustering`. В sherpa-onnx 1.13.6 отдельный эмбеддинг вычисляется для каждой
пары локального говорящего и окна; это подтверждает
[upstream-реализация](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.6/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h).
Суммарное разложение выше показывает, что локальные изменения embeddings имеют
большой доступный бюджет, однако принятый 30%-й результат на Ryzen не получен.
Для сравнения архитектур используется одна формула:
```text
B = S + E + C + O
T_candidate = B - R + A
speedup = (R - A) / B
идеальный предел при A = 0: R / B
```
`B` - measured outer wall текущего W0; `S`, `E` и `C` - измеренные native
стадии; `O` - derived residual `outer wall - native engine total`; `R` - работа
старого пути, которую удаляет гипотеза; `A` - измеряемая работа новой
архитектуры. Для полной замены `R = B`, поэтому фактическое сокращение равно
`1 - A / B`. Upstream не публикует переносимый CPU RTF для выбранной модели;
числовой прогноз `A` отсутствует. Его определит свежий A/B.
## Реестр архитектурных гипотез
### A1. Streaming Sortformer v2 Q8_0 через NeMo-Speech.cpp - выбран
**Механизм и затронутые стадии.** NEST/Fast-Conformer encoder и следующий за
ним Transformer выдают четыре sigmoid-потока активности. Arrival-Order Speaker
Cache связывает говорящих между streaming-чанками; порядок меток задается
первым появлением голоса. Модель возвращает `T x 4` вероятностей с шагом 80 мс
и заменяет текущие segmentation, embeddings, подсчет/кластеризацию и
финализацию. Архитектура описана в
[карточке модели NVIDIA](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2)
и [первичной статье](https://arxiv.org/abs/2507.18446).
**Поведение quality gate.** Потери W1-W4 возникли после прореживания
10-секундных окон перед независимыми эмбеддингами. Sortformer оценивает
перекрывающиеся активности общей сетью и не использует эту сетку или порог AHC.
Ошибки могут перейти в границы 80-мс кадров, рождение канала и сохранение метки
в speaker cache, поэтому точное число кластеров и слуховая проверка остаются
обязательными.
**Предел и риск.** Полная замена дает идеальный предел 100% вызова, а
фактическое сокращение `1 - A_sortformer / B`. Опубликованный RTF `0,002`
измерен на RTX 6000 Ada, batch 1, и не является CPU-прогнозом
([model card](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#setting-up-streaming-configuration)).
Модель имеет четыре выходных канала говорящих; NVIDIA предупреждает о
деградации на пяти и более слышимых голосах, неанглийской речи, другом домене и
очень длинных записях
([technical limitations](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#technical-limitations)).
**Самая дешевая фальсификация.** На Ryzen выполнить один свежий W0 и одну
соседнюю пару W0/Sortformer на трех заново определенных пятиминутных
фрагментах. Остановить ветвь при несовпадении с новыми ожидаемыми числами
кластеров, нарушении текстовой инвариантности, OOM/swap/росте RSS без плато или
ускорении меньше 10%. Точный контракт находится в
[спецификации следующего эксперимента](../specs/2026-09-01-streaming-sortformer-cpu-experiment.md).
**Повторное использование.** Переносятся рецепт Ryzen W0, правила gate,
каноническое сведение `Word` с `SpeakerInterval`, безопасные агрегаты и
сохраненные исходные записи. Sidecar, опорная разметка, фрагменты и закрытое
хранилище создаются заново. Prepared matrices, старые интервалы, патч и wheel
не используются.
**Исполнимость.** NVIDIA публикует готовый Q8_0 GGUF, нативный Apache-2.0
runtime с CPU backend и отдельным API диаризации
([NeMo-Speech.cpp](https://github.com/NVIDIA/NeMo-Speech.cpp),
[C ABI](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/include/nemo_speech/diar.h)).
Веса имеют CC BY 4.0. Runtime и модель закреплены в следующей спецификации.
Выбор разрешает только изолированный исследовательский стенд.
### A2-A6. Фальсифицируемые альтернативы
| ID | Механизм и отличие quality gate | Предел скорости | Главный риск | Самый дешевый тест | Что можно использовать повторно | Решение |
|---|---|---|---|---|---|---|
| `A2` LS-EEND | Causal encoder, online attractors и retention полностью заменяют `B`; нет окон Pyannote, отдельных voice embeddings и AHC. [Официальный код](https://github.com/Audio-WestlakeU/FS-EEND), [статья](https://doi.org/10.1109/TASLPRO.2025.3597446). | `1 - A_ls-eend / B`; CPU `A` не опубликована. | Публичные checkpoints привязаны к 8 кГц и доменам; готовой ONNX/GGUF CPU-поставки нет; возможен дрейф attractor. | До контрольного аудио проверить официальный checkpoint, лицензию, CPU device и однозначный 16-кГц/resampling contract; затем синтетический 60-секундный smoke. | Свежие входы, sidecar и общие gate; prepared embeddings неприменимы. | Отложить: A1 проверяет полную streaming-замену с готовым CPU runtime дешевле. |
| `A3` EEND-EDA | Self-attentive encoder и LSTM attractors выдают одновременные активности и собственный stop condition вместо AHC-N. [Статья](https://arxiv.org/abs/2005.09921), [официальная реализация](https://github.com/hitachi-speech/EEND). | `1 - A_eend-eda / B`; full-context attention может расти квадратично по длине. | Нет закрепленного универсального checkpoint и документированного CPU deployment; ошибка stop condition меняет N. | Сначала найти официальный checkpoint с лицензией, 16-кГц контрактом и CPU inference. До этого аудио не запускать. | Только свежие входы и общие gate. | Отложить до появления измеримого CPU artifact. |
| `A4` EEND-VC / DiariZen | Local EEND совместно строит активности и vectors, constrained clustering связывает чанки; старые пороговые отказы не обязаны повториться. [EEND-VC](https://arxiv.org/abs/2105.09040), [DiariZen](https://github.com/BUTSpeechFIT/DiariZen), [pruning](https://arxiv.org/abs/2506.18623). | Удаляются `S+E+C`, добавляются WavLM/Conformer и новый clustering; CPU `A` не опубликована. | Даже pruned-модель требует 3,8 GMAC/s; официальный рецепт ориентирован на CUDA/Python 3.10; веса CC BY-NC 4.0. | В изолированной среде проверить импорт CPU и 30-секундный публичный sample; закрыть при несовместимости или RTF хуже W0. | Свежие входы и общие gate. | Отклонить для короткого списка: CPU path и лицензия не подходят следующему дешевому срезу. |
| `A5` pyannote Community-1 | Powerset segmentation, WeSpeaker, VBx и exclusive diarization меняют границы и оценку N, сохраняя embedding cascade. [Исходник pipeline](https://github.com/pyannote/pyannote-audio/blob/main/src/pyannote/audio/pipelines/speaker_diarization.py), [model card](https://huggingface.co/pyannote/speaker-diarization-community-1), [powerset loss](https://arxiv.org/abs/2310.13025). | Гарантированно удаляется лишь старая clustering/часть сведения; ее measured ceiling близок к нулю. Batching остается неизмеренной реализационной гипотезой. | Тяжелый PyTorch runtime, предварительная авторизованная загрузка; dense segmentation и embedding cost сохраняются. | 60-секундный публичный CPU stage-profile с RSS; закрыть, если total не лучше W0 минимум на 10%. | Свежие слова, assignment-диагностика и категории прослушивания. | Отклонить: не атакует полную доминирующую работу каскада. |
| `A6` NeMo MSDD | MarbleNet VAD, multi-scale TitaNet embeddings, clustering и neural decoder меняют все стадии и восстанавливают overlap. [Статья](https://arxiv.org/abs/2203.15974), [официальный код](https://github.com/NVIDIA-NeMo/Speech/tree/main/examples/speaker_tasks/diarization). | Знак ускорения неизвестен: VAD сокращает тишину, несколько embedding scales и decoder добавляют работу. | Штатный pretrained MSDD ориентирован на телефонию; готового meeting GGUF/ONNX CPU path нет; начальная ошибка N переходит в decoder. | До аудио потребовать подходящий meeting checkpoint и CPU contract; затем публичный 30-секундный stage smoke. | Свежие входы и общие gate. | Отклонить: добавляет embedding work без готового целевого CPU runtime. |
Реестр оставляет A2-A6 проверяемыми: для каждой ветви записан факт, который
может изменить решение. Второй кандидат в короткий список не добавлен, потому
что для него сначала пришлось бы создавать runtime или выбирать новые веса.
## Решение и владение соседними задачами
Следующее измерение проверяет ровно A1. Причины выбора: полная замена старого
вызова, готовые Q8_0 веса, закрепляемый нативный CPU runtime и standalone
разметка говорящих без изменения ASR. Контракт исполняет
[задача #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46).
В задаче #45 модель и аудио не запускались.
Параллельное планирование ASR и диаризации целиком принадлежит
[задаче #22](https://git.dementev.space/ddmitry/local-transcriber/issues/22).
Автоматическое включение принадлежит
[задаче #23](https://git.dementev.space/ddmitry/local-transcriber/issues/23).
Joint ASR+diarization и shared-ASR-encoder меняют путь распознавания и исключены
из этой карты. ASR-sidecar используется только как свежий quality input.
Принятие A1 после эксперимента означает право подготовить отдельное
production-решение. Оно не меняет [ADR-007](../adr/007-word-level-speaker-diarization.md)
и не разрешает поставку NeMo-Speech.cpp в основном dependency graph.
@@ -0,0 +1,472 @@
# Эксперимент Streaming Sortformer v2 Q8_0 на Ryzen CPU
**Дата:** 2026-09-01
**Статус:** готовый контракт следующего эксперимента, исполняемый в
[задаче #46](https://git.dementev.space/ddmitry/local-transcriber/issues/46).
Запуск модели и аудио начинается после подготовки свежей закрытой основы.
## Цель и исходы
Эксперимент отвечает на один вопрос: сокращает ли Streaming Sortformer v2
Q8_0 через NeMo-Speech.cpp время построения разметки говорящих на целевом Ryzen
минимум на 30% относительно свежего последовательного W0 при приемлемом
качестве и памяти.
Допустимы три исхода:
- `go`: медиана пяти финальных парных ускорений не ниже 30%, автоматические и
слуховые gates пройдены;
- `stop`: нарушен обязательный gate, первичное ускорение ниже 10%, финальная
медиана ниже 30% или слуховая проверка подтвердила регрессию;
- `needs-user`: отсутствует подтверждение свежей опоры или требуемое решение
по прослушиванию. Это состояние ожидания, его нельзя публиковать как результат
кандидата.
`go` разрешает подготовку отдельного production-решения. Экспериментальный
runtime остается изолированным, а production-профиль, CLI, config и модель по
умолчанию сохраняются.
## Сначала прочитать
1. Прочитать [карту архитектур](../research/2026-09-01-end-to-end-cpu-diarization-map.md),
особенно разделы "Граница выводов", A1 и формулу полной замены.
Критерий завершения: агент может объяснить, почему старые значения
`3 / 2 / 2`, unmatched words и purity не являются порогами нового опыта.
2. Прочитать [ADR-007](../adr/007-word-level-speaker-diarization.md) и
[старую спецификацию](2026-08-30-cpu-diarization-experiment.md). Критерий
завершения: сохранены каноническое сведение слов, правила 5%/10%/30% и
memory gate.
3. Прочитать `src/local_transcriber/benchmark_experiment.py` и
`tests/test_benchmark_experiment.py` перед реализацией. Критерий завершения:
новый стенд использует отдельный модуль и schema, не принимает старый
manifest или закрытые хранилища задач #41/#42.
## Границы
В scope входят свежая закрытая основа, точный W0/Sortformer A/B на Ryzen,
проверка трех полных записей после прохождения 30%-го порога на фрагментах и
слуховая приемка.
За пределами scope остаются:
- replay W/M/Q/E/C и восстановление старых patch/wheel;
- C1; ее повторное открытие требует отдельной свежей W0/C1 пары и нового
слухового материала;
- Intel и другие CPU;
- production integration, dependency lock, упаковка runtime и изменение ADR;
- параллельное планирование ASR/диаризации - это
[задача #22](https://git.dementev.space/ddmitry/local-transcriber/issues/22);
- автоматическое включение - это
[задача #23](https://git.dementev.space/ddmitry/local-transcriber/issues/23);
- joint ASR+diarization и shared-ASR-encoder. ASR создает только quality
sidecar до измерений.
## Закрепленные реализации
### W0
W0 использует действующий последовательный путь на восьми физических ядрах:
- `sherpa-onnx/core 1.13.6`;
- Pyannote Segmentation 3.0 FP32, SHA-256
`220ad67ca923bef2fa91f2390c786097bf305bceb5e261d4af67b38e938e1079`;
- WeSpeaker ResNet34 LM FP32, SHA-256
`e9848563da86f263117134dfd7ad63c92355b37de492b55e325400c9d9c39012`;
- `window_shift_ratio=0.1`;
- `FastClustering(auto, 0.89)`;
- `provider=cpu` для segmentation и embeddings;
- `debug=True` для обеих model configs: stage times читаются из штатного
debug-журнала и входят в измерительный рецепт;
- `min_duration_on=0.3`, `min_duration_off=0.5`;
- один worker, одна session, `intra_op_threads=8`, `inter_op_threads=1`, batch 1;
- штатный непатченный `diarizer.process(samples)`.
Старые experimental patch/wheel не загружаются и не восстанавливаются. До
первого запуска стенд проверяет версии, хеши и все effective values рецепта.
Критерий завершения: `--validate-only` показывает рецепт `W0-FRESH` и
останавливается при любом несовпадении.
### Sortformer
Runtime:
- репозиторий [NVIDIA/NeMo-Speech.cpp](https://github.com/NVIDIA/NeMo-Speech.cpp);
- commit `4f9676226f667d14608487df744f375db87127f8`;
- license Apache-2.0;
- source build штатным CPU-only preset:
`git submodule update --init ggml`, затем `scripts/configure.sh cpu-diar` и
`cmake --build --preset cpu-diar`
([инструкция build](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/docs/build.md),
[точный preset](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/CMakePresets.json));
- preset использует Release/Ninja, включает только diarization и отключает
patched GGML; runtime запускается с `gpu=-1`, одним процессом и одним stream;
- CPU graph использует закрепленные в этом commit четыре внутренних потока;
runtime knob для их изменения отсутствует
([вызов scheduler](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/src/runtime/ggml/session.cpp#L903));
- submodule commits, compiler, CMake options и SHA-256 готового executable и
shared library сохраняются в новом manifest.
Модель:
- repo `nvidia/diar_streaming_sortformer_4spk-v2`;
- revision `5240a64075176943f677d30fa2171c780229f341`;
- файл `diar_streaming_sortformer_4spk-v2.q8_0.gguf`;
- размер `147075776` байт;
- SHA-256 `0679cfeb1ce356d0dea9470b31274f4bfc7eb927497d82005483770666da998a`;
- license CC BY 4.0.
Эти revision, размер и хеш также закреплены в
[официальном model index выбранного runtime](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/models/index.json#L75-L88).
После подготовки байтов стенд работает без сети.
Кандидат вызывается через
[standalone C ABI](https://github.com/NVIDIA/NeMo-Speech.cpp/blob/4f9676226f667d14608487df744f375db87127f8/include/nemo_speech/diar.h),
чтобы сохранить effective config и покадровую диагностику. Stateful streaming
получает точную геометрию NVIDIA в 80-мс кадрах:
| Поле | Значение |
|---|---:|
| `chunk_frames` | `340` |
| `right_context_frames` | `40` |
| `left_context_frames` | `0` |
| `fifo_frames` | `40` |
| `spkcache_frames` | `188` |
| `update_period_frames` | `300` |
Сумма chunk и right context дает input buffer latency 30,4 с. Опубликованный
RTF этой конфигурации измерен на RTX 6000 Ada и в CPU-гипотезу не переносится
([model card](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#setting-up-streaming-configuration)).
Full-attention `--offline` не используется.
Post-processing передается явно при каждом запросе:
| Поле | Значение |
|---|---:|
| `onset` | `0.641` |
| `offset` | `0.561` |
| `pad_onset_sec` | `0.229` |
| `pad_offset_sec` | `0.079` |
| `min_duration_sec` | `0.511` |
| `min_gap_sec` | `0.296` |
Это callhome-tuned конфигурация закрепленного runtime. Она остается quality
риском для русских рабочих встреч и не настраивается по трем контрольным
фрагментам. Любое изменение геометрии, post-processing, quantization или
runtime commit создает другую гипотезу и требует новой спецификации.
Критерий завершения подготовки кандидата: сборка выполняет синтетический
ABI-тест без аудио, `nemo_speech_diar_num_speakers()` возвращает 4,
`nemo_speech_diar_seconds_per_frame()` возвращает 0,08, а manifest содержит все
закрепленные значения и хеши. Синтетический тест не заменяет smoke-проверку с
аудио.
## Свежая закрытая основа
### Шаг 1. Выбрать источники и фрагменты
Владелец выбирает три сохраненные исходные записи и по одной 300-секундной
области в каждой. Набор должен включать русскую рабочую речь, смену говорящего,
короткий ответ и перекрытие хотя бы где-то в совокупности. В каждой области и
каждой полной записи должно быть от двух до четырех слышимых голосов. Запись с
пятью голосами закрывает кандидат до измерения: модель выдает не более четырех
выходных голосовых кластеров
([technical limitations](https://huggingface.co/nvidia/diar_streaming_sortformer_4spk-v2#technical-limitations)).
В закрытом manifest сохраняются source hash, точные границы и recipe
mono/16 kHz float32. Публичные документы получают только число фрагментов,
длительность и агрегаты.
Критерий завершения: три source hash уникальны, три WAV имеют ровно 300 секунд
после единственного закрепленного преобразования, а ни один путь, ID или
интервал не попадает в stdout режима `--validate-only`.
### Шаг 2. Создать ASR-sidecar и опорную разметку
Действующий ASR выполняется один раз для каждой полной записи до измерений.
Sidecar хранит канонические слова с временной привязкой и нормализованный текст;
fragment-sidecar получается детерминированным срезом той же версии. Затем
человек прослушивает каждый фрагмент целиком, фиксирует ожидаемое число
голосовых кластеров для фрагментов и полных записей и создает опорную разметку,
достаточную для mapped purity и выбора мест прослушивания. Опорная разметка
остается приближенной и не объявляется истиной границ.
Критерий завершения: для каждого фрагмента и каждой полной записи есть
подтвержденное человеком `expected_cluster_count` в диапазоне `2..4`, один
неизменяемый sidecar и SHA-256; для фрагментов также есть опорная разметка.
Числа старых контролей не копируются.
### Шаг 3. Пройти пользовательский gate
До первого модельного запуска владелец подтверждает:
- право использовать выбранные записи в закрытом локальном опыте;
- пригодность трех областей и полных записей, а также
`expected_cluster_count` каждого фрагмента и каждой полной записи;
- доступность исходных полных записей для этапа после 30%-го порога на
фрагментах;
- готовность выполнить безусловный финальный слуховой gate, если медиана
достигнет 30%. Diagnostics только добавляют обязательные места прослушивания.
Критерий завершения: в закрытом manifest есть timestamped decision `approved`.
Без него исход `needs-user`; стенд не загружает модели.
## Измеряемая величина и A/B
Время W0 - wall вокруг одного `diarizer.process(samples)`. Время кандидата -
wall вокруг `stream_open -> push all prepared samples -> finish -> segment
post-processing`. Для обеих ветвей модель уже загружена, WAV уже декодирован в
один и тот же mono/16 kHz float32 buffer. ASR, скачивание, build, model load,
декодирование, чтение sidecar, запись artifacts и расчет метрик находятся вне
таймера.
Полная замена оценивается только измерением:
```text
B = measured W0 wall
A = measured Sortformer wall
speedup_percent = 100 * (B - A) / B
```
Восьмиклеточное историческое Ryzen-разложение не подставляется вместо свежего
`B`. CPU prediction для `A` отсутствует.
Каждая ячейка запускается новым дочерним процессом на одной машине, от сети, в
стабильном power mode. Process affinity разрешает те же восемь физических ядер
обеим ветвям. W0 получает `intra_op_threads=8`; закрепленный candidate runtime
исполняет CPU graph четырьмя внутренними потоками. Это часть сравниваемого
runtime recipe. Изменение hardcoded числа потоков образует новую гипотезу.
Фиксируются logical/physical cores, CPUID/ISA, RAM, ОС, compiler/runtime
versions, affinity, effective threads, power mode, governor, turbo, throttling
и swap. Прогон со сменой режима или throttling не учитывается и повторяется
один раз после стабилизации.
### Шаг 4. Принять свежий W0
Один `W0-FRESH` обрабатывает три фрагмента последовательно. Для каждого
сохраняются raw wall/CPU/RSS и stage times; публично выводится сумма. W0 должен
дать ровно подтвержденные `expected_cluster_count`, сохранить sidecar-текст и
пройти memory gate.
Критерий завершения: W0 прошел все обязательные gates и его закрытый result
имеет SHA-256. При провале итог `stop`: база сравнения непригодна.
### Шаг 5. Выполнить первичный A/B
Сразу после принятого W0 запускается одна ячейка кандидата на тех же buffers и в
том же порядке фрагментов. Primary order закреплен как `W0 -> candidate`; пять
дорогих циклов ниже устраняют постоянный порядок чередованием. Один primary
cycle равен сумме трех wall times каждой ветви.
Критерий завершения: обе ячейки имеют raw result, одинаковый список input
hashes и один pair ID; рассчитаны ускорение, CPU и RSS delta.
Решение после primary:
- `abs(speedup) <= 5%` - результат фиксируется как равенство по скорости;
- `speedup < 10%` - `stop`: причина `equal-speed` для равенства по скорости,
иначе `below-continuation`;
- `speedup >= 10%` при всех gates - перейти к пяти финальным циклам;
- любой обязательный quality/memory/Sortformer gate - немедленный `stop`.
### Шаг 6. Выполнить пять финальных циклов
Каждый цикл содержит свежую ячейку W0 и свежую ячейку кандидата на всех трех
фрагментах. Primary pair в пять финальных циклов не входит. Порядок закреплен
заранее и чередуется так:
1. final 1: `candidate -> W0`;
2. final 2: `W0 -> candidate`;
3. final 3: `candidate -> W0`;
4. final 4: `W0 -> candidate`;
5. final 5: `candidate -> W0`.
Вместе с primary `W0 -> candidate` это исключает выбор порядка по уже видимому
результату. Дополнительные варианты, модели и настройки не добавляются.
Критерий завершения: получены пять парных ускорений, их медиана и MAD; все
десять ячеек прошли gates. Медиана не ниже 30% открывает проверку полных записей.
Медиана ниже 30% дает `stop`. Дополнительные повторы не назначаются.
### Шаг 7. Проверить полные записи и пройти слуховой gate
После медианы не ниже 30% W0 и candidate однократно обрабатывают три полные
исходные записи. Скорость полных прогонов остается диагностикой и не заменяет
пятипарную медиану.
Владелец получает blinded A/B материал по пяти категориям:
- короткие ответы и перебивки;
- смены говорящего;
- перекрывающаяся речь;
- устойчивость соответствия голоса кластеру;
- слова без говорящего и малые остаточные кластеры.
Материал включает все места, отмеченные сравнительными diagnostics, и
фиксированную случайную выборку совпадающих мест. Порядок A/B и имя кандидата
раскрываются после вердикта. По каждой категории владелец записывает `pass` или
`fail` и короткую причину в закрытом хранилище; публично остается только
вердикт.
Критерий завершения: три пары на полных записях прошли автоматические gates,
все пять слуховых категорий имеют решение. Любой подтвержденный `fail` дает
`stop`; полный `pass` завершает эксперимент как `go`.
## Gates и диагностика
### Обязательное качество и корректность
Каждая ячейка W0 и кандидата обязана выполнить все условия:
- число голосовых кластеров каждого обрабатываемого контрольного объекта -
фрагмента или полной записи - точно равно свежему, подтвержденному человеком
`expected_cluster_count`;
- нормализованный текст и порядок слов побайтно совпадают с одним sidecar;
- нет потерянных, продублированных или переставленных слов;
- интервалы конечны, имеют положительную длительность, лежат в пределах audio
duration с учетом явно заданного padding и отсортированы по началу;
- каноническая нумерация меток детерминирована на повторном расчете метрик;
- все inputs, параметры и output artifacts имеют SHA-256.
Purity и число слов без говорящего сравниваются с новым `W0-FRESH`. Ухудшение
не дает автоматического `fail`, но обязательно добавляет все затронутые места в
слуховой gate. Старые абсолютные значения не участвуют.
Открытый диагностикой слуховой gate не прерывает шаги 5-6. Стенд накапливает
затронутые места и добавляет их в материал шага 7. Состояние `needs-user` из-за
прослушивания возможно только после достижения 30%-го порога и подготовки
полного blinded-материала.
### Sortformer-specific gate
Для каждой ячейки кандидата сохраняются и проверяются:
- `num_speakers=4`, `seconds_per_frame=0.08` и точная geometry/post-processing
из этой спецификации;
- `frame_count`, `frame_probs_start` и размер доступной матрицы `T x 4`;
- каждое сохраненное probability конечно и принадлежит `[0, 1]`;
- каждая итоговая speaker label принадлежит `1..4`;
- число и суммарная длительность интервалов по каждому каналу;
- число коротких интервалов, overlap ratio, unmatched words, малые/остаточные
кластеры и mapped purity относительно свежей опорной разметки;
- wall отдельно для model inference и segment post-processing.
Любой неверный shape/value/label или эффективная конфигурация, отличная от
закрепленной, дает `stop`. Появление лишнего/пропажа ожидаемого кластера дает
`stop`. Изменение channel stability, short-turn, overlap, unmatched-word или
purity diagnostics открывает слуховую проверку; числовой порог для этих proxy
не вводится.
Жесткий предел четырех выходных голосовых кластеров, риск записей с пятью и
более слышимыми голосами и риск non-English/out-of-domain сохраняются в
публичном решении даже после `go`; опыт не доказывает применимость к другим
встречам.
### Память
Peak RSS измеряется process-wide для каждой ячейки. Также сохраняются swap до и
после, page faults и RSS после каждого фрагмента.
Memory gate дает `stop` при OOM, активной подкачке во время ячейки, RSS без плато
между повторами или линейном росте RSS с числом обработанных фрагментов. Peak
RSS выше 2 ГиБ на машине с 16 ГиБ требует отдельного разбора владельца и
состояния `needs-user`; одно превышение не является автоматическим `fail`.
## Артефакты и приватность
Checkout и дерево сборки NeMo-Speech.cpp, скачанные веса и новое закрытое
хранилище размещаются вне рабочей копии local-transcriber. Обычная `.scratch`
внутри репозитория не подходит: `.gitignore` покрывает там только
`.scratch/handoffs/`. Ни один runtime/model/build artifact или закрытый вход не
попадает в Git.
Новое закрытое хранилище содержит:
- versioned manifest со schema, clean local-transcriber source commit,
source/model/runtime/build hashes и расписанием;
- исходные ссылки, подготовленные WAV и recipe преобразования;
- ASR-sidecar и опорную разметку;
- raw per-fragment results, intervals, word assignments и probability
diagnostics;
- machine/power/runtime guards и stderr/stdout logs;
- материал и решения слуховой проверки;
- байты runtime/model либо проверяемый download recipe с revision, size и hash.
Хранилище не ссылается на закрытые хранилища задач #41/#42 и остается
достаточным для разбора собственного результата. Удаление старых хранилищ не
блокирует ни один шаг.
Публичный отчет содержит `clean local-transcriber source commit`, точные
рецепты, хеши, агрегаты, вердикты gates и итоговое решение. В Git, комментарии
Gitea, задания на ревью и вывод `--validate-only` не попадают пути,
`source/recording IDs`, текст, временные интервалы, сырые строки, содержимое
manifest, логи, имена участников или аудио.
При нарушении приватности стенд останавливается до публикации. Уже
сформированный публичный артефакт удаляется из рабочей ветки и создается заново
из безопасных агрегатов.
## Реализация стенда и тесты
Новый стенд реализуется отдельным модулем
`src/local_transcriber/streaming_sortformer_experiment.py`; существующий
`benchmark_experiment.py` остается источником общих проверенных механизмов.
Стенд получает отдельную schema и только две recipe: `W0-FRESH` и
`SORTFORMER-V2-Q8`. Manifest validation выполняется до загрузки моделей.
`--validate-only` проверяет:
- три уникальных source hashes, 300-секундные фрагменты и fresh-basis marker;
- human approval и `expected_cluster_count` каждого фрагмента и каждой полной
записи в диапазоне `2..4`;
- W0 model/runtime hashes и effective values: CPU provider, `debug=True`,
`min_duration_on=0.3`, `min_duration_off=0.5`, threads и clustering;
- clean local-transcriber worktree и source commit из manifest;
- Sortformer revision, size, SHA-256, runtime commit, submodules, build hashes,
CPU backend, geometry и post-processing;
- одинаковые input/sidecar hashes обеих ветвей;
- заранее созданные primary/final schedules;
- отсутствие корней старых хранилищ и legacy ID ячеек.
Минимальные детерминированные тесты выполняются без модели и аудио:
```bash
uv run pytest -q \
tests/test_streaming_sortformer_experiment.py \
tests/test_benchmark_experiment.py \
tests/test_speaker_diarizer.py \
tests/test_diarization.py
```
Новый модуль тестов покрывает отклонение `schema`, несовпадение пинов и хешей,
требование свежей опоры, обезличенный вывод `--validate-only`, правило
`expected_cluster_count`, точное расписание A/B, границы 5%/10%/30%, gates
качества, памяти и Sortformer, переход к прослушиванию, безопасную сериализацию
и остановку при первом отказе. Стенд C ABI имеет отдельный синтетический тест
размеров структур, выбора CPU и эффективной конфигурации; модель в модульных
тестах не загружается.
Критерий завершения реализации стенда: все детерминированные тесты проходят,
`--validate-only` проходит на закрытом manifest и публичный вывод проходит
privacy scan. Реализация стенда фиксируется чистым коммитом исходников,
совпадающим с manifest. Только после этого разрешен первый запуск модели W0.
## Критерии завершения эксперимента
- [ ] Свежая опора подтверждена владельцем и не наследует старые контрольные
значения.
- [ ] Runtime, модель, сборка и оба рецепта закреплены и проверены до моделей.
- [ ] Clean local-transcriber source commit сохранен в закрытом хранилище и
публичном отчете, а `--validate-only` проверяет его до моделей.
- [ ] Свежий W0 принят либо итог `stop` объясняет провал базы сравнения.
- [ ] Первичный A/B завершен; решение 10% применено без дополнительных ветвей.
- [ ] При продолжении завершены ровно пять финальных циклов, медиана и MAD.
- [ ] Каждая выполненная ячейка имеет вердикты качества, текста, памяти и
Sortformer.
- [ ] При медиане 30% выполнены три пары на полных записях и пять слуховых
решений.
- [ ] Все остановленные шаги имеют первую точную причину и хеш результата.
- [ ] Закрытое хранилище самодостаточно; публичный отчет содержит только безопасные
агрегаты и SHA-256.
- [ ] Итог равен `go` или `stop`; `needs-user` остается открытым до решения.
- [ ] Ни один результат автоматически не меняет production-зависимости,
профиль, CLI, config или модель по умолчанию.