feat(diarization): добавлено разделение транскрипта по говорящим #26
@@ -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% назначений; лимиты по словам и округлённому времени не отделили короткие ответы другого участника |
|
||||
|
||||
@@ -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 заметного выигрыша не даст.
|
||||
Reference in New Issue
Block a user