Local-first voice-to-text for macOS. Transcribe audio files or dictate with a hotkey — everything stays on your machine unless you opt into LLM cleanup.
Beta — Under active development. Core transcription and dictation work. Expect rough edges, incomplete features, and breaking changes. Not production-ready.
You want to dictate text on your Mac without sending audio to the cloud. Apple's built-in dictation is limited. Cloud services like Otter or Whisper API work but your audio leaves your machine. You want something that runs locally, transcribes accurately, and optionally cleans up the output with your own API keys.
- Local transcription — faster-whisper runs entirely on your machine
- File mode — Drop in
wav,mp3,m4a,flac,aac,ogg,mp4files - Dictation mode — Record live audio in-app (up to 6 minutes)
- Background service — Global hotkey triggers record → transcribe → paste into any app
- BYOK cleanup — Bring your own API key (Grok, OpenAI, OpenRouter) for LLM-powered text cleanup
- Deterministic fallback — Local cleanup (filler removal, punctuation) always runs, even if API fails
- Menu bar icon — Quick toggle and status visibility from the macOS menu bar
git clone https://github.com/bennjph/whisper-clean-mac.git
cd whisper-clean-mac
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txtGUI mode:
python -m app_whisper_clean.mainBackground dictation service:
python -m app_whisper_clean.main --serviceCLI mode:
python -m app_whisper_clean.cli --file /path/to/audio.wav --provider none┌──────────────────────────────────────────────────────┐
│ GUI / Hotkey / CLI │
├──────────────────────────────────────────────────────┤
│ Audio Input │
│ ├─ File picker (wav, mp3, m4a, flac, aac, ogg) │
│ └─ Live recording (sounddevice, up to 6 min) │
├──────────────────────────────────────────────────────┤
│ Pipeline │
│ ├─ 1. Transcribe (faster-whisper, local) │
│ ├─ 2. Deterministic cleanup (fillers, punctuation) │
│ └─ 3. LLM cleanup (optional, BYOK) │
├──────────────────────────────────────────────────────┤
│ Output │
│ ├─ Display in app │
│ ├─ Copy to clipboard │
│ ├─ Save to file (.txt / .md) │
│ └─ Paste into active app (service mode) │
└──────────────────────────────────────────────────────┘
Service mode flow: Press Option+Space → recording starts → press again → transcription runs → cleaned text is pasted into whatever app has focus.
| Feature | Status | Notes |
|---|---|---|
| Local transcription (faster-whisper) | Working | All supported audio formats |
| File mode GUI | Working | Select file, transcribe, copy/save |
| Live dictation recording | Working | Requires sounddevice + soundfile |
| Deterministic text cleanup | Working | Filler removal, punctuation normalization |
| LLM cleanup (BYOK) | Working | Grok, OpenAI, OpenRouter |
| GUI (customtkinter) | Working | Dark/light mode, card-based layout |
Background service (--service) |
Working | Requires Accessibility permission for hotkey |
Global hotkey (Option+Space) |
Working | Needs Accessibility granted to terminal |
| Menu bar icon | Working | State feedback (idle/recording/transcribing) |
| Clipboard paste injection | Working | save → write → Cmd+V → restore |
| Audio feedback (beeps) | Working | Start, stop, success, error tones |
macOS .app bundle |
Untested | py2app config exists but not validated |
| Windows / Linux | Not supported | macOS-only |
- Accessibility permission required for the global hotkey. Without it, service mode falls back to menu bar toggle only. Grant it in System Settings > Privacy & Security > Accessibility for your terminal app.
- Short recordings (<1s) are padded to 16k samples to prevent Whisper hallucination. This is intentional.
simpleaudio(audio feedback) may need PortAudio on some systems.- Dark mode is supported but not fully QA'd across all components.
- No auto-update mechanism — manual
git pullfor now. - First transcription takes longer as faster-whisper downloads the model.
| Package | Purpose |
|---|---|
faster-whisper |
Local transcription engine |
numpy |
Audio array processing |
sounddevice |
Live audio recording |
soundfile |
WAV file I/O |
customtkinter |
Modern Tkinter UI |
pynput |
Global keyboard hotkey listener |
pystray |
macOS menu bar icon |
simpleaudio |
Audio feedback beeps |
Pillow |
Icon generation |
The background service needs Accessibility permission for the global hotkey:
- System Settings > Privacy & Security > Accessibility
- Click +, add your terminal app (Terminal, iTerm2, Ghostty, etc.)
- Toggle it ON
- Restart the terminal (macOS caches trust per-process)
Without this, the service still runs — menu bar toggle works, hotkey doesn't.
python -m pytest tests/ -q
# 52 passedTests cover: pipeline, providers, config, CLI, hotkey listener, clipboard injection, recording quality, service state machine.
Settings persist at ~/.whisper-clean/config.json:
- Whisper model and language
- Provider endpoints and API keys (stored locally, never transmitted except to your chosen provider)
- Custom replacement map for domain-specific corrections
allow_unsafe_base_url(defaultfalse) — strict HTTPS + host allowlist for provider endpoints
whisper-clean-mac/
├── app_whisper_clean/
│ ├── ui/ # UI package (customtkinter)
│ │ ├── app.py # Main window, event wiring
│ │ ├── theme.py # Design tokens (colors, typography, spacing)
│ │ ├── components.py # CardFrame, StatusIndicator, button factories
│ │ ├── input_panel.py # File picker + dictation recorder
│ │ ├── settings_panel.py # Model settings + advanced disclosure
│ │ └── output_panel.py # Output text + copy/save
│ ├── config.py # AppConfig, load/save
│ ├── recorder.py # Audio recording (sounddevice)
│ ├── transcriber.py # faster-whisper wrapper
│ ├── text_cleaning.py # Deterministic cleanup
│ ├── providers.py # BYOK LLM provider clients
│ ├── pipeline.py # Transcribe + cleanup orchestration
│ ├── clipboard.py # Clipboard paste injection
│ ├── hotkey.py # Global hotkey (pynput + SIGTRAP prevention)
│ ├── audio_feedback.py # Beep tones (simpleaudio)
│ ├── service.py # Background dictation state machine
│ ├── menubar.py # Menu bar icon (pystray)
│ ├── main.py # Entry point (GUI or --service)
│ └── cli.py # CLI mode
├── tests/ # 52 tests
├── assets/ # App icons (PNG, ICNS)
├── docs/ # QA plan, verification receipts
├── DESIGN-GUIDE.md # Design system documentation
├── TRANSFER-LEARNINGS-AND-ROADMAP.md
├── SPEC.md # Original specification
├── CONTRIBUTING.md
├── LICENSE # MIT
└── requirements.txt
See TRANSFER-LEARNINGS-AND-ROADMAP.md for the full roadmap:
| Version | Focus |
|---|---|
| v1.5 (current) | Global hotkey, clipboard paste, menu bar, recording quality, modern UI |
| v2.0 (planned) | Voice Activity Detection, push-to-talk modes, floating overlay |
| v3.0 (planned) | App-aware context, multi-language auto-detect, plugin system |
- Audio stays on your machine. Transcription is fully local.
- API keys are stored in your user profile (
~/.whisper-clean/config.json), never committed or transmitted except to your chosen provider endpoint. - Provider endpoints are validated (HTTPS + trusted host allowlist) unless you explicitly set
allow_unsafe_base_url: true. - No telemetry, no analytics, no phone-home.
See CONTRIBUTING.md. Bug reports, test improvements, and macOS compatibility fixes are especially welcome.
Built by Benison Joseph. This project is in beta — feedback and contributions welcome.