This document provides comprehensive technical details about the OpenWhispr project architecture for AI assistants working on the codebase.
OpenWhispr is an Electron-based desktop dictation application that uses whisper.cpp for speech-to-text transcription. It supports both local (privacy-focused) and cloud (OpenAI API) processing modes.
- Frontend: React 19, TypeScript, Tailwind CSS v4, Vite
- Desktop Framework: Electron 41 with context isolation
- Database: better-sqlite3 for local transcription history
- UI Components: shadcn/ui with Radix primitives
- Speech Processing: whisper.cpp + NVIDIA Parakeet (via sherpa-onnx) + OpenAI API
- Audio Processing: FFmpeg (bundled via ffmpeg-static)
- Node.js: 24 (pinned in
.nvmrc— CI uses Node 24, do NOT regeneratepackage-lock.jsonwith a different major version)
-
Dual Window Architecture:
- Main Window: Minimal overlay for dictation (draggable, always on top)
- Control Panel: Full settings interface (normal window)
- Both use same React codebase with URL-based routing
-
Process Separation:
- Main Process: Electron main, IPC handlers, database operations
- Renderer Process: React app with context isolation
- Preload Script: Secure bridge between processes
- ONNX Utility Process: hosts all
onnxruntime-nodeinference (text embeddings, speaker embeddings, fbank). Lazy-spawned on first use viasrc/helpers/onnxWorkerClient.js→src/workers/onnxWorker.js. Native crashes (e.g., ORTbad_alloc) confine to the worker; main process rejects in-flight requests and respawns with backoff. Stopped inwill-quit.
-
Audio Pipeline:
- MediaRecorder API → Blob → ArrayBuffer → IPC → File → whisper.cpp
- Automatic cleanup of temporary files after processing
- main.js: Application entry point, initializes all managers
- preload.js: Exposes safe IPC methods to renderer via window.api
- windows-key-listener.c: C source for Windows low-level keyboard hook (Push-to-Talk)
- windows-mic-listener.c: C source for WASAPI mic session monitor (event-driven mic detection)
- windows-system-audio-helper.c: C source for WASAPI process-loopback system audio capture (meeting transcription). Excludes OpenWhispr's own process tree, so it hears every app on every output device. Requires Windows 10 2004+; falls back to Chromium display-media loopback when unavailable. Outputs 24 kHz mono s16le PCM on stdout, line-delimited JSON events on stderr (same protocol as linux-system-audio-helper)
- macos-mic-listener.swift: Swift source for CoreAudio mic property listener (event-driven mic detection)
- globe-listener.swift: Swift source for macOS Globe/Fn key detection
- bin/: Directory for compiled native binaries (whisper-cpp, nircmd, key/mic listeners)
- audioManager.js: Handles audio device management
- clipboard.js: Cross-platform clipboard operations
- macOS: AppleScript-based paste with accessibility permission check
- Windows: PowerShell SendKeys with nircmd.exe fallback
- Linux: Native XTest binary + compositor-aware fallbacks (xdotool, wtype, ydotool)
- database.js: SQLite operations for transcription history
- debugLogger.js: Debug logging system with file output
- devServerManager.js: Vite dev server integration
- dockManager.js: Single owner of the macOS Dock icon
- The icon follows the control panel: it appears when the panel opens and goes away when the panel closes to the tray, so no other caller (in particular the dictation panel's hide path) can resurrect it
- Every path that surfaces or hides the control panel (tray, app menu, deep links,
activate,ready-to-show,hideControlPanelToTray) reports that state explicitly - Never derive that state from the window's
show/hideevents: on macOS those are occlusion events, so they also fire when the panel is merely covered by another window, minimized, or on another Space, and the icon flickers as the user switches windows - Decision logic lives in
dockPolicy.js(pure, unit-tested intest/helpers/dockPolicy.test.js)
- dragManager.js: Window dragging functionality
- environment.js: Environment variable and OpenAI API management
- hotkeyManager.js: Global hotkey registration and management
- Named hotkey slots:
dictation,agent(chat agent overlay),voiceAgent(dictation routed straight to the dictation agent),meeting - Handles platform-specific defaults (GLOBE on macOS, Control+Super on Windows/Linux)
- Auto-fallback to F8/F9 if default hotkey is unavailable
- Notifies renderer via IPC when hotkey registration fails
- Integrates with GnomeShortcutManager for GNOME Wayland support
- Integrates with HyprlandShortcutManager for Hyprland Wayland support
- Integrates with KDEShortcutManager for KDE Wayland support
- Named hotkey slots:
- gnomeShortcut.js: GNOME Wayland global shortcut integration
- Uses D-Bus service to receive hotkey toggle commands
- Registers shortcuts via gsettings (visible in GNOME Settings → Keyboard → Shortcuts)
- Converts Electron hotkey format to GNOME keysym format
- Only active on Linux + Wayland + GNOME desktop
- D-Bus transport:
@homebridge/dbus-native(pure JavaScript, no native addons)
- hyprlandShortcut.js: Hyprland Wayland global shortcut integration
- Uses D-Bus service to receive hotkey toggle commands (same
com.openwhispr.Appservice) - Registers shortcuts via
hyprctl keyword bind(runtime keybinding) - Converts Electron hotkey format to Hyprland bind format (
MODS, key) - Only active on Linux + Wayland + Hyprland (detected via
HYPRLAND_INSTANCE_SIGNATURE) - D-Bus transport:
@homebridge/dbus-native(pure JavaScript, no native addons)
- Uses D-Bus service to receive hotkey toggle commands (same
- kdeShortcut.js: KDE Wayland global shortcut integration
- Uses D-Bus to communicate with KGlobalAccel for global hotkey registration
- Registers hotkeys via
setShortcut/doRegisterD-Bus calls on the KGlobalAccel interface - Listens for
globalShortcutPressedsignals to trigger callbacks - Converts Electron hotkey format to Qt key codes
- Only active on Linux + KDE desktop (detected via
XDG_CURRENT_DESKTOP) - D-Bus transport:
@homebridge/dbus-native(pure JavaScript, no native addons)
- ipcHandlers.js: Centralized IPC handler registration
- windowsKeyManager.js: Windows Push-to-Talk support with native key listener
- Spawns native
windows-key-listener.exebinary for low-level keyboard hooks - Supports compound hotkeys (e.g.,
Ctrl+Shift+F11,CommandOrControl+Space) - Emits
key-downandkey-upevents for push-to-talk functionality - Graceful fallback if binary unavailable
- Spawns native
- meetingDetectionEngine.js: Orchestrates meeting detection from all sources
- Gates notifications during recording (tap-to-talk and push-to-talk)
- Post-recording cooldown (2.5s) before showing queued notifications
- Priority-based coalescing (process > audio) — one notification, not three
- meetingProcessDetector.js: Detects running meeting apps
- macOS: Event-driven via
systemPreferences.subscribeWorkspaceNotification(zero CPU) - Windows/Linux: Shared
processListCachepolling (30s interval)
- macOS: Event-driven via
- audioActivityDetector.js: Detects microphone usage for unscheduled meetings
- macOS: Event-driven via
macos-mic-listenerbinary (CoreAudio property listeners) - Windows: Event-driven via
windows-mic-listener.exe(WASAPI sessions, self-PID exclusion) - Linux: Event-driven via
pactl subscribe(PulseAudio source-output events) - All platforms: Graceful fallback to polling if native approach fails
- macOS: Event-driven via
- processListCache.js: Shared singleton process list cache (5s TTL,
ps-listnpm) - googleCalendarManager.js: Google Calendar sync with exponential backoff
- 10s socket timeout on API requests
- Backoff: 2min → 4min → 8min → cap 30min on consecutive failures
- Reset to normal interval on success
- menuManager.js: Application menu management
- tray.js: System tray icon and menu
- whisper.js: Local whisper.cpp integration and model management
- parakeet.js: NVIDIA Parakeet model management via sherpa-onnx
- parakeetServer.js: sherpa-onnx CLI wrapper for transcription
- qdrantManager.js: Qdrant vector DB sidecar process lifecycle (spawn, health check, shutdown)
- localEmbeddings.js: Local text embedding via ONNX Runtime + all-MiniLM-L6-v2 (384-dim vectors)
- vectorIndex.js: Qdrant collection management — upsert, delete, search, batch reindex
- windowConfig.js: Centralized window configuration
- windowManager.js: Window creation and lifecycle management
- cliBridge.js: Loopback HTTP server on ports 8200–8219, bearer-token auth (token at
~/.openwhispr/cli-bridge.json), 127.0.0.1-only. Used by the unified CLI to talk to a running desktop app. - postMigrationDetector.js: Detects users returning from the pre-Gizmo bundle ID via a
.bundle-migratedsentinel in userData; consumed byipcHandlers.jsto drive thePostMigrationOnboardingmodal
- App.jsx: Main dictation interface with recording states
- ControlPanel.tsx: Settings, history, model management UI
- OnboardingFlow.tsx: 8-step first-time setup wizard
- PostMigrationOnboarding.tsx: One-time modal for users returning from the pre-Gizmo bundle ID; reuses
PermissionsSectionto walk through re-granting Microphone, Accessibility, and System Audio. Triggered bypostMigrationDetector.js(see Helper Modules) - SettingsPage.tsx: Comprehensive settings interface
- WhisperModelPicker.tsx: Model selection and download UI
- ui/: Reusable UI components (buttons, cards, inputs, etc.)
- useAudioRecording.js: MediaRecorder API wrapper with error handling
- useClipboard.ts: Clipboard operations hook
- useDialogs.ts: Electron dialog integration
- useHotkey.js: Hotkey state management
- useLocalStorage.ts: Type-safe localStorage wrapper
- usePermissions.ts: System permission checks and settings access
openMicPrivacySettings(): Opens OS microphone privacy settingsopenSoundInputSettings(): Opens OS sound input device settingsopenAccessibilitySettings(): Opens OS accessibility settings (macOS only)
- useSettings.ts: Application settings management
- useWhisper.ts: Whisper binary availability check
- ReasoningService.ts: AI processing for agent-addressed commands
- Detects when user addresses their named agent and removes the agent name from final output
- Provider implementations live in a registry at
src/services/ai/inferenceProviders/index.tscovering 8 providers (anthropic,enterprise,gemini,groq,lan,local,openai,openwhispr), each implementing theInferenceProviderinterface fromtypes.ts - Per-scope LLM config: 4 scopes (
dictationCleanup,dictationAgent,noteFormatting,chatIntelligence) defined insrc/config/inferenceScopes.ts selectResolvedLLMConfig(state, scope)insettingsStore.tsresolves provider/model per scope with fallback chains
- whisper.js: Native binary wrapper for local transcription
- Bundled binaries in
resources/bin/whisper-cpp-{platform}-{arch} - Falls back to system installation (
brew install whisper-cpp) - GGML model downloads from HuggingFace
- Models stored in
~/.cache/openwhispr/whisper-models/
- Bundled binaries in
-
parakeet.js: Model management for NVIDIA Parakeet ASR models
- Uses sherpa-onnx runtime for cross-platform ONNX inference
- Bundled binaries in
resources/bin/sherpa-onnx-{platform}-{arch} - INT8 quantized models for efficient CPU inference
- Models stored in
~/.cache/openwhispr/parakeet-models/ - Server pre-warming on startup when
LOCAL_TRANSCRIPTION_PROVIDER=nvidiais set - Provider preference persisted to
.envviasaveAllKeysToEnvFile()on server start/stop
-
Available Models:
parakeet-tdt-0.6b-v3: Multilingual (25 languages), ~680MBparakeet-unified-en-0.6b: English-only, ~631MB, state-of-the-art EN accuracy (5.91% avg WER on Open ASR Leaderboard)nemotron-speech-streaming-en-0.6b: English-only, ~632MB, cache-aware streaming FastConformer ("runtime": "online"in the registry)nemotron-3.5-asr-streaming-0.6b: Multilingual (15 transcription-ready languages, auto detection), ~650MB, cache-aware streaming FastConformer ("runtime": "online")
-
Runtimes: Models are
offline(default) oronlineper their registryruntimefield. Offline models use the bundledsherpa-onnx-ws-{platform}-{arch}(offline websocket server); online models usesherpa-onnx-online-ws-{platform}-{arch}(online websocket server). Both are downloaded byscripts/download-sherpa-onnx.js. Partial/final JSON results are merged byparakeetWsResult.js. -
Streaming Commit (online models): For online-runtime models, dictation streams worklet PCM to a persistent websocket stream during capture (regardless of the preview toggle) and commits the flushed text at stop as the final transcript — no second decode of the recording. The stop flush is truncation-aware (
finish()extends its deadline while results keep arriving and flagstruncated); anything but a clean flush falls back to the record-then-transcribe path (transcribe-local-parakeet), which for online models decodes the whole recording over a single stream (no 15s segmentation; segments only bound memory for very long files). -
Live Transcription Preview: When the preview toggle is on and an online-runtime model is selected, the preview shares the streaming-commit websocket: partial results update the preview window live (replacing text via
showTranscriptionPreview). Offline models keep the 1.5s buffered-chunk path (appending viaappendTranscriptionPreview). If the stream can't start or dies mid-dictation, the preview falls back to the chunked path and the final transcript falls back to the full decode. Tests:test/helpers/parakeetOnlineStream.test.js(mock websocket server). -
Download URLs: Models from sherpa-onnx ASR models release on GitHub
Always-on offline semantic search that finds notes by meaning, not just keywords. Used by the AI agent's search_notes tool. Qdrant starts automatically on app launch; embedding model auto-downloads on first run if missing.
Architecture:
- Qdrant sidecar: Rust binary spawned as child process (
qdrantManager.js), port 6333–6350 - Embedding model:
all-MiniLM-L6-v2via ONNX Runtime (localEmbeddings.js), 384-dim vectors - Vector index: Qdrant collection management (
vectorIndex.js), cosine distance - Hybrid search: FTS5 + Qdrant in parallel → Reciprocal Rank Fusion (K=60) with 0.3 cosine score threshold
Pipeline:
- App launches → Qdrant binary starts → collection created. Embedding model auto-downloads if missing (~22MB)
- Note create/update/delete → SQLite write → background vector upsert/delete via
_asyncVectorUpsert()/_asyncVectorDelete() - Agent searches →
db-semantic-search-notesIPC → parallel FTS5 + vector search → RRF merge → ranked results
Search fallback chain (in searchNotesTool.ts): cloud search → local semantic → FTS5 keyword
Storage:
- Qdrant data:
~/.cache/openwhispr/qdrant-data/(qdrant-data-dev/in development) - Qdrant binary:
resources/bin/qdrant-{platform}-{arch}(bundled — downloaded duringprebuild/predev:main) - Embedding model:
~/.cache/openwhispr/embedding-models/all-MiniLM-L6-v2/(auto-downloaded on first launch)
Dependencies: @qdrant/js-client-rest, onnxruntime-node
Dev setup: The Qdrant binary downloads automatically via predev/prestart. The embedding model auto-downloads on first app launch. To manually download: npm run download:qdrant and npm run download:embedding-model.
- download-whisper-cpp.js: Downloads whisper.cpp binaries from GitHub releases
- download-llama-server.js: Downloads llama.cpp server for local LLM inference
- download-nircmd.js: Downloads nircmd.exe for Windows clipboard operations
- download-windows-key-listener.js: Downloads prebuilt Windows key listener binary
- download-windows-mic-listener.js: Downloads prebuilt Windows mic listener binary
- download-sherpa-onnx.js: Downloads sherpa-onnx binaries for Parakeet support
- download-qdrant.js: Downloads Qdrant vector DB binary for local semantic search
- download-minilm.js: Downloads all-MiniLM-L6-v2 ONNX model + tokenizer for local embeddings
- build-globe-listener.js: Compiles macOS Globe key listener from Swift source
- build-macos-mic-listener.js: Compiles macOS mic listener from Swift source
- build-windows-key-listener.js: Compiles Windows key listener (for local development)
- run-electron.js: Development script to launch Electron with proper environment
- lib/download-utils.js: Shared utilities for downloading and extracting files
fetchLatestRelease(repo, options): Fetches latest release from GitHub APIdownloadFile(url, dest): Downloads file with progress and retry logicextractZip(zipPath, destDir): Cross-platform zip extractionparseArgs(): Parses CLI arguments for platform/arch targeting- Supports
GITHUB_TOKENfor authenticated requests (higher rate limits)
FFmpeg is bundled with the app and doesn't require system installation:
// FFmpeg is unpacked from ASAR to app.asar.unpacked/node_modules/ffmpeg-static/- User presses hotkey → MediaRecorder starts
- Audio chunks collected in array
- User presses hotkey again → Recording stops
- Blob created from chunks → Converted to ArrayBuffer
- Sent via IPC
- Main process writes to temporary file
- whisper.cpp processes file → Result sent back
- Temporary file deleted
Models stored in ~/.cache/openwhispr/whisper-models/:
- tiny: ~75MB (fastest, lowest quality)
- base: ~142MB (recommended balance)
- small: ~466MB (better quality)
- medium: ~1.5GB (high quality)
- large: ~3GB (best quality)
- turbo: ~1.6GB (fast with good quality)
CREATE TABLE transcriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
original_text TEXT NOT NULL,
processed_text TEXT,
is_processed BOOLEAN DEFAULT 0,
processing_method TEXT DEFAULT 'none',
agent_name TEXT,
error TEXT
);Settings stored in localStorage with these keys:
whisperModel: Selected Whisper modeluseLocalWhisper: Boolean for local vs cloudlanguage: Selected language codeagentName: User's custom agent namereasoningModel: Selected AI model for processingreasoningProvider: AI provider (openai/anthropic/gemini/local)hotkey: Custom hotkey configurationhasCompletedOnboarding: Onboarding completion flagcustomDictionary: JSON array of words/phrases for improved transcription accuracy
Secret env vars (12 total: 7 BYOK API keys + 5 enterprise cloud creds — see SECRET_KEYS in environment.js) are encrypted at rest via Electron safeStorage and stored as per-key files under userData/secure-keys/. They are loaded into process.env at startup by EnvironmentManager.init(). Renderer reads them via IPC (get-*-key) and writes via debounced IPC (save-*-key). On Linux without a keyring, secrets fall back to plaintext.
Non-secret env vars persisted to .env (via saveAllKeysToEnvFile()):
LOCAL_TRANSCRIPTION_PROVIDER: Transcription engine (nvidiafor Parakeet)PARAKEET_MODEL: Selected Parakeet model name (e.g.,parakeet-tdt-0.6b-v3)
58 languages supported (see src/utils/languages.ts):
- Each language has a two-letter code and label
- "auto" for automatic detection
- Passed to whisper.cpp via -l parameter
- User names their agent during onboarding (step 6/8)
- Name stored in localStorage and database
- ReasoningService detects "Hey [AgentName]" patterns
- AI processes command and removes agent reference from output
- Supports multiple AI providers (all models defined in
src/models/modelRegistryData.json):- OpenAI (Responses API):
- GPT-5.5 (
gpt-5.5) - Latest flagship frontier model, 1M context - GPT-5.2 (
gpt-5.2) - Strong reasoning model - GPT-5 Mini (
gpt-5-mini) - Fast and cost-efficient - GPT-5 Nano (
gpt-5-nano) - Ultra-fast, low latency - GPT-4.1 Series (
gpt-4.1,gpt-4.1-mini,gpt-4.1-nano) - Strong baseline with 1M context
- GPT-5.5 (
- Anthropic (Via IPC bridge to avoid CORS):
- Claude Opus 4.7 (
claude-opus-4-7) - Most capable Claude model, 1M context - Claude Sonnet 4.6 (
claude-sonnet-4-6) - Balanced performance - Claude Haiku 4.5 (
claude-haiku-4-5) - Fast with near-frontier intelligence - Claude Opus 4.6 (
claude-opus-4-6) - Previous Opus generation, 1M context - Claude Sonnet 4.5 (
claude-sonnet-4-5) - Previous Sonnet generation - Claude Opus 4.5 (
claude-opus-4-5) - Earlier Opus model
- Claude Opus 4.7 (
- Google Gemini (Direct API integration):
- Gemini 3.1 Pro (
gemini-3.1-pro-preview) - Most capable Gemini model - Gemini 3 Flash (
gemini-3-flash-preview) - Ultra-fast, high-capability next-gen model - Gemini 2.5 Flash Lite (
gemini-2.5-flash-lite) - Lowest latency and cost
- Gemini 3.1 Pro (
- Local: GGUF models via llama.cpp (Qwen, Llama, Mistral, GPT-OSS)
- OpenAI (Responses API):
All AI model definitions are centralized in src/models/modelRegistryData.json as the single source of truth:
{
"cloudProviders": [...], // OpenAI, Anthropic, Gemini API models
"localProviders": [...] // GGUF models with download URLs
}Key files:
src/models/modelRegistryData.json- Single source of truth for all modelssrc/models/ModelRegistry.ts- TypeScript wrapper with helper methodssrc/config/aiProvidersConfig.ts- Derives AI_MODES from registrysrc/utils/languages.ts- Derives REASONING_PROVIDERS from registrysrc/helpers/modelManagerBridge.js- Handles local model downloads
Local model features:
- Each model has
hfRepofor direct HuggingFace download URLs promptTemplatedefines the chat format (ChatML, Llama, Mistral)- Download URLs constructed as:
{baseUrl}/{hfRepo}/resolve/main/{fileName}
OpenAI Responses API (September 2025):
- Migrated from Chat Completions to new Responses API
- Endpoint:
https://api.openai.com/v1/responses - Simplified request format with
inputarray instead ofmessages - New response format with
outputarray containing typed items - Automatic handling of GPT-5 and o-series model requirements
- No temperature parameter for newer models (GPT-5, o-series)
Anthropic Integration:
- Routes through IPC handler to avoid CORS issues in renderer process
- Uses main process for API calls with proper error handling
- Model IDs use alias format (e.g.,
claude-sonnet-4-6not date-suffixed versions)
Gemini Integration:
- Direct API calls from renderer process
- Increased token limits for Gemini 3.1 Pro (2000 minimum)
- Proper handling of thinking process in responses
- Error handling for MAX_TOKENS finish reason
API Key Persistence:
- All API keys now properly persist to
.envfile - Keys stored in environment variables and reloaded on app start
- Centralized
saveAllKeysToEnvFile()method ensures consistency
The app can open OS-level settings for microphone permissions, sound input selection, and accessibility:
IPC Handlers (in ipcHandlers.js):
open-microphone-settings: Opens microphone privacy settingsopen-sound-input-settings: Opens sound/audio input device settingsopen-accessibility-settings: Opens accessibility privacy settings (macOS only)
Platform-specific URLs:
| Platform | Microphone Privacy | Sound Input | Accessibility |
|---|---|---|---|
| macOS | x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone |
x-apple.systempreferences:com.apple.preference.sound?input |
x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility |
| Windows | ms-settings:privacy-microphone |
ms-settings:sound |
N/A |
| Linux | Manual (no URL scheme) | Manual (e.g., pavucontrol) | N/A |
UI Component (MicPermissionWarning.tsx):
- Shows platform-appropriate buttons and messages
- Linux only shows "Open Sound Settings" (no separate privacy settings)
- macOS/Windows show both sound and privacy buttons
Enable with --log-level=debug or OPENWHISPR_LOG_LEVEL=debug (can be set in .env):
- Logs saved to platform-specific app data directory
- Comprehensive logging of audio pipeline
- FFmpeg path resolution details
- Audio level analysis
- Complete reasoning pipeline debugging with stage-by-stage logging
Native Windows support for true push-to-talk functionality using low-level keyboard hooks:
Architecture:
resources/windows-key-listener.c: Native C program using WindowsSetWindowsHookExfor keyboard hookssrc/helpers/windowsKeyManager.js: Node.js wrapper that spawns and manages the native binary- Binary outputs
KEY_DOWNandKEY_UPto stdout when target key is pressed/released
Compound Hotkey Support:
- Parses hotkey strings like
CommandOrControl+Shift+F11 - Maps modifiers:
CommandOrControl/Ctrl→ VK_CONTROL,Alt/Option→ VK_MENU,Shift→ VK_SHIFT - Verifies all required modifiers are held before emitting key events
Binary Distribution:
- Prebuilt binary downloaded from GitHub releases (
windows-key-listener-v*tags) - Download script:
scripts/download-windows-key-listener.js - CI workflow:
.github/workflows/build-windows-key-listener.yml - Fallback to tap mode if binary unavailable
IPC Events:
windows-key-listener:key-down: Fired when hotkey pressed (start recording)windows-key-listener:key-up: Fired when hotkey released (stop recording)
Improve transcription accuracy for specific words, names, or technical terms:
How it works:
- User adds words/phrases through Settings → Custom Dictionary
- Words stored as JSON array in localStorage (
customDictionarykey) - On transcription, words are joined and passed as
promptparameter to Whisper - Works with both local whisper.cpp and cloud OpenAI Whisper API
Implementation:
src/hooks/useSettings.ts: ManagescustomDictionarystatesrc/components/SettingsPage.tsx: UI for adding/removing dictionary wordssrc/helpers/audioManager.js: Reads dictionary and adds to transcription optionssrc/helpers/whisperServer.js: Includes dictionary aspromptin API request
Whisper Prompt Parameter:
- Whisper uses the prompt as context/hints for transcription
- Words in the prompt are more likely to be recognized correctly
- Useful for: uncommon names, technical jargon, brand names, domain-specific terms
On GNOME Wayland, Electron's globalShortcut API doesn't work due to Wayland's security model. OpenWhispr uses native GNOME shortcuts:
Architecture:
main.jsenablesGlobalShortcutsPortalfeature flag for WaylandhotkeyManager.jsdetects GNOME + Wayland and initializesGnomeShortcutManagergnomeShortcut.jscreates D-Bus service atcom.openwhispr.App- Shortcuts registered via
gsettingsas custom GNOME keybindings - GNOME triggers
dbus-sendcommand which calls the D-BusToggle()method
Key Constants:
- D-Bus service:
com.openwhispr.App - D-Bus path:
/com/openwhispr/App - gsettings path:
/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/openwhispr/
IPC Integration:
get-hotkey-mode-info: Returns{ isUsingGnome, isUsingHyprland, isUsingNativeShortcut }to renderer- UI hides activation mode selector when
isUsingNativeShortcutis true - Forces tap-to-talk mode (push-to-talk not supported)
Hotkey Format Conversion:
- Electron format:
F8,CommandOrControl+Shift+Space - GNOME format:
F8,<Control><Shift>space - Backtick (
) →grave` in GNOME keysym format
On Hyprland (wlroots Wayland compositor), Electron's globalShortcut API and the GlobalShortcutsPortal feature don't work reliably. OpenWhispr uses native Hyprland keybindings:
Architecture:
main.jsenablesGlobalShortcutsPortalfeature flag for Wayland (fallback)hotkeyManager.jsdetects Hyprland + Wayland and initializesHyprlandShortcutManagerhyprlandShortcut.jscreates D-Bus service atcom.openwhispr.App(same as GNOME)- Shortcuts registered via
hyprctl keyword bind(runtime keybinding) - Hyprland triggers
dbus-sendcommand which calls the D-BusToggle()method
Detection:
- Primary:
HYPRLAND_INSTANCE_SIGNATUREenvironment variable (set by Hyprland) - Fallback:
XDG_CURRENT_DESKTOPcontains "hyprland"
Hotkey Format Conversion:
- Electron format:
Control+Super,CommandOrControl+Shift+Space - Hyprland format:
CTRL, Super_L,CTRL SHIFT, space - Modifier-only combos (e.g.,
Control+Super) →CTRL, Super_L
Bind/Unbind Commands:
- Register:
hyprctl keyword bind "ALT, R, exec, dbus-send --session ..." - Unregister:
hyprctl keyword unbind "ALT, R" - Bindings are ephemeral (don't survive Hyprland restart) but re-registered on app startup
Limitations:
- Push-to-talk not supported (Hyprland
bindfires a single exec, not key-down/key-up) - Requires
hyprctlon PATH (ships with Hyprland)
Detects meetings via three independent sources, orchestrated by MeetingDetectionEngine:
Architecture:
MeetingDetectionEnginelistens to events fromMeetingProcessDetectorandAudioActivityDetectorGoogleCalendarManagerprovides calendar context (imminent events, active meetings)- All three sources feed into a unified notification pipeline
Process Detection (known meeting apps — Zoom, Teams, Webex, FaceTime):
- macOS:
systemPreferences.subscribeWorkspaceNotification— zero CPU, instant detection - Windows/Linux:
processListCacheshared polling (30s interval,ps-listnpm)
Microphone Detection (unscheduled/browser meetings like Google Meet):
- macOS:
macos-mic-listenerbinary — CoreAudiokAudioDevicePropertyDeviceIsRunningSomewhereproperty listeners with hot-plug support - Windows:
windows-mic-listener.exe— WASAPIIAudioSessionManager2session monitoring,--exclude-pidfor self-mic exclusion - Linux:
pactl subscribe— PulseAudio source-output events - All platforms: Graceful fallback to polling if native binary/command unavailable
Calendar Reminders (scheduled meetings):
GoogleCalendarManagerfiresmeetingDetectionEngine.handleCalendarReminder(event)1 minute before the scheduled start (MEETING_REMINDER_LEAD_MS) — no native OS notifications; all meeting prompts use the in-app overlay so they survive Focus/DND and screen-share notification muting- Calendar-sourced prompts show a Join primary action when the event has a meeting link (
getMeetingJoinUrlinsrc/helpers/meetingJoinUrl.js, shared with the renderer's Upcoming Meetings join button) — Join opens the link and starts the note
UX Rules:
- All prompts render in one always-on-top overlay window (
MeetingNotificationCard), content-protected so it never appears in screen shares - Prompt copy is derived in the renderer from
{ variant, event, joinUrl }(meetingNotification.*i18n keys); variants:detected(mic evidence),starting(calendar event not yet started),underway(event in progress) - Per-source notification prefs:
notifyCalendarRemindersgates calendar prompts,notifyMeetingDetectiongates mic/process prompts - During recording (tap-to-talk or push-to-talk): ALL notifications suppressed
- After recording: 2.5s cooldown before showing queued notifications
- Multiple signals coalesced: one overlay at a time; a newer prompt replaces the current one
- Calendar-aware: if an ongoing or imminent calendar event exists, the prompt shows the event name and links the note to the event
- Active meeting recording (meeting mode): all detections suppressed
Binary Distribution:
- macOS: Compiled from Swift source via
scripts/build-macos-mic-listener.jsduringcompile:native - Windows: Prebuilt binary downloaded via
scripts/download-windows-mic-listener.jsduringprebuild:win - CI workflow:
.github/workflows/build-windows-mic-listener.ymlauto-builds on push to main
Calendar Sync Resilience:
- 10s socket timeout on all Google Calendar API requests
- Exponential backoff on consecutive failures: 2min → 4min → 8min → cap 30min
- Reset to normal 2min interval on any successful sync
A dedicated global hotkey that starts a dictation whose transcript is sent straight to the dictation agent as a command — no wake word ("Hey [AgentName]") needed — and that always bypasses the cleanup model. Separate from the chat agent hotkey (CHAT_AGENT_KEY), which toggles the agent overlay window.
Flow:
- Hotkey pressed →
voiceAgentslot callback inmain.js→windowManager.sendToggleVoiceAgent()→toggle-voice-agentIPC to the main window useAudioRecording.jsstarts a recording withaudioManager.setVoiceAgentRequested(true)(any other start resets it tofalse)- On transcription,
resolveReasoningRouteconsultsresolveDictationRouteKind()(src/helpers/dictationRouting.js): a voice agent recording always takes the agent route; if the dictation agent is disabled or has no model, the raw transcript is returned — it never falls back to cleanup
Storage & IPC:
- Env var:
VOICE_AGENT_KEY(persisted viaenvironment.js), store key:voiceAgentKey(no default — user opt-in) - IPC handlers:
update-voice-agent-hotkey,get-voice-agent-key - Hotkey slot:
voiceAgent(tap-to-toggle; GNOME-native slot viaToggleVoiceAgentD-Bus method, KDE via KGlobalAccel, otherwiseglobalShortcut)
UI:
- Settings → Hotkeys → "Voice Agent Hotkey" (with cross-slot conflict validation)
- Onboarding: optional step right after the dictation hotkey (activation) step
- Requires the dictation agent to be enabled (Settings → AI Models) for the agent route to apply
Tests: test/helpers/dictationRouting.test.js (run with node --test)
All user-facing strings must use the i18n system. Never hardcode UI text in components.
Setup: react-i18next (v15) with i18next (v25). Translation files in src/locales/{lang}/translation.json.
Supported languages: en, es, fr, de, pt, it, ru, zh-CN, zh-TW
How to use:
import { useTranslation } from "react-i18next";
const { t } = useTranslation();
// Simple: t("notes.list.title")
// With interpolation: t("notes.upload.using", { model: "Whisper" })Rules:
- Every new UI string must have a translation key in
en/translation.jsonand all other language files - Use
useTranslation()hook in components and hooks - Keep
{{variable}}interpolation syntax for dynamic values - Do NOT translate: brand names (OpenWhispr, Pro), technical terms (Markdown, Signal ID), format names (MP3, WAV), AI system prompts
- Group keys by feature area (e.g.,
notes.editor.*,referral.toasts.*)
- New IPC Channel: Add to both ipcHandlers.js and preload.js
- New Setting: Update useSettings.ts and SettingsPage.tsx
- New UI Component: Follow shadcn/ui patterns in src/components/ui
- New Manager: Create in src/helpers/, initialize in main.js
- New UI Strings: Add translation keys to all 10 language files (see i18n section above)
- New Sidecar Binary: Add download script in
scripts/, add toprebuild*scripts in package.json, add manager insrc/helpers/, initialize inmain.js. Spawn the child withdetached: process.platform !== "win32"so it has its own process group on Unix. Right after spawn callsidecarPidFile.write(name, child.pid)and onclosecallsidecarPidFile.clear(name). Add the binary fragment toEXPECTED_BINARY_FRAGMENTSinsidecarReaper.js. Register a stop function viasidecarRegistry.register(name, () => manager.stop())inregisterSidecars()— that single registration replaces the oldwill-quitline.
- Test both local and cloud processing modes
- Verify hotkey works globally
- Check clipboard pasting on all platforms
- Test with different audio input devices
- Verify whisper.cpp binary detection
- Test all Whisper models
- Check agent naming functionality
- Test custom dictionary with uncommon words
- Verify Windows Push-to-Talk with compound hotkeys
- Test GNOME Wayland hotkeys (if on GNOME + Wayland)
- Test Hyprland Wayland hotkeys (if on Hyprland + Wayland)
- Verify activation mode selector is hidden on GNOME Wayland and Hyprland Wayland
- Verify meeting detection works with event-driven mode (check debug logs for "event-driven")
- Test meeting notification suppression during recording
- Test post-recording cooldown (notifications shouldn't flash immediately)
- Create a note about "quarterly revenue projections", search via agent for "financial forecast" — should match semantically
- Verify Qdrant starts on app launch (check debug logs for "qdrant started successfully")
- Kill Qdrant process manually — verify FTS5 keyword search still works as fallback
-
No Audio Detected:
- Check FFmpeg path resolution
- Verify microphone permissions
- Check audio levels in debug logs
-
Transcription Fails:
- Ensure whisper.cpp binary is available
- Check model is downloaded
- Check temporary file creation
- Verify FFmpeg is executable
-
Clipboard Not Working:
- macOS: Check accessibility permissions (required for AppleScript paste)
- Linux: Native
linux-fast-pastebinary (XTest) is tried first, works for X11 and XWayland apps- X11: xdotool fallback if native binary unavailable
- GNOME/KDE Wayland: xdotool (XWayland apps) → ydotool (requires ydotoold daemon)
- wlroots Wayland (Sway, Hyprland): wtype → xdotool → ydotool
- Windows: PowerShell SendKeys (built-in) or nircmd.exe (bundled)
-
Build Issues:
- Use
npm run packfor unsigned builds (CSC_IDENTITY_AUTO_DISCOVERY=false) - Signing requires Apple Developer account
- ASAR unpacking needed for FFmpeg
- Run
npm run download:whisper-cppbefore packaging (current platform) - Use
npm run download:whisper-cpp:allfor multi-platform packaging - afterSign.js automatically skips signing when CSC_IDENTITY_AUTO_DISCOVERY=false
- Lockfile: Always use Node 24 when running
npm install(matches CI). If your local Node version differs, usenvm exec 24 npm install. Runningnpm installwith a different major version will produce an incompatiblepackage-lock.jsonthat breaksnpm ciin CI.
- Use
-
Windows Push-to-Talk Binary:
- Prebuilt binary downloaded automatically on Windows during build
- If download fails, push-to-talk falls back to tap mode
- To compile locally: install Visual Studio Build Tools or MinGW-w64
- CI workflow (
.github/workflows/build-windows-key-listener.yml) auto-builds on push to main
-
Meeting Detection Not Working:
- Check debug logs for "event-driven" vs "polling" mode
- macOS: Verify
macos-mic-listenerbinary exists inresources/bin/(compiled duringnpm run compile:native) - Windows: Verify
windows-mic-listener.exeexists inresources/bin/(downloaded duringprebuild:win) - Linux: Verify
pactlis installed (pulseaudio-utilsorpipewire-pulsepackage) - If event-driven binary is missing, detection falls back to polling automatically
-
Local Semantic Search Not Working:
- Qdrant binary should be in
resources/bin/qdrant-{platform}-{arch}(auto-downloaded duringpredev/prebuild) - Embedding model should be in
~/.cache/openwhispr/embedding-models/all-MiniLM-L6-v2/model.onnx(auto-downloaded on first app launch) - Run
npm run download:qdrantandnpm run download:embedding-modelmanually if missing - Check debug logs for "qdrant" entries (port, health check, errors)
- If Qdrant fails to start, search still works via FTS5 keyword fallback
- Semantic search is only available through the AI agent's
search_notestool, not the manual search UI
- Qdrant binary should be in
macOS:
- Requires accessibility permissions for clipboard (auto-paste)
- Requires microphone permission (prompted by system)
- Uses AppleScript for reliable pasting
- Notarization needed for distribution
- Shows in dock with indicator dot when running (LSUIElement: false)
- whisper.cpp bundled for both arm64 and x64
- System settings accessible via
x-apple.systempreferences:URL scheme
Windows:
- No special accessibility permissions needed
- Microphone privacy settings at
ms-settings:privacy-microphone - Sound settings at
ms-settings:sound - NSIS installer for distribution
- whisper.cpp bundled for x64
- Push-to-Talk: Native key listener binary (
windows-key-listener.exe) enables true push-to-talk- Uses Windows Low-Level Keyboard Hook (
WH_KEYBOARD_LL) - Supports compound hotkeys (e.g.,
Ctrl+Shift+F11) - Prebuilt binary auto-downloaded from GitHub releases
- Falls back to tap mode if unavailable
- Uses Windows Low-Level Keyboard Hook (
Linux:
- Multiple package manager support
- Standard XDG directories
- AppImage for distribution
- whisper.cpp bundled for x64
- No standardized URL scheme for system settings (user must open manually)
- Privacy settings button hidden in UI (not applicable on Linux)
- Recommend
pavucontrolfor audio device management - Clipboard paste tools (at least one required for auto-paste):
- X11:
xdotool(recommended) - Wayland (non-GNOME):
wtype(requires virtual keyboard protocol) orxdotool(works via XWayland, recommended for Electron apps) - GNOME Wayland:
xdotoolfor XWayland apps only (native Wayland apps require manual paste) - Terminal detection: Auto-detects terminal emulators and uses Ctrl+Shift+V
- Fallback: Text copied to clipboard with manual paste instructions
- X11:
- GNOME Wayland global hotkeys:
- Uses native GNOME shortcuts via D-Bus and gsettings (no special permissions needed)
- Hotkeys visible in GNOME Settings → Keyboard → Shortcuts → Custom
- Default fallback:
F8whenControl+Supercannot be registered - Push-to-talk unavailable (GNOME shortcuts only fire single toggle event)
- Falls back to X11/globalShortcut if GNOME integration fails
- D-Bus transport:
@homebridge/dbus-native(pure JavaScript, no native addons)
- Use TypeScript for new React components
- Follow existing patterns in helpers/
- Descriptive error messages for users
- Comprehensive debug logging
- Clean up resources (files, listeners)
- Handle edge cases gracefully
- Whisper model size vs speed tradeoff
- Audio blob size limits for IPC (10MB)
- Temporary file cleanup
- Memory usage with large models
- Process timeout protection (5 minutes)
- Meeting detection uses event-driven OS APIs (near-zero CPU) with polling fallback
- Process list cache shared between detectors to avoid duplicate
tasklist/pgrepcalls - Google Calendar sync uses exponential backoff to avoid hammering API on network failures
- API keys and enterprise cloud creds (12 secrets total) encrypted at rest via Electron
safeStorage→ OS keychain (Keychain / DPAPI / libsecret), stored as per-key files inuserData/secure-keys/. Linux without a keyring falls back to plaintext (Electron default). Closed in #629. - Context isolation enabled
- No remote code execution
- Sanitized file paths
- Limited IPC surface area
- Streaming transcription support
- Custom wake word detection
Multi-language UI(implemented — 9 languages via react-i18next)- Cloud model selection
- Batch transcription
- Export formats beyond clipboard