![]()
14 Oct 2026 Dictee is my own dictation app. I press Caps Lock, speak, press Caps Lock again, and the cleaned text lands where my cursor is. It's also on the clipboard. Nothing leaves the Mac: speech recognition, cleanup and storage all run locally.
The name is the French dictée, and it fits the -ee family of my other tools.

Why I built it
I liked Wispr Flow for its output. It still sends every dictation to its cloud, even with Privacy Mode on, and it runs on a subscription. I wanted 3 things it couldn't give me:
- Local and private. Client names and deal details never leave the machine.
- Accurate in my 3 languages. English, French and German, with my names and Kaltura terms spelled right.
- Flexible. My hotkey, my formatting rules, my dictionary, my apps.
I compared the options first (VoiceInk, Handy, superwhisper and others, see macOS Voice Dictation Solutions), then decided to build my own.
What it does
| Feature | How it works |
|---|---|
| Caps Lock trigger | Press to start, press to stop. Shift+Caps records privately, Esc cancels |
| Live transcript | The panel shows my words as I speak, refreshed every second |
| Timer and meter | A running counter and an input level bar, so I know the mic hears me |
| Own logo and name | "Dictee" and my edelweiss logo on the panel, the menu bar and notifications |
| 1 sentence per line | Every sentence starts on a new line. Terminals get 1 line, so nothing runs by accident |
| Voice commands | "New line", "nouvelle ligne", "neuer Absatz" and so on, in EN, FR and DE |
| Cleanup | Punctuation, filler words, self-corrections, digits instead of words, my spellings |
| Dictionary | Terms and their mishearings. "Nick" becomes "Nic", "culture a" becomes "Kaltura" |
| Paste and clipboard | Pastes into the app I was in, with that app's own Paste shortcut, and keeps a copy |
| Music pause | Dictee tells Hifee, my music player, to pause while I record and resume after |
| Clear errors | A red panel plus a macOS notification that says what failed and what to do |
| Menu bar status | Logo in white when idle, red while recording, orange while transcribing, "!" on a problem |

The live transcript is raw recognition. The pasted text is the cleaned version, 1 sentence per line:

A few details I care about:
- Guards on the cleanup model. If the model changes the language or rewrites too much, Dictee pastes the raw text and tells me why. It never "improves" my meaning.
- It learns from corrections. I fix a pasted text, copy it, and save it as a correction from the menu bar. The corrections become my personal test set, and
dictee.py evalscores speech engines on my own voice. - Error messages are written for me. "The microphone sent pure digital silence. Check the microphone permission for Hammerspoon." beats a stack trace.
02 Nov 2026 3 changes since the first version matter most:
- Long dictations. I can now talk for up to 30 minutes. From about 75 seconds, Dictee transcribes and cleans my speech in pieces of about 1 minute while I talk, so only the last piece is left when I stop. On a 3-minute test, the wait at stop fell from 11.6 s to 5.6 s, and no text went missing.
- Nothing gets lost. A crash restarts the daemon in about 1 second and puts the unfinished recording on the clipboard. A cancelled recording can be recovered from the menu. Dictee also keeps recording 0.4 s after I press the key, because I tend to stop right on my last word.
- Numbers, dates and times come out as I write them. "Twenty-ninth October" becomes 29th October, "ten thirty AM" becomes 10:30 AM, "Q four" becomes Q4, and "80,000 euros" becomes 80,000 EUR.
All features
Recording
- Caps key trigger. Press to start, press again to stop and paste. Works with Caps Lock on any keyboard, and with F13.
- Private dictation. Shift+Caps pastes the text and stores nothing, not the audio, not the text.
- Cancel. Esc cancels. Only a real key press counts: an Esc sent by a script is ignored.
- Pause and resume. Shift+Esc or the button on the panel. Pauses don't count towards the limit.
- Long dictations. Up to 30 minutes, with an orange countdown in the last minute and an automatic restart at the limit.
- Pieces while I talk. Long recordings are transcribed and cleaned in pieces of about 1 minute in the background.
- Post-roll. Dictee records 0.4 s after the stop press, so the last word is never cut.
- Music pause. Dictee tells Hifee when a recording starts and ends. Hifee pauses and resumes only what it paused.
- No dictation in password fields. Dictee won't start when macOS Secure Input is on.
Panel and menu bar
- Live transcript. 8 lines of raw text, refreshed every second, including the pieces already transcribed.
- Timer, level meter and memory gauge. The gauge shows the GPU memory Dictee holds, orange from 8 GB.
- Corner radius setting. Square or rounded panel corners, the same setting as in my other apps.
- Menu bar status. White when idle, red while recording, orange while transcribing, "!" on a problem.
- Menu actions. Copy last dictation, open last recording, save a correction, recover a recording, restart the daemon or the model server, open the log.
- Stats page. Dictations, words, speaking time, time saved against typing, words per app, languages, most used terms. Counts only, never the text.
Text cleanup
- Speech recognition. Parakeet TDT 0.6B v3, local, in English, French, German and 22 more languages.
- Cleanup model. gemma3:12b on Dictee's own llama-server: punctuation, filler words, self-corrections, names.
- Guards. If the model changes the language, drops text or rewrites too much, Dictee pastes the raw text and says why.
- Restored endings. If the model drops my closing sentence, Dictee puts it back.
- Dictionary of about 1,100 terms. Gathered from my apps, projects, notes and setup, plus terms I add by hand with their mishearings.
- Sound-alike matching. My
-eeapp names heard as "-y" or "-ie", and names heard as 2 words ("Nik AI" becomes NicAI). - Undo wrong terms. If the model turns an ordinary word into a product name ("task dashboard" into an app name), Dictee puts the heard word back.
- Numbers as digits. In English, French and German: "quatre-vingt-dix" becomes 90, "dreiundzwanzig" becomes 23.
- Dates, times and quarters. "Thirteenth of October" becomes 13th October, "nine oh five" becomes 9:05, "Q one" becomes Q1.
- Euro amounts. "€80,000" and "80,000 euros" become 80,000 EUR.
- Folder names. "Slash ka" stays
/ka: Dictee hides it from the model and puts it back after. - Formatting. 1 sentence per line with an empty line between, voice commands in 3 languages, no em-dashes, straight quotes.
- Signature. A dictation that ends with my name ends with "Nic" alone on the last line.
- Per-app profiles. Terminals get 1 line, so a line break never runs a command.
Paste, storage and recovery
- Paste and clipboard. Pastes with each app's own Paste shortcut and keeps a copy on the clipboard, marked as Dictee's for Clipee.
- Crash recovery. The daemon restarts in about 1 second and puts the unfinished recording on the clipboard.
- Cancelled recordings. The last 5 are kept and can be transcribed from the menu.
- History. Every dictation in SQLite: raw text, final text, timings. Audio kept 180 days, then deleted.
- Corrections and evaluation. Saved corrections become my test set, and
evalscores speech engines on my own voice. - Model on demand. The cleanup model loads when I start a recording and unloads after 20 idle minutes.
- Clear errors. A red panel and a macOS notification that say what failed and what to do.
For agents
- Agent CLI.
dictee describe,status,last,history,timings,transcribe,reloadandrecover, all with--json. Reads work without the daemon; writes reportconfirmed: true. - Transcribe a file.
dictee transcribe call.m4a --jsonruns any audio file through the full pipeline.
For business people
Dictation saves the most time on the writing I do all day: emails, Teams messages, notes after calls, prompts for Claude.
The cloud tools are good, but they send every word to someone else's server. For a seller, that means client names, deal sizes and internal plans. Dictee gives me the same workflow with none of that exposure, no subscription, and full control over how the text comes out.
The trade-off: I maintain it myself. It runs on 1 machine, my Mac Studio, and it relies on the tools already installed there.
For technical people
Architecture
Caps Lock ──> Hammerspoon (Lua)
- Caps Lock tap, overlay panel, live text, menu bar, notifications
- tells Hifee to pause and resume the music
- paste with the app's own shortcut + clipboard
│ HTTP on 127.0.0.1
▼
Dictee daemon (Python)
1. record External Microphone (Rode VideoMic on the jack), 48 kHz
2. trim silence cut against the room's noise floor, resample to 16 kHz
3. ASR Parakeet TDT 0.6B v3 on MLX (mlx-audio), model kept warm
4. preview every second: re-transcribe the recording so far, no LLM
5. aliases dictionary replacements from SQLite
6. cleanup gemma3:12b on its own llama-server, temperature 0, closest dictionary terms in the prompt
7. guards language, dropped text and drift checks, else keep the raw text
8. format voice commands, 1 sentence per line, no em-dashes, per-app profiles
9. log SQLite: raw text, final text, timings. Audio kept 180 days
| Part | Choice | Why |
|---|---|---|
| Speech model | NVIDIA Parakeet TDT 0.6B v3 | 25 European languages, 0.2-0.3 s for a 9 s clip |
| Runtime | mlx-audio on Apple Silicon | 1 API for Parakeet, Whisper and Voxtral |
| Cleanup model | gemma3:12b on llama.cpp | Won a blind test against mistral-small at half the memory |
| Hotkeys and UI | Hammerspoon | Already running, reads the accessibility tree, draws overlays |
| Storage | SQLite | Dictations, corrections, dictionary |
A 9-second dictation takes about 2 seconds from stop to paste: 0.2-0.3 s for speech recognition, 1.5-2.5 s for cleanup. Voxtral Realtime 4B scored better on French and German in published benchmarks, but took 2 s per clip on my M1 Ultra, so it stays a 1-line config switch.
02 Nov 2026 The cleanup model changed twice:
- Its own server. Dictee runs its own
llama-serveron the model file Ollama already has. Ollama answers 1 request at a time, so a dictation could wait 12-20 s behind another app. Speculative decoding on the dictated words made it about 20% faster, with identical output. - gemma3:12b instead of mistral-small. I re-ran 323 of my own dictations through both. In a blind comparison, gemma3 won 64 to 46, with 64 ties, and it holds about 8 GB instead of 15-17 GB. Its main fault is putting a product name on an ordinary word, so Dictee now undoes that in code.
- Loaded only while I dictate. The model starts loading when I press Caps and stops after 20 idle minutes. It runs about 4 hours a day instead of 24.




Why not my own voice model
My fine-tuned model (see Fine-tuning my own model (LLM): NicAI) stays out of the dictation path. It's trained to turn a brief into my writing, so it rewrites. Dictation needs the opposite: change as little as possible. It also takes about 13 seconds per text, and cleanup has to take about 1.
Gotchas I hit
- A launchd agent can't use the microphone. macOS silently hands a background Python process pure zeros, with no permission prompt. Hammerspoon starts the daemon instead, and the daemon inherits Hammerspoon's microphone permission.
- Remapping Caps Lock failed twice. A
hidutilremap from Caps Lock to F18 produced no key event at all, and my old Hyperkey setup had Caps Lock set to "No Action" in System Settings. Dictee now listens to Caps Lock itself and turns the lock straight back off. - macOS ignores quick Caps Lock taps. A built-in delay drops short presses that would turn Caps Lock on. Since Dictee resets the lock after each press, every press is an "on" press, so I sometimes had to press twice. Setting
CapsLockDelayOverrideto 0 fixed it. - Menu-based paste isn't reliable. In TextMate, choosing Edit > Paste only opened the menu. Dictee now presses each app's own Paste shortcut, read from the app's menu or from my custom shortcuts (Cmd+F is Paste on my Macs).
- Parakeet invents words from silence. Room noise alone produced "Yeah." The live preview only runs when the recording holds real speech.
- A hidden Hammerspoon hides the panel. On 14th October 2026 the menu bar icon turned red, but no panel appeared. Hammerspoon was hidden, most likely by "Hide Others" (Opt+Cmd+H) in another app, and a hidden Hammerspoon hides everything it draws. Hammerspoon now unhides itself 0.2 s after any app is hidden or activated. See Tilee: my own tiling window manager in Hammerspoon.
- 02 Nov 2026 The keyboard couldn't send F18. macOS puts its Caps Lock delay back each time my Kinesis keyboard reconnects, so a quick press was sometimes lost. I flashed new firmware to make the Caps key send a normal key. F18 sent nothing: the keyboard's report stops before F13-F24. Print Screen sits inside the report, and macOS reads it as F13, so Dictee now listens to F13 as well.
- 02 Nov 2026 Unbounded GPU cache. macOS killed the daemon 3 times in long dictations. The live preview sent a new audio length every second, and MLX cached a buffer for each one. A cache limit of 2 GB fixed it at the same speed.
- 02 Nov 2026 A test session cancelled my dictations. A Claude Code session that tested another app sent Esc key presses and cancelled 4 recordings. Dictee now ignores any Esc that doesn't come from the keyboard, and keeps the last 5 cancelled recordings.
How I got there: 22 prompts
I built Dictee in 1 session with Claude Code, over 2 days. I wrote 22 prompts. Claude wrote the code, tested each part on the machine, and pushed every change to a private GitHub repo.
| Phase | Prompts | What happened |
|---|---|---|
| Research | 1 | Local alternatives to Wispr Flow, pros, cons, my setup's constraints |
| Architecture | 1 | Name, requirements, full design, the voice model question |
| First build | 1 | Daemon, Hammerspoon module, dictionary, tests, repo |
| Caps key | 8 | Key loggers, raw HID monitor, Hyperkey removed, System Settings fix |
| Features and fixes | 9 | Live transcript, dictionary, errors, Cmd+F paste, logo, double press, TextMate, Audirvana |
| Questions | 2 | Where the database is, how many prompts |
The Caps key took 8 of the 22 prompts. The code was right early on. The cause sat in 2 places Claude could only diagnose: an old Hyperkey setting in System Settings and a macOS timing rule. Claude couldn't press the key or click Allow, so each test needed me at the keyboard.
What worked well:
- Start with research, then architecture, then build. The first 2 prompts produced a design I could judge before any code existed.
- Measure, don't guess. The engine and the cleanup model came from tests on my Mac, not from blog benchmarks. 1 candidate model translated my English into French.
- Ask for features in plain words. "Pause Audirvana while I record" became a feature, a test and a commit in 1 prompt.
The first version was about 1,850 lines of Python and Lua, 12 commits and 16 unit tests. 02 Nov 2026 Today it's about 4,600 lines, 64 commits and 55 unit tests. It replaces Wispr Flow on my Mac Studio.
Further reading
- macOS Voice Dictation Solutions: the dictation apps I compared first
- Fine-tuning my own model (LLM): NicAI: the voice model and why it stays out of this path
- The NicAI voice stack: the rest of the NicAI voice stack
- Wispr Flow, the cloud app Dictee replaces
- Hifee: my own music player for the Mac: my music player, which Dictee pauses while I record
- Kinesis Advantage360, the keyboard with the Caps key I use