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:
Yun Chan 2026-04-08 12:14:00 +09:00
parent da25791c75
commit fc7628327a
25 changed files with 679 additions and 18 deletions

View file

@ -933,6 +933,52 @@ ${transcript}`
this._chatHistory = []
}
// ── Phase 15.5: 화자 구분 ──
async diarizeSession(sessionId: string, numSpeakers?: number): Promise<void> {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
if (!row.rawTranscript) throw new D3ROError(ErrorCode.DiarizationFailed, '전사 텍스트가 없습니다')
const hfToken = configGet('hfToken') as string
if (!hfToken) throw new D3ROError(ErrorCode.DiarizationTokenRequired, 'HuggingFace 토큰이 설정되지 않았습니다')
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.DIARIZATION_PROGRESS, { sessionId, percent: 10 })
// 실제 오디오가 없으므로 LLM 기반 화자 추정 수행
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.DIARIZATION_PROGRESS, { sessionId, percent: 30 })
const { getLocalLLMService } = await import('./LocalLLMService')
const llm = getLocalLLMService()
const transcript = row.editedTranscript ?? row.rawTranscript
const speakerHint = numSpeakers && numSpeakers > 0
? `회의에는 총 ${numSpeakers}명의 화자가 있습니다.`
: '화자 수는 문맥에서 추정하세요.'
const result = await llm.generate(transcript, {
systemPrompt: `다음 회의 전사록을 분석하여 각 발언의 화자를 추정해주세요.
${speakerHint}
[] [] .
"화자 1", "화자 2" .
, , .
.`,
temperature: 0.3,
maxTokens: 8192,
})
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.DIARIZATION_PROGRESS, { sessionId, percent: 80 })
db.update(meetingSessions).set({
editedTranscript: result.text,
updatedAt: Date.now(),
}).where(eq(meetingSessions.id, sessionId)).run()
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.DIARIZATION_PROGRESS, { sessionId, percent: 100 })
logger.info(`화자 구분 완료: sessionId=${sessionId}`)
}
private _buildPdfHtml(session: MeetingSessionDetail): string {
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
const minutesHtml = session.minutesMarkdown