d3ro-voice/docs/phases/phase-15.5-speaker-diarization.md
Yun Chan fc7628327a Phase 15.5 구현: 화자 구분 (Speaker Diarization)
- Sidecar: pyannote /diarize 엔드포인트 + requirements.txt 업데이트
- LLM 기반 화자 추정 (Phase 1 — 오디오 보존 없이 전사 텍스트 분석)
- CaptionSegment에 speaker 필드 추가
- EditableSegment: 화자별 색상 바 + 화자 Chip 표시
- TranscriptTab: 화자 구분 버튼 + 진행률
- 설정: HuggingFace 토큰 입력 + Diarization 토글
- IPC: DIARIZE + DIARIZATION_PROGRESS 채널
- 에러코드: 895-897
- 12개 locale i18n
2026-04-08 12:14:00 +09:00

6.5 KiB

Phase 15.5: 화자 구분 (Speaker Diarization)

pyannote-audio community-1 기반 배치 모드 화자 구분 녹음 종료 후 전체 오디오에 diarization 적용 → 화자별 세그먼트 태깅


1. 개요

1.1 동작 방식

[녹음 종료] → 전사 완료
    │
    ├── CaptionService가 세그먼트 생성 (기존)
    │
    └── [후처리] Diarization 파이프라인
            ├── 1. 녹음된 오디오 파일을 sidecar에 전송
            ├── 2. pyannote가 화자 세그먼트 분석
            ├── 3. 전사 세그먼트와 화자 세그먼트 매칭
            ├── 4. 각 전사 세그먼트에 speaker 라벨 부여
            └── 5. UI에 화자별 색상 구분 표시

1.2 핵심 결정

  • 배치 모드: 실시간이 아닌, 녹음 종료 후 전체 오디오 분석
  • pyannote community-1: 오픈소스(MIT), 오프라인 가능
  • HF 토큰: 최초 모델 다운로드 시 1회 필요 → 설정에서 입력
  • 선택적: diarization ON/OFF 토글 (기본 OFF, 성능 부담)

2. Sidecar 수정

2.1 requirements.txt 추가

pyannote.audio>=3.3.0
torch>=2.0.0

2.2 새 엔드포인트: POST /diarize

@app.post("/diarize")
async def diarize(
    file: UploadFile = File(...),
    hf_token: str = Form(default=""),
    num_speakers: int = Form(default=0),  # 0 = 자동 감지
) -> JSONResponse:
    """오디오 파일의 화자 구분 수행."""
    # 1. 오디오 파일 임시 저장
    # 2. pyannote Pipeline 로드 (캐시)
    # 3. pipeline(audio_file) 실행
    # 4. 결과: [{ speaker: "SPEAKER_00", start: 0.5, end: 3.2 }, ...]
    return JSONResponse(content={"segments": segments})

2.3 Pipeline 캐시

_diarization_pipeline = None

def _get_diarization_pipeline(hf_token: str):
    global _diarization_pipeline
    if _diarization_pipeline is None:
        from pyannote.audio import Pipeline
        _diarization_pipeline = Pipeline.from_pretrained(
            "pyannote/speaker-diarization-community-1",
            use_auth_token=hf_token,
        )
        # CPU/GPU 자동 선택
        if _gpu_available:
            import torch
            _diarization_pipeline.to(torch.device("cuda"))
    return _diarization_pipeline

3. Electron 앱 수정

3.1 shared/types.ts

// CaptionSegment 확장
export interface CaptionSegment {
  id: string
  text: string
  timestamp: number
  isFinal: boolean
  speaker?: string  // 추가: "SPEAKER_00", "SPEAKER_01" 등
}

// Diarization 관련 타입
export interface DiarizationSegment {
  speaker: string
  start: number   // seconds
  end: number     // seconds
}

export interface DiarizationResult {
  segments: DiarizationSegment[]
  numSpeakers: number
}

3.2 shared/ipc-channels.ts

MEETING_MODE: {
  // 기존 + 추가
  DIARIZE: 'meetingMode:diarize',
  DIARIZATION_PROGRESS: 'meetingMode:diarizationProgress',
}

3.3 MeetingModeService 확장

async diarizeSession(sessionId: string): Promise<DiarizationResult> {
  // 1. 녹음된 오디오 파일 경로 확인
  // 2. sidecar /diarize 엔드포인트 호출
  // 3. 결과의 화자 세그먼트와 전사 세그먼트 매칭
  // 4. 각 전사 세그먼트에 speaker 라벨 부여
  // 5. DB 저장 (rawTranscript에 화자 정보 포함)
}

3.4 오디오 파일 보존

현재 CaptionService는 오디오를 STT 후 폐기합니다. 회의 모드에서는 오디오를 임시 파일로 보존해야 합니다.

// MeetingModeService.startRecording()에서
// AudioCaptureService의 raw PCM 데이터를 WAV 파일로 저장
private _audioFilePath: string | null = null

3.5 설정 UI

  • HuggingFace 토큰 입력 (설정 > STT 탭)
  • Diarization 활성화 토글
  • 화자 수 설정 (0=자동, 2~10)

3.6 UI — 화자별 색상 구분

const SPEAKER_COLORS = [
  d3roPalette.tag.purple,   // Speaker 1
  d3roPalette.tag.green,    // Speaker 2
  d3roPalette.tag.orange,   // Speaker 3
  d3roPalette.accent.amber, // Speaker 4
  d3roPalette.tag.red,      // Speaker 5
]

TranscriptTab + EditableSegment에서 화자별 좌측 색상 바 표시.


4. 에러 코드

DiarizationFailed = 895,
DiarizationModelNotLoaded = 896,
DiarizationTokenRequired = 897,

5. 구현 순서

Step A: Sidecar (Python)

  1. requirements.txt 업데이트
  2. /diarize 엔드포인트 구현
  3. Pipeline 캐시 + GPU/CPU 자동 선택

Step B: 오디오 파일 보존

  1. MeetingModeService에서 녹음 중 WAV 파일 저장
  2. 녹음 종료 후 파일 경로를 DB에 저장

Step C: 기반 레이어

  1. types.ts — CaptionSegment.speaker, DiarizationSegment 등
  2. ipc-channels.ts — DIARIZE, DIARIZATION_PROGRESS
  3. errors.ts — 895-897
  4. AppConfig — hfToken, diarizationEnabled, diarizationNumSpeakers

Step D: 서비스

  1. MeetingModeService.diarizeSession() — sidecar 호출 + 세그먼트 매칭
  2. LocalSTTService — /diarize 호출 래퍼

Step E: IPC + Preload

  1. 핸들러 + preload API

Step F: UI

  1. 설정 — HF 토큰 + diarization 토글
  2. TranscriptTab — 화자별 색상 바
  3. EditableSegment — speaker 라벨 표시
  4. 상세 페이지 — "화자 구분 실행" 버튼

Step G: i18n + 검증


6. 파일 목록

수정 (Python)

  • sidecar/main.py — /diarize 엔드포인트
  • sidecar/requirements.txt — pyannote.audio, torch

수정 (TypeScript)

  • src/shared/types.ts — CaptionSegment.speaker, DiarizationResult
  • src/shared/ipc-channels.ts — DIARIZE, DIARIZATION_PROGRESS
  • src/shared/errors.ts — 895-897
  • src/main/services/MeetingModeService.ts — diarizeSession, 오디오 보존
  • src/main/ipc/meeting-mode-handlers.ts — DIARIZE 핸들러
  • src/preload/index.ts — diarize API
  • src/renderer/components/meeting/TranscriptTab.tsx — 화자 색상
  • src/renderer/components/meeting/EditableSegment.tsx — speaker 표시
  • src/renderer/components/meeting/MeetingDetailTabs.tsx — diarize 버튼
  • src/renderer/components/SettingsModal.tsx — HF 토큰 + 토글
  • src/renderer/i18n/*.json — 12개 locale

7. 리스크

  1. PyInstaller 번들: torch 추가 시 ~1GB 증가 → lazy import로 완화
  2. CPU 성능: diarization은 GPU 없으면 느림 (30초 오디오 ~10초) → 프로그레스 바 필수
  3. HF 토큰 UX: 사용자에게 HuggingFace 가입 + 토큰 생성을 요구 → 가이드 UI
  4. 정확도: 소규모 회의(2~3인)에서 최적, 대규모(5인+)에서 정확도 하락