Files
local-transcriber/docs/adr/007-word-level-speaker-diarization.md
T
Dmitriy Dementiev 98baf55572 docs(diarization): зафиксированы результаты приёмки и анализа
- Зачем:
  - решения по неопределённым словам и CPU-стоимости должны быть проверяемыми до публикации реализации.
- Что:
  - задокументировано отклонение post-hoc сглаживания Speaker ? по соседним кластерам.
  - добавлено исследование стоимости каскада и облегчённых embedding-моделей.
- Проверка:
  - `git diff --cached --check` — без ошибок.
2026-08-14 21:56:10 +03:00

13 KiB
Raw Blame History

ADR-007: Пословная диаризация через sherpa-onnx

Статус: Принято Дата: 2026-08-14

Контекст

Разделение говорящих — главный структурный разрыв между локальным транскриптом и облачными сервисами в сценарии подготовки конспектов и протоколов встреч. Диаризация при этом не является ещё одним движком распознавания: она независимо строит разметку говорящих, которую затем нужно свести с результатом 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-го поколения не проверен, поскольку такого устройства нет.

Исходные данные и ограничения зафиксированы в отчётах о калибровке, смешении говорящих и производительности на Intel.

Сглаживание неизвестного говорящего

После приёмки отдельно проверена идея автоматически назначать 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 и автоматическое число кластеров; известное число участников можно передать явно.

Говорящий назначается слову с временной привязкой, а не сегменту распознавания. Каждый ASR-бэкенд приводит свой результат к общему набору слов с положением на временной шкале. Проходы ASR и диаризации независимо получают одно аудио, после чего отдельная операция сводит слова с интервалами разметки говорящих и объединяет соседние слова одного говорящего в реплики. Распознавание по-прежнему выполняется на полных сегментах и сохраняет контекст модели.

Диаризация не вводит грубый fallback на целый сегмент и не переключает устройство ASR ради получения пословных таймкодов. Существующий GPU→CPU fallback распознавания сохраняется и завершается до диаризации. Отсутствие пословных таймкодов или ошибка инициализации диаризатора останавливают запуск до ASR. Ошибка диаризации конкретного файла после успешного ASR не уничтожает полезный результат: сохраняется обычный транскрипт с явным предупреждением и ненулевым статусом, а батч продолжает остальные файлы. Порядок первой реализации, время жизни диаризатора, кеш и подробная матрица поведения находятся в спецификации.

Последствия

  • Общий контракт результата распознавания расширяется каноническими словами с временной привязкой; сегменты распознавания сохраняются для совместимости и контроля качества.
  • 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% назначений; лимиты по словам и округлённому времени не отделили короткие ответы другого участника