- TextInsertService: clipboard save→set→Ctrl+V→restore (@nut-tree-fork/nut-js) - RecordingTip 팝업: 9개 웨이브바 cos분포, thinking 점근수렴, 2-phase 리사이즈 - ResultPopup 팝업: 복사 버튼, auto-close, 마우스 호버 유지, 다크모드 - WindowManager: 팝업 프리로딩, 커서 위치 표시, 멀티모니터 보정 - Settings 모달: General/Audio/STT/LLM 탭 - VoiceModeService: 전사 완료 시 자동 텍스트 삽입 + 팝업 연동 - electron-vite: 팝업 HTML 멀티 엔트리 + popup preload 빌드
7 KiB
D3RO-VOICE — 로컬 AI 음성 어시스턴트
Genspark Speakly를 리버스엔지니어링하여 얻은 노하우를 기반으로 만드는 완전 로컬 음성 어시스턴트. 클라우드 의존성 없이 Ollama + Whisper + TTS를 사용한다.
기술 스택 (고정)
- Framework: Electron 33+ (contextIsolation: true, nodeIntegration: false)
- Frontend: React 19 + MUI 7 + Vite
- Language: TypeScript 5.7+ (strict mode)
- DB: better-sqlite3 + drizzle-orm
- STT: faster-whisper (Python sidecar) 또는 whisper.cpp
- TTS: piper-tts 또는 kokoro
- LLM: Ollama REST API (localhost:11434)
- Audio: Web Audio API (renderer) + node-record-lpcm16 (main)
- Hotkey: uiohook-napi (글로벌 키보드 후킹)
- Text Insert: @nut-tree/nut-js (클립보드 + Ctrl+V)
- Config: electron-store
프로젝트 구조
D3ROVoice/
├── src/
│ ├── main/ # Electron 메인 프로세스
│ │ ├── services/ # STT, TTS, LLM, Audio, Hotkey, TextInsert
│ │ ├── windows/ # 윈도우 매니저 (팁, 팝업, 메인)
│ │ ├── ipc/ # IPC 핸들러 등록
│ │ ├── db/ # SQLite (history, dictionary)
│ │ └── index.ts # 앱 진입점
│ ├── renderer/ # React 앱
│ │ ├── components/ # React 컴포넌트
│ │ ├── popups/ # Vanilla JS 팝업 (recording-tip, result-popup)
│ │ └── main.tsx # 렌더러 진입점
│ ├── preload/ # contextBridge IPC 브릿지
│ │ └── index.ts
│ └── shared/ # 공유 타입, IPC 채널 정의, 에러 코드
│ ├── ipc-channels.ts
│ ├── types.ts
│ └── errors.ts
├── docs/ # 설계 문서
│ ├── phases/ # 페이즈별 요구사항
│ └── re-findings/ # Speakly 리버스엔지니어링 결과
├── scripts/ # 빌드/유틸 스크립트
└── tests/
빌드 & 실행
npm run dev # Electron + Vite dev server
npm run build # 프로덕션 빌드
npm run test # vitest 테스트
npm run lint # eslint + prettier
npm run typecheck # tsc --noEmit
코딩 규칙
- TypeScript strict mode 필수 (noImplicitAny, strictNullChecks)
- 함수형 React 컴포넌트 + hooks만 사용
- 메인 앱 = React + MUI, 팝업 윈도우 = Vanilla JS (Speakly 패턴)
- IPC 채널명:
${feature}:${action}(예:voice:startRecording,config:getLanguage) - 에러 처리: NXError 패턴 (에러 코드 + 메시지)
- 로거: electron-log 사용, console.log 금지
절대 하지 말 것
- any 타입 사용 금지
- renderer에서 Node.js API 직접 import 금지
- electron의 remote 모듈 사용 금지
- console.log 남기지 말 것 (logger 사용)
- 하드코딩된 시크릿/비밀번호 금지
- Co-Authored-By, Claude 관련 문구 커밋 메시지에 추가 금지
Speakly에서 채택한 핵심 패턴
- 상태 머신: RecognitionState + AudioState 분리 추적
- 이중 조건 플러시: 모델 로딩 + 오디오 버퍼링 동시 진행, 둘 다 준비 시 플러시
- 텍스트 삽입: 클립보드 save → set → Ctrl+V → restore
- 윈도우 관리: 프리로딩 + 2-phase 리사이즈 (측정→resize→show)
- 팝업: 메인 앱=React, 경량 팝업=Vanilla JS
- 녹음 UI: 9개 웨이브 바, cos 분포 가중치, 100ms 애니메이션
현재 상태
Phase: 3 완료 마지막 완료: Phase 3 — TextInsertService + RecordingTip/ResultPopup 팝업 + Settings 모달 다음 작업: Phase 3.5 — 커서 위치 히스토리 팝업 (D3RO 고유 기능) 차단 이슈: SoX 미설치 시 AudioCaptureService 동작 불가, @nut-tree/nut-js → @nut-tree-fork/nut-js 포크 사용
Phase 3 구현 내용
- TextInsertService: clipboard save→set→Ctrl+V→restore (@nut-tree-fork/nut-js, lazy dynamic import)
- RecordingTip 팝업: Vanilla JS, 9개 웨이브바 cos분포 가중치, thinking 점근수렴, 2-phase 리사이즈
- ResultPopup 팝업: Vanilla JS, 복사 버튼, auto-close(5초), 마우스 호버 유지, 다크모드
- WindowManager 리팩토링: 팝업 프리로딩, 커서 위치 표시, 멀티모니터 보정
- Settings 모달: General/Audio/STT/LLM 탭, 실시간 설정 변경
- VoiceModeService 연동: 전사 완료 시 자동 텍스트 삽입 + RecordingTip→ResultPopup 전환
- Bootstrap: 9단계 초기화 (popup-preload 추가)
- electron-vite: 팝업 HTML 멀티 엔트리 + popup preload 빌드
Phase 2 구현 내용
- AudioCaptureService: node-record-lpcm16 + SoX 실제 마이크 캡처 (PCM16 16kHz mono, 60ms 프레임, RMS 레벨)
- HotkeyService: uiohook-napi 글로벌 키후킹, 더블프레스(300ms), holdMode/toggleMode, VK→uiohook 매핑
- LocalSTTService: faster-whisper Python sidecar 관리, STTState 상태머신, 이중 조건 플러시, 자동 재시작(3회)
- VoiceModeService: RecognitionState(9상태) + AudioState(4상태) 이중 상태머신, Action Queue 직렬화, accidentalPress(<700ms), Dictation/HandsFree 모드
- Python sidecar: FastAPI (GET /health, POST /load, POST /transcribe, POST /shutdown)
- IPC 핸들러: voice, stt, hotkey 추가 (기존 audio, config, window, system에 추가)
- Preload: voice/stt/hotkey API 전체 노출
- Bootstrap: 7단계 초기화 (logger→config→windows→tray→ipc→hotkey→voice-mode)
Phase 1 구현 내용
- 프로젝트 초기화: package.json, TypeScript strict, electron-vite, ESLint, Prettier
- shared 타입: ipc-channels.ts (113채널), types.ts, errors.ts (ErrorCode enum), constants.ts
- 메인 프로세스: index.ts (단일 인스턴스), bootstrap.ts, lifecycle.ts
- 서비스: LoggerService, ConfigService (ESM dynamic import)
- Renderer: React 19 + MUI 7, AppLayout (Drawer 240px), DashboardPage
- 시스템 트레이, closeToTray
페이즈 로드맵
- Phase 1: 프로젝트 초기화 + Electron 뼈대 + 마이크 캡처
- Phase 2: 로컬 STT 연동 (Whisper) + 핫키
- Phase 3: 텍스트 삽입 + 기본 UI (Dashboard, RecordingTip)
- Phase 3.5: 커서 위치 히스토리 팝업 (D3RO 고유 기능)
- Phase 4: Ollama LLM 연동 (텍스트 다듬기, 번역)
- Phase 5: TTS + 히스토리/사전 DB
- Phase 6: 커스텀 명령어 + 설정 UI 고도화
- Phase 7: 테스트 + 빌드 + 배포
설계 문서 (구현 시 반드시 참조)
@docs/design/00-master-architecture.md @docs/design/01-service-specifications.md @docs/design/02-ipc-and-types.md @docs/design/03-db-and-ui.md
페이즈 문서
@docs/phases/phase-1.md @docs/phases/phase-2.md @docs/phases/phase-3.md @docs/phases/phase-3.5.md @docs/phases/phase-4.md @docs/phases/phase-5.md @docs/phases/phase-6.md @docs/phases/phase-7.md
RE 노하우 (패턴 적용 근거)
@docs/re-findings/speakly-architecture.md @docs/re-findings/voice-pipeline-patterns.md @docs/re-findings/text-insertion-patterns.md @docs/re-findings/ui-patterns.md @docs/re-findings/native-dll-patterns.md