화자 분리

다화자 녹음에서 누가 언제 말했는지 식별합니다. 경량 기본 Pyannote, 전체 파이프라인 Community-1(CoreML + 네이티브 PLDA/VBx), 스트리밍 Sortformer의 세 엔진을 제공합니다.

엔진

--engine pyannote(기본), --engine community1 또는 --engine sortformer로 선택합니다.

Pyannote (기본)

두 단계 파이프라인: Pyannote 분할이 활동 기반 화자 체이닝(오버랩 존에서의 Pearson 상관)과 함께 겹치는 윈도를 처리하여 전역 화자 레이블을 할당합니다. 사후 WeSpeaker 임베딩 추출로 등록 오디오를 통한 타겟 화자 식별이 가능합니다.

Community-1 (CoreML + 네이티브 VBx)

Community-1 CoreML 번들은 10초 컨텍스트, 1초 스텝, 마스킹된 WeSpeaker 임베딩, 네이티브 PLDA/VBx와 제약 할당을 사용하며 화자별 256차원 중심을 반환합니다. 5개 파일 패리티 검사는 4.66% DER / 21.43% JER입니다.

같은 하위 집합을 speech-swift 프레임 그리드 스코어러로 측정하면 Community-1은 9.31% DER / 16.20% JER, 실시간 대비 25.5×, 최대 RSS 1.54 GB, 기본 Pyannote는 5.03% / 9.28%, 실시간 대비 105.7×, 393 MB였습니다. 따라서 Community-1은 화자 수 제한과 중심 벡터를 제공하는 선택적 업스트림 패리티 파이프라인이며, 모든 경우에 정확도나 속도가 향상되는 것은 아닙니다. 두 채점 방식의 결과는 직접 비교할 수 없습니다.

Sortformer (CoreML)

NVIDIA의 엔드투엔드 신경 화자 분리 모델. 별도의 임베딩이나 클러스터링 단계 없이 최대 4명의 화자에 대해 프레임별 화자 활동을 직접 예측합니다. 스트리밍 상태 버퍼(FIFO + 화자 캐시)와 함께 CoreML을 통해 Neural Engine에서 실행됩니다.

라이브 오디오에서는 SortformerStreamingSession.streaming Sortformer 모델을 증분 실행하며, 임의 크기의 16 kHz 모노 PCM 버퍼를 받습니다. 560 ms 룩어헤드가 확보되면 480 ms 오디오 스텝으로 진행하며, M 시리즈 Neural Engine에서 5개 파일을 시험한 결과 500 ms 푸시당 지연 시간 중앙값은 12.4 ms였습니다. 화자 레이블은 각 윈도를 다시 클러스터링하지 않고 Arrival-Order Speaker Cache를 따르며, 모든 푸시는 전체 스트림 스냅숏을 반환합니다. finish()는 버퍼에 남은 꼬리를 플러시합니다. 모델 기반 동등성 테스트에서 최종 세그먼트는 전체 버퍼 .streaming 화자 분리 결과와 약 한 프레임(80 ms) 이내로 일치했습니다.

참고

Sortformer는 화자 임베딩을 생성하지 않습니다. --target-speaker--embedding-engine 플래그는 Pyannote 엔진에서만 사용할 수 있습니다.

공개된 CALLHOME 기준선

다음 표는 NVIDIA가 diar_streaming_sortformer_4spk-v2에 대해 공개한 익명 화자 원시 diarization 기준선입니다. 외부 참고값이며 speech-swift CoreML 변환의 로컬 측정값이나 이름이 지정된 화자 귀속 결과가 아닙니다.

데이터셋조건DER
CALLHOME Part 2참조 화자 2명6.57%
CALLHOME Part 2참조 화자 3명10.05%
CALLHOME Part 2참조 화자 4명12.44%

모든 행은 1.04 초 입력 버퍼 지연, 중첩 음성 포함, 0.25 초 collar와 별도 CALLHOME Part 1에서 조정한 후처리를 사용합니다. CALLHOME은 라이선스 데이터이며 번들에 포함되지 않습니다. 로컬 diarization-bench는 참조 화자 수별 비교 결과를 콘솔과 JSON의 byReferenceSpeakerCount에 따로 보고합니다.

Pyannote 파이프라인

기본 파이프라인은 두 단계로 실행됩니다:

1단계: 분할 + 화자 체이닝

Pyannote segmentation-3.0은 50% 오버랩의 10초 슬라이딩 윈도를 처리합니다. powerset 디코더가 7-클래스 출력을 화자별 확률로 변환합니다(윈도당 최대 3명의 로컬 화자). 인접한 윈도는 5초의 오버랩을 공유하며, 오버랩 존에서 확률 트랙 간 Pearson 상관을 계산하고 greedy 배타적 매칭을 통해 일관된 전역 화자 ID를 윈도 전체에 전파합니다.

2단계: 사후 임베딩

화자 분리 이후 WeSpeaker ResNet34-LM이 화자당 256차원 centroid 임베딩을 추출합니다. 이 임베딩은 타겟 화자 추출(--target-speaker)을 가능하게 하지만 화자 할당 자체를 결정하지는 않습니다.

pyannote.audio에서 마이그레이션

Python pyannote.audio 라이브러리에서 옮겨오는 경우 — pipeline.segmentation = ...을 설정하는 Pipeline 서브클래스를 대체하거나 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 체크포인트에서 변환되었으므로 세그멘테이션 logits는 부동소수점 정밀도 허용 범위 내에서 수치적으로 동등합니다. 세그멘테이션 후 체이닝(중첩 윈도우의 Pearson 상관 + 그리디 배타 매칭)과 사후 WeSpeaker 임베딩 단계는 Swift로 재구현되었지만 참조 Python 파이프라인과 비교 가능한 RTTM 출력을 생성합니다.

아직 지원되지 않음

Pyannote 엔진에는 OnlineSpeakerDiarization에 해당하는 스트리밍 API가 없습니다. 실시간 화자 분리는 --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-filterSilero VAD로 사전 필터링 (pyannote 전용)
--rttmRTTM 형식으로 출력
--jsonJSON 출력 형식
--score-againstDER 평가를 위한 레퍼런스 RTTM 파일
중요

화자 분리는 명확한 화자 턴이 있는 녹음에서 가장 잘 동작합니다. 심하게 겹치는 음성은 정확도를 떨어뜨릴 수 있습니다. 화자 수는 자동으로 결정됩니다.

모델 다운로드

모델은 첫 사용 시 자동으로 다운로드됩니다:

구성 요소모델크기HuggingFace
분할Pyannote-Segmentation-3.0약 5.7 MBaufklarer/Pyannote-Segmentation-MLX
화자 임베딩WeSpeaker-ResNet34-LM (MLX)약 25 MBaufklarer/WeSpeaker-ResNet34-LM-MLX
화자 임베딩WeSpeaker-ResNet34-LM (CoreML)약 25 MBaufklarer/WeSpeaker-ResNet34-LM-CoreML
SortformerSortformer Diarization (CoreML)약 240 MBaufklarer/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
)