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

@@ -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 или модель по умолчанию.