A macOS menu-bar push-to-talk transcription tool inspired by WisprFlow. Hold a hotkey, speak, release — your words appear at the cursor. Fully local, no cloud dependency, no NVIDIA GPU required.
Hold (or double-tap) the hotkey → speak → release → transcript is auto-pasted at the cursor. If auto-paste fails (password field, secure input), the text lands on your clipboard. Recent transcripts are always available from the menu bar.
All audio and transcripts are stored locally. Transcription uses whisper.cpp via pywhispercpp.
v1 feature-complete. Core app, .app bundle, dialog UX, search, and custom vocabulary are all shipped and working on the developer machine. Next up: validating the install experience on a second machine (M10 — Gatekeeper friction, Intel compatibility, INSTALL.md end-to-end with a non-technical user).
See INSTALL.md — step-by-step from a fresh machine to a working first transcription, including model download and permissions.
src/
├── main.py # rumps menu bar app, state machine, UI-sync timer
├── hotkey.py # pynput hold-down / double-tap hotkey capture
├── recorder.py # sounddevice audio recording → .wav
├── transcriber.py # pywhispercpp wrapper, model discovery
├── postprocess.py # disfluency removal (pure Python, regex-based)
├── paste.py # pyperclip clipboard + osascript auto-paste
├── db.py # SQLite transcript storage and search
├── settings.py # JSON settings load/save with defaults
└── cleanup.py # audio file retention / WAV deletion logic
scripts/
├── setup.sh # creates venv, installs deps, installs launchd plist
├── build-app.sh # generates /Applications/MyWispr.app bundle
├── run.sh # app entrypoint (exec'd by the .app trampoline)
└── trampoline.c # Mach-O bundle executable source (arm64+x86_64)
launchd/
└── com.mywispr.cleanup.plist # daily audio cleanup job
tests/ # pytest unit tests (db, settings, postprocess, cleanup)
There are exactly three thread contexts. Violating this causes crashes.
- Main thread — owned by
rumps/ AppKit. All menu and icon updates happen here via a 0.25 srumps.Timer. Never touchrumpsfrom another thread. - pynput listener thread — fires on key events. Callbacks must be near-instant: record a timestamp, flip a flag, done. No I/O.
- Worker thread — single long-lived daemon thread that serializes the pipeline: stop recording → write WAV → transcribe → postprocess → save to SQLite → clipboard → paste.
All shared state lives in an AppState object guarded by a threading.Lock in main.py. The UI-sync timer reads it and updates the menu bar; the worker writes to it. Nothing else touches rumps.
/Applications/MyWispr.app is a hand-rolled minimal bundle — not py2app. The executable is a precompiled Mach-O trampoline (scripts/trampoline, universal arm64+x86_64) that execvs /bin/bash run.sh. This chain matters for macOS TCC:
- LaunchServices requires a Mach-O bundle executable (bare bash scripts are silently ignored on macOS 15)
LSRequiresNativeExecutioninInfo.plistforces arm64 on Apple Silicon (prevents Rosetta selecting x86_64, which would mismatch the arm64 venv)- The
execchain (trampoline → bash →exec python) keeps the LaunchServices-tracked PID intact through to the NSApplication
Do not replace the trampoline with a bash script. See CLAUDE.md for the full rationale.
Three TCC grants, all attributed to MyWispr.app:
| Permission | How granted |
|---|---|
| Microphone | System prompt at first launch |
| Accessibility | Manual: System Settings → Privacy & Security → Accessibility |
| Automation → System Events | System prompt on first paste (cannot be pre-granted) |
Accessibility alone is sufficient for pynput global key capture (validated M3 + M7; no Input Monitoring grant needed).
transcriber.py tries these paths in order, stopping at the first .bin that exists:
- User-configured model path (settings override)
~/Library/Application Support/MyWispr/models/ggml-model-whisper-turbo.bin~/Library/Application Support/MacWhisper/models/ggml-model-whisper-turbo.bin~/Library/Application Support/MyWispr/models/ggml-model-whisper-base.bin~/Library/Application Support/MacWhisper/models/ggml-model-whisper-base.bin- Model-needed state (in-app download offered)
MacWhisper paths are checked as an optimization — the app must not require MacWhisper to be installed.
The app runs source from the install root, not the dev tree:
~/Library/Application Support/MyWispr/app/
To see changes, copy the edited file to the install root and restart the app:
cp src/main.py "$HOME/Library/Application Support/MyWispr/app/src/main.py"
# then quit and relaunch MyWispr from ApplicationsNo cache clearing or bundle rebuild needed for source-only changes. Rebuild the bundle only when changing scripts/ or Info.plist.
source venv/bin/activate
pytest tests/Tests cover: settings round-trip, db insert/query/search/date-boundary math, disfluency removal (20+ input→expected pairs), cleanup with faked mtimes, vocabulary prompt composition. macOS-specific behavior (TCC, osascript, pynput) is not unit-tested.
- pynput on macOS 15:
TISCopyCurrentKeyboardInputSourcerequires the main queue but pynput calls it from its listener thread → SIGTRAP. Fixed by_patch_pynput_keycode_context()inmain.py, called beforeMyWisprApp().run(). Do not remove. - Dialog activation:
activateIgnoringOtherApps_is unreliable forLSUIElementapps on macOS 14/15. All alerts use_alert()/_window()wrappers that also setNSFloatingWindowLevelon the panel. - UI timer during modal sessions: the
rumps.TimerNSTimer is added toNSRunLoopCommonModesafter start so it keeps firing while an alert or window is open. The private attribute is_nstimer. - Paste order: save to SQLite → clipboard → paste. A paste failure never loses the transcript.
- Portability boundary: all
rumpsusage stays inmain.py; allosascript/subprocess calls stay inpaste.py. A future port only touches those two files.
REQUIREMENTS.md— what to build and why; source of truth on any conflictIMPLEMENTATION_PLAN.md— resolved design decisions and milestone history; explains the reasoning behind non-obvious choicesCLAUDE.md— project instructions for AI-assisted development; mirrors the key decisions above
Note on planning documents:
IMPLEMENTATION_PLAN.md,REQUIREMENTS.md, andCLAUDE.mdare published intentionally as a transparent build record. They document how this project was designed and built — including the AI-assisted development process — in the hope that others building similar tools find the decisions and tradeoffs useful. They are not polished end-user documentation.
VAD, system notifications, speaker diarization, multi-format export, onboarding wizard, cloud transcription, py2app packaging. See REQUIREMENTS.md for the full list.