Диаризация спикеров
Определите, кто и когда говорил в многоспикерной записи. Доступны три движка: лёгкий 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 2 | 2 эталонных спикера | 6,57% |
| CALLHOME Part 2 | 3 эталонных спикера | 10,05% |
| CALLHOME Part 2 | 4 эталонных спикера | 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 |
| Sortformer | Sortformer 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
)