- vitest 41개 단위 테스트 (HistoryService, DictionaryService, CustomInstructionService, VoiceModeService, D3ROError) - electron-builder.yml (NSIS, asarUnpack, extraResources) - .gitlab-ci.yml (lint, typecheck, test, build, release) - SoundEffectService: WAV 프리로드 + PowerShell 재생 + VoiceMode 연동 - AutoLaunchService: app.setLoginItemSettings + ConfigService 동기화 - TextInsertService: 간이 삽입 검증 (EditMonitor 경량) - 번들링 인프라: SoX 다운로드 스크립트, PyInstaller 빌드, 경로 해상도 유틸 - AudioCaptureService/LocalSTTService: 번들 경로 자동 감지 - 08-design-system.md 기반 MUI 테마 (다크+라이트+auto 테마 시스템) - 전체 UI 컴포넌트 리디자인: AppLayout, Dashboard, StatusBar, History, Dictionary, Commands, Settings - 효과음 WAV 생성: recording-start, recording-stop, error - EPIPE 에러 핸들링 추가
214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# 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/
|
|
```
|
|
|
|
## 빌드 & 실행
|
|
```bash
|
|
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에서 채택한 핵심 패턴
|
|
1. **상태 머신**: RecognitionState + AudioState 분리 추적
|
|
2. **이중 조건 플러시**: 모델 로딩 + 오디오 버퍼링 동시 진행, 둘 다 준비 시 플러시
|
|
3. **텍스트 삽입**: 클립보드 save → set → Ctrl+V → restore
|
|
4. **윈도우 관리**: 프리로딩 + 2-phase 리사이즈 (측정→resize→show)
|
|
5. **팝업**: 메인 앱=React, 경량 팝업=Vanilla JS
|
|
6. **녹음 UI**: 9개 웨이브 바, cos 분포 가중치, 100ms 애니메이션
|
|
|
|
## 현재 상태
|
|
Phase: 8 완료 (Phase 1~8 + 3.5 전체 완료)
|
|
마지막 완료: Phase 8 — UI 전면 리디자인 (08-design-system.md SSOT 적용)
|
|
다음 작업: 전사 테스트 (npm run dev) → 온보딩 위저드
|
|
차단 이슈: @nut-tree-fork/nut-js 포크 사용
|
|
|
|
### Phase 8 구현 내용
|
|
- 08-design-system.md SSOT 기반 MUI 테마 전면 교체 (다크+라이트+auto 테마 시스템)
|
|
- theme.ts: d3roPalette(SSOT), createD3ROTheme('dark'|'light'), getTheme(mode, prefersDark)
|
|
- App.tsx: auto 모드 → 시스템 설정 따름 (나중에 커스텀 테마 확장 가능)
|
|
- AppLayout: 앰버 LED, 라벨 스타일, 아이콘 색상, 네비게이션 앰버 선택
|
|
- DashboardPage: hero 수치(28px mono), 카드 그리드, LED 상태 패널, 태그 시스템
|
|
- StatusBar: LED 인디케이터 + 모노 폰트 + 핫키 힌트
|
|
- HistoryPage: 카드 레이아웃, 앰버/퍼플 태그, 모노 메타데이터
|
|
- DictionaryPage: 카드 레이아웃, 카테고리 태그, 사용 횟수 모노
|
|
- CommandsPage: 카드 레이아웃, BUILT-IN/CUSTOM 태그
|
|
- SettingsModal: 디자인 시스템 border/close 스타일
|
|
|
|
### Phase 7.5 구현 내용
|
|
- SoundEffectService: WAV 프리로드, fire-and-forget, PowerShell SoundPlayer로 재생
|
|
- AutoLaunchService: app.setLoginItemSettings, ConfigService 연동, syncWithConfig
|
|
- TextInsertService: 간이 삽입 검증 (EditMonitor 경량), 클립보드 확인
|
|
- VoiceModeService 효과음 연동: session-started(start), completed(stop), cancelled(cancel), error(error)
|
|
- IPC: system:playSound/setSoundEnabled/isSoundEnabled, config:setAutoLaunch/setCloseToTray
|
|
- Bootstrap: sound-effects, auto-launch 초기화 단계 추가
|
|
- 효과음 WAV 파일: scripts/generate-sounds.js로 생성 (recording-start/stop, error)
|
|
|
|
### Phase 7 구현 내용
|
|
- vitest 테스트 환경: vitest.config.ts, tests/setup.ts (electron/electron-log 모킹)
|
|
- 테스트 헬퍼: tests/helpers/createTestDb.ts (in-memory SQLite + drizzle)
|
|
- 단위 테스트 41개: HistoryService, DictionaryService, CustomInstructionService, VoiceModeService, D3ROError
|
|
- electron-builder: electron-builder.yml (NSIS, asarUnpack, extraResources)
|
|
- GitLab CI/CD: .gitlab-ci.yml (lint, typecheck, test, build, release)
|
|
- 빌드 스크립트: pack, dist, test:unit 추가
|
|
- 참고: better-sqlite3는 Electron용 빌드라 vitest에서 직접 사용 불가 → DB 서비스는 모킹 테스트
|
|
- 번들링 인프라: SoX 다운로드 스크립트, PyInstaller 빌드 스크립트, 경로 해상도 유틸
|
|
- src/main/utils/paths.ts: dev vs production 경로 자동 감지 (SoX, sidecar, sounds)
|
|
- AudioCaptureService: 번들 SoX 경로 사용 (getSoxPath)
|
|
- LocalSTTService: 번들 sidecar 경로 사용 (getSidecarCommand)
|
|
- scripts/download-sox.ps1: SoX Windows 바이너리 다운로드 → resources/sox/
|
|
- scripts/build-sidecar.py: PyInstaller → sidecar-dist/sidecar/sidecar.exe
|
|
|
|
### Phase 6 구현 내용
|
|
- CustomInstructionService: electron-store 기반, 프리셋 5개 (번역/요약/전문리라이트/코드설명/자유프롬프트)
|
|
- 커스텀 명령어 CRUD: create/update/delete/reorder, 프리셋 보호 (삭제 불가)
|
|
- CommandsPage: React MUI 명령어 목록 + 추가/편집 다이얼로그
|
|
- i18n: ko.json/en.json 리소스, t() 함수, React 컨텍스트 (useI18n)
|
|
- IPC: instruction:getAll/getById/create/update/delete/reorder 핸들러
|
|
|
|
### Phase 3.5 구현 내용
|
|
- HistoryPopup: Vanilla JS 팝업, 다크 카드(#242427), 앰버 악센트(#f25b29)
|
|
- Ctrl+Shift+V 글로벌 단축키 → 커서 위치에 최근 10건 히스토리 팝업
|
|
- Arrow↑↓ 선택, Enter 붙여넣기, 1-9 직접 선택, ESC 닫기
|
|
- focusable: false → 활성 앱 포커스 유지
|
|
- 2-phase 리사이즈, 등장/퇴장 애니메이션 (0.15s/0.1s)
|
|
|
|
### Phase 5 구현 내용
|
|
- DB: better-sqlite3 + drizzle-orm (history/dictionary/stats 테이블, WAL 모드)
|
|
- HistoryService: CRUD + 검색 + 통계 + 보존 정책(30일), 세션 완료 시 자동 이력 저장
|
|
- DictionaryService: CRUD + 검색 + 사용 횟수 추적 + STT 프롬프트 힌트
|
|
- History UI: 목록 + 검색 + 삭제 + 복사 + 페이지네이션
|
|
- Dictionary UI: 목록 + 검색 + 추가 다이얼로그 + 삭제
|
|
- Dashboard: 실데이터 통계 (총/오늘 세션수, 시간, 단어수, 연속일수)
|
|
- IPC: history, dictionary, stats 핸들러 + preload API
|
|
- Bootstrap: DB 초기화 단계 추가 (critical)
|
|
|
|
### Phase 4 구현 내용
|
|
- LocalLLMService: Ollama REST API 연동, 스트리밍 NDJSON 파싱, 가용성 폴링(5초)
|
|
- 시스템 프롬프트: refine/translate/summarize/grammar/expand/custom 6개 액션
|
|
- VoiceModeService LLM 연동: 전사→LLM 후처리→텍스트 삽입, LLM 실패 시 원본 폴백
|
|
- LLM IPC 핸들러: llm:getStatus/getModels/setModel/process/cancelProcess/getServerUrl/setServerUrl
|
|
- StatusBar: Ollama 연결 상태 + 활성 모델 표시
|
|
- Bootstrap: LLM 가용성 폴링 초기화 단계 추가
|
|
|
|
### 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: 테스트 + 빌드 + 배포
|
|
- Phase 7.5: Speakly 패턴 보강 (SoundEffect, AutoLaunch, EditMonitor, 오디오 뮤트)
|
|
|
|
## 설계 문서 (구현 시 반드시 참조)
|
|
@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
|
|
@docs/phases/phase-7.5.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
|