# 04 — Desktop App (Electron) Map > Surface: `apps/desktop` > Stack: Electron 33 + React 19 + MUI 7 + Vite (electron-vite) + better-sqlite3/drizzle + uiohook-napi + nut-js > Source root: `apps/desktop/src` (`main/`, `preload/`, `renderer/`) --- ## 1. Process architecture | Layer | Path | Contents | |---|---|---| | Main | `src/main/` | Services, IPC handlers, windows, bootstrap/lifecycle, DB | | Preload | `src/preload/` | `index.ts` exposes `window.electronAPI`; `popup.ts` exposes `window.popupAPI` | | Renderer | `src/renderer/` | React app: `AppLayout` + 7 pages + modals + 5 vanilla popups | **Main entry** `src/main/index.ts`: sets app name/AppUserModelId, disables GPU acceleration, EPIPE/uncaught handlers, registers `d3ro-voice://` deep-link protocol (Supabase OAuth implicit + PKCE), single-instance lock, then `bootstrap()` + `setupLifecycle()`. **Bootstrap** `src/main/bootstrap.ts`: ordered `BootstrapStep[]` — logger, config, **database (critical)**, license, create-windows (critical), tray, **ipc-handlers (critical)**, custom-instructions, voice-commands, sound-effects, auto-launch, popup-preload, hotkey, voice-mode, llm-polling, meeting-summary-wiring, meeting-mode, cloud-sync, auto-update. Wires VoiceMode events to sound + history persistence. --- ## 2. Main services (`src/main/services/`) Singleton + `EventEmitter` pattern (`getXService()` accessors). ### Core voice pipeline | Service | Purpose | |---|---| | `VoiceModeService` | Orchestrator: 9-state `RecognitionState` + 4-state `AudioState`, dual-condition flush, action queue. Events: session-started/completed/cancelled, transcription-update, audio-level, recognition/audio-state-changed, premium-llm-fallback, error | | `AudioCaptureService` | Mic PCM16 16kHz mono (bundled SoX on Windows, node-record-lpcm16 elsewhere). Spawns hidden (`windowsHide`); a missing SoX fails with the exact fix command | | `LocalSTTService` | faster-whisper Python sidecar manager (state machine, dual-flush, model download/cancel, background warm-up, live partial transcription). Connects over IPv4 loopback (`getSidecarBaseUrl`) and fails fast with an actionable message when the bundled engine or virtualenv is missing | | `HotkeyService` | uiohook-napi global hooking (dictation/hands-free/command/caption). Events: hotkey-pressed/released, double-press, error | | `TextInsertService` | Clipboard save→set→Ctrl+V→restore via nut-js | | `SoundEffectService` | Preloaded WAV feedback (start/stop/error/cancel/chime) | ### STT engine layer (`services/stt/`) | File | Purpose | |---|---| | `STTManager` | Dispatcher across local + 6 cloud providers, auto-fallback (events provider-changed, config-changed, fallback-to-local). `transcribePartial`/`warmUpLocal` route to the local engine only | | `types.ts` | `ISTTDriver` contract | | `audio-utils.ts` | `pcmToWav`, `createProbeWav` | | `drivers/OpenAI|Groq|Deepgram|AssemblyAI|Google|Custom|D3ROCloud` | Provider drivers; `D3ROCloudDriver` uses Supabase access token | ### LLM layer | Service | Purpose | |---|---| | `LocalLLMService` | Ollama REST (models, pull w/ progress, server start, NDJSON streaming) | | `PremiumLLMService` | Claude via Supabase `llm-proxy`, local fallback | | `OnlineLLMService` | JWT-authenticated .NET backend client | | `llm-prompts.ts` | `resolveSystemPrompt` SSOT for action prompts | ### Memory & knowledge | Service | Purpose | |---|---| | `HistoryService` | SQLite history CRUD/search/stats | | `DictionaryService` | Custom vocabulary CRUD/search + cloud sync hooks + JSON/CSV import/export (`dictionary:import`/`export`, save/open dialogs) | | `MemoService` | Memo tags over history (`memo_tags`) | | `RAGService` | Local RAG: `nomic-embed-text` embeddings, cosine search over `rag_chunks` | | `CustomInstructionService` | User LLM commands (5 built-ins) | | `VoiceCommandService` | Keyword → command rule matching | | `ChainService` | Multi-step LLM pipelines (LLMChain) | | `ScreenContextService` | Active-window + selected-text context | ### Phase 10+ features | Service | Purpose | |---|---| | `CaptionService` | Live captions from system/loopback audio; caption overlay (events segment, state-changed, session-saved, error) | | `FileTranscriptionService` | Audio/video file → ffmpeg → 30s chunks → STT merge (events progress, complete, error, state-changed) | | `MeetingSummaryService` | Post-caption LLM summary | | `DictationTemplateService` | Field-by-field voice form filling | | `VoiceConversationService` | STT→LLM→TTS loop, 10-turn memory | | `TTSPlaybackService` | Platform TTS (macOS `say`, Windows SAPI), sentence queue | | `VoiceActionService` | Voice → LLM JSON action plan → OS execution (dangerous blocked) | ### Phase 12–15 | Service | Purpose | |---|---| | `MeetingModeService` | Meeting recording: live transcript, timestamp memos, doc generation/export, diarization | | `MeetingDocTemplateService` | Meeting-doc templates (built-ins + CRUD) | ### Account / infra / monetization | Service | Purpose | |---|---| | `ConfigService` | electron-store `AppConfig` (`configGet/Set`, defaults) | | `LicenseService` | Freemium tiers, quotas (`daily_usage`), activation, upgrade prompts | | `CloudSyncService` | Supabase sync, per-user DB switching, history/dictionary/meeting mirror | | `CloudSTTService` | Thin cloud STT wrapper over `D3ROCloudDriver` | | `UpdateService` | electron-updater (canonical Forgejo feed, channels, mandatory/full-vs-delta policy, staged rollout, restart dialog) | | `AutoLaunchService` | OS login-item auto-start | | `LoggerService` | electron-log wrapper + category loggers | | `checkout`/payment | `payment-handlers.ts` — authenticated Edge-only Stripe/Payple checkout + server readback | ### Ads (`services/ads/`) | File | Purpose | |---|---| | `AdMediationEngine` | Multi-ad mediation + header bidding | | `AdSettlementService` | Revenue settlement, withholding, payout ledger | | `BaseAdAdapter` / `UnavailableAdAdapter` | Adapter contract + fail-closed base | | `DirectHouseSponsorAdapter` | **Real configurable adapter**: bids/reports against an operator HTTPS `endpointUrl` (`AdNetworkConfig.endpointUrl`), validates creatives, fail-closed (`adapter_not_configured`) when unconfigured | | 9 placeholder adapters (AppLovin, Carbon, EthicalAds, GoogleAdManager, InMobi, Mintegral, Playwire, PubMatic, Unity) | Extend `UnavailableAdAdapter` — registered, no live bids (`provider_not_integrated`) | --- ## 3. IPC layer Registry: `src/main/ipc/index.ts` calls 29 `registerXHandlers()` in fixed order. Channel SSOT: `packages/core/src/ipc-channels.ts`. | Handler | Channel group(s) | |---|---| | `ads-handlers` | ADS | | `audio-handlers` | AUDIO | | `caption-handlers` | CAPTION + SYSTEM_AUDIO | | `chain-handlers` | CHAIN | | `cloud-sync-handlers` | CLOUD_SYNC | | `config-handlers` | CONFIG | | `context-handlers` | CONTEXT | | `dictionary-handlers` | DICTIONARY | | `file-transcription-handlers` | FILE_TRANSCRIPTION | | `history-handlers` | HISTORY + `stats:getSummary` | | `hotkey-handlers` | HOTKEY | | `instruction-handlers` | INSTRUCTION | | `license-handlers` | LICENSE | | `llm-handlers` | LLM + `llm:premium:*` + ONLINE_AUTH | | `meeting-doc-template-handlers` | MEETING_DOC_TEMPLATE | | `meeting-mode-handlers` | MEETING_MODE + MEETING_CHAT | | `meeting-summary-handlers` | MEETING_SUMMARY | | `memo-handlers` | MEMO | | `payment-handlers` | PAYMENT | | `rag-handlers` | RAG | | `stt-handlers` | STT | | `support-handlers` | SUPPORT | | `system-handlers` | SYSTEM | | `template-handlers` | DICTATION_TEMPLATE | | `voice-action-handlers` | VOICE_ACTION | | `voice-command-handlers` | VOICE_COMMAND | | `voice-conversation-handlers` | VOICE_CONVERSATION | | `voice-handlers` | VOICE | | `window-handlers` | WINDOW + `SYSTEM.OPEN_EXTERNAL` | Preload exposes **`window.electronAPI`** with 33 namespaces: `platform, audio, config, voice, stt, hotkey, llm (incl. premium), history, dictionary, stats, window, system, instruction, app, memo, voiceCommand, context, chain, caption, license, fileTranscription, meetingSummary, dictationTemplate, rag, voiceAction, voiceConversation, meetingMode, meetingChat, meetingDocTemplate, cloudSync, onlineAuth, ads, support, payment`. Envelope: `IPCResult` (success/error); `app.onDataChanged` is the global refresh channel. --- ## 4. Windows & popups `windows/WindowManager.ts` creates 6 windows: main (borderless, custom TitleBar; macOS `hiddenInset`), recording-tip, result-popup, history-popup, command-popup, caption-overlay. Injects popup theme CSS + i18n strings; 2-phase resize. `windows/TrayManager.ts` — tray icon + menu + double-click show. **Popup invariants** (each shipped broken once — do not regress): - 팝업 HTML의 스크립트는 반드시 `