Vibecode Wispr Flow
track this build6 phases, 17 steps, beginner friendly0%Hotkey → record → Whisper → paste at cursor. One of the most-cloned apps of the trend for a reason.
You are building a lean indie version of Wispr Flow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wispr Flow · indie build System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | App | Swift 6, SwiftUI, MenuBarExtra, macOS 14+ | global hotkeys and synthetic text insertion are native APIs | | Audio | AVAudioEngine | microphone capture with explicit resampling to what whisper needs | | Transcription | whisper.cpp small.en via whisper-cli | latency matters more than the last word of accuracy for dictation | | Cleanup | An LLM API behind an interface, optional | filler words and punctuation, with the raw transcript as fallback | | Insertion | Pasteboard plus a synthetic Cmd-V | far more reliable across apps than typing characters via CGEvent | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac with Apple Silicon on macOS 14 or newer** · the Mac you have - Why: Local transcription latency is only acceptable on Apple Silicon. - Get it: Apple menu > About This Mac. - Verify: sw_vers prints 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Swift and the macOS SDK. - Get it: Mac App Store; open once to install components. - Verify: xcodebuild -version prints 16 or higher - [ ] **whisper.cpp built, with the small.en model** · free - Why: Dictation is judged on the wait. small.en answers a short clip in well under three seconds; medium roughly doubles it. - Get it: git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build --config Release && ./models/download-ggml-model.sh small.en - Verify: ./build/bin/whisper-cli -h prints usage and models/ggml-small.en.bin exists (about 500 MB) - [ ] **An LLM API key for the cleanup pass (optional)** (optional) · pay per use; fractions of a cent per dictation - Why: Removes filler words and fixes punctuation. Without it the raw transcript is inserted, which is still useful. - Get it: Anthropic: console.anthropic.com > API Keys. OpenAI: platform.openai.com/api-keys. Into .env; never in source. - [ ] **A Groq key for hosted transcription (optional)** (optional) · free tier available - Why: A faster path on slower machines. Local stays the default. - Get it: console.groq.com > API Keys. - [ ] **Two separate permissions: Input Monitoring and Accessibility** · free - Why: Input Monitoring lets the app observe the global hotkey; Accessibility lets it post the synthetic paste. Confusing them costs an afternoon. Each prompt appears once per launch; if dismissed, relaunch. - Get it: System Settings > Privacy & Security > Input Monitoring, and > Accessibility. Add the built app to both when prompted. ## Quick start ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. - their tuned auto-editing voice model - per-app tone formatting - the mobile keyboard - polish on edge cases (accents, noise) If one of those is essential to you, that is the reason to keep paying for Wispr Flow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wispr Flow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a system-wide AI dictation tool like Wispr Flow for macOS. Build it in phases, in the order below. Do not write the whole app in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Phase 1 and Phase 5 are where this project dies · prove both before building any UI. ### Stack (fixed, do not substitute) - Swift 6 and SwiftUI, macOS 14+, a `MenuBarExtra` background app. Not Electron · global hotkeys and synthetic text insertion are native APIs here. - whisper.cpp invoked as the `whisper-cli` binary for local transcription. - A JSON settings file on disk. No database, no accounts, no telemetry. ### Permissions (understand these before Phase 1) Two separate grants, and confusing them costs an afternoon: - **Input Monitoring** · to observe the global hotkey. - **Accessibility** · to post synthetic keystrokes into another app. The system permission prompt can only appear once per app launch. If the user dismisses it, the app must be restarted before it can ask again, so detect the denied state explicitly and tell the user to relaunch rather than silently doing nothing forever. ### Phase 1 · Hotkey and recording Build: a global hold-to-talk hotkey (configurable, default a chosen key) that records the microphone with AVAudioEngine while held and stops on release. Write 16-bit PCM WAV, 16 kHz, mono · not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only, and resampling later means re-testing everything. Add: double-tap for a hands-free session ended by one more tap, a hard auto-stop at 5 minutes so a forgotten session cannot record all afternoon, and Esc to cancel. Observe Esc passively · it must still reach the app the user is typing in. Done when: hold-record-release produces a playable 16 kHz mono 16-bit WAV (verify with `afinfo`), double-tap starts and stops a hands-free session, the 5-minute cap fires, Esc discards the recording, and Esc still works normally in the foreground app. Do not build yet: transcription, the LLM, insertion, any UI. ### Phase 2 · Transcription Build: shell out to `whisper-cli -m models/ggml-small.en.bin -f clip.wav -otxt`. Fetch the model with `./models/download-ggml-model.sh small.en`. Default to small for latency · dictation is judged on the wait, and medium roughly doubles it for a gain most dictation does not need. Offer the Groq or OpenAI Whisper API as an opt-in fast path when a key is in `.env`, with local as the default. Done when: a 10-second clip transcribes accurately, the local path runs with the network off, and the elapsed time from key release to transcript is under three seconds on the small model for a short clip. ### Phase 3 · Cleanup pass Build: pipe the raw transcript through an LLM (key in `.env`) with a short fixed prompt that removes filler words, fixes punctuation and casing, and returns only the cleaned text with no preamble. Enforce that: strip a leading "Here is" style response defensively. If the key is missing or the call fails, fall through to the raw transcript rather than inserting nothing · a degraded result beats silence when the user is mid-sentence. Done when: "um so like send him the thing tomorrow" becomes a clean sentence, an LLM failure still inserts the raw transcript, and no response ever arrives wrapped in quotes or an explanation. ### Phase 4 · Insertion Build: insert the text at the cursor in whatever app has focus. Two approaches exist and they are not equivalent: - Posting each character via `CGEvent` requires mapping characters to key codes (macOS keyboard events carry key codes, not characters), and it breaks on layouts and on some apps. - Writing to the pasteboard and posting Cmd-V is far more reliable across apps. Use the pasteboard approach as the default. Save and restore the user's previous clipboard behind a config flag, defaulting to leaving the transcript on the clipboard so it can be pasted again. Both approaches need Accessibility trust · check `AXIsProcessTrustedWithOptions` and guide the user to the right pane. Done when: dictating into TextEdit, a browser address bar, Slack and a terminal all insert correctly; the clipboard-restore flag behaves in both positions; and with Accessibility denied the app shows an actionable message instead of failing silently. Note in the README that password fields refuse synthetic input by design · this is not a bug to fix. ### Phase 5 · Feedback UI Build: a small floating pill while recording, showing live state and which mode is active (hold vs hands-free), plus a level meter so the user can see the mic is actually hearing them. Then the menu bar item: on/off toggle, launch at login, open settings, and recent transcripts. Done when: the pill appears within 100ms of the hotkey, never steals focus from the app being typed into, and shows a distinct state for recording, transcribing and cleaning. ### Phase 6 · Settings, offline, README Build: the settings file (hotkey, model, local vs API, clipboard behavior, LLM prompt), verified offline operation with the local model and no LLM key, and the README. Done when: the full path works with the network off, and a fresh machine can go from clone to first dictation using only the README. ### Out of scope (and why) - Their tuned auto-editing voice model and per-app tone formatting. That is the actual product · a generic LLM cleanup pass is close, not equal. - The mobile keyboard. - Polish on accents, noise and crosstalk, which is where hosted models are ahead. ### README must contain - Both permissions, which pane each lives in, and the relaunch caveat. - The password-field limitation, stated as expected behavior. - Model download size and the latency difference between small and medium. ===== AGENTS.md ===== # Agent instructions · Wispr Flow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6, SwiftUI, MenuBarExtra, macOS 14+, AVAudioEngine, whisper.cpp small.en via whisper-cli, An LLM API behind an interface, optional, Pasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ===== BUILD_PLAN.md ===== # Build plan · Wispr Flow System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Hotkey and recording Hold to record, release to stop, with a WAV whisper accepts, plus hands-free, a hard cap and Esc to cancel. ### Steps 1. Create the Xcode project with a CLI target and the app target Prove capture in the CLI target first. Files: `Dictate.xcodeproj` 2. Observe the global hotkey with a CGEvent tap Requires Input Monitoring. Detect key down and key up for HOTKEY. Detect a double-tap for hands-free. 3. Record the microphone with AVAudioEngine while held Tap the input node; on release stop and write 16 kHz mono 16-bit PCM WAV via AVAudioConverter. Not 44.1 kHz, not float. 4. Add the 5-minute auto-stop and Esc to cancel Observe Esc passively so it still reaches the app being typed in. 5. Verify the WAV ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` ### Done when - [ ] Hold-record-release produces a playable 16 kHz mono 16-bit WAV - [ ] Double-tap starts and stops a hands-free session - [ ] The 5-minute cap fires - [ ] Esc discards the recording and still works normally in the foreground app - [ ] Denying Input Monitoring produces a clear message naming the pane ### Watch out - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. ## Phase 2 · Transcription Under three seconds from key release to text for a short clip, local by default. ### Steps 1. Shell out to whisper-cli with -otxt and read the result 2. Add the hosted path behind TRANSCRIBE_MODE with a Groq or OpenAI key 3. Measure latency and log it Key release to transcript, in the console, so regressions are visible. ### Done when - [ ] A 10-second clip transcribes accurately - [ ] Local works with Wi-Fi off - [ ] Key release to transcript is under three seconds for a short clip on small.en ## Phase 3 · Cleanup pass Filler removed, punctuation fixed, and never silence when the model fails. ### Steps 1. Write the provider interface and a fixed prompt Return only the cleaned text, no preamble. Strip a leading 'Here is' defensively. 2. Fall through to the raw transcript on any failure or missing key ### Done when - [ ] 'um so like send him the thing tomorrow' becomes a clean sentence - [ ] An LLM failure still inserts the raw transcript - [ ] No response arrives wrapped in quotes or an explanation ## Phase 4 · Insertion Text appears at the cursor in any app, with the clipboard handled the way the user chose. ### Steps 1. Write to the pasteboard and post Cmd-V via CGEvent Requires Accessibility: check AXIsProcessTrustedWithOptions and guide the user to the pane. Per-character CGEvent typing needs key-code mapping and breaks on layouts; the paste approach is the default. 2. Implement RESTORE_CLIPBOARD Save the previous pasteboard contents, paste, then restore after a short delay when the flag is true. ### Done when - [ ] Dictation inserts correctly in TextEdit, a browser address bar, Slack and a terminal - [ ] The clipboard flag behaves in both positions - [ ] With Accessibility denied the app shows an actionable message ### Watch out - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ## Phase 5 · Feedback UI You always know whether it is listening. ### Steps 1. Build the floating pill A small always-on-top panel that never takes focus, showing recording, transcribing or cleaning, the mode, and a live level meter. 2. Build the menu bar item On/off, launch at login, settings, recent transcripts. ### Done when - [ ] The pill appears within 100 ms of the hotkey - [ ] It never steals focus from the app being typed into - [ ] Three distinct states are visible ## Phase 6 · Settings, offline, README Configurable, works offline, documented. ### Steps 1. Read settings from a JSON file and reload on change 2. Verify the full path with the network off and no LLM key 3. Write the README Both permissions and their panes, the relaunch caveat, the password-field limitation, model sizes and latency. Files: `README.md` ### Done when - [ ] Works with the network off - [ ] A fresh Mac goes from clone to first dictation using only the README ## Not in this build - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. ## After v1, if you want it - Custom vocabulary passed to the cleanup prompt - Per-app cleanup styles (terse in a terminal, prose in mail) ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Path to the binary you built. WHISPER_BIN=/Users/you/whisper.cpp/build/bin/whisper-cli # Required. small.en for latency; medium.en if accuracy matters more. WHISPER_MODEL=/Users/you/whisper.cpp/models/ggml-small.en.bin # Required. The hold-to-talk key. fn, right-option, or a key combination. HOTKEY=fn # Optional · secret. Anthropic console. Empty disables the cleanup pass. ANTHROPIC_API_KEY=sk-ant-... # Required. A fast, cheap model is right for a cleanup pass. LLM_MODEL=claude-haiku-4-5-20251001 # Optional · secret. Enables hosted transcription when TRANSCRIBE_MODE=hosted. GROQ_API_KEY=gsk_... # Required. local or hosted. TRANSCRIBE_MODE=local # Required. true restores your previous clipboard after inserting; false leaves the transcript on it. RESTORE_CLIPBOARD=false
You are building a lean indie version of Wispr Flow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Wispr Flow · indie build System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | App | Swift 6, SwiftUI, MenuBarExtra, macOS 14+ | global hotkeys and synthetic text insertion are native APIs | | Audio | AVAudioEngine | microphone capture with explicit resampling to what whisper needs | | Transcription | whisper.cpp small.en via whisper-cli | latency matters more than the last word of accuracy for dictation | | Cleanup | An LLM API behind an interface, optional | filler words and punctuation, with the raw transcript as fallback | | Insertion | Pasteboard plus a synthetic Cmd-V | far more reliable across apps than typing characters via CGEvent | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac with Apple Silicon on macOS 14 or newer** · the Mac you have - Why: Local transcription latency is only acceptable on Apple Silicon. - Get it: Apple menu > About This Mac. - Verify: sw_vers prints 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Swift and the macOS SDK. - Get it: Mac App Store; open once to install components. - Verify: xcodebuild -version prints 16 or higher - [ ] **whisper.cpp built, with the small.en model** · free - Why: Dictation is judged on the wait. small.en answers a short clip in well under three seconds; medium roughly doubles it. - Get it: git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build --config Release && ./models/download-ggml-model.sh small.en - Verify: ./build/bin/whisper-cli -h prints usage and models/ggml-small.en.bin exists (about 500 MB) - [ ] **An LLM API key for the cleanup pass (optional)** (optional) · pay per use; fractions of a cent per dictation - Why: Removes filler words and fixes punctuation. Without it the raw transcript is inserted, which is still useful. - Get it: Anthropic: console.anthropic.com > API Keys. OpenAI: platform.openai.com/api-keys. Into .env; never in source. - [ ] **A Groq key for hosted transcription (optional)** (optional) · free tier available - Why: A faster path on slower machines. Local stays the default. - Get it: console.groq.com > API Keys. - [ ] **Two separate permissions: Input Monitoring and Accessibility** · free - Why: Input Monitoring lets the app observe the global hotkey; Accessibility lets it post the synthetic paste. Confusing them costs an afternoon. Each prompt appears once per launch; if dismissed, relaunch. - Get it: System Settings > Privacy & Security > Input Monitoring, and > Accessibility. Add the built app to both when prompted. ## Quick start ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. - their tuned auto-editing voice model - per-app tone formatting - the mobile keyboard - polish on edge cases (accents, noise) If one of those is essential to you, that is the reason to keep paying for Wispr Flow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Wispr Flow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a system-wide AI dictation tool like Wispr Flow for macOS. Build it in phases, in the order below. Do not write the whole app in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Phase 1 and Phase 5 are where this project dies · prove both before building any UI. ### Stack (fixed, do not substitute) - Swift 6 and SwiftUI, macOS 14+, a `MenuBarExtra` background app. Not Electron · global hotkeys and synthetic text insertion are native APIs here. - whisper.cpp invoked as the `whisper-cli` binary for local transcription. - A JSON settings file on disk. No database, no accounts, no telemetry. ### Permissions (understand these before Phase 1) Two separate grants, and confusing them costs an afternoon: - **Input Monitoring** · to observe the global hotkey. - **Accessibility** · to post synthetic keystrokes into another app. The system permission prompt can only appear once per app launch. If the user dismisses it, the app must be restarted before it can ask again, so detect the denied state explicitly and tell the user to relaunch rather than silently doing nothing forever. ### Phase 1 · Hotkey and recording Build: a global hold-to-talk hotkey (configurable, default a chosen key) that records the microphone with AVAudioEngine while held and stops on release. Write 16-bit PCM WAV, 16 kHz, mono · not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only, and resampling later means re-testing everything. Add: double-tap for a hands-free session ended by one more tap, a hard auto-stop at 5 minutes so a forgotten session cannot record all afternoon, and Esc to cancel. Observe Esc passively · it must still reach the app the user is typing in. Done when: hold-record-release produces a playable 16 kHz mono 16-bit WAV (verify with `afinfo`), double-tap starts and stops a hands-free session, the 5-minute cap fires, Esc discards the recording, and Esc still works normally in the foreground app. Do not build yet: transcription, the LLM, insertion, any UI. ### Phase 2 · Transcription Build: shell out to `whisper-cli -m models/ggml-small.en.bin -f clip.wav -otxt`. Fetch the model with `./models/download-ggml-model.sh small.en`. Default to small for latency · dictation is judged on the wait, and medium roughly doubles it for a gain most dictation does not need. Offer the Groq or OpenAI Whisper API as an opt-in fast path when a key is in `.env`, with local as the default. Done when: a 10-second clip transcribes accurately, the local path runs with the network off, and the elapsed time from key release to transcript is under three seconds on the small model for a short clip. ### Phase 3 · Cleanup pass Build: pipe the raw transcript through an LLM (key in `.env`) with a short fixed prompt that removes filler words, fixes punctuation and casing, and returns only the cleaned text with no preamble. Enforce that: strip a leading "Here is" style response defensively. If the key is missing or the call fails, fall through to the raw transcript rather than inserting nothing · a degraded result beats silence when the user is mid-sentence. Done when: "um so like send him the thing tomorrow" becomes a clean sentence, an LLM failure still inserts the raw transcript, and no response ever arrives wrapped in quotes or an explanation. ### Phase 4 · Insertion Build: insert the text at the cursor in whatever app has focus. Two approaches exist and they are not equivalent: - Posting each character via `CGEvent` requires mapping characters to key codes (macOS keyboard events carry key codes, not characters), and it breaks on layouts and on some apps. - Writing to the pasteboard and posting Cmd-V is far more reliable across apps. Use the pasteboard approach as the default. Save and restore the user's previous clipboard behind a config flag, defaulting to leaving the transcript on the clipboard so it can be pasted again. Both approaches need Accessibility trust · check `AXIsProcessTrustedWithOptions` and guide the user to the right pane. Done when: dictating into TextEdit, a browser address bar, Slack and a terminal all insert correctly; the clipboard-restore flag behaves in both positions; and with Accessibility denied the app shows an actionable message instead of failing silently. Note in the README that password fields refuse synthetic input by design · this is not a bug to fix. ### Phase 5 · Feedback UI Build: a small floating pill while recording, showing live state and which mode is active (hold vs hands-free), plus a level meter so the user can see the mic is actually hearing them. Then the menu bar item: on/off toggle, launch at login, open settings, and recent transcripts. Done when: the pill appears within 100ms of the hotkey, never steals focus from the app being typed into, and shows a distinct state for recording, transcribing and cleaning. ### Phase 6 · Settings, offline, README Build: the settings file (hotkey, model, local vs API, clipboard behavior, LLM prompt), verified offline operation with the local model and no LLM key, and the README. Done when: the full path works with the network off, and a fresh machine can go from clone to first dictation using only the README. ### Out of scope (and why) - Their tuned auto-editing voice model and per-app tone formatting. That is the actual product · a generic LLM cleanup pass is close, not equal. - The mobile keyboard. - Polish on accents, noise and crosstalk, which is where hosted models are ahead. ### README must contain - Both permissions, which pane each lives in, and the relaunch caveat. - The password-field limitation, stated as expected behavior. - Model download size and the latency difference between small and medium. ===== AGENTS.md ===== # Agent instructions · Wispr Flow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6, SwiftUI, MenuBarExtra, macOS 14+, AVAudioEngine, whisper.cpp small.en via whisper-cli, An LLM API behind an interface, optional, Pasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ===== BUILD_PLAN.md ===== # Build plan · Wispr Flow System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Hotkey and recording Hold to record, release to stop, with a WAV whisper accepts, plus hands-free, a hard cap and Esc to cancel. ### Steps 1. Create the Xcode project with a CLI target and the app target Prove capture in the CLI target first. Files: `Dictate.xcodeproj` 2. Observe the global hotkey with a CGEvent tap Requires Input Monitoring. Detect key down and key up for HOTKEY. Detect a double-tap for hands-free. 3. Record the microphone with AVAudioEngine while held Tap the input node; on release stop and write 16 kHz mono 16-bit PCM WAV via AVAudioConverter. Not 44.1 kHz, not float. 4. Add the 5-minute auto-stop and Esc to cancel Observe Esc passively so it still reaches the app being typed in. 5. Verify the WAV ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` ### Done when - [ ] Hold-record-release produces a playable 16 kHz mono 16-bit WAV - [ ] Double-tap starts and stops a hands-free session - [ ] The 5-minute cap fires - [ ] Esc discards the recording and still works normally in the foreground app - [ ] Denying Input Monitoring produces a clear message naming the pane ### Watch out - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. ## Phase 2 · Transcription Under three seconds from key release to text for a short clip, local by default. ### Steps 1. Shell out to whisper-cli with -otxt and read the result 2. Add the hosted path behind TRANSCRIBE_MODE with a Groq or OpenAI key 3. Measure latency and log it Key release to transcript, in the console, so regressions are visible. ### Done when - [ ] A 10-second clip transcribes accurately - [ ] Local works with Wi-Fi off - [ ] Key release to transcript is under three seconds for a short clip on small.en ## Phase 3 · Cleanup pass Filler removed, punctuation fixed, and never silence when the model fails. ### Steps 1. Write the provider interface and a fixed prompt Return only the cleaned text, no preamble. Strip a leading 'Here is' defensively. 2. Fall through to the raw transcript on any failure or missing key ### Done when - [ ] 'um so like send him the thing tomorrow' becomes a clean sentence - [ ] An LLM failure still inserts the raw transcript - [ ] No response arrives wrapped in quotes or an explanation ## Phase 4 · Insertion Text appears at the cursor in any app, with the clipboard handled the way the user chose. ### Steps 1. Write to the pasteboard and post Cmd-V via CGEvent Requires Accessibility: check AXIsProcessTrustedWithOptions and guide the user to the pane. Per-character CGEvent typing needs key-code mapping and breaks on layouts; the paste approach is the default. 2. Implement RESTORE_CLIPBOARD Save the previous pasteboard contents, paste, then restore after a short delay when the flag is true. ### Done when - [ ] Dictation inserts correctly in TextEdit, a browser address bar, Slack and a terminal - [ ] The clipboard flag behaves in both positions - [ ] With Accessibility denied the app shows an actionable message ### Watch out - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ## Phase 5 · Feedback UI You always know whether it is listening. ### Steps 1. Build the floating pill A small always-on-top panel that never takes focus, showing recording, transcribing or cleaning, the mode, and a live level meter. 2. Build the menu bar item On/off, launch at login, settings, recent transcripts. ### Done when - [ ] The pill appears within 100 ms of the hotkey - [ ] It never steals focus from the app being typed into - [ ] Three distinct states are visible ## Phase 6 · Settings, offline, README Configurable, works offline, documented. ### Steps 1. Read settings from a JSON file and reload on change 2. Verify the full path with the network off and no LLM key 3. Write the README Both permissions and their panes, the relaunch caveat, the password-field limitation, model sizes and latency. Files: `README.md` ### Done when - [ ] Works with the network off - [ ] A fresh Mac goes from clone to first dictation using only the README ## Not in this build - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. ## After v1, if you want it - Custom vocabulary passed to the cleanup prompt - Per-app cleanup styles (terse in a terminal, prose in mail) ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Path to the binary you built. WHISPER_BIN=/Users/you/whisper.cpp/build/bin/whisper-cli # Required. small.en for latency; medium.en if accuracy matters more. WHISPER_MODEL=/Users/you/whisper.cpp/models/ggml-small.en.bin # Required. The hold-to-talk key. fn, right-option, or a key combination. HOTKEY=fn # Optional · secret. Anthropic console. Empty disables the cleanup pass. ANTHROPIC_API_KEY=sk-ant-... # Required. A fast, cheap model is right for a cleanup pass. LLM_MODEL=claude-haiku-4-5-20251001 # Optional · secret. Enables hosted transcription when TRANSCRIBE_MODE=hosted. GROQ_API_KEY=gsk_... # Required. local or hosted. TRANSCRIBE_MODE=local # Required. true restores your previous clipboard after inserting; false leaves the transcript on it. RESTORE_CLIPBOARD=false
You are building a production product version of Wispr Flow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Wispr Flow · product brief ## Problem Hotkey → record → Whisper → paste at cursor. One of the most-cloned apps of the trend for a reason. ## Product outcome A dictation tool you could ship to others: notarized, private by default, honest about the one field type it cannot type into. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Implement the core workflow described in ARCHITECTURE.md ## Explicit non-goals for v1 - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. - their tuned auto-editing voice model - per-app tone formatting - the mobile keyboard - polish on edge cases (accents, noise) ## Success criteria - Notarized build runs on a clean Mac - Offline test passes - Password-field limitation documented - One Sparkle update delivered ===== BRIEF.md ===== # Build brief · Wispr Flow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a system-wide AI dictation tool like Wispr Flow for macOS. Build it in phases, in the order below. Do not write the whole app in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Phase 1 and Phase 5 are where this project dies · prove both before building any UI. ### Stack (fixed, do not substitute) - Swift 6 and SwiftUI, macOS 14+, a `MenuBarExtra` background app. Not Electron · global hotkeys and synthetic text insertion are native APIs here. - whisper.cpp invoked as the `whisper-cli` binary for local transcription. - A JSON settings file on disk. No database, no accounts, no telemetry. ### Permissions (understand these before Phase 1) Two separate grants, and confusing them costs an afternoon: - **Input Monitoring** · to observe the global hotkey. - **Accessibility** · to post synthetic keystrokes into another app. The system permission prompt can only appear once per app launch. If the user dismisses it, the app must be restarted before it can ask again, so detect the denied state explicitly and tell the user to relaunch rather than silently doing nothing forever. ### Phase 1 · Hotkey and recording Build: a global hold-to-talk hotkey (configurable, default a chosen key) that records the microphone with AVAudioEngine while held and stops on release. Write 16-bit PCM WAV, 16 kHz, mono · not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only, and resampling later means re-testing everything. Add: double-tap for a hands-free session ended by one more tap, a hard auto-stop at 5 minutes so a forgotten session cannot record all afternoon, and Esc to cancel. Observe Esc passively · it must still reach the app the user is typing in. Done when: hold-record-release produces a playable 16 kHz mono 16-bit WAV (verify with `afinfo`), double-tap starts and stops a hands-free session, the 5-minute cap fires, Esc discards the recording, and Esc still works normally in the foreground app. Do not build yet: transcription, the LLM, insertion, any UI. ### Phase 2 · Transcription Build: shell out to `whisper-cli -m models/ggml-small.en.bin -f clip.wav -otxt`. Fetch the model with `./models/download-ggml-model.sh small.en`. Default to small for latency · dictation is judged on the wait, and medium roughly doubles it for a gain most dictation does not need. Offer the Groq or OpenAI Whisper API as an opt-in fast path when a key is in `.env`, with local as the default. Done when: a 10-second clip transcribes accurately, the local path runs with the network off, and the elapsed time from key release to transcript is under three seconds on the small model for a short clip. ### Phase 3 · Cleanup pass Build: pipe the raw transcript through an LLM (key in `.env`) with a short fixed prompt that removes filler words, fixes punctuation and casing, and returns only the cleaned text with no preamble. Enforce that: strip a leading "Here is" style response defensively. If the key is missing or the call fails, fall through to the raw transcript rather than inserting nothing · a degraded result beats silence when the user is mid-sentence. Done when: "um so like send him the thing tomorrow" becomes a clean sentence, an LLM failure still inserts the raw transcript, and no response ever arrives wrapped in quotes or an explanation. ### Phase 4 · Insertion Build: insert the text at the cursor in whatever app has focus. Two approaches exist and they are not equivalent: - Posting each character via `CGEvent` requires mapping characters to key codes (macOS keyboard events carry key codes, not characters), and it breaks on layouts and on some apps. - Writing to the pasteboard and posting Cmd-V is far more reliable across apps. Use the pasteboard approach as the default. Save and restore the user's previous clipboard behind a config flag, defaulting to leaving the transcript on the clipboard so it can be pasted again. Both approaches need Accessibility trust · check `AXIsProcessTrustedWithOptions` and guide the user to the right pane. Done when: dictating into TextEdit, a browser address bar, Slack and a terminal all insert correctly; the clipboard-restore flag behaves in both positions; and with Accessibility denied the app shows an actionable message instead of failing silently. Note in the README that password fields refuse synthetic input by design · this is not a bug to fix. ### Phase 5 · Feedback UI Build: a small floating pill while recording, showing live state and which mode is active (hold vs hands-free), plus a level meter so the user can see the mic is actually hearing them. Then the menu bar item: on/off toggle, launch at login, open settings, and recent transcripts. Done when: the pill appears within 100ms of the hotkey, never steals focus from the app being typed into, and shows a distinct state for recording, transcribing and cleaning. ### Phase 6 · Settings, offline, README Build: the settings file (hotkey, model, local vs API, clipboard behavior, LLM prompt), verified offline operation with the local model and no LLM key, and the README. Done when: the full path works with the network off, and a fresh machine can go from clone to first dictation using only the README. ### Out of scope (and why) - Their tuned auto-editing voice model and per-app tone formatting. That is the actual product · a generic LLM cleanup pass is close, not equal. - The mobile keyboard. - Polish on accents, noise and crosstalk, which is where hosted models are ahead. ### README must contain - Both permissions, which pane each lives in, and the relaunch caveat. - The password-field limitation, stated as expected behavior. - Model download size and the latency difference between small and medium. ===== ARCHITECTURE.md ===== # Architecture · Wispr Flow ## Stack | Part | Choice | Why | | --- | --- | --- | | App | Swift 6, SwiftUI, MenuBarExtra, macOS 14+ | global hotkeys and synthetic text insertion are native APIs | | Audio | AVAudioEngine | microphone capture with explicit resampling to what whisper needs | | Transcription | whisper.cpp small.en via whisper-cli | latency matters more than the last word of accuracy for dictation | | Cleanup | An LLM API behind an interface, optional | filler words and punctuation, with the raw transcript as fallback | | Insertion | Pasteboard plus a synthetic Cmd-V | far more reliable across apps than typing characters via CGEvent | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | the CGEvent tap and modes | The only Input Monitoring code | | Recorder | AVAudioEngine and WAV writing | Any source producing 16 kHz mono 16-bit | | Transcriber | whisper-cli and hosted fallback | WhisperKit in-process | | Cleaner | the LLM interface and prompt | Any provider; Ollama locally | | Inserter | pasteboard and Cmd-V, Accessibility checks | Per-character CGEvent typing as an alternative strategy | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `WHISPER_BIN` · required · Path to the binary you built. - `WHISPER_MODEL` · required · small.en for latency; medium.en if accuracy matters more. - `HOTKEY` · required · The hold-to-talk key. fn, right-option, or a key combination. - `ANTHROPIC_API_KEY` · optional, secret · Anthropic console. Empty disables the cleanup pass. - `LLM_MODEL` · required · A fast, cheap model is right for a cleanup pass. - `GROQ_API_KEY` · optional, secret · Enables hosted transcription when TRANSCRIBE_MODE=hosted. - `TRANSCRIBE_MODE` · required · local or hosted. - `RESTORE_CLIPBOARD` · required · true restores your previous clipboard after inserting; false leaves the transcript on it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Wispr Flow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6, SwiftUI, MenuBarExtra, macOS 14+, AVAudioEngine, whisper.cpp small.en via whisper-cli, An LLM API behind an interface, optional, Pasteboard plus a synthetic Cmd-V. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ===== MILESTONES.md ===== # Delivery milestones · Wispr Flow Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Hotkey and recording Hold to record, release to stop, with a WAV whisper accepts, plus hands-free, a hard cap and Esc to cancel. ### Steps 1. Create the Xcode project with a CLI target and the app target Prove capture in the CLI target first. Files: `Dictate.xcodeproj` 2. Observe the global hotkey with a CGEvent tap Requires Input Monitoring. Detect key down and key up for HOTKEY. Detect a double-tap for hands-free. 3. Record the microphone with AVAudioEngine while held Tap the input node; on release stop and write 16 kHz mono 16-bit PCM WAV via AVAudioConverter. Not 44.1 kHz, not float. 4. Add the 5-minute auto-stop and Esc to cancel Observe Esc passively so it still reaches the app being typed in. 5. Verify the WAV ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` ### Done when - [ ] Hold-record-release produces a playable 16 kHz mono 16-bit WAV - [ ] Double-tap starts and stops a hands-free session - [ ] The 5-minute cap fires - [ ] Esc discards the recording and still works normally in the foreground app - [ ] Denying Input Monitoring produces a clear message naming the pane ### Watch out - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. ## M2 · Transcription Under three seconds from key release to text for a short clip, local by default. ### Steps 1. Shell out to whisper-cli with -otxt and read the result 2. Add the hosted path behind TRANSCRIBE_MODE with a Groq or OpenAI key 3. Measure latency and log it Key release to transcript, in the console, so regressions are visible. ### Done when - [ ] A 10-second clip transcribes accurately - [ ] Local works with Wi-Fi off - [ ] Key release to transcript is under three seconds for a short clip on small.en ## M3 · Cleanup pass Filler removed, punctuation fixed, and never silence when the model fails. ### Steps 1. Write the provider interface and a fixed prompt Return only the cleaned text, no preamble. Strip a leading 'Here is' defensively. 2. Fall through to the raw transcript on any failure or missing key ### Done when - [ ] 'um so like send him the thing tomorrow' becomes a clean sentence - [ ] An LLM failure still inserts the raw transcript - [ ] No response arrives wrapped in quotes or an explanation ## M4 · Insertion Text appears at the cursor in any app, with the clipboard handled the way the user chose. ### Steps 1. Write to the pasteboard and post Cmd-V via CGEvent Requires Accessibility: check AXIsProcessTrustedWithOptions and guide the user to the pane. Per-character CGEvent typing needs key-code mapping and breaks on layouts; the paste approach is the default. 2. Implement RESTORE_CLIPBOARD Save the previous pasteboard contents, paste, then restore after a short delay when the flag is true. ### Done when - [ ] Dictation inserts correctly in TextEdit, a browser address bar, Slack and a terminal - [ ] The clipboard flag behaves in both positions - [ ] With Accessibility denied the app shows an actionable message ### Watch out - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ## M5 · Feedback UI You always know whether it is listening. ### Steps 1. Build the floating pill A small always-on-top panel that never takes focus, showing recording, transcribing or cleaning, the mode, and a live level meter. 2. Build the menu bar item On/off, launch at login, settings, recent transcripts. ### Done when - [ ] The pill appears within 100 ms of the hotkey - [ ] It never steals focus from the app being typed into - [ ] Three distinct states are visible ## M6 · Settings, offline, README Configurable, works offline, documented. ### Steps 1. Read settings from a JSON file and reload on change 2. Verify the full path with the network off and no LLM key 3. Write the README Both permissions and their panes, the relaunch caveat, the password-field limitation, model sizes and latency. Files: `README.md` ### Done when - [ ] Works with the network off - [ ] A fresh Mac goes from clone to first dictation using only the README ## M7 · Distribute it (production only) Notarized, self-updating, with opt-in crash reports. ### Steps 1. Sign with a Developer ID and notarize ```sh xcrun notarytool submit Dictate.zip --keychain-profile notary --wait xcrun stapler staple Dictate.app ``` 2. Add Sparkle with a signed appcast on your domain 3. Add opt-in crash reporting, off by default ### Done when - [ ] A notarized build opens on a second Mac without a right-click workaround - [ ] An appcast update is offered to an installed copy ===== OPERATIONS.md ===== # Operations · Wispr Flow ## Backup No state beyond settings.json and recent transcripts; both under ~/Library/Application Support. Time Machine covers it. ## Restore Reinstall; copy settings back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Opt-in crash reports only. ## Incident checklist A leaked API key is rotated at the provider; ship a release that clears stored keys. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Notarized build runs on a clean Mac - [ ] Offline test passes - [ ] Password-field limitation documented - [ ] One Sparkle update delivered ## Launch constraint Do not market omitted Wispr Flow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Path to the binary you built. WHISPER_BIN=/Users/you/whisper.cpp/build/bin/whisper-cli # Required. small.en for latency; medium.en if accuracy matters more. WHISPER_MODEL=/Users/you/whisper.cpp/models/ggml-small.en.bin # Required. The hold-to-talk key. fn, right-option, or a key combination. HOTKEY=fn # Optional · secret. Anthropic console. Empty disables the cleanup pass. ANTHROPIC_API_KEY=sk-ant-... # Required. A fast, cheap model is right for a cleanup pass. LLM_MODEL=claude-haiku-4-5-20251001 # Optional · secret. Enables hosted transcription when TRANSCRIBE_MODE=hosted. GROQ_API_KEY=gsk_... # Required. local or hosted. TRANSCRIBE_MODE=local # Required. true restores your previous clipboard after inserting; false leaves the transcript on it. RESTORE_CLIPBOARD=false
# Wispr Flow · indie build System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | App | Swift 6, SwiftUI, MenuBarExtra, macOS 14+ | global hotkeys and synthetic text insertion are native APIs | | Audio | AVAudioEngine | microphone capture with explicit resampling to what whisper needs | | Transcription | whisper.cpp small.en via whisper-cli | latency matters more than the last word of accuracy for dictation | | Cleanup | An LLM API behind an interface, optional | filler words and punctuation, with the raw transcript as fallback | | Insertion | Pasteboard plus a synthetic Cmd-V | far more reliable across apps than typing characters via CGEvent | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Mac with Apple Silicon on macOS 14 or newer** · the Mac you have - Why: Local transcription latency is only acceptable on Apple Silicon. - Get it: Apple menu > About This Mac. - Verify: sw_vers prints 14 or higher - [ ] **Xcode 16 or newer** · free - Why: Swift and the macOS SDK. - Get it: Mac App Store; open once to install components. - Verify: xcodebuild -version prints 16 or higher - [ ] **whisper.cpp built, with the small.en model** · free - Why: Dictation is judged on the wait. small.en answers a short clip in well under three seconds; medium roughly doubles it. - Get it: git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build --config Release && ./models/download-ggml-model.sh small.en - Verify: ./build/bin/whisper-cli -h prints usage and models/ggml-small.en.bin exists (about 500 MB) - [ ] **An LLM API key for the cleanup pass (optional)** (optional) · pay per use; fractions of a cent per dictation - Why: Removes filler words and fixes punctuation. Without it the raw transcript is inserted, which is still useful. - Get it: Anthropic: console.anthropic.com > API Keys. OpenAI: platform.openai.com/api-keys. Into .env; never in source. - [ ] **A Groq key for hosted transcription (optional)** (optional) · free tier available - Why: A faster path on slower machines. Local stays the default. - Get it: console.groq.com > API Keys. - [ ] **Two separate permissions: Input Monitoring and Accessibility** · free - Why: Input Monitoring lets the app observe the global hotkey; Accessibility lets it post the synthetic paste. Confusing them costs an afternoon. Each prompt appears once per launch; if dismissed, relaunch. - Get it: System Settings > Privacy & Security > Input Monitoring, and > Accessibility. Add the built app to both when prompted. ## Quick start ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. - their tuned auto-editing voice model - per-app tone formatting - the mobile keyboard - polish on edge cases (accents, noise) If one of those is essential to you, that is the reason to keep paying for Wispr Flow, and the README should say so rather than pretend.
# Build brief · Wispr Flow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a system-wide AI dictation tool like Wispr Flow for macOS. Build it in phases, in the order below. Do not write the whole app in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Phase 1 and Phase 5 are where this project dies · prove both before building any UI. ### Stack (fixed, do not substitute) - Swift 6 and SwiftUI, macOS 14+, a `MenuBarExtra` background app. Not Electron · global hotkeys and synthetic text insertion are native APIs here. - whisper.cpp invoked as the `whisper-cli` binary for local transcription. - A JSON settings file on disk. No database, no accounts, no telemetry. ### Permissions (understand these before Phase 1) Two separate grants, and confusing them costs an afternoon: - **Input Monitoring** · to observe the global hotkey. - **Accessibility** · to post synthetic keystrokes into another app. The system permission prompt can only appear once per app launch. If the user dismisses it, the app must be restarted before it can ask again, so detect the denied state explicitly and tell the user to relaunch rather than silently doing nothing forever. ### Phase 1 · Hotkey and recording Build: a global hold-to-talk hotkey (configurable, default a chosen key) that records the microphone with AVAudioEngine while held and stops on release. Write 16-bit PCM WAV, 16 kHz, mono · not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only, and resampling later means re-testing everything. Add: double-tap for a hands-free session ended by one more tap, a hard auto-stop at 5 minutes so a forgotten session cannot record all afternoon, and Esc to cancel. Observe Esc passively · it must still reach the app the user is typing in. Done when: hold-record-release produces a playable 16 kHz mono 16-bit WAV (verify with `afinfo`), double-tap starts and stops a hands-free session, the 5-minute cap fires, Esc discards the recording, and Esc still works normally in the foreground app. Do not build yet: transcription, the LLM, insertion, any UI. ### Phase 2 · Transcription Build: shell out to `whisper-cli -m models/ggml-small.en.bin -f clip.wav -otxt`. Fetch the model with `./models/download-ggml-model.sh small.en`. Default to small for latency · dictation is judged on the wait, and medium roughly doubles it for a gain most dictation does not need. Offer the Groq or OpenAI Whisper API as an opt-in fast path when a key is in `.env`, with local as the default. Done when: a 10-second clip transcribes accurately, the local path runs with the network off, and the elapsed time from key release to transcript is under three seconds on the small model for a short clip. ### Phase 3 · Cleanup pass Build: pipe the raw transcript through an LLM (key in `.env`) with a short fixed prompt that removes filler words, fixes punctuation and casing, and returns only the cleaned text with no preamble. Enforce that: strip a leading "Here is" style response defensively. If the key is missing or the call fails, fall through to the raw transcript rather than inserting nothing · a degraded result beats silence when the user is mid-sentence. Done when: "um so like send him the thing tomorrow" becomes a clean sentence, an LLM failure still inserts the raw transcript, and no response ever arrives wrapped in quotes or an explanation. ### Phase 4 · Insertion Build: insert the text at the cursor in whatever app has focus. Two approaches exist and they are not equivalent: - Posting each character via `CGEvent` requires mapping characters to key codes (macOS keyboard events carry key codes, not characters), and it breaks on layouts and on some apps. - Writing to the pasteboard and posting Cmd-V is far more reliable across apps. Use the pasteboard approach as the default. Save and restore the user's previous clipboard behind a config flag, defaulting to leaving the transcript on the clipboard so it can be pasted again. Both approaches need Accessibility trust · check `AXIsProcessTrustedWithOptions` and guide the user to the right pane. Done when: dictating into TextEdit, a browser address bar, Slack and a terminal all insert correctly; the clipboard-restore flag behaves in both positions; and with Accessibility denied the app shows an actionable message instead of failing silently. Note in the README that password fields refuse synthetic input by design · this is not a bug to fix. ### Phase 5 · Feedback UI Build: a small floating pill while recording, showing live state and which mode is active (hold vs hands-free), plus a level meter so the user can see the mic is actually hearing them. Then the menu bar item: on/off toggle, launch at login, open settings, and recent transcripts. Done when: the pill appears within 100ms of the hotkey, never steals focus from the app being typed into, and shows a distinct state for recording, transcribing and cleaning. ### Phase 6 · Settings, offline, README Build: the settings file (hotkey, model, local vs API, clipboard behavior, LLM prompt), verified offline operation with the local model and no LLM key, and the README. Done when: the full path works with the network off, and a fresh machine can go from clone to first dictation using only the README. ### Out of scope (and why) - Their tuned auto-editing voice model and per-app tone formatting. That is the actual product · a generic LLM cleanup pass is close, not equal. - The mobile keyboard. - Polish on accents, noise and crosstalk, which is where hosted models are ahead. ### README must contain - Both permissions, which pane each lives in, and the relaunch caveat. - The password-field limitation, stated as expected behavior. - Model download size and the latency difference between small and medium.
# Agent instructions · Wispr Flow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Swift 6, SwiftUI, MenuBarExtra, macOS 14+, AVAudioEngine, whisper.cpp small.en via whisper-cli, An LLM API behind an interface, optional, Pasteboard plus a synthetic Cmd-V. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix.
# Build plan · Wispr Flow System-wide dictation for your Mac: hold a key to record, release to transcribe locally with whisper.cpp, run the text through a quick LLM cleanup, and insert it at the cursor in whatever app has focus. A floating pill shows it is live. Works offline with the local model; no accounts, no telemetry. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Hotkey and recording Hold to record, release to stop, with a WAV whisper accepts, plus hands-free, a hard cap and Esc to cancel. ### Steps 1. Create the Xcode project with a CLI target and the app target Prove capture in the CLI target first. Files: `Dictate.xcodeproj` 2. Observe the global hotkey with a CGEvent tap Requires Input Monitoring. Detect key down and key up for HOTKEY. Detect a double-tap for hands-free. 3. Record the microphone with AVAudioEngine while held Tap the input node; on release stop and write 16 kHz mono 16-bit PCM WAV via AVAudioConverter. Not 44.1 kHz, not float. 4. Add the 5-minute auto-stop and Esc to cancel Observe Esc passively so it still reaches the app being typed in. 5. Verify the WAV ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` ### Done when - [ ] Hold-record-release produces a playable 16 kHz mono 16-bit WAV - [ ] Double-tap starts and stops a hands-free session - [ ] The 5-minute cap fires - [ ] Esc discards the recording and still works normally in the foreground app - [ ] Denying Input Monitoring produces a clear message naming the pane ### Watch out - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. ## Phase 2 · Transcription Under three seconds from key release to text for a short clip, local by default. ### Steps 1. Shell out to whisper-cli with -otxt and read the result 2. Add the hosted path behind TRANSCRIBE_MODE with a Groq or OpenAI key 3. Measure latency and log it Key release to transcript, in the console, so regressions are visible. ### Done when - [ ] A 10-second clip transcribes accurately - [ ] Local works with Wi-Fi off - [ ] Key release to transcript is under three seconds for a short clip on small.en ## Phase 3 · Cleanup pass Filler removed, punctuation fixed, and never silence when the model fails. ### Steps 1. Write the provider interface and a fixed prompt Return only the cleaned text, no preamble. Strip a leading 'Here is' defensively. 2. Fall through to the raw transcript on any failure or missing key ### Done when - [ ] 'um so like send him the thing tomorrow' becomes a clean sentence - [ ] An LLM failure still inserts the raw transcript - [ ] No response arrives wrapped in quotes or an explanation ## Phase 4 · Insertion Text appears at the cursor in any app, with the clipboard handled the way the user chose. ### Steps 1. Write to the pasteboard and post Cmd-V via CGEvent Requires Accessibility: check AXIsProcessTrustedWithOptions and guide the user to the pane. Per-character CGEvent typing needs key-code mapping and breaks on layouts; the paste approach is the default. 2. Implement RESTORE_CLIPBOARD Save the previous pasteboard contents, paste, then restore after a short delay when the flag is true. ### Done when - [ ] Dictation inserts correctly in TextEdit, a browser address bar, Slack and a terminal - [ ] The clipboard flag behaves in both positions - [ ] With Accessibility denied the app shows an actionable message ### Watch out - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ## Phase 5 · Feedback UI You always know whether it is listening. ### Steps 1. Build the floating pill A small always-on-top panel that never takes focus, showing recording, transcribing or cleaning, the mode, and a live level meter. 2. Build the menu bar item On/off, launch at login, settings, recent transcripts. ### Done when - [ ] The pill appears within 100 ms of the hotkey - [ ] It never steals focus from the app being typed into - [ ] Three distinct states are visible ## Phase 6 · Settings, offline, README Configurable, works offline, documented. ### Steps 1. Read settings from a JSON file and reload on change 2. Verify the full path with the network off and no LLM key 3. Write the README Both permissions and their panes, the relaunch caveat, the password-field limitation, model sizes and latency. Files: `README.md` ### Done when - [ ] Works with the network off - [ ] A fresh Mac goes from clone to first dictation using only the README ## Not in this build - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. ## After v1, if you want it - Custom vocabulary passed to the cleanup prompt - Per-app cleanup styles (terse in a terminal, prose in mail)
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Path to the binary you built. WHISPER_BIN=/Users/you/whisper.cpp/build/bin/whisper-cli # Required. small.en for latency; medium.en if accuracy matters more. WHISPER_MODEL=/Users/you/whisper.cpp/models/ggml-small.en.bin # Required. The hold-to-talk key. fn, right-option, or a key combination. HOTKEY=fn # Optional · secret. Anthropic console. Empty disables the cleanup pass. ANTHROPIC_API_KEY=sk-ant-... # Required. A fast, cheap model is right for a cleanup pass. LLM_MODEL=claude-haiku-4-5-20251001 # Optional · secret. Enables hosted transcription when TRANSCRIBE_MODE=hosted. GROQ_API_KEY=gsk_... # Required. local or hosted. TRANSCRIBE_MODE=local # Required. true restores your previous clipboard after inserting; false leaves the transcript on it. RESTORE_CLIPBOARD=false
# Wispr Flow · product brief ## Problem Hotkey → record → Whisper → paste at cursor. One of the most-cloned apps of the trend for a reason. ## Product outcome A dictation tool you could ship to others: notarized, private by default, honest about the one field type it cannot type into. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Implement the core workflow described in ARCHITECTURE.md ## Explicit non-goals for v1 - Their tuned auto-editing voice model and per-app tone formatting. - The mobile keyboard. - Polish on accents, noise and crosstalk where hosted models lead. - their tuned auto-editing voice model - per-app tone formatting - the mobile keyboard - polish on edge cases (accents, noise) ## Success criteria - Notarized build runs on a clean Mac - Offline test passes - Password-field limitation documented - One Sparkle update delivered
# Build brief · Wispr Flow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a system-wide AI dictation tool like Wispr Flow for macOS. Build it in phases, in the order below. Do not write the whole app in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Phase 1 and Phase 5 are where this project dies · prove both before building any UI. ### Stack (fixed, do not substitute) - Swift 6 and SwiftUI, macOS 14+, a `MenuBarExtra` background app. Not Electron · global hotkeys and synthetic text insertion are native APIs here. - whisper.cpp invoked as the `whisper-cli` binary for local transcription. - A JSON settings file on disk. No database, no accounts, no telemetry. ### Permissions (understand these before Phase 1) Two separate grants, and confusing them costs an afternoon: - **Input Monitoring** · to observe the global hotkey. - **Accessibility** · to post synthetic keystrokes into another app. The system permission prompt can only appear once per app launch. If the user dismisses it, the app must be restarted before it can ask again, so detect the denied state explicitly and tell the user to relaunch rather than silently doing nothing forever. ### Phase 1 · Hotkey and recording Build: a global hold-to-talk hotkey (configurable, default a chosen key) that records the microphone with AVAudioEngine while held and stops on release. Write 16-bit PCM WAV, 16 kHz, mono · not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only, and resampling later means re-testing everything. Add: double-tap for a hands-free session ended by one more tap, a hard auto-stop at 5 minutes so a forgotten session cannot record all afternoon, and Esc to cancel. Observe Esc passively · it must still reach the app the user is typing in. Done when: hold-record-release produces a playable 16 kHz mono 16-bit WAV (verify with `afinfo`), double-tap starts and stops a hands-free session, the 5-minute cap fires, Esc discards the recording, and Esc still works normally in the foreground app. Do not build yet: transcription, the LLM, insertion, any UI. ### Phase 2 · Transcription Build: shell out to `whisper-cli -m models/ggml-small.en.bin -f clip.wav -otxt`. Fetch the model with `./models/download-ggml-model.sh small.en`. Default to small for latency · dictation is judged on the wait, and medium roughly doubles it for a gain most dictation does not need. Offer the Groq or OpenAI Whisper API as an opt-in fast path when a key is in `.env`, with local as the default. Done when: a 10-second clip transcribes accurately, the local path runs with the network off, and the elapsed time from key release to transcript is under three seconds on the small model for a short clip. ### Phase 3 · Cleanup pass Build: pipe the raw transcript through an LLM (key in `.env`) with a short fixed prompt that removes filler words, fixes punctuation and casing, and returns only the cleaned text with no preamble. Enforce that: strip a leading "Here is" style response defensively. If the key is missing or the call fails, fall through to the raw transcript rather than inserting nothing · a degraded result beats silence when the user is mid-sentence. Done when: "um so like send him the thing tomorrow" becomes a clean sentence, an LLM failure still inserts the raw transcript, and no response ever arrives wrapped in quotes or an explanation. ### Phase 4 · Insertion Build: insert the text at the cursor in whatever app has focus. Two approaches exist and they are not equivalent: - Posting each character via `CGEvent` requires mapping characters to key codes (macOS keyboard events carry key codes, not characters), and it breaks on layouts and on some apps. - Writing to the pasteboard and posting Cmd-V is far more reliable across apps. Use the pasteboard approach as the default. Save and restore the user's previous clipboard behind a config flag, defaulting to leaving the transcript on the clipboard so it can be pasted again. Both approaches need Accessibility trust · check `AXIsProcessTrustedWithOptions` and guide the user to the right pane. Done when: dictating into TextEdit, a browser address bar, Slack and a terminal all insert correctly; the clipboard-restore flag behaves in both positions; and with Accessibility denied the app shows an actionable message instead of failing silently. Note in the README that password fields refuse synthetic input by design · this is not a bug to fix. ### Phase 5 · Feedback UI Build: a small floating pill while recording, showing live state and which mode is active (hold vs hands-free), plus a level meter so the user can see the mic is actually hearing them. Then the menu bar item: on/off toggle, launch at login, open settings, and recent transcripts. Done when: the pill appears within 100ms of the hotkey, never steals focus from the app being typed into, and shows a distinct state for recording, transcribing and cleaning. ### Phase 6 · Settings, offline, README Build: the settings file (hotkey, model, local vs API, clipboard behavior, LLM prompt), verified offline operation with the local model and no LLM key, and the README. Done when: the full path works with the network off, and a fresh machine can go from clone to first dictation using only the README. ### Out of scope (and why) - Their tuned auto-editing voice model and per-app tone formatting. That is the actual product · a generic LLM cleanup pass is close, not equal. - The mobile keyboard. - Polish on accents, noise and crosstalk, which is where hosted models are ahead. ### README must contain - Both permissions, which pane each lives in, and the relaunch caveat. - The password-field limitation, stated as expected behavior. - Model download size and the latency difference between small and medium.
# Architecture · Wispr Flow ## Stack | Part | Choice | Why | | --- | --- | --- | | App | Swift 6, SwiftUI, MenuBarExtra, macOS 14+ | global hotkeys and synthetic text insertion are native APIs | | Audio | AVAudioEngine | microphone capture with explicit resampling to what whisper needs | | Transcription | whisper.cpp small.en via whisper-cli | latency matters more than the last word of accuracy for dictation | | Cleanup | An LLM API behind an interface, optional | filler words and punctuation, with the raw transcript as fallback | | Insertion | Pasteboard plus a synthetic Cmd-V | far more reliable across apps than typing characters via CGEvent | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Hotkey | the CGEvent tap and modes | The only Input Monitoring code | | Recorder | AVAudioEngine and WAV writing | Any source producing 16 kHz mono 16-bit | | Transcriber | whisper-cli and hosted fallback | WhisperKit in-process | | Cleaner | the LLM interface and prompt | Any provider; Ollama locally | | Inserter | pasteboard and Cmd-V, Accessibility checks | Per-character CGEvent typing as an alternative strategy | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `WHISPER_BIN` · required · Path to the binary you built. - `WHISPER_MODEL` · required · small.en for latency; medium.en if accuracy matters more. - `HOTKEY` · required · The hold-to-talk key. fn, right-option, or a key combination. - `ANTHROPIC_API_KEY` · optional, secret · Anthropic console. Empty disables the cleanup pass. - `LLM_MODEL` · required · A fast, cheap model is right for a cleanup pass. - `GROQ_API_KEY` · optional, secret · Enables hosted transcription when TRANSCRIBE_MODE=hosted. - `TRANSCRIBE_MODE` · required · local or hosted. - `RESTORE_CLIPBOARD` · required · true restores your previous clipboard after inserting; false leaves the transcript on it. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Wispr Flow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Swift 6, SwiftUI, MenuBarExtra, macOS 14+, AVAudioEngine, whisper.cpp small.en via whisper-cli, An LLM API behind an interface, optional, Pasteboard plus a synthetic Cmd-V. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix.
# Delivery milestones · Wispr Flow Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Hotkey and recording Hold to record, release to stop, with a WAV whisper accepts, plus hands-free, a hard cap and Esc to cancel. ### Steps 1. Create the Xcode project with a CLI target and the app target Prove capture in the CLI target first. Files: `Dictate.xcodeproj` 2. Observe the global hotkey with a CGEvent tap Requires Input Monitoring. Detect key down and key up for HOTKEY. Detect a double-tap for hands-free. 3. Record the microphone with AVAudioEngine while held Tap the input node; on release stop and write 16 kHz mono 16-bit PCM WAV via AVAudioConverter. Not 44.1 kHz, not float. 4. Add the 5-minute auto-stop and Esc to cancel Observe Esc passively so it still reaches the app being typed in. 5. Verify the WAV ```sh afinfo /tmp/clip.wav $WHISPER_BIN -m $WHISPER_MODEL -f /tmp/clip.wav ``` ### Done when - [ ] Hold-record-release produces a playable 16 kHz mono 16-bit WAV - [ ] Double-tap starts and stops a hands-free session - [ ] The 5-minute cap fires - [ ] Esc discards the recording and still works normally in the foreground app - [ ] Denying Input Monitoring produces a clear message naming the pane ### Watch out - The permission prompt appears once per launch. If the user dismisses it, the app must be relaunched before it can ask again; detect the denied state and say so. ## M2 · Transcription Under three seconds from key release to text for a short clip, local by default. ### Steps 1. Shell out to whisper-cli with -otxt and read the result 2. Add the hosted path behind TRANSCRIBE_MODE with a Groq or OpenAI key 3. Measure latency and log it Key release to transcript, in the console, so regressions are visible. ### Done when - [ ] A 10-second clip transcribes accurately - [ ] Local works with Wi-Fi off - [ ] Key release to transcript is under three seconds for a short clip on small.en ## M3 · Cleanup pass Filler removed, punctuation fixed, and never silence when the model fails. ### Steps 1. Write the provider interface and a fixed prompt Return only the cleaned text, no preamble. Strip a leading 'Here is' defensively. 2. Fall through to the raw transcript on any failure or missing key ### Done when - [ ] 'um so like send him the thing tomorrow' becomes a clean sentence - [ ] An LLM failure still inserts the raw transcript - [ ] No response arrives wrapped in quotes or an explanation ## M4 · Insertion Text appears at the cursor in any app, with the clipboard handled the way the user chose. ### Steps 1. Write to the pasteboard and post Cmd-V via CGEvent Requires Accessibility: check AXIsProcessTrustedWithOptions and guide the user to the pane. Per-character CGEvent typing needs key-code mapping and breaks on layouts; the paste approach is the default. 2. Implement RESTORE_CLIPBOARD Save the previous pasteboard contents, paste, then restore after a short delay when the flag is true. ### Done when - [ ] Dictation inserts correctly in TextEdit, a browser address bar, Slack and a terminal - [ ] The clipboard flag behaves in both positions - [ ] With Accessibility denied the app shows an actionable message ### Watch out - Password fields refuse synthetic input by design. Say so in the README; it is not a bug to fix. ## M5 · Feedback UI You always know whether it is listening. ### Steps 1. Build the floating pill A small always-on-top panel that never takes focus, showing recording, transcribing or cleaning, the mode, and a live level meter. 2. Build the menu bar item On/off, launch at login, settings, recent transcripts. ### Done when - [ ] The pill appears within 100 ms of the hotkey - [ ] It never steals focus from the app being typed into - [ ] Three distinct states are visible ## M6 · Settings, offline, README Configurable, works offline, documented. ### Steps 1. Read settings from a JSON file and reload on change 2. Verify the full path with the network off and no LLM key 3. Write the README Both permissions and their panes, the relaunch caveat, the password-field limitation, model sizes and latency. Files: `README.md` ### Done when - [ ] Works with the network off - [ ] A fresh Mac goes from clone to first dictation using only the README ## M7 · Distribute it (production only) Notarized, self-updating, with opt-in crash reports. ### Steps 1. Sign with a Developer ID and notarize ```sh xcrun notarytool submit Dictate.zip --keychain-profile notary --wait xcrun stapler staple Dictate.app ``` 2. Add Sparkle with a signed appcast on your domain 3. Add opt-in crash reporting, off by default ### Done when - [ ] A notarized build opens on a second Mac without a right-click workaround - [ ] An appcast update is offered to an installed copy
# Operations · Wispr Flow ## Backup No state beyond settings.json and recent transcripts; both under ~/Library/Application Support. Time Machine covers it. ## Restore Reinstall; copy settings back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Opt-in crash reports only. ## Incident checklist A leaked API key is rotated at the provider; ship a release that clears stored keys. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Notarized build runs on a clean Mac - [ ] Offline test passes - [ ] Password-field limitation documented - [ ] One Sparkle update delivered ## Launch constraint Do not market omitted Wispr Flow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Path to the binary you built. WHISPER_BIN=/Users/you/whisper.cpp/build/bin/whisper-cli # Required. small.en for latency; medium.en if accuracy matters more. WHISPER_MODEL=/Users/you/whisper.cpp/models/ggml-small.en.bin # Required. The hold-to-talk key. fn, right-option, or a key combination. HOTKEY=fn # Optional · secret. Anthropic console. Empty disables the cleanup pass. ANTHROPIC_API_KEY=sk-ant-... # Required. A fast, cheap model is right for a cleanup pass. LLM_MODEL=claude-haiku-4-5-20251001 # Optional · secret. Enables hosted transcription when TRANSCRIBE_MODE=hosted. GROQ_API_KEY=gsk_... # Required. local or hosted. TRANSCRIBE_MODE=local # Required. true restores your previous clipboard after inserting; false leaves the transcript on it. RESTORE_CLIPBOARD=false
$ choose a build depth, inspect the files, then open the complete pack in your agent
xtheir tuned auto-editing voice model
xper-app tone formatting
xthe mobile keyboard
xpolish on edge cases (accents, noise)
Don't feel like building it? These folks already made it free.
all 8 free alternatives to Wispr Flow →· no votes, no pay-to-list · just what's real
Wispr Flow pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | Desktop: 2,000 dictated words/week; iPhone: 1,000 words/week; Android: unlimited words; recordings under 5 minutes do not count. |
| pro | $15/user | $12/user | Unlimited dictation across supported devices; 1 paid user. |
| enterprise | custom | — | Custom paid-user count; admin seats are free and no minimum seat count is published. |
free tierDesktop 2,000 words/week; iPhone 1,000 words/week; Android unlimited; recordings under 5 minutes do not count.
billingmonthly + annual (20% off); no commitment and cancel anytime
hidden costsEnterprise admin seats are free, but regular users are billed; volume discounts are custom. Students and educators with accepted .edu eligibility can receive Pro free, while nonprofit discounts are custom.
verified 2026-08-11 · source ↗
Is Wispr Flow free?
The free plan caps dictation at 2,000 words per week on desktop. Paid is the paid plan at $15/mo (checked 2026-08-07).
Vibecode Wispr Flow
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Wispr Flow replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Wispr Flow cost?
Wispr Flow costs about $15/month (paid plan, checked 2026-08-07), which is $180 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Wispr Flow?
Honestly: their tuned auto-editing voice model; per-app tone formatting; the mobile keyboard; polish on edge cases (accents, noise). If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Wispr Flow?
Yes: Handy (The literal replacement: press, record, transcribe locally, paste.) FluidVoice (Local Mac dictation with on-device cleanup and no weekly word allowance.) OpenWhispr (Hotkey, local model, cleaned text at the cursor, plus searchable history and meeting capture.) All 8 curated free alternatives are at vibecodeit.com/wispr-flow/alternatives. The prompt is for when you want it exactly your way.