# Диаризация говорящих в транскрипте ## Проблема Текущий транскрипт знает только сегменты распознавания. Их границы проходят по тишине и не совпадают со сменой говорящего, поэтому один сегмент может содержать несколько реплик. Назначение одной метки всему сегменту искажает структуру диалога и делает транскрипт слабым сырьём для конспекта или протокола встречи. [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-пути обязаны предоставлять пословный контракт до включения диаризации: | Путь | Источник временных привязок | |---|---| | 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; - `FastClusteringConfig.threshold = 0.89`; - автоматическое число кластеров. `--speakers N` передаёт явное число кластеров вместо автоматического. Малый кластер — кластер с речью короче максимума из 5 секунд и 2% длительности записи. Он не отбрасывается и получает обычный номер, но вызывает предупреждение. ## Формат Markdown При двух и более найденных кластерах тело состоит из линейных реплик: ```markdown [09:07] Speaker 1: Мы же у них не разворачиваемся… [09:18] Speaker 2: Мне гораздо проще накатывать обновления… ``` - Печатается только начало реплики, округлённое до целой секунды. Для записей длиннее часа используется `[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 ?`, паузы 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).