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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue