- Зачем: - решения по неопределённым словам и CPU-стоимости должны быть проверяемыми до публикации реализации. - Что: - задокументировано отклонение post-hoc сглаживания Speaker ? по соседним кластерам. - добавлено исследование стоимости каскада и облегчённых embedding-моделей. - Проверка: - `git diff --cached --check` — без ошибок.
134 lines
13 KiB
Markdown
134 lines
13 KiB
Markdown
# ADR-007: Пословная диаризация через sherpa-onnx
|
||
|
||
**Статус**: Принято
|
||
**Дата**: 2026-08-14
|
||
|
||
## Контекст
|
||
|
||
Разделение говорящих — главный структурный разрыв между локальным транскриптом
|
||
и облачными сервисами в сценарии подготовки конспектов и протоколов встреч.
|
||
Диаризация при этом не является ещё одним движком распознавания: она независимо
|
||
строит [разметку говорящих](../../CONTEXT.md#language), которую затем нужно
|
||
свести с результатом ASR.
|
||
|
||
Привязка одного говорящего ко всему сегменту распознавания оказалась слишком
|
||
грубой. На трёх русскоязычных рабочих созвонах чужая реплика не короче секунды
|
||
встретилась в 6–7% сегментов разговоров на двоих и в 27% сегментов встречи
|
||
втроём. Сегменты распознавания проходят по тишине, а не по смене говорящего,
|
||
поэтому сохранить контекст RNN-T и получить реплики можно только через более
|
||
мелкую единицу сведения.
|
||
|
||
## Эксперимент
|
||
|
||
Локальная связка `sherpa-onnx` с сегментацией Pyannote 3.0 и эмбеддингами
|
||
WeSpeaker ResNet34 LM проверена на трёх записях с известным составом. Для
|
||
автоматического определения числа голосовых кластеров выбран порог 0,89: это
|
||
единственное проверенное значение, которое на трёх контрольных фрагментах дало
|
||
3 / 2 / 2 кластера. На полной встрече втроём остался ложный кластер длительностью
|
||
19,1 секунды; поэтому малые кластеры нельзя молча отбрасывать, а разметку нельзя
|
||
считать эталоном точных границ и перекрывающейся речи.
|
||
|
||
Двуязычная CAMPPlus zh/en оказалась примерно на 30% быстрее и при известном
|
||
числе участников улучшила прокси-метрику на двух записях, но для неё не нашлось
|
||
общего автоматического порога без лишних кластеров или склейки реальных голосов.
|
||
Поэтому она остаётся кандидатом только для будущего режима с обязательным
|
||
явным числом участников, а не для первой версии.
|
||
|
||
На доступном слабом Intel baseline, Core i7-6820HQ с урезанным питанием,
|
||
последовательные ASR и диаризация обработали час записи примерно за 23 минуты.
|
||
Диаризация увеличивает полное время примерно в 2,4 раза, но остаётся быстрее
|
||
реального времени и приемлема как явно включаемая функция. Конкретный Core i5
|
||
11-го поколения не проверен, поскольку такого устройства нет.
|
||
|
||
Исходные данные и ограничения зафиксированы в отчётах о
|
||
[калибровке](../benchmarks/2026-08-14-diarization-calibration.md),
|
||
[смешении говорящих](../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`.
|
||
Первая версия использует Pyannote segmentation 3.0, WeSpeaker ResNet34 LM,
|
||
порог кластеризации 0,89 и автоматическое число кластеров; известное число
|
||
участников можно передать явно.
|
||
|
||
Говорящий назначается [слову с временной
|
||
привязкой](../../CONTEXT.md#language), а не сегменту распознавания. Каждый
|
||
ASR-бэкенд приводит свой результат к общему набору слов с положением на
|
||
временной шкале. Проходы ASR и диаризации независимо получают одно аудио, после
|
||
чего отдельная операция сводит слова с интервалами разметки говорящих и
|
||
объединяет соседние слова одного говорящего в реплики. Распознавание по-прежнему
|
||
выполняется на полных сегментах и сохраняет контекст модели.
|
||
|
||
Диаризация не вводит грубый fallback на целый сегмент и не переключает
|
||
устройство ASR ради получения пословных таймкодов. Существующий GPU→CPU fallback
|
||
распознавания сохраняется и завершается до диаризации. Отсутствие пословных
|
||
таймкодов или ошибка инициализации диаризатора останавливают запуск до ASR.
|
||
Ошибка диаризации конкретного файла после успешного ASR не уничтожает полезный
|
||
результат: сохраняется обычный транскрипт с явным предупреждением и ненулевым
|
||
статусом, а батч продолжает остальные файлы. Порядок первой реализации, время
|
||
жизни диаризатора, кеш и подробная матрица поведения находятся в
|
||
[спецификации](../specs/2026-08-14-speaker-diarization.md).
|
||
|
||
## Последствия
|
||
|
||
- Общий контракт результата распознавания расширяется каноническими словами с
|
||
временной привязкой; сегменты распознавания сохраняются для совместимости и
|
||
контроля качества.
|
||
- FasterWhisper, ONNX-ASR и OpenVINO должны экспортировать один и тот же
|
||
пословный контракт. OpenVINO GenAI 2026.x уже предоставляет нужные таймкоды,
|
||
поэтому ограничение находится в адаптере проекта, а не в движке.
|
||
- `sherpa-onnx` становится обычной runtime-зависимостью, а две модели
|
||
диаризации скачиваются и кешируются лениво при первом запросе.
|
||
- Выход остаётся линейным Markdown с анонимными метками `Speaker N`.
|
||
Сопоставление голосов с именами и специальная запись перекрывающейся речи не
|
||
входят в ядро CLI.
|
||
- Последовательный режим задаёт корректный baseline. Параллельный запуск и
|
||
автоматическое включение на мощных устройствах требуют отдельных измерений
|
||
после стабилизации.
|
||
|
||
## Отклонённые альтернативы
|
||
|
||
| Альтернатива | Почему отклонена |
|
||
|---|---|
|
||
| Не делать диаризацию | Оставляет главный продуктовый разрыв, хотя измеренная стоимость допустима для явной функции |
|
||
| Мажоритарный говорящий на весь сегмент распознавания | Теряет чужие реплики на всех трёх проверенных записях |
|
||
| Сначала диаризация, затем ASR коротких интервалов | Лишает RNN-T длинного контекста и ухудшает согласование и пунктуацию |
|
||
| `pyannote.audio` | Тянет PyTorch и требует Hugging Face token с принятием лицензии |
|
||
| Сборка поверх приватных деталей `onnx-asr` | Экономит небольшую отдельную зависимость ценой нестабильного внутреннего API и собственной кластеризации |
|
||
| Параллельные проходы в первой версии | Нет прямого benchmark и измеренного общего пика памяти; сначала нужен корректный последовательный baseline |
|
||
| Автоматически назначать `Speaker ?` одинаковому соседнему кластеру | На двух записях с независимой опорой совпало только 52% назначений; лимиты по словам и округлённому времени не отделили короткие ответы другого участника |
|