Hold Control, speak, release. Fully offline dictation, no account, no per-word cost.
Unzip this archive anywhere you like. Then open Terminal and get it sitting inside the unzipped folder, without typing a path by hand:
cd (with a trailing space) but do not press Enter yet.cd ~/Downloads/local-dictate && bash install.sh
It checks for ffmpeg (and for Homebrew only if ffmpeg is missing), sets up its own Python environment, installs what it needs, lists your microphones and lets you pick one, then installs and starts the background service.
Safe to re-run any time. Re-running it updates the existing install in place, it does not create a second one.
The installer prints an exact file path at the end. Copy it, then:
Cmd+Shift+G, paste the path, click Open, and make sure it is toggled on.cd ~/Downloads/local-dictate && bash install.sh again (or the restart command it printed) so the change takes effect.Without these, the transcript still lands on your clipboard, so Cmd+V always works as a manual fallback while you sort out permissions.
Click into any text field. Hold either Control key, say a full sentence out loud, then release.
The installer checks your Mac's chip and sets up the right engine by itself. You do not have to choose.
mlx-whisper on the GPU with a large model (about 1.5GB). It is quick, usually well under a second to a couple of seconds after you release Control.faster-whisper on the CPU from prebuilt packages, with a smaller model (about 480 MB, downloaded once by the installer). Nothing is compiled. We measured about a second between releasing Control and the text appearing on a 6-core Intel Mac; older Intel Macs may take a few seconds.ffmpeg is already on your Mac, the installer never touches Homebrew, and it installs no Homebrew packages on an Intel Mac. If ffmpeg is missing and you do not have Homebrew, install it from brew.sh first.To see which engine is running, look for backend=mlx or backend=ct2 near the top of the log (grep backend ~/Library/Logs/local-dictate.log). The custom word list, vocabulary.txt, works on both. Keep it short on Intel.
Say a command out loud and Magnum Vox types the character instead of the words. It only converts when you say it as a command, not inside a normal sentence. "Hello, new line, how are you" becomes two lines; "we launched a new line of products" stays exactly as you said it. On by default, no setup.
new line, newline, next line, line break. A blank line: new paragraph, next paragraph.bullet point, next bullet, new bullet start a new line with - .open bracket / close bracket (also "square", plurals such as open brackets, and the forms closed brackets, opening bracket), open paren / close paren (also "parenthesis", "parentheses"), open brace / close brace (also "curly bracket", "curly brace"). left bracket / right bracket work the same way, and so do left and right with paren, parenthesis, brace and curly. Left and right never convert after a direction word: "turn right bracket the car" stays as typed. A full stop or comma Whisper writes just before a closing bracket or quote is moved outside it.open quote, begin quote or start quote (plural too) and close quote, end quote or unquote type a double quote.dot dot dot (also "dot, dot, dot") or ellipsis, question mark, exclamation point, full stop, semicolon, colon.Spacing is tidied for you, including the comma or period Whisper tends to add next to a command, and the next word after a new line or a sentence ending gets a capital letter.
To type the words themselves, say literally right before the phrase. "literally new line" types the words new line.
To turn voice commands off, create an empty file named commands_off next to dictate_daemon.py, or set the environment variable LOCAL_DICTATE_COMMANDS=0. Delete the file to turn them back on. No restart needed.
touch ~/Developer/local-dictate/commands_off
Whisper puts a full stop at every pause, even in the middle of a thought. "I think we should go ahead. And ship it on Monday. If the team agrees." Flow punctuation joins those stray full stops, so it reads "I think we should go ahead and ship it on Monday if the team agrees." On by default, no setup.
It joins a full stop when the next sentence carries the thought on (and, but, so, because, which, then, if, when and a few more), or when the sentence before is a short opener like "In fact." or "Honestly." A whole new clause, a question, or a long sentence keeps its full stop. It never joins across a new line, never touches a full stop you said as a command, and never removes the last full stop of a dictation.
To turn it off, create an empty file named flow_off next to dictate_daemon.py, or set the environment variable LOCAL_DICTATE_FLOW=0. Delete the file to turn it back on. No restart needed.
touch ~/Developer/local-dictate/flow_off
Watch the log while you test:
tail -f ~/Library/Logs/local-dictate.log
Three real bugs showed up building this. Worth knowing since any of them can resurface:
cd ~/Downloads/local-dictate && bash install.sh and pick your real mic by name, not whatever macOS calls default.ProcessType: Interactive in the background service definition). If it ever comes back, that setting is the first thing to check.Full detail on all three, plus every other command you might need, is in README.md in this same folder.