Files
local-transcriber/docs/adr/007-word-level-speaker-diarization.md
T
Dmitry Dementiev 1e14f2687a test(diarization): добавлен стенд сравнения CPU-моделей
- Зачем:
  - требовалось сравнить облегченные модели диаризации на старом CPU без изменения рабочей конфигурации.
- Что:
  - добавлен воспроизводимый и возобновляемый прогон матрицы Pyannote FP32/INT8 с WeSpeaker и TitaNet.
  - зафиксированы измерения 108 ячеек, ограничения эксперимента и решение сохранить модель по умолчанию.
  - локальные записи в каталоге tmp исключены из индекса Git.
- Проверка:
  - .venv/bin/python -m pytest.
  - /tmp/local-transcriber-ruff-bin/ruff check scripts/benchmarks/diarization_calibration.py tests/test_diarization_calibration.py.
2026-08-30 12:02:34 +03:00

15 KiB
Raw Blame History

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

Статус: Принято Дата: 2026-08-14 Обновлено: 2026-08-30

Решение

Диаризацию реализуем как явно включаемый пост-процессинг через sherpa-onnx. Говорящий назначается слову с временной привязкой, а не целому сегменту распознавания. Модель по умолчанию остаётся прежней: Pyannote Segmentation 3.0 FP32, WeSpeaker ResNet34 LM, порог кластеризации 0,89 и автоматическое число кластеров. Известное число говорящих можно передать явно.

Повторный CPU-тест FP32/INT8 × WeSpeaker/TitaNet не меняет это решение. INT8 с WeSpeaker дал только около 2% среднего ускорения без существенного выигрыша памяти и с худшими прокси-метриками. TitaNet оказался примерно на четверть быстрее, но потребовал ещё 147–149 МиБ RSS и не прошёл условие приёмки для общего автоматического порога. TitaNet можно исследовать только для режима с явно известным числом говорящих. Такой эксперимент должен завершиться слуховой проверкой. Код и модели по умолчанию на него не переключаем.

Контекст

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

Привязка одного говорящего ко всему сегменту распознавания оказалась слишком грубой. На трёх русскоязычных рабочих созвонах чужая реплика не короче секунды встретилась в 6–7% сегментов разговоров на двоих и в 27% сегментов встречи втроём. Сегменты распознавания проходят по тишине, а не по смене говорящего, поэтому сохранить контекст RNN-T и получить реплики можно только через более мелкую единицу сведения.

Эксперимент

Локальная связка sherpa-onnx с сегментацией Pyannote 3.0 и эмбеддингами WeSpeaker ResNet34 LM проверена на трёх записях с известным составом. В исходной калибровке для автоматического определения числа голосовых кластеров выбран порог 0,89: среди проверенных тогда значений только он дал на трёх контрольных фрагментах 3 / 2 / 2 кластера. На полной встрече втроём остался ложный кластер длительностью 19,1 секунды; поэтому малые кластеры нельзя молча отбрасывать, а разметку нельзя считать эталоном точных границ и перекрывающейся речи.

В повторном CPU-переборе на тех же пятиминутных фрагментах FP32 + WeSpeaker прошёл условие приёмки 3 / 2 / 2 на плато 0,85–0,89, а INT8 + WeSpeaker — на 0,85–0,90. Это не отменяет порог 0,89: новый прогон не имел повторов и новой слуховой проверки. Обе связки TitaNet провалили условие приёмки во всех проверенных точках; число содержательных кластеров не использовалось как оправдание лишних голосовых кластеров.

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

Сведение слов и разметки говорящих

Говорящий назначается слову с временной привязкой, а не сегменту распознавания. Каждый 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. Параллельный запуск и автоматическое включение на мощных устройствах требуют отдельных измерений после стабилизации.
  • Pyannote INT8 и TitaNet не становятся конфигурациями по умолчанию только на основании CPU-теста. Для TitaNet следующий допустимый эксперимент — режим с явно известным числом говорящих, повторами и слуховой проверкой.

Отклонённые альтернативы

Альтернатива Почему отклонена
Не делать диаризацию Оставляет главный продуктовый разрыв, хотя измеренная стоимость допустима для явной функции
Мажоритарный говорящий на весь сегмент распознавания Теряет чужие реплики на всех трёх проверенных записях
Сначала диаризация, затем ASR коротких интервалов Лишает RNN-T длинного контекста и ухудшает согласование и пунктуацию
pyannote.audio Тянет PyTorch и требует Hugging Face token с принятием лицензии
Сборка поверх приватных деталей onnx-asr Экономит небольшую отдельную зависимость ценой нестабильного внутреннего API и собственной кластеризации
Параллельные проходы в первой версии Нет прямого benchmark и измеренного общего пика памяти; сначала нужен корректный последовательный baseline
Автоматически назначать Speaker ? одинаковому соседнему кластеру На двух записях с независимой опорой совпало только 52% назначений; лимиты по словам и округлённому времени не отделили короткие ответы другого участника