Phase 12~13 전체 구현: Pro+ 피처 6종 + 음성 대화 + RAG + OS 자동화

Phase 12:
- FileTranscriptionService: ffmpeg PCM 변환 + 30초 청크 순차 STT
- MeetingSummaryService: 자막 세션 → LLM 자동 요약 + DB summaryText
- DictationTemplateService: 필드별 음성 입력 상태 머신 + 프리셋 3개

Phase 13.1:
- VoiceConversationService: STT→Ollama /api/chat→TTS 대화 루프 (10턴)
- TTSPlaybackService: Windows SAPI 문장 단위 큐 재생
- LocalLLMService.chatStream: Ollama /api/chat 스트리밍

Phase 13.2:
- RAGService: Ollama 임베딩 + SQLite 벡터 + 코사인 유사도 검색
- KnowledgeBasePage: 문서 관리 + 질문/답변 UI
- PDF 파서: zlib FlateDecode 해제 + BT/ET 텍스트 추출

Phase 13.3:
- VoiceActionService: LLM JSON 액션 플랜 생성 + 실행
- 프리셋 6개 (크롬/메모장/탐색기/볼륨), 위험 명령 차단

공통: IPC ~70채널, 에러코드 780-878, i18n 100+키
버그픽스: 라이선스 로컬 키 우선, i18n featureLabel, DOM 중첩
This commit is contained in:
Yun Chan 2026-04-05 23:52:14 +09:00
parent a31f96bbb8
commit eb83682269
38 changed files with 5678 additions and 19 deletions

View file

@ -95,6 +95,47 @@ import type {
UsageQuota,
UpgradePromptEvent,
TierComparison,
// Phase 12
FileTranscriptionStartParams,
// Phase 13.1
ConversationSessionInfo,
ConversationMessage,
ConversationSendParams,
ConversationAssistantDelta,
ConversationAssistantMessage,
ConversationError,
// Phase 13.2
RAGDocument,
RAGQueryParams,
RAGQueryResult,
RAGStateInfo,
RAGIndexProgress,
RAGAddDocumentParams,
RAGRemoveDocumentParams,
// Phase 13.3
VoiceActionExecuteParams,
VoiceActionPreset,
VoiceActionHistoryEntry,
VoiceActionPlannedEvent,
VoiceActionExecutedEvent,
VoiceActionErrorEvent,
FileTranscriptionProgress,
FileTranscriptionResult,
FileTranscriptionStateInfo,
MeetingSummarizeParams,
MeetingSummaryResult,
MeetingSummaryGetParams,
MeetingSummaryExportParams,
MeetingSummaryProgress,
DictationTemplate,
CreateTemplateParams,
UpdateTemplateParams,
DeleteTemplateParams,
StartTemplateSessionParams,
SetFieldValueParams,
TemplateSessionInfo,
TemplateFieldCompletedEvent,
TemplateSessionCompletedEvent,
} from '@shared/types'
import { Feature } from '@shared/types'
import type { IPCResult } from '@shared/errors'
@ -382,6 +423,136 @@ const electronAPI = {
onTierChanged: (cb: (e: LicenseInfo) => void): Unsubscribe =>
on(IPC_CHANNELS.LICENSE.TIER_CHANGED, cb),
},
// ── File Transcription (Phase 12.1) ────────────────────
fileTranscription: {
start: (params: FileTranscriptionStartParams) =>
invoke<FileTranscriptionResult>(IPC_CHANNELS.FILE_TRANSCRIPTION.START, params),
cancel: () =>
invoke<void>(IPC_CHANNELS.FILE_TRANSCRIPTION.CANCEL),
getState: () =>
invoke<FileTranscriptionStateInfo>(IPC_CHANNELS.FILE_TRANSCRIPTION.GET_STATE),
onProgress: (cb: (data: FileTranscriptionProgress) => void): Unsubscribe =>
on(IPC_CHANNELS.FILE_TRANSCRIPTION.PROGRESS, cb),
onComplete: (cb: (data: FileTranscriptionResult) => void): Unsubscribe =>
on(IPC_CHANNELS.FILE_TRANSCRIPTION.COMPLETE, cb),
onError: (cb: (data: { message: string }) => void): Unsubscribe =>
on(IPC_CHANNELS.FILE_TRANSCRIPTION.ERROR, cb),
},
// ── Meeting Summary (Phase 12.2) ─────────────────────
meetingSummary: {
summarize: (params: MeetingSummarizeParams) =>
invoke<MeetingSummaryResult>(IPC_CHANNELS.MEETING_SUMMARY.SUMMARIZE, params),
getSummary: (params: MeetingSummaryGetParams) =>
invoke<MeetingSummaryResult | null>(IPC_CHANNELS.MEETING_SUMMARY.GET_SUMMARY, params),
exportMarkdown: (params: MeetingSummaryExportParams) =>
invoke<string>(IPC_CHANNELS.MEETING_SUMMARY.EXPORT_MARKDOWN, params),
onSummaryReady: (cb: (data: MeetingSummaryResult) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_SUMMARY.SUMMARY_READY, cb),
onProgress: (cb: (data: MeetingSummaryProgress) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_SUMMARY.SUMMARY_PROGRESS, cb),
},
// ── Dictation Templates (Phase 12.3) ─────────────────
dictationTemplate: {
getAll: () =>
invoke<DictationTemplate[]>(IPC_CHANNELS.DICTATION_TEMPLATE.GET_ALL),
create: (params: CreateTemplateParams) =>
invoke<DictationTemplate>(IPC_CHANNELS.DICTATION_TEMPLATE.CREATE, params),
update: (params: UpdateTemplateParams) =>
invoke<DictationTemplate>(IPC_CHANNELS.DICTATION_TEMPLATE.UPDATE, params),
delete: (params: DeleteTemplateParams) =>
invoke<void>(IPC_CHANNELS.DICTATION_TEMPLATE.DELETE, params),
startSession: (params: StartTemplateSessionParams) =>
invoke<void>(IPC_CHANNELS.DICTATION_TEMPLATE.START_SESSION, params),
cancelSession: () =>
invoke<void>(IPC_CHANNELS.DICTATION_TEMPLATE.CANCEL_SESSION),
getSessionState: () =>
invoke<TemplateSessionInfo | null>(IPC_CHANNELS.DICTATION_TEMPLATE.GET_SESSION_STATE),
setFieldValue: (params: SetFieldValueParams) =>
invoke<void>(IPC_CHANNELS.DICTATION_TEMPLATE.SET_FIELD_VALUE, params),
onSessionStateChanged: (cb: (data: TemplateSessionInfo) => void): Unsubscribe =>
on(IPC_CHANNELS.DICTATION_TEMPLATE.SESSION_STATE_CHANGED, cb),
onFieldCompleted: (cb: (data: TemplateFieldCompletedEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.DICTATION_TEMPLATE.FIELD_COMPLETED, cb),
onSessionCompleted: (cb: (data: TemplateSessionCompletedEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.DICTATION_TEMPLATE.SESSION_COMPLETED, cb),
},
// ── RAG (Phase 13.2) ──────────────────────────────────
rag: {
addDocument: (params?: RAGAddDocumentParams) =>
invoke<RAGDocument>(IPC_CHANNELS.RAG.ADD_DOCUMENT, params ?? {}),
removeDocument: (params: RAGRemoveDocumentParams) =>
invoke<void>(IPC_CHANNELS.RAG.REMOVE_DOCUMENT, params),
getDocuments: () =>
invoke<RAGDocument[]>(IPC_CHANNELS.RAG.GET_DOCUMENTS),
query: (params: RAGQueryParams) =>
invoke<RAGQueryResult>(IPC_CHANNELS.RAG.QUERY, params),
getState: () =>
invoke<RAGStateInfo>(IPC_CHANNELS.RAG.GET_STATE),
reindex: (documentId: string) =>
invoke<void>(IPC_CHANNELS.RAG.REINDEX, { documentId }),
onIndexProgress: (cb: (data: RAGIndexProgress) => void): Unsubscribe =>
on(IPC_CHANNELS.RAG.INDEX_PROGRESS, cb),
onIndexComplete: (cb: (data: { documentId: string; fileName: string }) => void): Unsubscribe =>
on(IPC_CHANNELS.RAG.INDEX_COMPLETE, cb),
},
// ── Voice Action (Phase 13.3) ────────────────────────
voiceAction: {
execute: (params: VoiceActionExecuteParams) =>
invoke<void>(IPC_CHANNELS.VOICE_ACTION.EXECUTE, params),
getPresets: () =>
invoke<VoiceActionPreset[]>(IPC_CHANNELS.VOICE_ACTION.GET_PRESETS),
getHistory: () =>
invoke<VoiceActionHistoryEntry[]>(IPC_CHANNELS.VOICE_ACTION.GET_HISTORY),
clearHistory: () =>
invoke<void>(IPC_CHANNELS.VOICE_ACTION.CLEAR_HISTORY),
setEnabled: (enabled: boolean) =>
invoke<void>(IPC_CHANNELS.VOICE_ACTION.SET_ENABLED, { enabled }),
isEnabled: () =>
invoke<boolean>(IPC_CHANNELS.VOICE_ACTION.IS_ENABLED),
onActionPlanned: (cb: (data: VoiceActionPlannedEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_ACTION.ACTION_PLANNED, cb),
onActionExecuted: (cb: (data: VoiceActionExecutedEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_ACTION.ACTION_EXECUTED, cb),
onActionError: (cb: (data: VoiceActionErrorEvent) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_ACTION.ACTION_ERROR, cb),
},
// ── Voice Conversation (Phase 13.1) ───────────────────
voiceConversation: {
startSession: () =>
invoke<void>(IPC_CHANNELS.VOICE_CONVERSATION.START_SESSION),
stopSession: () =>
invoke<void>(IPC_CHANNELS.VOICE_CONVERSATION.STOP_SESSION),
sendMessage: (params: ConversationSendParams) =>
invoke<void>(IPC_CHANNELS.VOICE_CONVERSATION.SEND_MESSAGE, params),
getState: () =>
invoke<ConversationSessionInfo>(IPC_CHANNELS.VOICE_CONVERSATION.GET_STATE),
getHistory: () =>
invoke<ConversationMessage[]>(IPC_CHANNELS.VOICE_CONVERSATION.GET_HISTORY),
clearHistory: () =>
invoke<void>(IPC_CHANNELS.VOICE_CONVERSATION.CLEAR_HISTORY),
cancelResponse: () =>
invoke<void>(IPC_CHANNELS.VOICE_CONVERSATION.CANCEL_RESPONSE),
finishListening: () =>
invoke<void>('voiceConversation:finishListening'),
onStateChanged: (cb: (data: ConversationSessionInfo) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.STATE_CHANGED, cb),
onUserMessage: (cb: (data: ConversationMessage) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.USER_MESSAGE, cb),
onAssistantDelta: (cb: (data: ConversationAssistantDelta) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.ASSISTANT_DELTA, cb),
onAssistantMessage: (cb: (data: ConversationAssistantMessage) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.ASSISTANT_MESSAGE, cb),
onTTSStarted: (cb: () => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.TTS_STARTED, cb),
onTTSFinished: (cb: () => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.TTS_FINISHED, cb),
onError: (cb: (data: ConversationError) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.ERROR, cb),
},
} as const
contextBridge.exposeInMainWorld('electronAPI', electronAPI)