A minimal macOS dictation daemon. Hold-to-talk or hands-free latch, on-device transcription, text inserted at the cursor.
This is a fork of digimata/parrot. It adds transcript cleanup, clipboard fallback, literal correction aliases, dictation history, and audio cues. Upstream lists AI post-processing and transcript history as explicit non-goals, so these won't be merged there — this fork diverges deliberately. See Fork additions.
This fork isn't published as a release, so build and install the native app from source:
git clone https://github.com/psabbagh/parrot.git
cd parrot
./scripts/install-app.shThis creates a signed, menu-bar-only Parrot.app in ~/Applications, opens it, and enables the native Open at Login item on a fresh installation. Updates preserve the existing login-item choice. You can start Parrot later from Applications or Spotlight; there is no plist or launchctl command to manage.
Re-running the installer stages and verifies the complete replacement before it asks the running Parrot to drain. Once Parrot accepts, it blocks new captures, finishes any admitted recording, transcription, cleanup, delivery, or dismissed-history write, and quits only when that work is durable. The installer then atomically exchanges the bundles, verifies the installed copy, and opens one instance. A refusal, timeout, premature exit, or failed replacement leaves the previous bundle in place; a timed-out app resumes accepting captures. Builds from before this coordination protocol are never killed by the installer—quit such a build after its current dictation and run the installer again once.
The first build using the com.nstrmnts identity may need fresh Microphone and Accessibility approval in macOS. If the native login item was enabled under the former app identity, turn Open at Login on once under the new identity.
On the first app launch, macOS asks for Microphone access and adds Parrot.app to Privacy & Security → Accessibility. Allow the microphone prompt and enable Parrot there; the running app notices the Accessibility grant and continues setup without a manual restart. If Parrot is already shown as enabled but the current copy is still denied, remove that stale row and add the current Parrot.app again; Parrot’s bottom-center surface explains this recovery. The same surface that becomes the recording pill expands during setup to show real download progress, distinguish CoreML preparation from downloading, and say when dictation is ready. If macOS still refuses the event tap after granting access, it offers Quit and Reopen Parrot as a visible fallback. The app has a stable identity separate from the legacy command-line binary, while both continue sharing your Parrot settings. Later signed updates keep the app grants.
The installer signs the app with a code-signing identity from your login keychain (an Apple Development certificate from Xcode is enough). This matters more than it sounds: a stable signature lets macOS keep Parrot's Accessibility and Microphone grants across rebuilds.
Without a signing identity the script stops and tells you. ./scripts/build-app.sh builds the signed bundle without installing it. The legacy ./scripts/dev-install.sh remains available for CLI-only development; its current and former LaunchAgent labels are both retired when moving to the native app.
Requires: macOS 14+ on Apple Silicon (M1 or newer). Transcription runs on the Apple Neural Engine via CoreML. --cleanup additionally requires macOS 26+ with Apple Intelligence enabled.
- Open Parrot. Use Applications or Spotlight. Its bird appears in the menu bar and fresh installations open automatically at later logins.
- Click into the text field you want to dictate into — Messages, the address bar, a Slack thread, anywhere a cursor blinks.
- Hold
fn, speak, and release for ordinary dictation. For a long capture, double-tapfnto latch recording hands-free, then tap it again or click the checkmark to stop. The hands-free pill also has a clickable X; Escape and X dismiss without inserting, retaining the transcript only when Save History was already on. Fn is the default; choose Dictation Shortcut… in the Parrot menu and the pill expands in place to record a modified key or function key instead. - The transcript types itself in at the cursor when recording stops.
That's it. Hold-to-talk remains shortcut-only; clickable stop and dismiss controls appear during hands-free recording.
Parrot recognizes familiar Mac dictation commands after transcription. Say punctuation names such as “colon”, “comma”, “semicolon”, “period” or “full stop”, “question mark”, and “exclamation point”. Say “new line” for one return or “new paragraph” for two.
For lists, start a line or a new dictation with “bullet point” or “number one”, “number two”, and so on. A bare number word is converted only when it is clearly separated as a marker: One. Fix this becomes 1. Fix this, while One option is safer stays unchanged. Starting the next explicit numbered item in a separate dictation moves it to a new line automatically.
For example, saying “Here’s some feedback colon new line number one fix the label new line number two reset the fields” produces:
Here’s some feedback:
1. fix the label
2. reset the fields
Note: on most modern Macs the
fnkey is the bottom-left key. While Fn is Parrot’s shortcut, Parrot consumes its presses so the configured Globe-key action does not also fire.parrot doctorstill reports that system setting and shows how to select Do Nothing if you want Fn to stay inert when Parrot is not running.
parrot # run in the foreground (^C to quit)
parrot setup # one-time permission setup
parrot doctor # check permissions, shortcut, literal aliases
parrot models list # list available models
parrot models download <id> # pre-download a model
parrot history # show recent dictations
parrot history --raw --limit 50 # include the pre-cleanup transcript
parrot history --clear # delete the log
parrot cleanup-check # is cleanup still working?
parrot cleanup-check "some text" # run it on your own text
parrot transcribe <audio> --model <id> # isolate recognition on a saved file
parrot compare-models <audio> # same samples through Base and Large Turbo
parrot vocab-suggest # find mishearings of your terms in the log
parrot fix "what it should have said" # learn from a dictation that came out wrongvocab-suggest scans history.jsonl for new variants of names already declared with literal aliases, and prints updated Term: alias lines to paste in. It changes nothing. Candidates that are also ordinary English are flagged, because declaring those rewrites innocent sentences too — that judgement stays yours.
cleanup-check runs the real cleanup pass on a deliberately messy sample and prints the outcome, exiting non-zero if it fell back. Worth running after a macOS update: the on-device model ships with the OS, and its prompt handling has changed before.
Daemon flags, all off by default:
--model <id> # override and save the transcription model
--cleanup # tidy transcripts with the on-device model
--clipboard-fallback # copy when no text field has focus
--history # log each dictation to disk
--sound # cues for start, stop, and hands-free mode
--no-overlay # disable the bottom-of-screen pill
--debug-transcripts # include transcript text in stderr (sensitive)
--dump-wav # keep the last capture as private debug audioparrot install remains as a compatibility command for the older CLI LaunchAgent. New installations should use Parrot.app; its menu settings persist without rewriting a plist.
parrot install --launch-at-login \
--model whisper-large-v3-turbo --cleanup --clipboard-fallback --history --sound --duck-audioThe menu bar item is the daemon's compact status and command surface. Its first group changes with Parrot's live state: ready shows focused product destinations, recording and transcription show dismissal, hands-free recording also shows Finish Dictation, and setup failures show only the relevant recovery commands. Control–Option–Command–C opens Correct Last Dictation from any app; the shortcut is listen-only and does not swallow the key event:
Ready
Hold Fn to dictate · double-tap for hands-free
──────────────────────────────────────────────
Correct Last Dictation… ⌃⌥⌘C # when available
Review Last Cleanup… # when available
Transcription History
──────────────────────────────────────────────
Settings… ⌘,
Diagnostics
──────────────────────────────────────────────
Quit Parrot ⌘Q
Settings… is a native, single-pane settings window for the dictation shortcut, sounds, audio ducking, cleanup, clipboard fallback, history retention, correction observation, literal aliases, saved correction evidence, launch at login, the current model, and reset-to-default controls. Cleanup remains visibly unavailable with its reason when Apple Intelligence can't be used. Command–Comma opens the window using the standard macOS shortcut.
Transcription History remains a dedicated window rather than moving into the transient bottom overlay. Its search, newest-first browsing, dismissed-dictation recovery, exact copy, and confirmed permanent deletion need a stable, resizable content surface. It stays directly available from the ready menu even when history retention is off so the useful privacy/off state remains discoverable.
Diagnostics is a separate maintenance window for build identity, the exact running app, the diagnostic log, and the latest cleanup outcome. Product data no longer shares a catch-all submenu with developer/runtime diagnostics.
Toggles persist and win over launch defaults. The bundled app starts with sounds, audio ducking, transcript cleanup, and clipboard fallback enabled. History and correction observation remain off pending a dedicated first-run privacy choice. Foreground CLI flags retain their opt-in defaults. Reset to Launch Defaults returns to the profile used by that entry point. Overrides live in ~/Library/Preferences/com.nstrmnts.parrot.plist, so they survive the reinstalls that come with iterating on the daemon. Existing com.digimata.parrot preferences migrate automatically on first launch, then the old preference domain and recovered LaunchAgent archives are removed.
Change Dictation Shortcut… in Settings opens the native recorder in the focused bottom surface. Parrot accepts modified keys and standalone function keys, rejects ordinary unmodified typing and protected system/Parrot shortcuts, and warns when macOS commonly owns a function key. Only the exact selected chord is consumed; every other keyboard event passes through. Use Fn restores the default, and changing the shortcut takes effect immediately without relaunching.
The model isn't switchable here: changing it reloads CoreML, which is a ~60s stall, so it stays a label in Settings rather than pretending to be instant.
A toggle flipped mid-dictation applies to the next one — each utterance completes under the settings it started with.
Lower Other Audio is on by default in the bundled app. Parrot checks whether the default output was already playing before dictation starts, lowers its volume during capture, and restores the exact prior value afterward. If you use the volume keys while dictating, your newer setting wins. Output-device switches are never applied to the newly selected device, and a private recovery receipt lets the next launch safely repair a Parrot-owned volume change after a crash.
Both volume changes use a short ramp rather than a single hard scalar step. If Bluetooth headphones are also the selected microphone, Parrot uses the Mac's built-in microphone when one is available. This keeps the headphones in their playback profile instead of interrupting music to open and close a bidirectional Bluetooth microphone session at every Fn edge.
When other audio is already playing, that volume change is the recording cue: Parrot suppresses its own start, hands-free, and stop tones for the entire capture. With no existing playback, Play Sounds continues to control those tones normally.
Runs the raw transcript through Apple's on-device language model to fix punctuation, capitalization, and spacing. Entirely local — no network, no API key, no per-token cost. Adds ~1s to a short phrase; cost rises with transcript length, and the timeout scales with it so long dictation still gets cleaned.
Context-aware insertion is a separate deterministic delivery pass, described below. It uses focused-app context only at paste time; that context never enters standalone transcript cleanup.
raw · um so i was thinking uh we should ship it friday
→ 0.91s (asr 0.31s) · Um, so I was thinking, uh, we should ship it Friday.
If cleanup fails, times out, or returns something implausible, the raw transcript is used. It never costs you text.
Cleanup is rejected if it comes back in a different language or at an implausible length. Punctuation, capitalization, and spacing improvements are delivered automatically. A rewrite that inserts, deletes, joins, splits, or reorders words is never applied silently: Parrot keeps the recognized text in the document and retains the polished version as a reviewable proposal. Substantive proposals show Polish available · Reveal edits in the overlay; smaller ones remain under Review Last Cleanup… in the menu. The review highlights removals and additions, and Accept Edits replaces the original only when the same destination still contains it. Otherwise the proposal is copied instead.
Because that fallback is silent by design, every one is reported. The log names the cause:
cleanup fell back — blocked by content guardrail
and the menu's Clean Up Transcripts subtitle reports the latest run. A fallback stays visible until the next successful cleanup clears it, while disabled and unavailable cleanup have their own explicit states.
The overlay also names why cleanup was skipped and confirms that the original was kept, so you notice at the time rather than on inspection. When a dictation is both copied and uncleaned, the clipboard notice wins — that's the one needing action.
Parrot fits a completed transcript into the text immediately around the caret. In a proven sentence continuation, it can turn This works. into this works before existing words; at a new line or sentence boundary it keeps the standalone capitalization and punctuation. It also avoids duplicate spaces and equivalent boundary punctuation. The pass changes presentation only, never the spoken word sequence or internal punctuation.
This is deliberately conservative. Names, acronyms, technical tokens, abbreviations, ambiguous quote or punctuation structures, mid-word carets, and insufficient context keep the original transcript byte-for-byte. Selection replacement is handled separately from a zero-width caret so Parrot evaluates the text that will actually remain on both sides.
The surrounding text stays inside the synchronous delivery decision. Parrot requires a single selection and reads at most 64 UTF-16 code units on either side—never the complete field as a fallback—then revalidates the same focus, selection, and boundary snapshot before delivery. Secure or protected content, multiple carets, read-only controls, unsupported attributes, stale snapshots, and any other uncertainty preserve the original transcript. Surrounding context is never logged, saved to history, or sent to transcription or cleanup models.
Choose Correct Last Dictation… or press Control–Option–Command–C from the app where you dictated. The editor starts with the exact delivered text and also shows raw recognition when it differs. If the original control still contains the exact insertion, Parrot replaces it in place; changed focus, changed text, protected content, or an unsupported control refuses the write and copies the correction instead.
The correction field receives the insertion cursor immediately and does not block shortcut capture. A dictation started while that field owns focus inserts into the draft at its current selection without replacing the original last-dictation session. If the panel closes or loses focus before transcription finishes, Parrot copies the auxiliary transcript instead of guessing at another destination.
Confirming a manual correction saves the raw, delivered, and corrected text with the same capture audio, model id, and already-bounded context in ~/Library/Application Support/parrot/corrections/. These files are local and owner-only (0700 directory, 0600 files). Open Saved Corrections explains what is retained; Clear Saved Corrections… removes all of it without changing literal aliases.
Distinctive names and identifier-shaped changes can be offered as literal aliases after confirmation. Adding one is a separate explicit choice. Grammar, punctuation, ordinary-word, and general rewrite changes are saved as evidence only and never become global replacement rules.
Observe Recent Insert for Corrections is a separate opt-in and is off by default. When enabled, Parrot watches only its just-inserted Accessibility element for 15 seconds. A qualifying observed edit is saved automatically with the raw, delivered, and corrected text, same-capture audio, model id, and already-bounded context; enabling the toggle is consent to that owner-only local retention. Parrot waits for editing to settle, verifies one character at each available boundary, reads only the original inserted range, and rejects large rewrites. Passive learning accepts equal-length substitutions only because an Accessibility value-change event cannot prove whether added text belongs inside the insertion or immediately beside it; insertions and deletions stay in the manual correction workflow. Observation stops immediately on app switch, opt-out, expiry, a new dictation, or opening the manual correction editor. Turning it off during recording or transcription also prevents a new observation from starting for that in-flight dictation. It does not watch arbitrary edits elsewhere in the application.
Parrot delivers text with a single clipboard transaction: it temporarily copies the transcript, posts Command-V, then restores the prior clipboard only if you have not copied something newer in the meantime. Without clipboard fallback, that paste is posted blind, so a non-editable target can still swallow it. With fallback enabled, parrot first asks the Accessibility API what has focus and leaves the transcript on the clipboard when nothing will accept text.
The overlay then expands into a Copied to clipboard notice with a countdown bar that drains over ten seconds. A brief flash was too easy to miss, and missing it is expensive — the transcript is on the clipboard and nowhere else, so an unnoticed copy reads as dictation having silently done nothing.
Press Escape to dismiss it — and that also puts your prior clipboard back if the transcript is still the current clipboard content. Copying a transcript displaces whatever you had copied, which you never asked for, so dismissing is treated as declining the transcript. Non-text items such as images and files are preserved, and a newer copy you make during the notice is never overwritten. Letting the notice time out keeps the transcript, which is the usual case.
Escape is observed without being swallowed, so the keypress still reaches whatever app is in front. During recording or transcription, Escape dismisses the dictation instead of inserting it. If Save History is on, Parrot finishes recognition and retains it as a concealed dismissed entry; if history is off, it leaves no transcript on disk. macOS Secure Input can intentionally hide ordinary keys from every global listener, so Dismiss Dictation in the Parrot menu provides the same action there.
Anything indeterminate resolves to the clipboard. Erring that way costs a ⌘V; erring the other way loses what you said.
Whisper has no reason to know your product names, colleagues, or internal jargon. Put exact corrections in ~/.config/parrot/vocabulary.txt, one per line:
# comments start with #
# preferred spelling, then the exact ways it gets misheard
RunTogether: run together, run to gether
Anything after the colon is corrected deterministically by literal replacement after transcription — no model judgement involved. Entries work whether cleanup is on or off. Bare terms and empty alias lists are ignored because they cannot perform a literal correction.
The native app adds Parrot: parrot once, so the product name receives its canonical capitalization by default. Existing vocabulary lines are preserved, and a rule that already owns parrot wins. The seeded line is ordinary, visible configuration: remove it if you dictate about the bird and want lowercase parrot preserved; Parrot remembers the seed and does not recreate it.
A declared alias is an exact string you wrote down, so it is matched exactly: whole words only, case-insensitive, and it never fires inside a longer word that merely contains it. Asking a model to apply corrections made them a judgement call: it fired on phrases nobody declared and missed ones that were.
The consequence to understand: an alias means exactly what it says, everywhere. If a mishearing you declare is also an ordinary English phrase, that phrase gets rewritten too. Predictable rather than clever, and yours to tune. parrot doctor warns when no active literal alias is configured.
Finding the mishearings is the real work, so two commands do it for you rather than leaving you to notice them:
parrot vocab-suggestscans the whole log for new variants that resemble a canonical spelling already present in an alias entry. It guesses, in bulk.parrot fix "what it should have said"takes the dictation that just went wrong and the answer you already know, diffs them, and writes the mapping. No guessing — you stated it.
One correction teaches one spelling, and Whisper mangles a name more than one way — a compound comes back split on one attempt and run together on the next. parrot fix --sweep scans the log and offers other spellings of the same term, rather than letting each variant surprise you once.
fix only learns name-shaped substitutions. A reworded sentence, an added clause or a grammar tweak is refused with the reason, because storing those would fire on every future sentence containing the words. It also puts the corrected text on your clipboard, so the wrong text already sitting in your document can be pasted over.
Feeding the terms to Whisper itself as a decoder prompt was tried and removed. On WhisperKit 0.18.0 it returns an empty transcript outright, and when forced to work it changed no output at all. Details in architecture.md.
Appends each retained dictation to ~/Library/Application Support/parrot/history.jsonl, recording the raw transcript alongside its final text so you can see what cleanup changed. Transcription History opens a native, searchable newest-first browser from the Parrot menu; transcript text is selectable and each entry has exact-copy and permanent-delete actions. Dismissed dictations are labeled but concealed until you choose Recover; recovery reveals the transcript in the browser without inserting it anywhere.
Off by default, deliberately. The premise of this tool is that your speech stays on your machine, and writing every utterance to disk in plaintext is a real change to that posture. The file is created 0600 inside a 0700 directory and trims to the newest 2000 entries once it passes 2 MB. The browser reads a bounded snapshot, tolerates malformed older lines, and refreshes while open when the file changes. parrot history --clear deletes it.
Daemon logs contain timings and operational status, but not transcript text. The privacy-sensitive --debug-transcripts flag opts into bodies explicitly; --dump-wav similarly opts into retaining the last capture. Logs, vocabulary, history, and debug audio are restricted to the current user when Parrot creates or touches them.
A rising tone when the mic opens, a falling one when it closes, and a distinct confirmation when hands-free recording latches — audible state without watching the screen. The tones and audio engine are created on first playback, so sound-off runs never open the output device and there is still no resource bundle.
| Path | What |
|---|---|
/usr/local/bin/parrot |
the binary |
~/Library/Application Support/parrot/models/ |
model weights |
~/Library/Application Support/parrot/history.jsonl |
dictation log (--history) |
~/Library/Application Support/parrot/corrections/ |
manual and qualifying observed correction text, bounded context, and same-capture audio (0600 inside 0700) |
~/.config/parrot/vocabulary.txt |
literal correction aliases |
~/Library/LaunchAgents/com.nstrmnts.parrot.plist |
the legacy CLI LaunchAgent |
~/Library/Logs/parrot/parrot.err.log |
daemon stderr (0600 in a 0700 directory, transcript-free by default) |
/tmp/parrot-last.wav |
last capture (--dump-wav, 0600) |
- Swift — single SPM executable target
- WhisperKit — Whisper inference via CoreML, ANE-accelerated
- FoundationModels — Apple's on-device LLM, for
--cleanup - AVAudioEngine — mic capture and tone playback
- CGEventTap — global hotkey
- CGEvent — global paste command at cursor
- Accessibility API — focus detection for clipboard fallback
- NSPanel (borderless, click-through) — recording-indicator pill
See docs/architecture.md for design notes and .plan/roadmap.md for what's considered but not built.
swift build -c release
.build/release/parrot --helpmaster tracks digimata/parrot unchanged; fork work lives on personal.
git fetch upstream && git switch master && git merge --ff-only upstream/master
git switch personal && git rebase master