// src/main/ipc/voice-handlers.ts import { ipcMain } from 'electron' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ipcSuccess, ipcError, ErrorCode } from '@d3ro/core/errors' import { getVoiceModeService } from '../services/VoiceModeService' import { getMainWindow } from '../windows/WindowManager' import type { StartRecordingParams, StopRecordingParams, CancelRecordingParams, SetVoiceModeParams, VoiceErrorEvent } from '@d3ro/core/types' function safeSendToRenderer(channel: string, data: unknown): void { const win = getMainWindow() if (win && !win.isDestroyed()) { win.webContents.send(channel, data) } } export function registerVoiceHandlers(): void { // 음성 세션 에러/경고를 메인 윈도우로 브로드캐스트 — // 모델 미설치/엔진 실패 등의 경고가 recording tip(3초) 외에도 // 메인 UI에서 명확히 보이도록 (설계서 02 voice:error 이벤트) getVoiceModeService().on('error', (payload) => { const event: VoiceErrorEvent = { sessionId: payload.session?.id ?? null, errorCode: payload.error.code, message: payload.error.message, severity: payload.severity ?? 'error', } safeSendToRenderer(IPC_CHANNELS.VOICE.ERROR, event) }) ipcMain.handle(IPC_CHANNELS.VOICE.START_RECORDING, async (_event, params: StartRecordingParams) => { try { const voice = getVoiceModeService() await voice.startSession('dictation') const session = voice.currentSession return ipcSuccess({ sessionId: session?.id ?? params.sessionId ?? '' }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.AudioCaptureStartFailed, `Failed to start recording: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.VOICE.STOP_RECORDING, async (_event, params: StopRecordingParams) => { try { const voice = getVoiceModeService() await voice.stopSession() return ipcSuccess({ sessionId: params.sessionId, text: voice.currentSession?.transcription ?? '', durationMs: voice.currentSession ? Date.now() - voice.currentSession.startedAt : 0 }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return ipcError(ErrorCode.STTTranscriptionFailed, `Failed to stop recording: ${message}`) } }) ipcMain.handle(IPC_CHANNELS.VOICE.CANCEL_RECORDING, async (_event, _params: CancelRecordingParams) => { getVoiceModeService().cancelSession() return ipcSuccess(undefined) }) ipcMain.handle(IPC_CHANNELS.VOICE.GET_STATE, async () => { return ipcSuccess(getVoiceModeService().getState()) }) ipcMain.handle(IPC_CHANNELS.VOICE.SET_MODE, async (_event, params: SetVoiceModeParams) => { // Phase 2: 모드만 설정에 저장 (실제 모드 전환은 핫키에서 처리) return ipcSuccess(undefined) }) ipcMain.handle(IPC_CHANNELS.VOICE.GET_MODE, async () => { const voice = getVoiceModeService() return ipcSuccess(voice.currentSession?.mode ?? 'dictation') }) }