docs(diarization): добавлены отчёт и скрипт калибровки

- Зачем:
  - необходим устойчивый дефолт модели эмбеддингов и порога на русской речи.
- Что:
  - задокументирован выбор WeSpeaker ResNet34 LM с порогом 0,89.
  - добавлен возобновляемый скрипт свипа моделей и параметров диаризации.
- Проверка:
  - uvx --cache-dir .uv-cache ruff check scripts/benchmarks/diarization_calibration.py.
  - uv run --cache-dir .uv-cache pytest: 243 passed, 1 skipped.
This commit is contained in:
Dmitriy Dementiev
2026-08-14 11:18:17 +03:00
parent 8c55eaa87f
commit 0fdeebb256
2 changed files with 564 additions and 0 deletions
@@ -0,0 +1,209 @@
# Калибровка модели эмбеддингов и порога диаризации
**Дата:** 2026-08-14
**Статус:** выбор конфигурации для проектирования. Не приёмка
производительности на целевом Intel Core i5 11-го поколения.
## Решение
Для автоматического определения числа участников использовать:
- эмбеддинги `wespeaker_en_voxceleb_resnet34_LM.onnx`;
- `FastClusteringConfig.threshold=0.89`;
- `num_clusters=-1` по умолчанию.
Если число участников известно, передавать его через `num_clusters`: это
устраняет остаточные кластеры и служит страховкой от особенностей записи. На
трёх проверенных фрагментах явное число участников не ухудшило прокси-метрику
качества WeSpeaker.
Двуязычная `3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx`
быстрее и при известном числе участников лучше на одной из двух записей с
таймкодами, но для автоматического режима не нашлось общего порога без лишних
кластеров или склейки реальных голосов. Поэтому она не выбрана по умолчанию.
## Что проверялось
Свип выполнялся на трёх русскоязычных рабочих созвонах с известным составом:
| Запись | Участников | Короткий фрагмент | Полный прогон кандидата | SHA-256 |
|---|---:|---:|---:|---|
| `2026-07-10 Data Test внутренний статус.mp4` | 3 | 07:0012:00 | 25:59,9 | `1057616B42E8ADD00E0EB975B02BDEF0EC9F6CDFEC6DBF488E0C60423C9B7B87` |
| `2026-07-29 T2 BDMA уточнение задачи от Ильи.mp4` | 2 | 00:0005:00 | 14:50,9 | `51866D247FE3EDA134CDD884F707B1F1DB8855B492E8BD14D2B56B62476255ED` |
| `2026-08-12 Созвон с Максом Мерлином по T2 Forecast и Yantar.mp4` | 2 | 00:0005:00 | 20:22,2 | `4422F04E2771091A0648E5422D14A31DD7CA2C8EEF7ED9F4A5C63F43D8CA6400` |
Для первой записи число участников взято из согласованного MoM и не зависит от
диаризации. Для двух остальных рядом с медиа лежат транскрипты Hypescribe с
таймкодами и метками спикеров. Они получены другим инструментом и использованы
как независимая грубая опорная разметка.
Hypescribe ставит метку только в начале реплики и не размечает точные границы,
тишину и наложения голосов. Поэтому ниже считается не DER, а **mapped speaker
purity**: лучший взаимно-однозначный маппинг кластеров на опорные метки по
суммарному перекрытию. Метрика подходит для сравнения конфигураций на одной
записи, но не является абсолютной оценкой диаризации.
Кластер считается содержательным, если в нём не меньше `max(5 с, 2% длины
записи)` речи. Это только диагностический показатель: готовый CLI не должен
молча отбрасывать малые кластеры без отдельного решения.
## Модели
Во всех прогонах использовалась одна сегментация
`sherpa-onnx-pyannote-segmentation-3-0`.
| Роль | Модель | Языки обучения | Размер | SHA-256 |
|---|---|---|---:|---|
| выбранная | `wespeaker_en_voxceleb_resnet34_LM.onnx` | английский, VoxCeleb2 | 26 530 550 | `E9848563DA86F263117134DFD7AD63C92355B37DE492B55E325400C9D9C39012` |
| многоязычная альтернатива | `3dspeaker_speech_campplus_sv_zh_en_16k-common_advanced.onnx` | китайский + английский | 28 281 164 | `AA3CFC16963A10586A9393F5035D6D6B57E98D358B347F80C2A30BF4F00CEBA2` |
| дополнительная разведка | `3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx` | китайский | 39 593 761 | `1A331345F04805BADBB495C775A6DDFFCDD1A732567D5EC8B3D5749E3C7A5E4B` |
| сегментация | `model.onnx` из `sherpa-onnx-pyannote-segmentation-3-0` | — | 5 992 913 | `220AD67CA923BEF2FA91F2390C786097BF305BCEB5E261D4AF67B38E938E1079` |
WeSpeaker сам помечает VoxCeleb-модель как английскую и распространяет её под
CC BY 4.0. Репозиторий 3D-Speaker и модель CAMPPlus на ModelScope используют
Apache 2.0; в исходниках 3D-Speaker модель явно описана как обученная на
большом китайско-английском корпусе. ONNX-файлы брались из официального релиза
`k2-fsa/sherpa-onnx`, а не из сторонних зеркал.
Источники:
- [список и лицензирование моделей WeSpeaker](https://github.com/wenet-e2e/wespeaker/blob/master/docs/pretrained.md);
- [карточка `wespeaker-voxceleb-resnet34-LM`](https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet34-LM);
- [исходники и лицензия 3D-Speaker](https://github.com/modelscope/3D-Speaker);
- [официальный релиз ONNX-моделей sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx/releases/tag/speaker-recongition-models).
## Свип WeSpeaker
Порог сначала проверялся крупным шагом, затем уточнялся около переходов между
числом кластеров. В ячейках — общее число кластеров; жирным выделено точное
совпадение с известным числом участников.
| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 |
|---:|---:|---:|---:|
| 0,85 | **3** | **2** | 3 |
| 0,87 | **3** | **2** | 3 |
| 0,88 | **3** | **2** | 3 |
| **0,89** | **3** | **2** | **2** |
| 0,90 | 2 | **2** | **2** |
| 0,95 | 2 | **2** | 1 |
| явное `num_clusters` | **3** | **2** | **2** |
`0,89` — единственное проверенное значение, которое без знания числа
участников дало правильное количество кластеров на всех трёх фрагментах. На
двух записях с опорными метками purity составила 0,767 и 0,787. Явное число
участников дало те же значения.
## Сравнение с 3D-Speaker
### CAMPPlus, китайский + английский
| Порог | Data Test, 3 | T2 BDMA, 2 | Yantar, 2 |
|---:|---:|---:|---:|
| 0,85 | 7 | 7 | 7 |
| 0,90 | 6 | 5 | 5 |
| 0,95 | 5 | 5 | 5 |
| 0,99 | 4 | 4 | 4 |
| 1,00 | 4 | 4 | 4 |
| 1,05 | **3** | 3 | 3 |
| 1,10 | 2 | **2** | 3 |
| явное `num_clusters` | **3** | **2** | **2** |
При `1,05` на двух записях остаётся по одному малому остаточному кластеру, а
при `1,10` трёхсторонняя встреча уже склеивается до двух голосов. Общего
автоматического порога нет.
При явном числе участников purity равна 0,801 на T2 BDMA и 0,911 на Yantar.
Это лучше WeSpeaker на 0,034 и 0,124 соответственно. Однако на контрольной
трёхсторонней записи один из трёх принудительных кластеров оказался меньше
порога содержательности, поэтому улучшение по двум текстовым прокси нельзя
обобщать на все записи.
### ERes2Net base, китайский
Эта модель проверялась дополнительно, но не считается выполнением требования
о многоязычной альтернативе. Даже на пороге 0,99 она дала 5 / 4 / 7 кластеров
вместо 3 / 2 / 2. При явном числе участников purity составила 0,688 и 0,907:
результат неоднородный и автоматический режим заметно хуже выбранного.
## Полные прогоны выбранного кандидата
После свипа `WeSpeaker + 0,89` прогнан на всех трёх записях целиком.
| Запись | Кластеры | Содержательные | Речь по кластерам, с | Остаток сверх ожидаемых | Purity | Время | RTF |
|---|---:|---:|---|---:|---:|---:|---:|
| Data Test | 4 | 3 | 573,1 / 487,4 / 342,2 / 19,1 | 1,3% | — | 189,0 с | 0,121 |
| T2 BDMA | 2 | 2 | 748,7 / 52,3 | 0% | 0,749 | 112,3 с | 0,126 |
| Yantar | 2 | 2 | 733,2 / 259,8 | 0% | 0,906 | 151,4 с | 0,124 |
На полной контрольной записи остаётся ложный кластер на 19,1 с, но три
содержательных кластера совпадают с известным составом. Повторный полный прогон
на 0,9 дал тот же результат: JSON-массивы всех 340 интервалов на 0,89 и 0,9
совпали в точности, включая границы и номера кластеров. Поэтому к кандидату
0,89 непосредственно применима слуховая проверка отрезка 07:00–12:00,
выполненная для результата из
[разведочного замера](2026-08-12-diarization-feasibility.md): три основных
голоса стабильны, остаточный кластер ложный, есть небольшие пропуски второго
голоса, а наложения голосов определяются не полностью. Новая калибровка не
устраняет эти ограничения сегментации.
На двух полных разговорах purity отличается от короткого фрагмента: 0,749
против 0,767 и 0,906 против 0,787. Это подтверждает, что короткий свип годится
для отсева конфигураций, а финальный кандидат надо проверять целиком.
## Производительность
Условия: AMD Ryzen 7 8845H, Windows 11 build 26200, Python 3.13.13,
`sherpa-onnx` 1.13.5, `onnxruntime` 1.28.0, NumPy 2.4.3, 8 потоков CPU.
Загрузка моделей и декодирование медиа не входят в измерение.
Средний RTF на коротких фрагментах:
| Модель | RTF | Относительно WeSpeaker |
|---|---:|---:|
| WeSpeaker ResNet34 LM | 0,118 | 1,00× |
| CAMPPlus zh/en | 0,083 | 0,70× |
| ERes2Net base zh | 0,152 | 1,29× |
CAMPPlus примерно на 30% быстрее WeSpeaker в этом эксперименте. Это плюс для
варианта с известным числом участников, но замер на AMD не заменяет приёмку на
целевом Intel Core i5 11-го поколения.
## Воспроизводимость
Свип выполняется скриптом
[`scripts/benchmarks/diarization_calibration.py`](../../scripts/benchmarks/diarization_calibration.py).
Он принимает JSON-манифест с путями к моделям и записям, декодирует указанные
фрагменты через ffmpeg, последовательно сохраняет каждый результат и может
возобновить прерванный прогон.
Пример:
```powershell
uv run python scripts/benchmarks/diarization_calibration.py `
--manifest diarization-calibration.json `
--output diarization-calibration-results.json `
--work-dir .scratch/diarization-calibration `
--threads 8
```
Сырые JSON содержат локальные пути к конфиденциальным рабочим записям и сами
интервалы диаризации, поэтому в репозиторий не добавляются. Для проверки
артефактов выше приведены SHA-256 медиа и моделей.
## Ограничения и следующий шаг
- Три записи принадлежат одному типу русскоязычных рабочих созвонов; это не
репрезентативная выборка для всех микрофонов, шумов и акцентов.
- Опорные метки двух записей грубые и не дают посчитать DER.
- На слух проверен только фрагмент 07:00–12:00 контрольной записи; перед
выпуском нужен слуховой контроль плотного диалога на финальной сборке.
- Калибровка выбирает эмбеддинги и кластеризацию, но не решает ошибки границ и
неполное распознавание наложений голосов.
- Производительность должна отдельно приниматься на целевом Intel Core i5.
Для спецификации зафиксировать WeSpeaker + 0,89 как автоматический дефолт,
отдельную опцию явного числа участников и отсутствие автоматического
отбрасывания малых кластеров. CAMPPlus zh/en можно оставить кандидатом для
будущего режима с обязательным `num_clusters` после расширенной слуховой
проверки.