Skip to content

Latest commit

 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🦜 parrot 🦜

Hands-free, 100% local Whisper dictation on macOS — triggered from your keyboard, with an optional on-device LLM cleanup pass and a notch-area parrot cage indicator.

macOS Python Dictation License tests


The parrot cage indicator hanging from the MacBook notch, with a green bird perched inside

The parrot cage hangs from the notch — the bird rests green when ready, turns yellow while warming, and goes red while recording / flying out hands-free.


Hammerspoon intercepts keyboard gestures system-wide and fires shortcuts directly; a small always-on Python daemon keeps a local Whisper model warm in memory and, on release, transcribes into the focused field — no cloud, no API keys. An optional local LLM cleanup pass then polishes the transcript (punctuation, filler removal, bulleting) before it lands. The default parrot cage indicator lives under the MacBook notch: the bird is green when ready, yellow while warming, and red while recording or hands-free.

Everything ships in one Hammerspoon script with the keyboard layer on by default. It's the sibling of supa-coder (same idea for a Bluetooth gamepad); the dictation engine is shared.

Controls

Parrot keyboard controls with only the active keyboard keys in color

Diagram source: assets/keybinds.html

Keyboard (on by default — no extra hardware):

Gesture Action
Fn / Globetap Hands-free: the bird flies out and recording starts. Tap Fn again to stop & paste
Fn / Globehold past 3s, then release Hold mode (push-to-talk): the bird stays out of the cage while you hold; record while held, paste on release
Right Shiftdouble-tap Region screenshot to clipboard (drag a box, then ⌘V)
Right Shifthold ½ second (alone) Toggle status view between the parrot cage and original notch outline
Fn / Globe + + / - Resize the cage larger or smaller

Required once for Fn: System Settings → Keyboard → Function Keys → set "Press Globe key to:" → "Do Nothing", so macOS doesn't pop the emoji / Character Viewer panel when you tap Fn (a Hammerspoon event tap can't suppress it — the popup is fired by the WindowServer below the tap, so this system setting is the only fix). (Prefer another key? Set FN_KEYCODE at the top of the script — e.g. Right Command is 54.)

Screenshots go to the clipboard, so ⌘V drops the grab wherever your cursor is.

Status Indicator

The normal UI uses notch-area views only. INDICATOR = "cage" is the default, and holding Right Shift alone for half a second switches live between the parrot cage and the original notch outline.

"cage" — a line-art parrot cage hanging from the notch:

The bird uses the same colour scheme as the notch outline — green ready, yellow warming, red in use:

  • Booting — the cage swings gently while the daemon is starting; the bird is yellow (warming, not ready yet).
  • Idle / armed — the bird rests green in the cage with a subtle motion and random blinks, so you know the system is ready.
  • Warming — the bird turns yellow while the mic opens (don't talk yet).
  • Recording — the bird turns red once the mic is live.
  • Hands-free — the bird stays red, flaps out, and patrols back and forth near the notch until you stop.
  • Transcribing / copied — the bird flies back, pulses while Whisper works, then gives a bright flash once text is copied.
  • Daemon stopped — the bird falls off the perch (grey) and lies at the bottom of the cage.

"notch" — the original outline around the MacBook notch:

  • Armed — steady green outline whenever the daemon is reachable. No outline ⇒ the daemon is down.
  • Ready — a green stem grows down from the notch the moment the daemon finishes loading.
  • Recordingpulses yellow while the mic warms up, then pulses red once it's live, back to green on stop.

There is no bottom-center popup; recording, transcribing, and copied feedback all stay in the notch area.

Notch geometry is hard-coded for a 14-inch MacBook Pro (NOTCH_W = 185, NOTCH_H = 32). On a different model, set those at the top of the script. Read your exact size: python3 -c "import AppKit;s=AppKit.NSScreen.screens()[0];i=s.safeAreaInsets();l=s.auxiliaryTopLeftArea();r=s.auxiliaryTopRightArea();print('H',i.top,'W',s.frame().size.width-l.size.width-r.size.width)"

Requirements

  • macOS on Apple Silicon (Hammerspoon, mlx-whisper, mlx-lm, and pyobjc are Mac-only; MLX needs an M-series chip)
  • Python 3.11+
  • ffmpeg on your PATHbrew install ffmpeg
  • Hammerspoonbrew install --cask hammerspoon

Setup

Setting up with a coding agent? Copy the prompt from the Agent setup prompt section below into Claude Code / Cursor / Codex and it'll run the whole setup for you (pausing for the few macOS permission toggles it can't click).

git clone https://github.com/aamonnaig4/parrot.git
cd parrot
./setup.sh          # venv + deps, checks ffmpeg, writes config.json, DOWNLOADS the models

setup.sh also pre-downloads the Whisper model (and the LLM cleanup model, if format is on) so your first dictation doesn't stall — it detects anything already cached and skips it.

1. Load it into Hammerspoon

Add this to ~/.hammerspoon/init.lua (point it at wherever you cloned the repo):

dofile(os.getenv("HOME") .. "/path/to/parrot/hammerspoon/parrot.lua")

Then Reload Config from the Hammerspoon menu. The keyboard layer is on by default — don't forget the one-time "Press Globe key to: Do Nothing" setting above.

2. Start the dictation daemon

./start.sh          # foreground; shows the [heard]/[typed] log. Ctrl+C to quit.

The daemon warms the mic, the Whisper model, and (if enabled) the cleanup model at startup, so the first dictation is as fast as the rest.

Auto-start on boot: the script sets Hammerspoon to launch at login (AUTOLAUNCH_AT_LOGIN), and Hammerspoon in turn starts the daemon in Terminal automatically if it isn't already running — Terminal carries the microphone permission a plain background agent would lack. So a fresh boot warms everything with nothing to click. Set AUTOSTART_DAEMON = false / AUTOLAUNCH_AT_LOGIN = false to manage it yourself.

Agent Setup Prompt

Want a coding agent (Claude Code, Cursor, Codex, …) running on your Mac to do the setup for you? Copy the entire prompt below and paste it into the agent. It runs the automatable steps and pauses for the 🧑 HUMAN steps (System Settings toggles it can't click).

Click to expand the copy-paste prompt
# parrot — setup guide for coding agents

## Agent: read this first

You are setting up **parrot**, a 100% local Whisper dictation tool for macOS. It has two
halves that must both be running:

1. **A Python daemon** (`parrot/daemon.py`) that keeps a local Whisper model warm and listens
   on a loopback control socket (`127.0.0.1:8765`). Started with `./start.sh`.
2. **A Hammerspoon Lua script** (`hammerspoon/parrot.lua`) that watches the Fn/Globe key,
   tells the daemon to start/stop recording, and draws the notch "parrot cage" indicator.
   Loaded by adding a `dofile(...)` line to `~/.hammerspoon/init.lua`.

**Hard requirements** (verify before starting; stop and tell the user if unmet):
- macOS on **Apple Silicon** (M-series). MLX will not run on Intel.
- **Python 3.11+**, **Homebrew**, and internet access (first run downloads the Whisper model).

**What you CANNOT do** (these are GUI toggles — pause and hand them to the user):
- Grant Accessibility / Input Monitoring / Microphone permissions.
- Change the "Press 🌐 key to" keyboard setting.
Do NOT claim setup is complete until the user confirms the 🧑 HUMAN steps are done and a
test dictation actually types text.

Work through the steps in order. After each command, check its output before moving on.

---

## Step 1 — Prerequisites

```bash
# Homebrew packages (safe to re-run; skips what's already installed)
brew install ffmpeg
brew install --cask hammerspoon

# Verify
sw_vers                       # macOS version
uname -m                      # must print: arm64
python3 --version             # must be 3.11+
command -v ffmpeg             # must print a path
ls -d /Applications/Hammerspoon.app   # Hammerspoon installed
```

If `uname -m` is not `arm64`, stop — this tool cannot run on this machine.

## Step 2 — Clone and install

```bash
# Clone wherever the user keeps projects (adjust the path if they already cloned it)
cd ~/Developer 2>/dev/null || cd ~
git clone https://github.com/aamonnaig4/parrot.git 2>/dev/null || true
cd parrot

# One-time setup: creates .venv, installs deps, checks ffmpeg, writes parrot/config.json,
# and DOWNLOADS the Whisper model (this can take a few minutes on first run).
./setup.sh
```

Record the absolute repo path — you need it in Step 3:

```bash
REPO="$(pwd)"; echo "$REPO"
```

## Step 3 — Wire it into Hammerspoon

Add a single `dofile` line to `~/.hammerspoon/init.lua`, pointing at this repo. Do it
idempotently (don't add a duplicate if it's already there):

```bash
mkdir -p ~/.hammerspoon
LINE="dofile(\"$REPO/hammerspoon/parrot.lua\")"
grep -qF "hammerspoon/parrot.lua" ~/.hammerspoon/init.lua 2>/dev/null \
  || echo "$LINE" >> ~/.hammerspoon/init.lua
tail -n 3 ~/.hammerspoon/init.lua
```

Then tell the user to open Hammerspoon and **Reload Config** from its menu-bar icon (the
first launch will also prompt for Accessibility — see Step 4).

## Step 4 — 🧑 HUMAN: grant permissions

Ask the user to grant these in **System Settings → Privacy & Security**. The agent cannot
toggle these; pause here until the user confirms.

- **Accessibility** — enable **Hammerspoon** AND the terminal app they'll run `./start.sh`
  from (Terminal / iTerm / their IDE).
- **Input Monitoring** — enable the same two apps.
- **Microphone** — enable the terminal app (macOS will also prompt on the first recording).

Without these you'll see `This process is not trusted!` or no text will appear.
(Screen Recording is **not** required for normal use.)

## Step 5 — 🧑 HUMAN: stop the Fn key from opening the emoji picker

parrot uses a lone Fn/Globe tap to control dictation. By default macOS pops the emoji /
Character Viewer on that tap, and a Hammerspoon event tap **cannot** suppress it (the popup
is fired by the WindowServer below the tap). The only fix is the system setting:

**System Settings → Keyboard → Function Keys → "Press 🌐 key to:" → "Do Nothing".**

The agent may set the underlying preference, but it only takes effect after a **logout/login**:

```bash
defaults write com.apple.HIToolbox AppleFnUsageType -int 0   # 0 = Do Nothing
```

Tell the user to either flip the toggle in System Settings (takes effect immediately) or log
out and back in after the command above.

## Step 6 — Start the daemon and reload Hammerspoon

```bash
# Start the daemon in the foreground (shows the [heard]/[typed] log). Leave it running.
# NOTE: run this in a terminal that has Microphone + Accessibility permission (Step 4).
cd "$REPO" && ./start.sh
```

On startup it prints a `mic check OK — '<name>' peak 0.NN` line (good) or a
`⚠️ mic … is SILENT` warning (no mic permission / wrong device — revisit Step 4).

If Hammerspoon's `hs` CLI is available you can reload from the agent; otherwise ask the user
to **Reload Config** from the Hammerspoon menu:

```bash
command -v hs >/dev/null && hs -c "hs.reload()" 2>/dev/null || echo "Reload Hammerspoon manually"
```

> parrot also auto-starts on boot: the script sets Hammerspoon to launch at login, and
> Hammerspoon launches the daemon in Terminal automatically if it isn't already up. So after
> this first setup, a reboot warms everything with nothing to click.

## Step 7 — Verify (do not declare done before this passes)

Ask the user to:
1. Click into any text field (Notes, a browser box, etc.).
2. **Tap Fn/Globe, say a sentence, tap Fn again.** Text should appear (pastes on stop).
3. Look at the **MacBook notch**: a small parrot cage hangs from it — the bird uses the same
   green/yellow/red scheme as the notch outline, so it sits **green** when ready. A single
   **tap of Fn** starts **hands-free** recording (the bird flies out **red**); tap Fn again to
   stop and paste. Holding Fn past 3s instead switches to **hold mode** — the bird stays **red**
   and **out of its cage** the whole time you hold (so you can see the hold is working); it
   becomes push-to-talk (record while held, release to paste).
4. **Hold Right Shift alone for half a second** — the indicator should toggle between the
   parrot cage and the original notch outline.

If nothing types: confirm the daemon terminal shows `[heard]`/`[typed]` activity, re-check
Step 4 permissions, and confirm Step 5 is applied.

## Step 8 — Optional configuration

Edit `parrot/config.json` (created by `setup.sh`, gitignored):

| Field | Default | What it does |
|-------|---------|--------------|
| `model` | `small` | `tiny·base·small·medium·large` (bigger = more accurate, slower/heavier) |
| `mic` | `""` | Substring to force a mic (e.g. `"MacBook Pro Microphone"`); empty = system default |
| `stream` | `false` | `false` = batch (paste on stop); `true` = type words live as you talk |
| `format` | `false` | `true` = local LLM cleanup pass (punctuation/filler/bulleting) after transcription |
| `format_model` | `""` | Optional cleanup model repo id; empty = a small default (Qwen2.5-3B-Instruct-4bit) |

After changing `model` or `format`, restart the daemon (`Ctrl+C`, then `./start.sh`) so it
reloads. Indicator/keybinding defaults (`INDICATOR`, `WAVE_ON_NOTCH`, key codes, timings)
live at the top of `hammerspoon/parrot.lua`; **Reload Config** after editing.

---

## Troubleshooting quick reference

- **Emoji picker still pops on the Fn tap** — Step 5 not applied, or applied but not yet
  logged out/in.
- **`This process is not trusted!` / no text appears** — Accessibility + Input Monitoring +
  Microphone not granted to both Hammerspoon and the daemon's terminal (Step 4).
- **"dictation daemon not running" alert** — the daemon isn't up; run `./start.sh`.
- **`⚠️ mic … is SILENT`** — no mic permission or wrong input device; fix Step 4 or set `mic`.
- **Verbose daemon log**`PARROT_DEBUG=1 ./start.sh`.
- **Cage/notch indicator wrong size or model** — notch geometry is hard-coded for a 14" MacBook
  Pro (`NOTCH_W`, `NOTCH_H` at the top of `hammerspoon/parrot.lua`); adjust for other models.

Full human-facing docs are in the repo's `README.md`.

Permissions

In System Settings → Privacy & Security, grant both apps under Accessibility and Input Monitoring:

  • Hammerspoon — to watch keyboard gestures and post keystrokes.
  • The terminal app you run ./start.sh from (Terminal, iTerm, your IDE) — the daemon pastes transcribed text via ⌘V.

The terminal also needs Microphone access (you'll be prompted on first record). Without these you'll see This process is not trusted! or no text appears.

Configuration

setup.sh copies parrot/config.example.json to parrot/config.json (gitignored, so it's yours to edit):

Field What it does
model tiny · base · small · medium · large (larger = more accurate, slower, bigger download) — or a full mlx-community/…-mlx repo id.
mic Substring to force a specific mic, e.g. "MacBook Pro Microphone" or "DJI". Empty = follow the macOS system default input (Settings → Sound → Input).
stream false (default) = batch: transcribe the whole utterance on stop and paste it all at once. true = type words live as you talk.
format true = run the local LLM cleanup pass after transcription (punctuation, filler removal, bulleting). Batch mode only.
format_model Optional mlx-community/…-Instruct-*bit repo id for the cleanup pass. Empty = a small default (Qwen2.5-3B-Instruct-4bit, ~1.6 GB, ~1 s/utterance).

Enabled/timed behavior — ENABLE_KEYBOARD, key codes, timings, and indicator defaults — lives at the top of hammerspoon/parrot.lua.

  • INDICATOR = "cage" (default) — the parrot cage hangs from the notch. Set it to "notch" for the original notch border. Hold Right Shift alone for half a second to toggle those two live, and Fn + + / - makes the cage larger or smaller.
  • WAVE_ON_NOTCH = false (default) — when true, live recording shows audio-driven ripple rings on the notch outline (only applies with INDICATOR = "notch").
  • To smoke-test the cage animation after reload, create /tmp/parrot-smoke-test before starting Hammerspoon. The next load runs a one-shot green → yellow → red → grey animation sequence and then returns to idle.

Whisper Model

Dictation runs mlx-whisper — OpenAI's Whisper ported to Apple's MLX, so it runs on the Apple-Silicon GPU, fully offline (no cloud, no API keys). Models are pulled from mlx-community on first use and cached under ~/.cache/huggingface. Set the size via model in config.json (or the WHISPER_MODEL env var, which wins); it's locked to English.

model Hugging Face repo ~Download Notes
tiny whisper-tiny-mlx ~75 MB Fastest, least accurate
base whisper-base-mlx ~145 MB Fast, okay accuracy
small whisper-small-mlx ~480 MB Recommended — best accuracy/speed balance for dictation on an M-series Mac
medium whisper-medium-mlx ~1.5 GB More accurate, slower/heavier
large whisper-large-v3-mlx ~3 GB Most accurate, slowest

Recommended: small (the default) — on an M-series MacBook it transcribes conversational speech accurately and keeps up with live dictation. Step up to medium if you want a bit more accuracy and have the headroom; drop to base/tiny on older hardware or if you want it snappier. Change it in config.json, then restart the daemon (Ctrl+C, ./start.sh) so it reloads the model.

How Dictation Works

Batch (default): you talk, and on release the whole utterance is transcribed once and pasted in one go — calmer than live typing, with no mid-sentence revisions. Auto-gain and hallucination guards keep it clean; spoken punctuation works ("comma", "period", "new line", …).

Cleanup pass ("format": true): the raw transcript is then run through a small resident MLX instruct model that fixes punctuation/capitalization, drops filler words and false starts, resolves self-corrections, and bullets clear lists — Wispr-style — without changing your wording. It's strictly guided by a few-shot prompt and falls back to the raw text on any error, so it can never lose or block a dictation. Runs ~1 s on the default 3B model.

Live streaming ("stream": true): the older mode — types words it's confident about a beat behind your voice, append-only (LocalAgreement-2). Formatting is skipped in this mode (it needs the whole utterance).

Details in parrot/whisper_dictate.py and parrot/reformat.py.

Tests

.venv/bin/pip install pytest
PYTHONPATH=parrot .venv/bin/python -m pytest -q

Covers the word-commit engine, voice commands, flush behavior, and the daemon's command dispatch.

Troubleshooting

  • Fn does nothing — set System Settings → Keyboard → "Press Globe key to: Do Nothing", then Reload Config. If it still feels laggy, switch FN_KEYCODE to Right Command (54).
  • This process is not trusted! — grant Accessibility + Input Monitoring to the terminal running the daemon (and to Hammerspoon).
  • "dictation daemon not running" alert — start it with ./start.sh.
  • No text appears — check the terminal's Microphone permission. On startup the daemon prints mic check OK — '<name>' peak 0.NN or a mic … is SILENT warning; the latter means no mic permission or the wrong device. Watch the [heard]/[typed] log to see whether audio is reaching the model.
  • Verbose daemon logPARROT_DEBUG=1 ./start.sh logs every control command and reply.

How It Fits Together

Hammerspoon (Lua)                         Python daemon
  hs.eventtap on Fn / Right-Shift           mlx-whisper model kept warm
                                            mlx-lm cleanup model kept warm (optional)
  ├─ posts ⌘V / screenshots / Enter         dictation engine + LLM cleanup pass
  ├─ draws the status indicator       ─────► socket server on 127.0.0.1:8765
  │  (parrot cage or notch outline)
  └─ sends start/stop for dictation   TCP    start | stop | toggle | ping | quit
        ▲ reads /tmp/parrot-dictation.status ── daemon writes transcribing / done:<chars>

Security & Privacy

Dictation is 100% local — audio never leaves your Mac and there are no API keys. The daemon's control socket binds loopback only (127.0.0.1) and is unauthenticated: any process running as your user on this machine could send it start/stop and thus toggle mic recording. That's an accepted trade-off for a personal tool; don't run the daemon on a shared/multi-user machine, and note that parrot.lua runs with Accessibility + Input Monitoring, so only load config you've read.

License

MIT

About

Hands-free, 100% local Whisper dictation on macOS — keyboard-triggered, with an optional on-device LLM cleanup pass and a parrot-cage status indicator in the MacBook notch.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages