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
This commit is contained in:
parent
da25791c75
commit
fc7628327a
25 changed files with 679 additions and 18 deletions
228
docs/phases/phase-15.5-speaker-diarization.md
Normal file
228
docs/phases/phase-15.5-speaker-diarization.md
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
# 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인+)에서 정확도 하락
|
||||
Loading…
Add table
Add a link
Reference in a new issue