From b8092aade51f942eb0b410fed212740cc96198d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Fri, 14 Aug 2026 11:44:02 +0300 Subject: [PATCH] =?UTF-8?q?docs(diarization):=20=D0=BF=D0=BE=D0=B4=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=20ASR-=D1=81?= =?UTF-8?q?=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - требовалось проверить долю смешанных ASR-сегментов на трёх записях, включая разговоры на двоих. - Что: - добавлен отчёт с долями сегментов и времени для трёх рабочих созвонов. - обвязка замеров переведена на откалиброванный порог 0,89. - исследовательский скрипт приведён к формату ruff. - Проверка: - выполнены три полных прогона bench_conflict.py с WeSpeaker и порогом 0,89. - uv run ruff check и ruff format --check прошли успешно. --- .scratch/diarization/README.md | 11 +- .scratch/diarization/bench_conflict.py | 19 +++- .scratch/diarization/common.py | 6 +- .../2026-08-14-asr-segment-speaker-mixing.md | 102 ++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md diff --git a/.scratch/diarization/README.md b/.scratch/diarization/README.md index b046210..a24d99c 100644 --- a/.scratch/diarization/README.md +++ b/.scratch/diarization/README.md @@ -41,17 +41,18 @@ curl -sSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-rec export PYTHONIOENCODING=utf-8 uv run python .scratch/diarization/bench_asr.py "<путь к записи>" -uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.9 +uv run --with sherpa-onnx python .scratch/diarization/bench_diar.py "<путь>" 8 0.89 uv run --with sherpa-onnx python .scratch/diarization/bench_sweep.py "<путь>" uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py "<путь>" ``` ## Что стоит знать до запуска -- **Порог кластеризации не откалиброван.** По умолчанию стоит 0,9 — значение из - разведки, подобранное на одной записи и на ней же проверенное. На пороге 0,5 - из примеров sherpa-onnx получалось 29 говорящих вместо трёх. Калибровка — это - тикет #10, до его закрытия любое значение считается временным. +- **Порог кластеризации откалиброван.** По умолчанию стоит 0,89 — единственное + проверенное значение, которое без знания числа участников дало правильные + 3 / 2 / 2 кластера на трёх калибровочных фрагментах. Решение и ограничения + описаны в + [отчёте о калибровке](../../docs/benchmarks/2026-08-14-diarization-calibration.md). - **Свип дорогой.** Каждая конфигурация — полный прогон сегментации и эмбеддингов, около 2,5 минут на 26-минутную запись, и время от настроек кластеризации практически не зависит. Свип вести на коротком фрагменте. diff --git a/.scratch/diarization/bench_conflict.py b/.scratch/diarization/bench_conflict.py index 46664ca..79c7555 100644 --- a/.scratch/diarization/bench_conflict.py +++ b/.scratch/diarization/bench_conflict.py @@ -74,7 +74,13 @@ def main(audio_path: str, threshold: float, threads: int) -> None: continue major = max(per_speaker, key=lambda k: per_speaker[k]) rows.append( - (seg, major, per_speaker[major] / total, total - per_speaker[major], dict(per_speaker)) + ( + seg, + major, + per_speaker[major] / total, + total - per_speaker[major], + dict(per_speaker), + ) ) n = len(rows) @@ -95,7 +101,9 @@ def main(audio_path: str, threshold: float, threads: int) -> None: (f"с чужой репликой (>={INTERJECTION_S:.0f} с)", lost), ("без говорящего вообще", unattributed), ): - print(f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин") + print( + f" {label:<34} {len(group):4d} {len(group) / n * 100:5.1f}% {minutes(group):5.1f} мин" + ) print() for level in PURITY_LEVELS: @@ -107,9 +115,12 @@ def main(audio_path: str, threshold: float, threads: int) -> None: print("\n" + "=" * 64) print("ХУДШИЕ 12 СЕГМЕНТОВ (больше всего чужой речи внутри):") - for seg, major, purity, others, per_speaker in sorted(attributed, key=lambda r: -r[3])[:12]: + for seg, major, purity, others, per_speaker in sorted( + attributed, key=lambda r: -r[3] + )[:12]: share = ", ".join( - f"spk{k}={v:.1f}с" for k, v in sorted(per_speaker.items(), key=lambda x: -x[1]) + f"spk{k}={v:.1f}с" + for k, v in sorted(per_speaker.items(), key=lambda x: -x[1]) ) print( f"\n [{seg.start:7.1f}-{seg.end:7.1f}] ({seg.end - seg.start:4.1f} с) " diff --git a/.scratch/diarization/common.py b/.scratch/diarization/common.py index 56d4a35..2302295 100644 --- a/.scratch/diarization/common.py +++ b/.scratch/diarization/common.py @@ -22,9 +22,9 @@ EMBEDDING = MODELS / "wespeaker_en_voxceleb_resnet34_LM.onnx" SAMPLE_RATE = 16_000 -# Настройки разведки 2026-08-12. Порог 0.9 дал верное число говорящих на -# контрольной записи; на 0.5 из примеров sherpa-onnx получалось 29 вместо трёх. -DISCOVERY_THRESHOLD = 0.9 +# Конфигурация, выбранная калибровкой 2026-08-14 на трёх записях. +# На 0.5 из примеров sherpa-onnx получалось 29 говорящих вместо трёх. +DISCOVERY_THRESHOLD = 0.89 DEFAULT_THREADS = 8 diff --git a/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md b/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md new file mode 100644 index 0000000..52c61da --- /dev/null +++ b/docs/benchmarks/2026-08-14-asr-segment-speaker-mixing.md @@ -0,0 +1,102 @@ +# Смешение говорящих внутри ASR-сегментов + +**Дата:** 2026-08-14 + +**Статус:** проверка на трёх русскоязычных рабочих созвонах. Не оценка DER и +не решение о единице привязки спикера к тексту. + +## Вопрос + +Воспроизводится ли смешение говорящих внутри ASR-сегментов на других записях, +или результат разведки — особенность одной встречи на троих? + +В [разведочном замере](2026-08-12-diarization-feasibility.md) 27% сегментов +контрольной записи содержали не меньше секунды чужой речи. На них приходилось +больше половины времени ASR-сегментов. Проверка повторена на тех же трёх +записях, на которых калибровалась диаризация, включая два разговора на двоих. + +## Метод + +ASR выполнялся через модель по умолчанию ONNX-пути +`gigaam-v3-e2e-rnnt` INT8 с языком `ru`. Диаризация выполнялась через +`sherpa-onnx` 1.13.5 с выбранной в +[калибровке](2026-08-14-diarization-calibration.md) конфигурацией: + +- сегментация `sherpa-onnx-pyannote-segmentation-3-0`; +- эмбеддинги `wespeaker_en_voxceleb_resnet34_LM.onnx`; +- `FastClusteringConfig.threshold=0.89`; +- автоматическое определение числа кластеров (`num_clusters=-1`). + +Для каждого ASR-сегмента считалось перекрытие с интервалами каждого кластера. +Кластер с максимальным перекрытием считался мажоритарным, а сумма перекрытий +остальных кластеров — чужой речью. Сегменты разделены на четыре категории: + +- **чистый** — перекрытие только с одним кластером; +- **с поддакиванием** — меньше 1 секунды чужой речи; +- **с чужой репликой** — не меньше 1 секунды чужой речи; +- **без спикера** — нет перекрытия с интервалами диаризации. + +Время категории — сумма длительностей попавших в неё ASR-сегментов. Это не +сумма времени речи: интервалы разных кластеров могут перекрываться при +наложенной речи. Метрика отвечает на узкий вопрос, насколько огрубляет текст +одна метка спикера на весь ASR-сегмент. Она не измеряет точность диаризации. + +## Результаты + +| Запись | Участников | ASR-сегментов | Чистые | Поддакивание <1 с | Чужая реплика ≥1 с | Без спикера | +|---|---:|---:|---:|---:|---:|---:| +| Data Test | 3 | 274 | 164 (59,9%) | 30 (10,9%) | **74 (27,0%)** | 6 (2,2%) | +| T2 BDMA | 2 | 223 | 167 (74,9%) | 34 (15,2%) | **14 (6,3%)** | 8 (3,6%) | +| Yantar | 2 | 339 | 275 (81,1%) | 20 (5,9%) | **24 (7,1%)** | 20 (5,9%) | + +| Запись | Время всех ASR-сегментов | Время сегментов с чужой репликой ≥1 с | Доля времени | +|---|---:|---:|---:| +| Data Test | 21,89 мин | **11,20 мин** | **51,2%** | +| T2 BDMA | 12,56 мин | **1,53 мин** | **12,2%** | +| Yantar | 15,93 мин | **2,67 мин** | **16,8%** | + +На двух разговорах на двоих вместе чужая реплика не меньше секунды встречается +в 38 из 562 сегментов (6,8%) и затрагивает 4,20 из 28,49 минуты (14,7%). По +сравнению со встречей на троих это в четыре раза меньше по доле сегментов и +примерно в 3,5 раза меньше по доле времени. + +## Вывод + +Смешение говорящих внутри ASR-сегмента — общее свойство проверенного материала, +а не аномалия одной записи: оно воспроизвелось на обоих разговорах на двоих. +Однако тяжесть сильно зависит от характера разговора. Значение 27% сегментов и +51% времени не переносится на двухсторонние созвоны: там получено 6–7% +сегментов и 12–17% времени. + +Мажоритарная метка на весь ASR-сегмент поэтому остаётся заметным огрублением +даже на разговорах на двоих, а на плотной встрече втроём теряет реплики в +массовом масштабе. Эти данные не выбирают единицу привязки сами по себе, но +исключают предположение, что сегментная привязка безопасна для всех обычных +созвонов без пословной привязки или явной пометки качества. + +## Ограничения + +- Все три записи — русскоязычные рабочие созвоны одного пользователя; другие + микрофоны, шумы и стили разговора не представлены. +- Диаризация служит опорной разметкой и сама содержит ошибки. На контрольной + записи есть ложный остаточный кластер, редкие пропуски и неполная разметка + перекрывающейся речи. +- На двухсторонних записях один голос заметно доминирует по времени. Баланс + реплик может влиять на долю смешанных сегментов. +- Порог 1 секунда разделяет короткие вставки и потенциально потерянные реплики, + но не доказывает смысловую важность каждого фрагмента. + +## Воспроизводимость + +Расчёт выполняется скриптом +[`bench_conflict.py`](../../.scratch/diarization/bench_conflict.py): + +```powershell +$env:PYTHONIOENCODING = "utf-8" +uv run --with sherpa-onnx python .scratch/diarization/bench_conflict.py ` + "<путь к записи>" 0.89 8 +``` + +Медиа и сырые JSON не добавлены в репозиторий: они содержат локальные пути и +относятся к рабочим созвонам. SHA-256 всех трёх исходных файлов зафиксированы в +[отчёте о калибровке](2026-08-14-diarization-calibration.md).