Phase 1+2 구현: Electron 뼈대 + STT/핫키/오케스트레이터
Phase 1: - 프로젝트 초기화 (TypeScript strict, electron-vite, ESLint, Prettier) - shared 타입 (ipc-channels 113채널, types, errors, constants) - 메인 프로세스 뼈대 (bootstrap, lifecycle, 단일 인스턴스) - LoggerService, ConfigService (electron-store ESM dynamic import) - React 19 + MUI 7 Dashboard, 시스템 트레이 Phase 2: - AudioCaptureService (node-record-lpcm16, PCM16 16kHz mono) - HotkeyService (uiohook-napi, 더블프레스, holdMode/toggleMode) - LocalSTTService (faster-whisper Python sidecar, 이중 조건 플러시) - VoiceModeService 오케스트레이터 (이중 상태머신, Action Queue) - Python sidecar (FastAPI: health/load/transcribe/shutdown) - IPC 핸들러 (voice, stt, hotkey) + Preload API 확장
This commit is contained in:
parent
e24bb8378c
commit
1d152d01a1
46 changed files with 10828 additions and 4 deletions
164
src/preload/index.ts
Normal file
164
src/preload/index.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// src/preload/index.ts
|
||||
// contextBridge로 렌더러에 노출할 API 정의
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { IPC_CHANNELS } from '@shared/ipc-channels'
|
||||
import type {
|
||||
AudioDevice,
|
||||
SetDeviceParams,
|
||||
TestDeviceParams,
|
||||
TestDeviceResult,
|
||||
AudioDeviceChangedEvent,
|
||||
AppConfig,
|
||||
ConfigGetParams,
|
||||
ConfigSetParams,
|
||||
ConfigResetParams,
|
||||
SetThemeParams,
|
||||
SetLanguageParams,
|
||||
ThemeMode,
|
||||
ConfigChangedEvent,
|
||||
VoiceState,
|
||||
VoiceMode,
|
||||
AudioLevelEvent,
|
||||
VoiceStateChangedEvent,
|
||||
TranscriptionDeltaEvent,
|
||||
TranscriptionCompleteEvent,
|
||||
VoiceErrorEvent,
|
||||
StartRecordingParams,
|
||||
StartRecordingResult,
|
||||
StopRecordingParams,
|
||||
StopRecordingResult,
|
||||
CancelRecordingParams,
|
||||
SetVoiceModeParams,
|
||||
STTStatus,
|
||||
STTModel,
|
||||
SetSTTModelParams,
|
||||
SetSTTLanguageParams,
|
||||
STTStatusChangedEvent,
|
||||
HotkeyBinding,
|
||||
SetHotkeyParams,
|
||||
SetEnabledParams,
|
||||
HotkeyTriggeredEvent,
|
||||
PermissionStatus
|
||||
} from '@shared/types'
|
||||
import type { IPCResult } from '@shared/errors'
|
||||
|
||||
type Unsubscribe = () => void
|
||||
|
||||
function invoke<TResult>(channel: string, ...args: unknown[]): Promise<IPCResult<TResult>> {
|
||||
return ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
|
||||
function send(channel: string, ...args: unknown[]): void {
|
||||
ipcRenderer.send(channel, ...args)
|
||||
}
|
||||
|
||||
function on<T>(channel: string, callback: (data: T) => void): Unsubscribe {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: T) => callback(data)
|
||||
ipcRenderer.on(channel, listener)
|
||||
return () => ipcRenderer.removeListener(channel, listener)
|
||||
}
|
||||
|
||||
const electronAPI = {
|
||||
// ── Audio ──────────────────────────────────────────────
|
||||
audio: {
|
||||
getDevices: () => invoke<AudioDevice[]>(IPC_CHANNELS.AUDIO.GET_DEVICES),
|
||||
getSelectedDevice: () => invoke<string | null>(IPC_CHANNELS.AUDIO.GET_SELECTED_DEVICE),
|
||||
setSelectedDevice: (params: SetDeviceParams) =>
|
||||
invoke<void>(IPC_CHANNELS.AUDIO.SET_SELECTED_DEVICE, params),
|
||||
testDevice: (params: TestDeviceParams) =>
|
||||
invoke<TestDeviceResult>(IPC_CHANNELS.AUDIO.TEST_DEVICE, params),
|
||||
onDeviceChanged: (cb: (e: AudioDeviceChangedEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.AUDIO.DEVICE_CHANGED, cb)
|
||||
},
|
||||
|
||||
// ── Config ─────────────────────────────────────────────
|
||||
config: {
|
||||
get: (params: ConfigGetParams) => invoke<unknown>(IPC_CHANNELS.CONFIG.GET, params),
|
||||
set: (params: ConfigSetParams) => invoke<void>(IPC_CHANNELS.CONFIG.SET, params),
|
||||
getAll: () => invoke<AppConfig>(IPC_CHANNELS.CONFIG.GET_ALL),
|
||||
reset: (params: ConfigResetParams) => invoke<void>(IPC_CHANNELS.CONFIG.RESET, params),
|
||||
getTheme: () => invoke<ThemeMode>(IPC_CHANNELS.CONFIG.GET_THEME),
|
||||
setTheme: (params: SetThemeParams) => invoke<void>(IPC_CHANNELS.CONFIG.SET_THEME, params),
|
||||
getLanguage: () => invoke<string>(IPC_CHANNELS.CONFIG.GET_LANGUAGE),
|
||||
setLanguage: (params: SetLanguageParams) =>
|
||||
invoke<void>(IPC_CHANNELS.CONFIG.SET_LANGUAGE, params),
|
||||
onChanged: (cb: (e: ConfigChangedEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.CONFIG.CHANGED, cb)
|
||||
},
|
||||
|
||||
// ── Voice ──────────────────────────────────────────────
|
||||
voice: {
|
||||
startRecording: (params: StartRecordingParams) =>
|
||||
invoke<StartRecordingResult>(IPC_CHANNELS.VOICE.START_RECORDING, params),
|
||||
stopRecording: (params: StopRecordingParams) =>
|
||||
invoke<StopRecordingResult>(IPC_CHANNELS.VOICE.STOP_RECORDING, params),
|
||||
cancelRecording: (params: CancelRecordingParams) =>
|
||||
invoke<void>(IPC_CHANNELS.VOICE.CANCEL_RECORDING, params),
|
||||
getState: () => invoke<VoiceState>(IPC_CHANNELS.VOICE.GET_STATE),
|
||||
setMode: (params: SetVoiceModeParams) =>
|
||||
invoke<void>(IPC_CHANNELS.VOICE.SET_MODE, params),
|
||||
getMode: () => invoke<VoiceMode>(IPC_CHANNELS.VOICE.GET_MODE),
|
||||
onStateChanged: (cb: (e: VoiceStateChangedEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.VOICE.STATE_CHANGED, cb),
|
||||
onTranscriptionDelta: (cb: (e: TranscriptionDeltaEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.VOICE.TRANSCRIPTION_DELTA, cb),
|
||||
onTranscriptionComplete: (cb: (e: TranscriptionCompleteEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.VOICE.TRANSCRIPTION_COMPLETE, cb),
|
||||
onError: (cb: (e: VoiceErrorEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.VOICE.ERROR, cb),
|
||||
onAudioLevel: (cb: (e: AudioLevelEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.VOICE.AUDIO_LEVEL, cb)
|
||||
},
|
||||
|
||||
// ── STT ────────────────────────────────────────────────
|
||||
stt: {
|
||||
getStatus: () => invoke<STTStatus>(IPC_CHANNELS.STT.GET_STATUS),
|
||||
getModels: () => invoke<STTModel[]>(IPC_CHANNELS.STT.GET_MODELS),
|
||||
getActiveModel: () => invoke<string | null>(IPC_CHANNELS.STT.GET_ACTIVE_MODEL),
|
||||
setModel: (params: SetSTTModelParams) =>
|
||||
invoke<void>(IPC_CHANNELS.STT.SET_MODEL, params),
|
||||
getLanguage: () => invoke<string>(IPC_CHANNELS.STT.GET_LANGUAGE),
|
||||
setLanguage: (params: SetSTTLanguageParams) =>
|
||||
invoke<void>(IPC_CHANNELS.STT.SET_LANGUAGE, params),
|
||||
onStatusChanged: (cb: (e: STTStatusChangedEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.STT.STATUS_CHANGED, cb)
|
||||
},
|
||||
|
||||
// ── Hotkey ─────────────────────────────────────────────
|
||||
hotkey: {
|
||||
getDictationShortcut: () =>
|
||||
invoke<HotkeyBinding>(IPC_CHANNELS.HOTKEY.GET_DICTATION_SHORTCUT),
|
||||
setDictationShortcut: (params: SetHotkeyParams) =>
|
||||
invoke<void>(IPC_CHANNELS.HOTKEY.SET_DICTATION_SHORTCUT, params),
|
||||
getHandsFreeShortcut: () =>
|
||||
invoke<HotkeyBinding>(IPC_CHANNELS.HOTKEY.GET_HANDS_FREE_SHORTCUT),
|
||||
setHandsFreeShortcut: (params: SetHotkeyParams) =>
|
||||
invoke<void>(IPC_CHANNELS.HOTKEY.SET_HANDS_FREE_SHORTCUT, params),
|
||||
isEnabled: () => invoke<boolean>(IPC_CHANNELS.HOTKEY.IS_ENABLED),
|
||||
setEnabled: (params: SetEnabledParams) =>
|
||||
invoke<void>(IPC_CHANNELS.HOTKEY.SET_ENABLED, params),
|
||||
onTriggered: (cb: (e: HotkeyTriggeredEvent) => void): Unsubscribe =>
|
||||
on(IPC_CHANNELS.HOTKEY.TRIGGERED, cb)
|
||||
},
|
||||
|
||||
// ── Window ─────────────────────────────────────────────
|
||||
window: {
|
||||
minimize: () => send(IPC_CHANNELS.WINDOW.MINIMIZE),
|
||||
maximize: () => send(IPC_CHANNELS.WINDOW.MAXIMIZE),
|
||||
close: () => send(IPC_CHANNELS.WINDOW.CLOSE),
|
||||
isMaximized: () => invoke<boolean>(IPC_CHANNELS.WINDOW.IS_MAXIMIZED)
|
||||
},
|
||||
|
||||
// ── System ─────────────────────────────────────────────
|
||||
system: {
|
||||
getPlatform: () => invoke<NodeJS.Platform>(IPC_CHANNELS.SYSTEM.GET_PLATFORM),
|
||||
getVersion: () => invoke<string>(IPC_CHANNELS.SYSTEM.GET_VERSION),
|
||||
checkMicPermission: () =>
|
||||
invoke<PermissionStatus>(IPC_CHANNELS.SYSTEM.CHECK_MIC_PERMISSION)
|
||||
}
|
||||
} as const
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', electronAPI)
|
||||
|
||||
export type ElectronAPI = typeof electronAPI
|
||||
Loading…
Add table
Add a link
Reference in a new issue