화자 분리

다화자 녹음에서 누가 언제 말했는지 식별합니다. 두 가지 화자 분리 엔진을 제공합니다: 두 단계의 Pyannote 파이프라인(분할 + 활동 기반 화자 체이닝, 이후 사후 임베딩)과 엔드투엔드 Sortformer 모델(CoreML, Neural Engine)입니다.

엔진

--engine pyannote (기본) 또는 --engine sortformer로 엔진을 선택합니다.

Pyannote (기본)

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

Sortformer (CoreML)

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

참고

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

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 사용법

# 기본 화자 분리 (pyannote, 기본)
.build/release/speech diarize meeting.wav

# 엔드투엔드 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-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]")
}

// 타겟 화자 추출
let targetEmb = pipeline.embeddingModel.embed(audio: enrollmentAudio, sampleRate: 16000)
let segments = pipeline.extractSpeaker(
    audio: meetingAudio, sampleRate: 16000,
    targetEmbedding: targetEmb
)