Files
local-transcriber/docs/specs/2026-08-14-speaker-diarization.md
Dmitriy Dementiev b8347004da docs(diarization): уточнены правила сведения и формат
- Зачем:
  - спецификация не должна оставлять неоднозначности перед реализацией диаризации.
- Что:
  - уточнены сведение слов, таймкоды и предупреждение о малом кластере.
  - очищены терминология и граница между ADR и спецификацией.
  - сохранён результат сравнения WeSpeaker и CAMPPlus.
- Проверка:
  - git diff --cached --check и проверка относительных ссылок.
2026-08-14 17:09:27 +03:00

208 lines
16 KiB
Markdown

# Диаризация говорящих в транскрипте
## Проблема
Текущий транскрипт знает только сегменты распознавания. Их границы проходят по
тишине и не совпадают со сменой говорящего, поэтому один сегмент может содержать
несколько реплик. Назначение одной метки всему сегменту искажает структуру
диалога и делает транскрипт слабым сырьём для конспекта или протокола встречи.
[ADR-007](../adr/007-word-level-speaker-diarization.md) выбирает явную
пословную диаризацию через `sherpa-onnx`. Эта спецификация фиксирует форму первой
реализации, не меняя принятые решения.
## Цели
- По явному запросу строить реплики говорящих, сохраняя текст и длинный контекст
ASR.
- Поддержать один контракт слов с временной привязкой на FasterWhisper,
ONNX-ASR и OpenVINO.
- Сохранить предсказуемый single- и batch-режим при отсутствии речи, ошибках
диаризации и малых голосовых кластерах.
- Выдать компактный Markdown, удобный и человеку, и последующей обработке LLM.
## Не входит
- Автоматическое включение диаризации без флага и `--no-diarize`.
- Параллельный запуск ASR и диаризации.
- Сопоставление `Speaker N` с именами участников.
- Постоянный кеш разметки говорящих, голосовые эмбеддинги и диагностические
файлы.
- Отдельный синтаксис для перекрывающейся речи.
- Изменение устройства ASR или диаризации ради восстановления функции.
## Пользовательский интерфейс
- `--diarize` включает диаризацию. По умолчанию она выключена; ключ в
`.transcriber.toml` в первой версии не добавляется.
- `--speakers N`, где `N >= 1`, задаёт известное число участников и сам включает
диаризацию. Без него число кластеров определяется автоматически.
- `--threads N` остаётся единым бюджетом активного CPU-прохода. Значение целиком
получает сначала ASR, затем диаризация; `0` оставляет настройки библиотек.
- `--verbose` показывает в консоли прогресс, число кластеров и интервалов,
длительность прохода и предупреждения, но не создаёт дополнительные файлы.
- `--force` пересчитывает и ASR, и диаризацию. Без него готовый транскрипт, как и
сейчас, пропускается целиком.
`sherpa-onnx` входит в обычные runtime-зависимости. Модели
`sherpa-onnx-pyannote-segmentation-3-0` и
`wespeaker_en_voxceleb_resnet34_LM.onnx` скачиваются и кешируются лениво при
первом запросе диаризации. Отдельного installation extra нет.
## Контракты данных
Результат ASR сохраняет существующие сегменты распознавания и дополнительно
содержит упорядоченные канонические слова. Для каждого слова известны текст,
начало и конец на временной шкале исходной записи. Backend-специфичные токены и
слова нормализуются в адаптере бэкенда; их обратная сборка должна сохранять
распознанный текст с точностью до нормализации пробелов.
Разметка говорящих хранится отдельно от результата ASR: это упорядоченные
временные интервалы с анонимным идентификатором голосового кластера. ASR-бэкенд
не знает о кластерах, а диаризатор не знает о распознанном тексте.
Операция сведения суммирует временное перекрытие слова с интервалами каждого
кластера и назначает кластер с единственным наибольшим ненулевым перекрытием.
Если пересечения нет либо несколько кластеров делят наибольшее значение, слово
получает неизвестного говорящего: порядок кластеров не используется как
искусственная развязка ничьей. Соседние слова одного говорящего объединяются в
реплику; порядок слов и исходная временная шкала не меняются.
Все три ASR-пути обязаны предоставлять пословный контракт до включения
диаризации:
| Путь | Источник временных привязок |
|---|---|
| FasterWhisper | word timestamps CTranslate2 |
| ONNX-ASR | timestamped result модели |
| OpenVINO | word-level timestamps `WhisperPipeline` |
Грубая подстановка метки на весь сегмент распознавания запрещена.
## Пайплайн и время жизни
После prescan и только при наличии файлов для обработки загружаются ASR-модель
и один batch-owned диаризатор. До первого ASR проверяются доступность пословных
таймкодов, модели диаризации и возможность создать диаризатор. Ошибка этого
этапа останавливает весь запуск без частичных транскриптов.
Каждый файл обрабатывается последовательно:
1. ASR;
2. диаризация, если ASR нашёл речь;
3. сведение слов с разметкой говорящих;
4. форматирование и запись Markdown.
Один диаризатор последовательно переиспользуется для всех файлов батча. Данные
конкретной записи не становятся состоянием следующей. Объект освобождается при
завершении команды и не переносится через `TranscribeFileResult`.
Разметка говорящих хранится только до сведения. Единственный постоянный
продуктовый артефакт — Markdown-транскрипт; локальный кеш файлов моделей живёт
по существующим правилам загрузчиков.
## Конфигурация диаризации
Автоматический режим использует:
- Pyannote segmentation 3.0;
- WeSpeaker ResNet34 LM;
- порог кластеризации 0,89;
- автоматическое число кластеров.
`--speakers N` передаёт явное число кластеров вместо автоматического. Для
предупреждения используется диагностическая граница из калибровки: малым
считается кластер с речью короче максимума из 5 секунд и 2% длительности записи.
Граница влияет только на предупреждение — кластер не отбрасывается, получает
обычный номер и не меняет статус команды.
## Формат Markdown
При двух и более найденных кластерах тело состоит из линейных реплик:
```markdown
[09:07] Speaker 1: Мы же у них не разворачиваемся…
[09:18] Speaker 2: Мне гораздо проще накатывать обновления…
```
- Печатается только начало реплики; доли секунды отбрасываются, а не округляются
(`09:07.96``[09:07]`). Для записей длиннее часа используется
`[HH:MM:SS]`, иначе `[MM:SS]`.
- Метка `Speaker N` не получает Markdown-выделение.
- Нумерация начинается заново для каждого файла; номера назначаются по порядку
первого появления кластера в словах транскрипта.
- Смена говорящего всегда начинает новую реплику.
- Речь одного говорящего дополнительно разбивается по паузе не меньше 2 секунд
и максимальной длительности реплики 60 секунд.
- Слова без назначенного кластера группируются под `Speaker ?`.
- Перекрывающаяся речь остаётся в хронологическом порядке без особого
синтаксиса.
- В шапку добавляется число голосовых кластеров и предупреждения. Таблица
длительности по кластерам не выводится.
Без успешной разметки нескольких говорящих сохраняется нынешний формат
обычного транскрипта с диапазонами времени.
## Деградация и статус команды
| Ситуация | Артефакт | Консоль и шапка | Статус |
|---|---|---|---|
| Диаризация не запрошена | Обычный транскрипт | Без новых сообщений | Текущий |
| ASR не нашёл речь | Текущий пустой Markdown | Речь не обнаружена; диаризация не запускалась | 0 |
| Найдено не меньше двух кластеров | Транскрипт с `Speaker N` | Число кластеров и предупреждения | 0, если нет иной ошибки |
| Найден один кластер | Обычный транскрипт без `Speaker 1` | Причина в консоли и шапке | Ненулевой |
| Есть слова без пересечения | Транскрипт с `Speaker ?` | Число таких слов | 0 |
| Есть малый кластер | Транскрипт со всеми кластерами | Длительность малого кластера | 0 |
| Разметка пуста при непустом ASR | Обычный транскрипт | Явное предупреждение | Ненулевой |
| Ошибка диаризации конкретного файла | Обычный транскрипт | Явное предупреждение | Ненулевой |
| Нет пословных таймкодов или не инициализировался диаризатор | Файлы не обрабатываются | Понятная ошибка до ASR | Ненулевой |
В батче деградированный файл записывается, учитывается как неуспешная
диаризация, а остальные файлы продолжают обрабатываться. Итоговый статус батча
ненулевой, если хотя бы один файл деградировал или завершился ошибкой.
## Критерии приёмки
### Автоматические проверки
- Адаптер каждого ASR-бэкенда возвращает монотонные слова с временной
привязкой; сборка слов сохраняет текст сегментов с точностью до пробелов.
- Сведение покрывает смену говорящего, отсутствие пересечения, равное
наибольшее перекрытие с результатом `Speaker ?`, пунктуацию на границе реплик
и хронологический порядок.
- Форматтер проверяется для обычных, часовых, неизвестных и малых кластеров,
`Speaker ?`, отбрасывания долей таймкода, паузы 2 секунды и предела 60 секунд.
- CLI проверяет несовместимые и граничные значения, не запускает ASR при ошибке
preflight и соблюдает всю матрицу деградации в single- и batch-режимах.
- Батч создаёт диаризатор ровно один раз, пропускает его для пустого ASR,
переиспользует между файлами и не пишет промежуточный кеш.
- `--threads`, `--verbose` и `--force` сохраняют описанную семантику.
Все автоматические тесты мокают движки и не скачивают реальные модели.
### Ручная проверка
Финальная сборка прогоняется на трёх записях из отчётов карты:
- текст до и после сведения совпадает с точностью до переносов и пробелов;
- на плотном диалоге вручную проверяются устойчивость «голос → кластер», смены
говорящего, пропуски, малый остаточный кластер и перекрывающаяся речь;
- автоматическая конфигурация воспроизводит наблюдённую форму результата:
три основных и один малый остаточный кластер на Data Test, по два кластера на
T2 BDMA и Yantar;
- последовательный прогон на доступном Intel baseline остаётся быстрее
реального времени; фактические wall time и peak RSS записываются рядом с
результатом проверки.
## Документация
README должен описать новые CLI-флаги, ленивую загрузку моделей, ожидаемую
стоимость, формат `Speaker N`, предупреждения и batch-поведение. Направления
«Диаризация» и «Ручка нарезки абзацев» удаляются из backlog: первое перешло в
эту спецификацию, второе закрыто разрывом реплики на смене говорящего.
После стабилизации отдельно рассматриваются
[параллельный запуск](https://git.dementev.space/ddmitry/local-transcriber/issues/22)
и [hardware-aware default](https://git.dementev.space/ddmitry/local-transcriber/issues/23).