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

228 lines
6.5 KiB
Markdown

# 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
```python
@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 캐시
```python
_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
```typescript
// 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
```typescript
MEETING_MODE: {
// 기존 + 추가
DIARIZE: 'meetingMode:diarize',
DIARIZATION_PROGRESS: 'meetingMode:diarizationProgress',
}
```
### 3.3 MeetingModeService 확장
```typescript
async diarizeSession(sessionId: string): Promise<DiarizationResult> {
// 1. 녹음된 오디오 파일 경로 확인
// 2. sidecar /diarize 엔드포인트 호출
// 3. 결과의 화자 세그먼트와 전사 세그먼트 매칭
// 4. 각 전사 세그먼트에 speaker 라벨 부여
// 5. DB 저장 (rawTranscript에 화자 정보 포함)
}
```
### 3.4 오디오 파일 보존
현재 CaptionService는 오디오를 STT 후 폐기합니다.
회의 모드에서는 오디오를 임시 파일로 보존해야 합니다.
```typescript
// MeetingModeService.startRecording()에서
// AudioCaptureService의 raw PCM 데이터를 WAV 파일로 저장
private _audioFilePath: string | null = null
```
### 3.5 설정 UI
- HuggingFace 토큰 입력 (설정 > STT 탭)
- Diarization 활성화 토글
- 화자 수 설정 (0=자동, 2~10)
### 3.6 UI — 화자별 색상 구분
```typescript
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. 에러 코드
```typescript
DiarizationFailed = 895,
DiarizationModelNotLoaded = 896,
DiarizationTokenRequired = 897,
```
---
## 5. 구현 순서
### Step A: Sidecar (Python)
1. requirements.txt 업데이트
2. /diarize 엔드포인트 구현
3. Pipeline 캐시 + GPU/CPU 자동 선택
### Step B: 오디오 파일 보존
4. MeetingModeService에서 녹음 중 WAV 파일 저장
5. 녹음 종료 후 파일 경로를 DB에 저장
### Step C: 기반 레이어
6. types.ts — CaptionSegment.speaker, DiarizationSegment 등
7. ipc-channels.ts — DIARIZE, DIARIZATION_PROGRESS
8. errors.ts — 895-897
9. AppConfig — hfToken, diarizationEnabled, diarizationNumSpeakers
### Step D: 서비스
10. MeetingModeService.diarizeSession() — sidecar 호출 + 세그먼트 매칭
11. LocalSTTService — /diarize 호출 래퍼
### Step E: IPC + Preload
12. 핸들러 + preload API
### Step F: UI
13. 설정 — HF 토큰 + diarization 토글
14. TranscriptTab — 화자별 색상 바
15. EditableSegment — speaker 라벨 표시
16. 상세 페이지 — "화자 구분 실행" 버튼
### 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인+)에서 정확도 하락