d3ro-voice/CLAUDE.md
Yun Chan c079f335c1 Phase 8 최종 마무리: 테스트 수정 + CLAUDE.md 갱신
- tests/setup.ts: WindowManager 모킹 추가 (VoiceModeService import 대응)
- 41개 테스트 전부 통과
2026-04-05 10:52:17 +09:00

14 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에서 채택한 핵심 패턴

  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~7.5 + 3.5 완료, Phase 8 UI+기능 보강 진행) 마지막 작업: SoX -t waveaudio 수정 → 마이크 캡처 동작 확인, VAD 에러 방어 다음 작업: Phase 9 기능 확장 (마이크 테스트, 온보딩, MetalDial 등) 차단 이슈: @nut-tree-fork/nut-js 포크 사용

⚠️ 핵심 병목: VoiceModeService ↔ 팝업 연동 미구현

Speakly RE 재조사로 발견된 치명적 갭:

  1. VoiceModeService가 RecordingTip/ResultPopup과 전혀 연동 안 됨
    • 녹음 시작 시 팝업 표시 안 함, 에러 시 팝업 숨김 안 함
    • Speakly: VoiceModeService가 이벤트 발행 → main/index.js에서 RecordingTipWindow 메서드 호출
  2. RecordingTipUIState 상태 머신 없음
    • Speakly: OPENING→RECORDING→THINKING→COMPLETED/ERROR/CANCELLED
    • D3RO: 상태 없이 수동 show/hide만
  3. session-aware 윈도우 관리 없음
    • Speakly: showForSession(sessionId) / hideForSession(sessionId) — race condition 방지
  4. 에러 후 복구 안 됨
    • 에러 시 RecordingTip이 "실패" 상태로 남아있고, 핫키가 안 먹힘
    • Speakly: 에러 표시 → 3초 후 자동 숨김 → idle 복귀

Speakly 음성 파이프라인 패턴 (RE 결과)

[핫키 press] → Action Queue에 enqueue → processActionQueue()
  → startRecording():
    1. 시스템 오디오 뮤트 (500ms 후)
    2. emit('recording-tip:show-opening') → RecordingTip 표시
    3. MicNativeService.start() → 오디오 스트리밍
    4. emit('recording-tip:mic-ready') → 웨이브 바 활성
    5. RecognitionSession 생성 → STT 연결
  → 녹음 중: 오디오 레벨 → sendAudioLevel() → 웨이브 바
  
[핫키 release] → stopAndProcess():
    1. MicNativeService.stop()
    2. accidentalPress 체크 (< 700ms → 취소)
    3. 언뮤트 + 종료 효과음
    4. RecordingTip → thinking 상태
    5. session.commitAndWait() → 전사 결과 대기
    6. 전사 완료 → 텍스트 삽입 시도
    7. 삽입 성공 → RecordingTip 숨김
    8. 삽입 실패 → ResultPopup 표시
    9. 에러 → RecordingTip에 에러 표시 → 3초 후 숨김

DS 컴포넌트 현황 (src/renderer/components/ds/)

  • CrtDisplay.tsx: WebGL CRT 셰이더 (파형+스캔라인+비네팅+글리치)
  • InstrumentPanel.tsx: 메탈 섀시 컨테이너 (각인 텍스트, 노이즈)
  • Led.tsx: LED 인디케이터 (amber/green/red/orange, pulse)
  • PhysicalButton.tsx: 물리 버튼 (돌출 그림자, 눌림 피드백)
  • MetalCard.tsx: 메탈 카드 컨테이너
  • PhosphorText.tsx: 인광 텍스트 (hero/value/label/dim)
  • MetalDial: 미구현 (시안 A의 핵심 요소)

Phase 8 구현 내용 (진행 중)

  • SSOT 완료: d3roPalette에 11개 토큰 추가 (sidebar, chassis, inactive, dimLabel 등)
  • 매직넘버 제거: tsx/ts 파일 매직넘버 0개 달성 (theme.ts 제외)
  • DS 컴포넌트 SSOT: 6개 모두 d3roPalette 참조로 교체
  • HotkeyRecordModal: 신규 — 커스텀 핫키 녹화 모달 (키 조합 입력→Chip 표시→저장)
  • SettingsModal 재작성: 음성 모드 3개(받아쓰기/Agent/원터치) 토글+핫키 표시+변경 UI
  • DashboardPage 재작성: 기능 중심 — Hero(핫키 표시) + 통계 4카드 + CRT 서비스 상태 + 최근 히스토리(날짜 그룹핑)
  • recording-tip 수정: 웨이브 바/프로그레스 바 색상 #1F5DF2(파란)→#f25b29(앰버)
  • 페이지 SSOT: AppLayout, StatusBar, HistoryPage, DictionaryPage, CommandsPage 모두 완료
  • 설계 문서: docs/phases/phase-8.md, docs/re-findings/speakly-settings-ui.md 작성

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