From 98baf55572f124c9f857c5752480d1d1ef07e515 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 21:56:10 +0300 Subject: [PATCH] =?UTF-8?q?docs(diarization):=20=D0=B7=D0=B0=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=91=D0=BC=D0=BA=D0=B8=20=D0=B8=20=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - решения по неопределённым словам и CPU-стоимости должны быть проверяемыми до публикации реализации. - Что: - задокументировано отклонение post-hoc сглаживания Speaker ? по соседним кластерам. - добавлено исследование стоимости каскада и облегчённых embedding-моделей. - Проверка: - `git diff --cached --check` — без ошибок. --- .../adr/007-word-level-speaker-diarization.md | 34 +++ .../2026-08-14-diarization-cpu-cost.md | 199 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 docs/research/2026-08-14-diarization-cpu-cost.md diff --git a/docs/adr/007-word-level-speaker-diarization.md b/docs/adr/007-word-level-speaker-diarization.md index 6e2d2dc..4d95883 100644 --- a/docs/adr/007-word-level-speaker-diarization.md +++ b/docs/adr/007-word-level-speaker-diarization.md @@ -45,6 +45,39 @@ WeSpeaker ResNet34 LM проверена на трёх записях с изв [смешении говорящих](../benchmarks/2026-08-14-asr-segment-speaker-mixing.md) и [производительности на Intel](../benchmarks/2026-08-14-diarization-intel-i7.md). +### Сглаживание неизвестного говорящего + +После приёмки отдельно проверена идея автоматически назначать `Speaker ?` +известному говорящему, если короткий неизвестный фрагмент находится между двумя +репликами одного и того же `Speaker N`. На трёх контрольных транскриптах найдено +143 таких неизвестных фрагмента, содержащих 309 слов. У 79 фрагментов соседи +имели одинаковую метку, у 64 — разные. + +Для T2 BDMA и Yantar результат грубо сопоставлен с независимыми Hypescribe- +транскриптами. Это не gold-разметка: их границы округлены до секунды, а готовый +speaker Markdown хранит только начало реплики. Тем не менее эвристика не показала +достаточной селективности: + +| Эвристика | Изменённых фрагментов на трёх записях | Совпадение на двух записях с опорой | +|---|---:|---:| +| Все неизвестные между одинаковыми соседями | 79 | 22 / 42 (52%) | +| Только один неизвестный word | 35 | 11 / 20 (55%) | +| Не больше двух words | 50 | 15 / 30 (50%) | +| Одинаковый отображённый timestamp | 42 | 14 / 22 (64%) | + +Короткие `Да`, `Нет` и `Угу` часто являются самостоятельной репликой другого +участника. В одном из контрольных случаев неизвестный фрагмент `— Угу. — А` +даже содержал границу двух голосов, хотя с обеих сторон находился один и тот же +кластер. Поэтому post-hoc сглаживание по соседям скрывает полезную +неопределённость и не применяется. `Speaker ?` остаётся явным результатом ничьей +или отсутствия временного перекрытия. + +Если к этой задаче возвращаться, проверять нужно исходные границы `Word` и +`SpeakerInterval` до группировки: отдельно различать нулевой timestamp, реальный +зазор между интервалами и ничью перекрытий. По готовому Markdown такая проверка +невозможна, потому что в нём уже потеряны доли секунды, конец слова и причина +неопределённости. + ## Решение Диаризацию реализуем как явно включаемый пост-процессинг через `sherpa-onnx`. @@ -97,3 +130,4 @@ ASR-бэкенд приводит свой результат к общему н | `pyannote.audio` | Тянет PyTorch и требует Hugging Face token с принятием лицензии | | Сборка поверх приватных деталей `onnx-asr` | Экономит небольшую отдельную зависимость ценой нестабильного внутреннего API и собственной кластеризации | | Параллельные проходы в первой версии | Нет прямого benchmark и измеренного общего пика памяти; сначала нужен корректный последовательный baseline | +| Автоматически назначать `Speaker ?` одинаковому соседнему кластеру | На двух записях с независимой опорой совпало только 52% назначений; лимиты по словам и округлённому времени не отделили короткие ответы другого участника | diff --git a/docs/research/2026-08-14-diarization-cpu-cost.md b/docs/research/2026-08-14-diarization-cpu-cost.md new file mode 100644 index 0000000..d96cde6 --- /dev/null +++ b/docs/research/2026-08-14-diarization-cpu-cost.md @@ -0,0 +1,199 @@ +# Стоимость CPU-диаризации и облегчённые альтернативы + +**Дата:** 2026-08-14 + +**Статус:** исследовательская записка. Production-конфигурация не менялась. + +## Краткий вывод + +Измеренная стоимость ожидаема для выбранного каскада, но не является +неизбежным минимумом. `WeSpeaker ResNet34 LM` — не аномально большая модель: +ONNX-файл занимает 26,5 МБ, а сама архитектура находится в младшей части +семейства WeSpeaker ResNet. Основная цена возникает из способа применения +моделей: Pyannote проходит запись перекрывающимися 10-секундными окнами с шагом +1 секунда, после чего sherpa-onnx отдельно считает speaker embedding для каждого +локального говорящего каждого окна. + +На минутном профиле текущей связки на этом ноутбуке эмбеддинги заняли 5,150 с +из 6,395 с, сегментация — 1,243 с, кластеризация — меньше измеримой миллисекунды. +То есть около 81% времени в этом прогоне пришлось на многократные вызовы +WeSpeaker. Ускорять только кластеризацию или менять её порог бессмысленно. + +Более лёгкие пути есть. Самый безопасный для исследования — официальная INT8 +версия той же Pyannote-сегментации. Самый большой подтверждённый upstream +потенциал даёт `nemo_en_titanet_small`, но её качество и порог кластеризации на +русских созвонах проекта ещё не проверялись. Локально проверенная CAMPPlus zh/en +быстрее WeSpeaker примерно на 30%, однако не прошла требование автоматического +определения числа говорящих с единым порогом. + +## Откуда берётся стоимость + +`sherpa-onnx` строит результат из трёх вычислительных стадий: speaker +segmentation, speaker embeddings и clustering. Это соответствует как +[официальному API](https://k2-fsa.github.io/sherpa/onnx/c-api/html/speaker_diarization.html), +так и [реализации конвейера +Pyannote](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h). + +### 1. Перекрывающаяся сегментация + +Pyannote segmentation 3.0 принимает 10 секунд mono 16 кГц и различает до трёх +локальных говорящих в окне, включая пары одновременно говорящих. Это явно +зафиксировано в [карточке исходной +модели](https://huggingface.co/pyannote/segmentation-3.0). + +В официальном ONNX-файле `window_size=160000`, то есть те же 10 секунд при +16 кГц. В sherpa-onnx 1.13.5 `window_shift_ratio` по умолчанию равен 0,1, а +число окон вычисляется из размера окна и этого шага. Поэтому обычная длинная +запись проходит через segmentation-модель примерно десятикратно +перекрывающимися окнами: новое окно начинается каждую секунду. См. +[конфигурацию шага](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-segmentation-pyannote-model-config.h) +и [цикл обработки +окон](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h). + +Это не признак «огромной» модели: FP32-файл сегментации весит лишь 5,7 МБ. +Стоимость создаёт прежде всего частота её запуска. Официальная поставка также +содержит `model.int8.onnx` размером 1,5 МБ +([документация sherpa-onnx](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html)). + +### 2. Embedding для каждого локального говорящего каждого окна + +После сегментации sherpa-onnx исключает перекрывающиеся кадры из материала для +эмбеддинга, собирает пары `(окно, локальный говорящий)` и для каждой пары создаёт +отдельный stream и запускает embedding extractor. Это видно непосредственно в +[методах `GetChunkSpeakerSampleIndexes` и +`ComputeEmbeddings`](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/csrc/offline-speaker-diarization-pyannote-impl.h). + +При одном активном голосе это уже примерно один embedding на секунду записи; +если в окне модель видит несколько локальных голосов, вызовов становится больше. +Текущий extractor — английский VoxCeleb `ResNet34_LM`; WeSpeaker поясняет, что +суффикс LM означает дополнительную large-margin донастройку, полезную на +фрагментах длиннее трёх секунд +([официальный список моделей](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md)). + +### 3. Кластеризация почти бесплатна + +В [первичном разведочном +замере](../benchmarks/2026-08-12-diarization-feasibility.md) смена порога не +изменила время: 153–157 с, а кластеризация занимала доли секунды. Минутный профиль +текущей production-конфигурации также показал `0.000 s` для clustering. Явное +число участников полезно для качества и стабильности количества кластеров, но не +является существенной оптимизацией CPU. + +## Что показывают локальные измерения + +| Среда и материал | Конфигурация | Результат | +|---|---|---:| +| Ryzen 7 8845H, Data Test 26:00 | Pyannote FP32 + WeSpeaker, 8 потоков | 140,7 с; 11,1× realtime | +| Ryzen 7 8845H, три 5-минутных фрагмента | WeSpeaker | средний RTF 0,118 | +| Ryzen 7 8845H, те же фрагменты | CAMPPlus zh/en | средний RTF 0,083; 0,70× от WeSpeaker | +| Intel Core i7-6820HQ, 61:13 аудио | WeSpeaker, 8 потоков | 832,3 с; 4,4× realtime | +| Текущий ноутбук, 60 с T2 BDMA | WeSpeaker, 8 потоков, debug profile | segmentation 1,243 с; embeddings 5,150 с; clustering 0,000 с; всего 6,395 с | + +Источники полных воспроизводимых замеров: [разведка на +Ryzen](../benchmarks/2026-08-12-diarization-feasibility.md), [калибровка +эмбеддингов](../benchmarks/2026-08-14-diarization-calibration.md) и [Intel +baseline](../benchmarks/2026-08-14-diarization-intel-i7.md). Минутный профиль — +диагностический одиночный прогон той же production-конфигурации на первых 60 +секундах T2 BDMA; его следует использовать для распределения стоимости по +стадиям, а не как новый общий benchmark. + +Увеличение `num_threads` не решает проблему. На Data Test переход с четырёх +потоков (154 с) на восемь (141 с) дал только 9%. Это согласуется с устройством +конвейера: он выполняет много последовательных ONNX-вызовов для отдельных окон +и локальных говорящих, поэтому добавление потоков внутри одного вызова быстро +перестаёт масштабироваться. + +## Официально поддерживаемые облегчённые варианты + +### INT8 Pyannote segmentation 3.0 + +Sherpa-onnx официально поставляет FP32 и INT8 варианты одной сегментации. На его +контрольной записи замена только segmentation-модели при 3D-Speaker embedding +снизила RTF с 0,297 до 0,241, то есть примерно на 19%. С TitaNet small разница +меньше: 0,119 против 0,110. Все числа опубликованы на одной странице +[официальных примеров и +замеров](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html). + +Trade-off: это минимальное архитектурное изменение, но INT8 меняет границы +интервалов даже в официальном примере. Перед заменой нужно повторить на трёх +контрольных записях число кластеров, purity, малые кластеры, неназначенные слова +и полный runtime. На текущем минутном профиле segmentation занимает лишь около +19% времени, поэтому одной квантизацией нельзя ожидать кратного ускорения всей +диаризации. + +### 3D-Speaker CAMPPlus zh/en + +Модель уже входит в официальный релиз speaker-recognition моделей sherpa-onnx, +а 3D-Speaker публикует CAM++ как штатную архитектуру своего набора +([репозиторий и таблица +моделей](https://github.com/modelscope/3D-Speaker)). Её ONNX-файл занимает +28,3 МБ — немного больше текущих 26,5 МБ, поэтому размер файла здесь плохо +предсказывает вычислительную стоимость. + +Локально CAMPPlus дала RTF 0,083 против 0,118 у WeSpeaker, то есть была примерно +на 30% быстрее. При известном числе участников она также улучшила proxy-purity +на двух разговорах. Но в автоматическом режиме не нашлось единого порога: один +порог оставлял лишние кластеры, следующий уже склеивал реальные голоса. Полные +данные находятся в [отчёте о +калибровке](../benchmarks/2026-08-14-diarization-calibration.md). + +Trade-off: хороший кандидат для режима с обязательным `--speakers N`, но не +готовая замена общего автоматического режима. + +### NeMo TitaNet small + +Sherpa-onnx официально показывает `nemo_en_titanet_small` как совместимый +embedding extractor. На его контрольной записи Pyannote FP32 + TitaNet small +дала RTF 0,119 вместо 0,297 у Pyannote FP32 + 3D-Speaker ERes2Net; с INT8 +сегментацией — 0,110 вместо 0,241. Это самый большой опубликованный upstream +выигрыш среди проверенных на одной странице комбинаций +([официальные замеры](https://k2-fsa.github.io/sherpa/onnx/speaker-diarization/models.html)). +TitaNet использует 1D depth-wise separable convolutions и channel-attention +statistics pooling +([документация NVIDIA](https://docs.nvidia.com/nemo-framework/user-guide/25.02/nemotoolkit/asr/speaker_recognition/models.html)). + +Trade-off: upstream-цифры сняты на другой embedding-модели сравнения, другом +материале и оборудовании, поэтому коэффициент нельзя переносить на этот ноутбук. +Кроме того, TitaNet small не проходила локальную калибровку на русской речи: +неизвестны подходящий clustering threshold, стабильность числа голосов и +качество коротких реплик. Сначала нужен тот же свип, который уже выполнен для +WeSpeaker и CAMPPlus. + +### Увеличение шага окна + +В C++-конфигурации sherpa-onnx 1.13.5 есть `window_shift_ratio`, поэтому на уровне +движка можно уменьшить число перекрывающихся окон ценой более грубой разметки. +Но [Python binding этой +версии](https://github.com/k2-fsa/sherpa-onnx/blob/v1.13.5/sherpa-onnx/python/csrc/offline-speaker-diarization.cc) +экспортирует только путь `model`, а не `window_shift_ratio`. Для текущего Python +приложения это не штатная ручка без изменения upstream binding или собственного +нативного слоя. Даже после появления ручки потребуется отдельная калибровка: +более редкие окна могут ухудшить границы смены голоса и короткие ответы — как раз +самую чувствительную часть текущего результата. + +## Практические следующие шаги + +1. Не считать текущую стоимость дефектом реализации: выбранный результат + соответствует устройству sherpa-onnx и остаётся быстрее realtime даже на + старом Intel. +2. Первым отдельным экспериментом прогнать INT8 Pyannote на тех же трёх файлах. + Это наименьший по масштабу вариант, хотя ожидаемый выигрыш умеренный. +3. Отдельно откалибровать TitaNet small. У неё лучший опубликованный потенциал + скорости, но пока нет локальных данных о качестве. +4. CAMPPlus предлагать только как кандидат для режима с известным числом + участников, если 30% экономии оправдывает второй production-профиль. +5. Параллельный ASR и diarization исследовать независимо от выбора модели. Он не + уменьшает CPU-работу, но может сократить wall-clock latency. Обязательно + измерить конкуренцию за ядра и общий peak RSS: на Intel последовательные пики + составляли 900–1035 МБ для ASR и 366–469 МБ для диаризации, а совместный пик + пока не измерен. + +## Вердикт + +Текущая диаризация тяжёлая **в основном из-за каскада и плотного перекрытия +окон**, а не потому, что случайно выбрана гигантская модель. Но выбранный +WeSpeaker ResNet34 LM не самый быстрый extractor. Реалистичный резерв — умеренное +ускорение через INT8 segmentation, около 30% по локальным данным через CAMPPlus +при известном числе участников и потенциально более крупное ускорение через +TitaNet small после обязательной русскоязычной калибровки. Простое добавление +потоков или настройка clustering заметного выигрыша не даст.