Dictee: my own local dictation app

Caps Lock to talk, a live transcript while I speak, clean text pasted where the cursor is. Everything runs on my Mac Studio. Built with Claude Code in 22 prompts.

The Dictee app icon: a white studio microphone with a red edelweiss on a red gradient square, with the name Dictee under it

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.

The Dictee panel while recording: logo, name, red dot, timer at 0:28, level meter and the live transcript of the sentence being spoken

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 Dictee panel after the recording stops: orange dot, Transcribing label, and the last lines of the live transcript

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

Three dictated sentences pasted into TextMate, each on its own 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 eval scores 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 -ee app 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 eval scores 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, reload and recover, all with --json. Reads work without the daemon; writes report confirmed: true.
  • Transcribe a file. dictee transcribe call.m4a --json runs 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-server on 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 hidutil remap 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 CapsLockDelayOverride to 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

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.