Phase 15 구현: Auto Polish + AI 채팅 + 마인드맵 + 공유
- Auto Polish: LLM으로 전사 다듬기 (필러 제거, 문장 교정) - AI 채팅: 회의 전사 기반 Q&A (LocalLLMService.chatStream 활용, 스트리밍) - 마인드맵: 빌트인 템플릿 추가 (마크다운 계층 트리) - 공유: 클립보드 복사 메뉴 추가 - MeetingChatPanel UI (접기/펼치기, 스트리밍 인디케이터) - TranscriptTab에 AI 다듬기 버튼 추가 - 12개 locale i18n 키 추가
This commit is contained in:
parent
d4928ffa60
commit
2b17bf47b7
26 changed files with 980 additions and 26 deletions
|
|
@ -65,6 +65,10 @@ class MeetingModeService extends EventEmitter {
|
|||
/** CaptionService session-saved 방지 플래그 */
|
||||
private _meetingModeActive = false
|
||||
|
||||
// Phase 15: Meeting AI Chat
|
||||
private _chatHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
|
||||
private _chatAbortController: AbortController | null = null
|
||||
|
||||
// ── 공개 접근자 ──
|
||||
|
||||
getState(): MeetingModeState {
|
||||
|
|
@ -823,6 +827,99 @@ class MeetingModeService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Phase 15: Auto Polish ──
|
||||
|
||||
async polishTranscript(sessionId: string): Promise<string> {
|
||||
const db = getDatabase()
|
||||
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
|
||||
if (!row) throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션 없음: ${sessionId}`)
|
||||
|
||||
const transcript = row.rawTranscript
|
||||
if (!transcript) throw new D3ROError(ErrorCode.MeetingPolishFailed, '전사 텍스트가 없습니다')
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
const result = await llm.generate(transcript, {
|
||||
systemPrompt: '다음 음성 전사 텍스트를 다듬어주세요. 필러 단어(음, 어, 그, 아 등)를 제거하고, 문장 구조를 자연스럽게 교정하되, 원래 의미와 내용은 절대 변경하지 마세요. 타임스탬프 형식 [MM:SS]은 그대로 유지하세요.',
|
||||
temperature: 0.3,
|
||||
})
|
||||
|
||||
db.update(meetingSessions).set({
|
||||
editedTranscript: result.text,
|
||||
updatedAt: Date.now(),
|
||||
}).where(eq(meetingSessions.id, sessionId)).run()
|
||||
|
||||
logger.info(`Auto Polish 완료: sessionId=${sessionId}`)
|
||||
return result.text
|
||||
}
|
||||
|
||||
// ── Phase 15: Meeting AI Chat ──
|
||||
|
||||
async chatWithMeeting(sessionId: string, userMessage: string): 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}`)
|
||||
|
||||
const transcript = row.editedTranscript ?? row.rawTranscript ?? ''
|
||||
|
||||
const systemPrompt = `당신은 회의 내용을 분석하는 AI 어시스턴트입니다.
|
||||
아래 회의 전사 내용을 참고하여 사용자의 질문에 정확하게 답변하세요.
|
||||
전사 내용에 없는 것은 "전사 내용에서 확인되지 않습니다"라고 답하세요.
|
||||
|
||||
## 회의 전사
|
||||
${transcript}`
|
||||
|
||||
this._chatHistory.push({ role: 'user', content: userMessage })
|
||||
|
||||
const { getLocalLLMService } = await import('./LocalLLMService')
|
||||
const llm = getLocalLLMService()
|
||||
|
||||
const messages = [
|
||||
{ role: 'system' as const, content: systemPrompt },
|
||||
...this._chatHistory,
|
||||
]
|
||||
|
||||
let fullResponse = ''
|
||||
this._chatAbortController = new AbortController()
|
||||
const signal = this._chatAbortController.signal
|
||||
|
||||
try {
|
||||
const generator = llm.chatStream(messages, { temperature: 0.5 })
|
||||
|
||||
for await (const token of generator) {
|
||||
if (signal.aborted) break
|
||||
fullResponse += token
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.DELTA, { token })
|
||||
}
|
||||
|
||||
this._chatHistory.push({ role: 'assistant', content: fullResponse })
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.MESSAGE, {
|
||||
role: 'assistant',
|
||||
content: fullResponse,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
this._sendToRenderer(IPC_CHANNELS.MEETING_CHAT.ERROR, {
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._chatAbortController = null
|
||||
}
|
||||
}
|
||||
|
||||
cancelMeetingChat(): void {
|
||||
if (this._chatAbortController) {
|
||||
this._chatAbortController.abort()
|
||||
this._chatAbortController = null
|
||||
}
|
||||
}
|
||||
|
||||
clearMeetingChatHistory(): void {
|
||||
this._chatHistory = []
|
||||
}
|
||||
|
||||
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