# Speakly Settings & Hotkey UI 분석 > Speakly의 Settings Modal 및 HotkeyRecordModal 리버스엔지니어링 결과. > D3RO-VOICE Phase 8 구현 시 참조 자료. --- ## 1. Settings Modal 구조 ### 1.1 전체 레이아웃 ``` Dialog (fullWidth, maxWidth='sm') ├── DialogTitle: "Settings" + 닫기(X) 아이콘 ├── Tabs: [Account | General | About] └── TabPanels ├── Account: 로그인 정보, 사용량, 구독 (D3RO 제거) ├── General: 핵심 설정 전체 └── About: 버전, 라이선스, 피드백 링크 ``` D3RO-VOICE 적용: Account 탭 제거, General 탭만 유지 (또는 탭 분할 확장: General / Audio / STT / LLM). ### 1.2 General 탭 세부 섹션 General 탭은 상단부터 순서대로 다음 섹션으로 구성된다: ``` General Tab ├── 🎤 Voice Modes (음성 모드) │ ├── Dictation Mode — Switch + Hotkey Chip │ ├── Agent Mode — Switch + "Dictation 더블프레스" 설명 │ └── Hands-free (One-touch) — Switch + Hotkey Chip │ ├── 🔊 Audio │ ├── Microphone — Select (디바이스 목록) │ ├── Mic Test — Button + Level Meter │ ├── Sound effects — Switch │ └── Mute audio when dictating — Switch │ ├── 🌐 Language & Appearance │ ├── UI Language — Select (ko/en) │ ├── Theme — Select (light/dark/system) │ └── Start minimized — Switch │ └── ⚙️ System ├── Launch at startup — Switch ├── Close to tray — Switch └── Reset all settings — Button (확인 다이얼로그) ``` ### 1.3 음성 모드 섹션 UI 패턴 각 모드는 동일한 행(row) 레이아웃을 따른다: ``` ┌──────────────────────────────────────────────────────────────┐ │ [Switch] Dictation Mode [Right Alt] Chip │ │ Hold to talk – press and hold the shortcut key │ │ to start recording │ ├──────────────────────────────────────────────────────────────┤ │ [Switch] Agent Mode │ │ Double-press the dictation shortcut to activate │ │ (requires Dictation Mode enabled) │ ├──────────────────────────────────────────────────────────────┤ │ [Switch] Hands-free (One-touch) [Ctrl+\] Chip │ │ Toggle on/off – press once to start, again to stop │ └──────────────────────────────────────────────────────────────┘ ``` **핵심 동작:** - Dictation Switch OFF → Agent Mode Switch 자동 비활성 (disabled + 툴팁 "Dictation 필요") - Hotkey Chip 클릭 → HotkeyRecordModal 열림 - Agent Mode에는 별도 핫키가 없음 (Dictation 핫키 더블프레스로 트리거) ### 1.4 마이크 선택 + 테스트 UI ``` ┌──────────────────────────────────────────────────┐ │ Microphone │ │ [Select: System Default (Realtek Audio)] │ │ │ │ [🎤 Test Microphone] ▓▓▓▓▓░░░░░ Level: 0.45 │ │ ← real-time RMS bar → │ └──────────────────────────────────────────────────┘ ``` - 테스트 버튼 클릭 → `audio:testDevice` IPC (2초간 캡처) - 캡처 중 실시간 RMS 레벨 바 표시 (100ms 간격 갱신) - 완료 후 평균/피크 레벨 표시 - 오디오 없으면 경고: "마이크에서 소리가 감지되지 않습니다" --- ## 2. HotkeyRecordModal 구현 ### 2.1 Modal 구조 ``` Dialog (maxWidth='xs') ├── DialogTitle: "Record Shortcut" ├── DialogContent │ ├── 안내 텍스트: "Press the key combination you want to use" │ ├── 키 표시 영역: [Chip][Chip][+][Chip] (예: Ctrl + Shift + F5) │ ├── 유효성 메시지 (성공/경고/에러) │ └── 현재 바인딩 표시: "Current: Right Alt" └── DialogActions ├── Cancel — 변경 없이 닫기 └── Save — 새 바인딩 저장 ``` ### 2.2 키 감지 로직 ```typescript // Modal mount 시 리스너 등록 useEffect(() => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); const binding: HotkeyBinding = { keyCode: e.keyCode, // deprecated지만 uiohook 호환용 ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey, displayLabel: buildDisplayLabel(e), }; // 수정자 키만 눌린 경우 → 아직 완성 안 됨 (Chip은 표시하되 Save 비활성) if (isModifierOnly(e)) { setPendingBinding(binding); setIsComplete(false); return; } // 일반 키 + 수정자 조합 → 완성 setPendingBinding(binding); setIsComplete(true); validate(binding); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [open]); ``` ### 2.3 키 표시 라벨 매핑 Speakly는 platform-specific 키 라벨을 사용한다. Windows 기준: ```typescript const KEY_LABELS: Record = { // 수정자 키 0xA0: 'Left Shift', 0xA1: 'Right Shift', 0xA2: 'Left Ctrl', 0xA3: 'Right Ctrl', 0xA4: 'Left Alt', 0xA5: 'Right Alt', 0x5B: 'Left Win', 0x5C: 'Right Win', // 기능 키 0x70: 'F1', 0x71: 'F2', 0x72: 'F3', 0x73: 'F4', 0x74: 'F5', 0x75: 'F6', 0x76: 'F7', 0x77: 'F8', 0x78: 'F9', 0x79: 'F10', 0x7A: 'F11', 0x7B: 'F12', // 특수 키 0x1B: 'Escape', 0x09: 'Tab', 0x14: 'CapsLock', 0x20: 'Space', 0x0D: 'Enter', 0x08: 'Backspace', 0x2D: 'Insert', 0x2E: 'Delete', 0x24: 'Home', 0x23: 'End', 0x21: 'PageUp', 0x22: 'PageDown', // 방향키 0x25: '←', 0x26: '↑', 0x27: '→', 0x28: '↓', // 숫자패드 0x90: 'NumLock', 0x6F: 'Num/', 0x6A: 'Num*', 0x6D: 'Num-', 0x6B: 'Num+', 0x6E: 'Num.', }; function buildDisplayLabel(e: KeyboardEvent): string { const parts: string[] = []; if (e.ctrlKey) parts.push('Ctrl'); if (e.altKey) parts.push('Alt'); if (e.shiftKey) parts.push('Shift'); if (e.metaKey) parts.push('Win'); // 수정자 키 자체는 중복 추가하지 않음 if (!isModifierOnly(e)) { const label = KEY_LABELS[e.keyCode] ?? e.key?.toUpperCase() ?? `Key${e.keyCode}`; parts.push(label); } return parts.join(' + '); } ``` ### 2.4 유효성 검증 ```typescript interface ValidationResult { valid: boolean; message: string; severity: 'success' | 'warning' | 'error'; } function validate(binding: HotkeyBinding): ValidationResult { const label = binding.displayLabel; // 1. 시스템 예약키 체크 if (RESERVED_HOTKEYS.has(label)) { return { valid: false, message: `${label} is reserved by the system`, severity: 'error', }; } // 2. 다른 모드와 중복 체크 const existing = findConflict(binding); if (existing) { return { valid: false, message: `Already used by "${existing.modeName}"`, severity: 'warning', }; } // 3. 단일 수정자 키 경고 (Right Alt 등은 허용하지만 경고) if (isModifierOnly({ keyCode: binding.keyCode })) { return { valid: true, message: 'Modifier-only shortcuts may conflict with other apps', severity: 'warning', }; } return { valid: true, message: 'Shortcut available', severity: 'success' }; } ``` ### 2.5 MUI Chip 표시 ```tsx // 키 조합을 개별 Chip으로 표시 function KeyChips({ label }: { label: string }) { const parts = label.split(' + '); return ( {parts.map((part, i) => ( {i > 0 && +} ))} ); } ``` --- ## 3. 음성 모드 3가지 상세 분석 ### 3.1 Dictation (받아쓰기) | 항목 | 값 | |------|-----| | 트리거 | 핫키 press (hold) | | 녹음 방식 | hold-to-talk: 키 누르는 동안 녹음, 놓으면 종료 | | 기본 핫키 | Right Alt (0xA5) | | 더블프레스 | Agent Mode 트리거로 사용 (300ms 이내) | | 최소 시간 | 700ms 이하 → accidentalPress 취소 | | 후처리 | 기본: 원본 삽입, 설정에 따라 polish/translate | ``` 키 press ─→ startRecording ─→ [holding] ─→ 키 release ─→ stopRecording 700ms↑ ↓ accidentalPress? STT → [LLM] → insert ``` ### 3.2 Agent Mode (에이전트) | 항목 | 값 | |------|-----| | 트리거 | Dictation 핫키 더블프레스 (300ms 이내) | | 녹음 방식 | Dictation과 동일 (hold-to-talk) | | 의존성 | Dictation Mode가 활성이어야 함 | | 후처리 | 항상 LLM 처리 (커스텀 명령어 또는 기본 polish) | | 별도 핫키 | 없음 (Dictation 핫키의 더블프레스) | ``` 키 press ─→ 300ms 이내 재press ─→ "Agent Mode" 활성 │ hold-to-talk (녹음) │ release → STT → LLM(agent) → insert ``` **HotkeyService 내부 처리:** ```typescript // 더블프레스 감지 private onKeyDown(hotkeyId: string, config: HotkeyConfig): void { const now = Date.now(); const lastPress = this.lastPressTime.get(hotkeyId) ?? 0; if (config.doublePressEnabled && (now - lastPress) < TIMING.DOUBLE_PRESS_DURATION) { // 더블프레스 → Agent Mode this.emit('double-press', { config, intervalMs: now - lastPress, timestamp: now }); this.lastPressTime.delete(hotkeyId); return; } this.lastPressTime.set(hotkeyId, now); this.emit('hotkey-pressed', { config, timestamp: now }); } ``` ### 3.3 Hands-free / One-touch (원터치) | 항목 | 값 | |------|-----| | 트리거 | 별도 핫키 press (toggle) | | 녹음 방식 | 토글: 1회 누르면 녹음 시작, 다시 누르면 종료 | | 기본 핫키 | 미설정 (사용자가 직접 바인딩) | | 의존성 | 독립적 (Dictation과 무관) | | 후처리 | 설정에 따라 분기 | | VAD | 선택적 (침묵 감지 시 자동 종료 옵션) | ``` 키 press(1회) ─→ startRecording ─→ [recording...] ─→ 키 press(2회) ─→ stopRecording ↓ STT → [LLM] → insert ``` **Speakly 구현 특징:** - `holdMode: false`로 HotkeyConfig 등록 - 내부 `isRecording` 상태 토글 - ESC 키로 녹음 취소 (별도 리스너) - UI에 녹음 중 상태 표시 (RecordingTip 지속 표시) --- ## 4. D3RO-VOICE 적용 방안 ### 4.1 HotkeyRecordModal 구현 계획 Speakly의 HotkeyRecordModal을 D3RO-VOICE의 IPC 채널에 맞게 재구현한다: ``` 렌더러 (React) 메인 프로세스 │ │ │── hotkey:getDictationShortcut ────────►│ │◄── HotkeyBinding ────────────────────│ │ │ │ [사용자가 키 조합 누름] │ │ → 로컬 keydown 리스너로 감지 │ │ → validate() 수행 │ │ → UI에 Chip 표시 │ │ │ │── hotkey:setDictationShortcut ────────►│ │ { binding: HotkeyBinding } │── HotkeyService.updateConfig() │◄── { success: true } ─────────────────│── uiohook 핫키 재등록 ``` ### 4.2 음성 모드 Settings UI 구현 SettingsModal의 General 탭에 음성 모드 섹션을 추가한다: ```tsx // Settings General 탭 — 음성 모드 섹션 VOICE MODES {/* Dictation */} Dictation Mode Hold to talk — press and hold the shortcut key openHotkeyRecord('dictation')} clickable /> {/* Agent */} Agent Mode Double-press the dictation shortcut {!dictationEnabled && ' (requires Dictation Mode)'} {/* Hands-free */} Hands-free (One-touch) openHotkeyRecord('handsFree')} clickable /> ``` ### 4.3 IPC 채널 매핑 | Speakly 패턴 | D3RO-VOICE IPC 채널 | 비고 | |-------------|---------------------|------| | getHotkeyConfig('dictation') | `hotkey:getDictationShortcut` | 기존 | | setHotkeyConfig('dictation', binding) | `hotkey:setDictationShortcut` | 기존 | | getHotkeyConfig('handsFree') | `hotkey:getHandsFreeShortcut` | 기존 | | setHotkeyConfig('handsFree', binding) | `hotkey:setHandsFreeShortcut` | 기존 | | isVoiceModeEnabled('dictation') | `config:get({ key: 'dictationEnabled' })` | 신규 | | setVoiceModeEnabled('dictation', bool) | `config:set({ key: 'dictationEnabled', value })` | 신규 | | isVoiceModeEnabled('agent') | `config:get({ key: 'agentModeEnabled' })` | 신규 | | isVoiceModeEnabled('handsFree') | `config:get({ key: 'handsFreeEnabled' })` | 신규 | ### 4.4 키코드 호환성 D3RO-VOICE는 두 가지 키코드 시스템을 사용한다: | 컨텍스트 | 키코드 시스템 | 예시 (Right Alt) | |---------|-------------|-----------------| | ConfigService (저장) | Windows VK 코드 | `0xA5` (165) | | uiohook-napi (런타임) | uiohook 키코드 | `UiohookKey.AltRight` (56) | | KeyboardEvent (렌더러) | DOM keyCode | `18` (Alt) | HotkeyRecordModal에서 DOM keyCode를 수신하여 Windows VK 코드로 변환 후 저장한다. HotkeyService에서는 VK 코드를 uiohook 키코드로 변환하여 매칭한다 (`vkToUiohook` 맵 기존 구현 참조). ### 4.5 시스템 예약키 블랙리스트 D3RO-VOICE에서 차단할 키 조합: ```typescript // Windows 시스템 예약키 const RESERVED = [ 'Ctrl+C', 'Ctrl+V', 'Ctrl+X', 'Ctrl+Z', 'Ctrl+Y', // 편집 'Ctrl+A', 'Ctrl+S', 'Ctrl+P', 'Ctrl+F', // 일반 'Alt+F4', 'Alt+Tab', 'Alt+Escape', // 윈도우 관리 'Ctrl+Alt+Delete', 'Ctrl+Shift+Escape', // 시스템 'Win+L', 'Win+D', 'Win+E', 'Win+R', 'Win+Tab', // Win 단축키 'PrintScreen', 'Ctrl+PrintScreen', // 캡처 'F1', // 도움말 ]; ``` --- *끝.*