Skip to content

griffinwork40/whisper-app

Repository files navigation

Whisper App

A minimal, local-only macOS menu-bar dictation app powered by mlx-whisper on Apple Silicon. Press a global hotkey, speak, release β€” and your speech is transcribed and dropped onto your clipboard (or auto-typed into the focused app).

No cloud. No telemetry. No Dock icon. Just a tray icon and a hotkey.

🌐 Website: whisper-app-ashy.vercel.app

Status: v0 β€” works on the developer's machine. Apple Silicon (M-series) only. Not yet code-signed.


Features

  • πŸŽ™οΈ Hold-to-talk hotkey. Default βŒ₯⌘ (hold both, speak, release).
  • 🧠 On-device transcription. Uses mlx-whisper against models cached under ~/.cache/huggingface/hub/.
  • πŸ“‹ Output modes. Clipboard, auto-type via AppleScript System Events, or both.
  • πŸŽ›οΈ Tray menu. Pick a model (whisper-turbo, whisper-large-v3-turbo, …) and an output mode. Settings persist to ~/Library/Application Support/whisper-app/config.json.
  • 🟒 Status icon. Idle β†’ recording β†’ processing β†’ done, with a clear error state.
  • 🚫 No Dock icon, no window. Pure menu-bar app (LSUIElement = true equivalent).

Requirements

Hardware Apple Silicon (M-series) β€” mlx-whisper runs on the Neural Engine / GPU
macOS Sequoia (26.x) or later
Python python3 on PATH with mlx_whisper importable
ffmpeg Required at /opt/homebrew/bin/ffmpeg (the Homebrew default on Apple Silicon β€” this path is currently hardcoded)
Models At least one model pre-downloaded to ~/.cache/huggingface/hub/
Permissions Microphone (always). Accessibility (only if you want auto-type).

One-time setup

# Python backend
pip3 install mlx-whisper

# ffmpeg
brew install ffmpeg

# Pre-cache a model (whisper-turbo is the default; ~810 MB)
python3 -c "from huggingface_hub import snapshot_download; snapshot_download('mlx-community/whisper-turbo')"

Install & run from source

git clone https://github.com/griffinwork40/whisper-app.git
cd whisper-app
npm install
npm run build
npm start

The tray icon appears in your menu bar. The default hotkey is βŒ₯⌘ (hold-to-talk). The first time you hold it, macOS will prompt for Microphone access (and Accessibility, if you've selected autotype).


Configuration

Settings live at ~/Library/Application Support/whisper-app/config.json:

{
  "hotkey": "Option+Cmd",                   // Electron accelerator string
  "hotkeyMode": "hold",                     // "hold" (push-to-talk) | "tap" (toggle)
  "model": "mlx-community/whisper-turbo",   // HuggingFace repo id
  "language": "en",                         // ISO 639-1 or "auto"
  "outputMode": "clipboard",                // "clipboard" | "autotype" | "both"
  "playSounds": true,                       // start/stop audio cues (toggle in tray menu)
  "pythonPath": "python3",
  "tempDir": "/tmp",
  "customVocabulary": "",                   // see "Custom vocabulary" below
  "replacementRules": []                    // see "Replacement rules" below
}

Sounds

Short start/stop chimes play when recording begins and ends. Toggle them from the tray menu's Play sounds item (persists to config.json as playSounds). The cues live at assets/sounds/{start,stop}.aiff and are regenerated by the optional scripts/gen-sounds.py dev tool.

Hotkey modes

The tray menu's Hotkey Mode group lets you pick between:

  • Hold to talk (default) β€” press the chord to start, release to stop. Only works for modifier-only hotkeys (e.g. Option+Cmd, Alt), because Electron cannot observe key-release on standard accelerators.
  • Tap to toggle β€” press once to start, press again to stop. Works for any hotkey, modifier-only or standard (e.g. Control+Alt+Shift+D).

If you choose Hold to talk with a non-modifier hotkey (e.g. Alt+Space), the app falls back to Tap to toggle and logs a warning. The selection persists to config.json as hotkeyMode: "hold" | "tap".

Recording is hotkey-only by design. Clicking the tray icon opens the context menu β€” it does not start a recording.

Models

Anything from mlx-community/* that's already cached will work. The tray menu currently exposes:

  • whisper-tiny (fastest, lowest quality)
  • whisper-turbo (default)
  • whisper-large-v3-turbo
  • whisper-large-v3-mlx

Custom vocabulary

customVocabulary (config-file only, no tray UI yet) is passed to Whisper as its initial_prompt β€” a short piece of text used to bias transcription toward vocabulary the model might otherwise get wrong: proper nouns, product names, acronyms, or a punctuation/style hint.

"customVocabulary": "Agent AFK, mlx-whisper, uiohook-napi, Griffin Long"

This is a nudge, not a guarantee β€” Whisper truncates it to roughly its last ~220 tokens, so keep it to a short, curated list rather than a paragraph. For a word the model reliably gets wrong the same way every time, a replacement rule (below) is the more reliable fix.

Replacement rules

replacementRules is a list of literal (non-regex), case-sensitive find/replace pairs applied to the transcript right before delivery β€” for words the model consistently mangles the same way, where you want a guaranteed fix rather than a probabilistic nudge.

"replacementRules": [
  { "from": "whisperapp", "to": "whisper-app" },
  { "from": "afk", "to": "AFK" }
]

Rules apply in array order; a rule with an empty "from" is ignored.


How it works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Electron Main Process (TypeScript)          β”‚
β”‚  β”œβ”€β”€ Tray icon + context menu                β”‚
β”‚  β”œβ”€β”€ HotkeyManager (uiohook-napi | globalShortcut)
β”‚  β”œβ”€β”€ AudioRecorder  ──► ffmpeg child-process β”‚
β”‚  β”‚      writes β†’ /tmp/whisper-<uuid>.wav     β”‚
β”‚  └── Transcriber                             β”‚
β”‚         python3 scripts/transcribe.py        β”‚
β”‚         β†’ clipboard / AppleScript keystroke  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  1. Hotkey pressed β†’ ffmpeg starts capturing 16 kHz mono PCM16 from the default input device.
  2. Hotkey released β†’ ffmpeg flushes the WAV.
  3. scripts/transcribe.py is spawned with the WAV path and model id. It imports mlx_whisper and prints the transcript to stdout.
  4. Transcript is delivered to clipboard and/or typed into the focused app.
  5. Temp WAV is deleted.

No network calls at any point.


Development

npm install
npm run build     # esbuild β†’ dist/main.js + dist/test/
npm run dev       # build + start
npm test          # node --test dist/test/**/*.test.js
npx tsc --noEmit  # typecheck only

See CONTRIBUTING.md for the full contributor guide, manual test plan, and project conventions.

Source layout:

Path Responsibility
src/main.ts Entry point; wires modules together
src/tray.ts Menu-bar icon, context menu, state machine
src/hotkey.ts Modifier-only (uiohook-napi) + accelerator (globalShortcut)
src/recorder.ts ffmpeg child process β†’ WAV
src/transcriber.ts Spawn transcribe.py, return transcript
src/output.ts Clipboard + AppleScript keystroke
src/replace.ts Literal find/replace on the transcript (replacementRules)
src/config.ts electron-store schema + getters
src/startup.ts Arch / python / mlx_whisper / device / mic checks
src/logger.ts Structured logger
scripts/transcribe.py Python shim around mlx_whisper.transcribe()

Roadmap

  • Code signing + notarization, ship a .dmg
  • Streaming / partial transcription
  • Per-app output-mode overrides
  • Custom dictionary / replacement rules β€” customVocabulary (Whisper initial_prompt) + replacementRules, config-file only for now
  • Settings UI (currently config file only beyond model + output mode)

Contributing

PRs welcome β€” see CONTRIBUTING.md. Release history lives in CHANGELOG.md.

License

MIT β€” see LICENSE.

Acknowledgements

About

Local macOS menu-bar dictation app powered by mlx-whisper. Apple Silicon, on-device, no cloud.

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages