Диаризация спикеров

Определите, кто и когда говорил в многоспикерной записи. Доступны три движка: лёгкий Pyannote по умолчанию, полный конвейер Community-1 (CoreML + нативный PLDA/VBx) и потоковый Sortformer.

Движки

Выберите --engine pyannote (по умолчанию), --engine community1 или --engine sortformer.

Pyannote (по умолчанию)

Двухэтапный пайплайн: сегментация Pyannote обрабатывает перекрывающиеся окна со связыванием спикеров на основе активности (корреляция Пирсона в зонах перекрытия) для назначения глобальных меток спикеров. Пост-обработка с извлечением эмбеддингов WeSpeaker обеспечивает идентификацию целевого спикера по аудио-регистрации.

Community-1 (CoreML + нативный VBx)

Пакет Community-1 CoreML использует контекст 10 с, шаг 1 с, маскированные эмбеддинги WeSpeaker, нативный PLDA/VBx и ограниченное назначение; возвращает 256-мерный центроид для каждого спикера. Проверка паритета на пяти файлах дала 4,66% DER / 21,43% JER.

При оценке того же поднабора фреймовым скорером speech-swift Community-1 показал 9,31% DER / 16,20% JER, 25,5× реального времени и пиковый RSS 1,54 ГБ; движок Pyannote по умолчанию — 5,03% / 9,28%, 105,7× реального времени и 393 МБ. Поэтому Community-1 — это опциональный конвейер для паритета с upstream, поддерживающий границы числа спикеров и возвращающий центроиды, а не универсальное улучшение точности или скорости. Результаты двух методов оценки нельзя сравнивать напрямую.

Sortformer (CoreML)

End-to-end нейронная модель диаризации от NVIDIA. Напрямую предсказывает активность спикеров по кадрам до 4 спикеров без отдельных стадий эмбеддинга или кластеризации. Работает на Neural Engine через CoreML со стейт-буферами для потокового режима (FIFO + кеш спикеров).

Для живого аудио SortformerStreamingSession инкрементально запускает модель Sortformer .streaming с монофоническими PCM-буферами 16 кГц любого размера. Когда доступны 560 мс опережающего контекста, обработка идёт шагами по 480 мс аудио; в тесте на пяти файлах с Neural Engine серии M медианная задержка составила 12,4 мс на push длительностью 500 мс. Метки говорящих следуют Arrival-Order Speaker Cache вместо повторной кластеризации каждого окна, а каждый push возвращает снимок всего потока. finish() обрабатывает оставшийся в буфере хвост. В проверке эквивалентности с моделью финальные сегменты совпали с полнобуферной диаризацией .streaming с точностью примерно до одного кадра 80 мс.

Примечание

Sortformer не создаёт эмбеддинги спикеров. Флаги --target-speaker и --embedding-engine доступны только с движком Pyannote.

Опубликованный baseline CALLHOME

В таблице приведён опубликованный NVIDIA исходный baseline диаризации анонимных спикеров для diar_streaming_sortformer_4spk-v2. Это внешняя справочная оценка, а не локальное измерение CoreML-конверсии speech-swift и не результат привязки имён к спикерам.

ДатасетУсловиеDER
CALLHOME Part 22 эталонных спикера6,57%
CALLHOME Part 23 эталонных спикера10,05%
CALLHOME Part 24 эталонных спикера12,44%

Для всех строк используются задержка входного буфера 1,04 с, учёт перекрывающейся речи, collar 0,25 с и постобработка, настроенная на непересекающейся части CALLHOME Part 1. CALLHOME лицензируется и не входит в комплект. Локальный diarization-bench отдельно выводит сопоставимые результаты по числу эталонных спикеров в консоли и в поле JSON byReferenceSpeakerCount.

Пайплайн Pyannote

Пайплайн по умолчанию работает в два этапа:

Этап 1: Сегментация + связывание спикеров

Pyannote segmentation-3.0 обрабатывает 10-секундные скользящие окна с перекрытием 50%. Powerset-декодер преобразует 7-классовый вывод в вероятности по спикерам (до 3 локальных спикеров на окно). Соседние окна имеют 5-секундное перекрытие — идентичность спикера распространяется между окнами через вычисление корреляции Пирсона между дорожками вероятностей в зоне перекрытия, с жадным эксклюзивным сопоставлением для консистентных глобальных ID спикеров.

Этап 2: Пост-обработка эмбеддингов

После диаризации WeSpeaker ResNet34-LM извлекает 256-мерный центроидный эмбеддинг на каждого спикера. Эти эмбеддинги позволяют извлечение целевого спикера (--target-speaker), но не влияют на само назначение спикеров.

Миграция с pyannote.audio

Если вы переходите с Python-библиотеки pyannote.audio — заменяете подкласс Pipeline, в котором задаётся pipeline.segmentation = ..., или мигрируете с сервера, на котором запущен pyannote/speaker-diarization-3.1 — Soniqo оборачивает ту же модель Pyannote-Segmentation-3.0 и запускает её полностью на устройстве на Apple Silicon. Без рантайма Python, без CUDA, без токена Hugging Face во время инференса.

pyannote.audio (Python)Soniqo (Swift)
Pipeline.from_pretrained("pyannote/speaker-diarization-3.1") DiarizationPipeline.fromPretrained()
pipeline(audio_file) pipeline.diarize(audio: samples, sampleRate: 16000)
pipeline.segmentation = ... (пользовательский подкласс) Фиксировано: Pyannote-Segmentation-3.0 (MLX или CoreML, автовыбор)
diarization.itertracks(yield_label=True) for seg in result.segments { ... }
diarization.write_rttm(file) CLI: --rttm
pyannote.metrics.diarization.DiarizationErrorRate CLI: --score-against reference.rttm

Веса Pyannote-Segmentation-3.0 конвертированы из upstream-чекпойнта HuggingFace, поэтому логиты сегментации численно эквивалентны в пределах точности с плавающей запятой. Пост-сегментационная сшивка (корреляция Пирсона в перекрытиях окон + жадное эксклюзивное сопоставление) и пост-обработка эмбеддингов WeSpeaker переписаны на Swift, но дают сравнимый с эталонной Python-пайплайн RTTM-вывод.

Пока не поддерживается

Для движка Pyannote нет потокового аналога OnlineSpeakerDiarization. Для диаризации в реальном времени используйте --engine sortformer, который запускает модель Sortformer с FIFO-буфером и кэшем спикеров.

Использование CLI

# Basic diarization (pyannote, default)
.build/release/speech diarize meeting.wav

# End-to-end Sortformer (CoreML)
.build/release/speech diarize meeting.wav --engine sortformer

# RTTM output format (for evaluation)
.build/release/speech diarize meeting.wav --rttm

# JSON output
.build/release/speech diarize meeting.wav --json

Извлечение целевого спикера

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

# Extract segments for a specific speaker
.build/release/speech diarize meeting.wav --target-speaker enrollment.wav

Оценка DER

Оцените качество диаризации, сопоставляя с эталонным файлом RTTM. Пайплайн вычисляет Diarization Error Rate (DER), который измеряет долю времени, отнесённого неправильно.

# Score against reference RTTM
.build/release/speech diarize meeting.wav --score-against reference.rttm

Вывод RTTM

Флаг --rttm выдаёт вывод в формате Rich Transcription Time Marked — стандартном формате для оценки диаризации. Каждая строка имеет вид:

SPEAKER filename 1 start_time duration <NA> <NA> speaker_id <NA> <NA>

Опции

ОпцияОписание
--target-speakerАудио-регистрация для извлечения целевого спикера (только pyannote)
--embedding-engineДвижок эмбеддингов спикеров: mlx или coreml (только pyannote)
--vad-filterПредварительная фильтрация через Silero VAD (только pyannote)
--rttmВывод в формате RTTM
--jsonФормат вывода JSON
--score-againstЭталонный RTTM-файл для оценки DER
Важно

Диаризация лучше всего работает на записях с чёткими сменами спикеров. Сильно перекрывающаяся речь может снижать точность. Количество спикеров определяется автоматически.

Загрузка моделей

Модели автоматически скачиваются при первом использовании:

КомпонентМодельРазмерHuggingFace
СегментацияPyannote-Segmentation-3.0~5.7 МБaufklarer/Pyannote-Segmentation-MLX
Эмбеддинг спикераWeSpeaker-ResNet34-LM (MLX)~25 МБaufklarer/WeSpeaker-ResNet34-LM-MLX
Эмбеддинг спикераWeSpeaker-ResNet34-LM (CoreML)~25 МБaufklarer/WeSpeaker-ResNet34-LM-CoreML
SortformerSortformer Diarization (CoreML)~240 МБaufklarer/Sortformer-Diarization-CoreML

Swift API

import SpeechVAD

let pipeline = try await DiarizationPipeline.fromPretrained()
let result = pipeline.diarize(audio: samples, sampleRate: 16000)
for seg in result.segments {
    print("Speaker \(seg.speakerId): [\(seg.startTime)s - \(seg.endTime)s]")
}

// Target speaker extraction
let targetEmb = pipeline.embeddingModel.embed(audio: enrollmentAudio, sampleRate: 16000)
let segments = pipeline.extractSpeaker(
    audio: meetingAudio, sampleRate: 16000,
    targetEmbedding: targetEmb
)