Диаризация спикеров
Определите, кто и когда говорил в многоспикерной записи. Доступны два движка диаризации: двухэтапный пайплайн Pyannote (сегментация + связывание спикеров на основе активности, затем пост-обработка с эмбеддингами) и end-to-end модель Sortformer (CoreML, Neural Engine).
Движки
Выберите движок через --engine pyannote (по умолчанию) или --engine sortformer.
Pyannote (по умолчанию)
Двухэтапный пайплайн: сегментация Pyannote обрабатывает перекрывающиеся окна со связыванием спикеров на основе активности (корреляция Пирсона в зонах перекрытия) для назначения глобальных меток спикеров. Пост-обработка с извлечением эмбеддингов WeSpeaker обеспечивает идентификацию целевого спикера по аудио-регистрации.
Sortformer (CoreML)
End-to-end нейронная модель диаризации от NVIDIA. Напрямую предсказывает активность спикеров по кадрам до 4 спикеров без отдельных стадий эмбеддинга или кластеризации. Работает на Neural Engine через CoreML со стейт-буферами для потокового режима (FIFO + кеш спикеров).
Sortformer не создаёт эмбеддинги спикеров. Флаги --target-speaker и --embedding-engine доступны только с движком Pyannote.
Пайплайн 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
# Базовая диаризация (pyannote, по умолчанию)
.build/release/speech diarize meeting.wav
# End-to-end Sortformer (CoreML)
.build/release/speech diarize meeting.wav --engine sortformer
# Формат вывода RTTM (для оценки)
.build/release/speech diarize meeting.wav --rttm
# JSON-вывод
.build/release/speech diarize meeting.wav --json
Извлечение целевого спикера
Предоставьте аудио-регистрацию известного спикера, чтобы извлечь из записи только его сегменты. Пайплайн вычисляет эмбеддинг спикера из аудио-регистрации и находит кластер с наибольшим косинусным сходством.
# Извлечь сегменты конкретного спикера
.build/release/speech diarize meeting.wav --target-speaker enrollment.wav
Оценка DER
Оцените качество диаризации, сопоставляя с эталонным файлом RTTM. Пайплайн вычисляет Diarization Error Rate (DER), который измеряет долю времени, отнесённого неправильно.
# Оценка против эталонного 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]")
}
// Извлечение целевого спикера
let targetEmb = pipeline.embeddingModel.embed(audio: enrollmentAudio, sampleRate: 16000)
let segments = pipeline.extractSpeaker(
audio: meetingAudio, sampleRate: 16000,
targetEmbedding: targetEmb
)