ParrotFlow does not send your transcript to a large language model to clean it up. The speech model, Parakeet, writes the words. Then a few very small models each check one thing: a full stop, a capital, a name, a filler. Each can make one small edit or leave the text alone. None of them writes a sentence.
The cost of a rewrite
An LLM rewrite reads your sentence and writes a new one. You wanted punctuation, and you get different words.
I measured this in ParrotFlow’s spoken corrections. You can say a name and spell it out loud, and a local model works out which word you meant. When the model did the whole job, it found the right word but mangled the letters it was copying. “S I O B H A N” came back as “Sibhan”. For names described in words, such as “Jerome with a G at the beginning”, gemma4:e4b wrote 5 of 10 right and gemma4:12b wrote 8 of 10. When the model only names the word and code applies the change, the score is 10 of 10.
Latency is the second cost. In the repo’s measurements, a prompt step takes about 1.5 s with the model loaded and 6.7 s cold. A replace: rule takes 0.035 s on a line. gemma4:e4b also holds 9.6 GB of memory while loaded. ParrotFlow without it uses about 1 GB.
The third cost is where your text goes. A model too big for your Mac runs remotely, and a remote model receives the text of every step that names it.
There used to be an LLM in the default path. The vocabulary stage sent each uncertain name to a local model for a KEEP or REVERT answer. It worked, with 63 of 74 right on one speaker’s dictations. It also cost about 900 ms per dictation and needed Ollama running. I removed it. The checks below do the same job.
What runs, in order
ParrotFlow does not stream. It records the whole clip, then runs:
asr → vad → sentence_repair → vocabulary → pipeline
Context spelling runs after the last pipeline step. Everything runs on your Mac. See Dictation that stays on your Mac.
| Stage | Model | Size | What it may change |
|---|---|---|---|
asr |
Parakeet TDT 0.6B v3 | 461 MB | Writes the transcript |
sentence_repair |
Qwen3 0.6B Base, 4-bit | 320 MB | Removes a full stop or a capital that a pause put in |
vocabulary |
CharsiuG2P, mmBERT-small, Qwen3 Embedding 0.6B | 81, 282 and 335 MB | Writes a name you taught it, or refuses to |
disfluency |
spaCy en_core_web_sm |
about 170 MB with its Python | Removes “like” or “you know” where they carry nothing |
context_spelling |
the same Qwen3 0.6B Base | Spells a word the way the screen writes it |
After asr, code builds every candidate edit. The models score candidates, compare sentences, parse, or turn words into sounds for matching.
On an Apple M2 Pro on 8 October 2026, the whole local pipeline took 0.378 s median and 1.993 s p95 per clip, decoder included. That was warm, with screen spelling off.
Sentence repair: three readings, one score
Pause for breath and Parakeet writes a full stop, then a capital:
said "you should see a parrot at the top right of your screen"
written "You should see a parrot. At the top right of your screen."
At each boundary, code builds three readings: the full stop as written, a comma, and the two halves joined. Qwen3 0.6B Base scores each continuation. The score is the log-probability divided by the token count, and the highest wins. There is no threshold to tune. The three readings run in one padded batch, which takes about 50 ms.
The division matters. With summed log-probability, the joined reading wins because it is shortest. That repaired 97% of the cuts and destroyed 33 real sentence endings.
On one speaker’s dictation, it repaired 81% of 140 false full stops and joined none of 172 real ones. For question marks it repaired 82% of 325 and made one wrong join in 111 real questions.
Parakeet also writes a capital with no mark: “imports name definitions And all the things”. Rules refuse most candidates before the model sees them. A run of capitals, a capital inside a word like TypeScript, a noun or adjective, or a name that Apple’s NLTagger recognises all stay as they are. On 110 hand-labelled candidates, this lowercased 64% of the wrong capitals and touched no correct one. This part is English only.
Names: checks that mostly say no
You teach names in vocabulary.yaml. A sound model, CharsiuG2P, turns words into phonemes, so “Priss y” can match Praisy. But matching by sound alone overwrites ordinary words. Before the checks below, “blocking merge” became “blocking Vercel”.
Each match now goes through these checks in order:
- Two word lists. If neither the macOS spell checker nor a 23,694-word list from a tokenizer vocabulary knows the heard word, the term is written. No model runs.
- The slot. mmBERT-small masks the word, takes the ten likeliest fillers and tags them. A name does not go where the sentence wants a verb, an adverb or a preposition. This check only refuses. It never writes a term.
- The term’s own sentences. Qwen3 Embedding 0.6B averages the sentences where you kept the term, and those where you took it out. A new sentence gets the term when it is closer to the first group. Until you have taken a term out once, a simpler score against the kept sentences decides.
- Anything still open becomes a question on the pill. Ignore it and you get what the checks settled on.
So “upgrade view” becomes “upgrade Vue”, and “re-render the whole view” stays as it is. On the 50 English test cases, the word lists wrote 12, the slot refused 15 and 23 stayed open, with no error either way. The sentence comparison got 20 of 20 held-out sentences right, over four terms. Both sets are small. More in Teach ParrotFlow your words.
Disfluency: string work, plus one parse
Repeats and restarts are string work. “the the prompt” becomes “the prompt”, and “in the ter in the terminal” becomes “in the terminal”. Sounds like “um” are a regex.
“Like” and “you know” are harder. Half the time, “you know” is a real verb with a real object. So the disfluency transform asks spaCy for a dependency parse:
| Input | Output |
|---|---|
| So use like you know five maximum eight lines. | So use five maximum eight lines. |
| Okay, we’ll let you know. | unchanged |
| Do you know who owns this account? | unchanged |
spaCy loads only when a marker phrase is in the text. The import and model load take 0.53 s and 0.21 s. The rest of the script takes 0.04 s.
Spelling from the screen
Claude Code just wrote CaretAnchor, and Parakeet hears “carrot anchor”. Context spelling takes terms off the screen when you press the key. Since release 0.18.0 it reads the window in any app. It matches runs of one to four dictated words against them, by letters or by sound. Then the same Qwen3 0.6B Base scores the sentence as dictated against each rewrite. A rewrite is kept only when it wins by a set margin. At most 8 rewrites are scored, and scoring stops at 5 s. See Spelling from what is on your screen.
What stays deterministic
Most of the pipeline has no model at all. Numbers, dates and money are scripts. The English numbers script parses a grammar over about seventy number words. When it cannot read two numbers as one, such as “sixty forty split”, it leaves them as words. A dates script runs first and handles clock times, so “meet at three thirty” becomes “meet at 3:30”. Your own rules are replace tables or scripts:
transforms:
- name: priorities
replace:
'P0': ['P zero']
'P1': ['P one']
'P2': ['P two']
See Rules, scripts and prompts for your dictation.
Where an LLM still fits
Some jobs do need a rewrite, such as fixing grammar or laying out an email. That is a prompt step. ParrotFlow runs none until you add one, and you choose the model:
models:
gemma: # on your Mac, through Ollama
api: ollama
model: gemma4:e4b-mlx
default: true # what a transform runs on when it names no model
transforms:
- name: grammar
description: fix grammar and punctuation
model: gemma # stays on your Mac
offer: true # put a chip on the pill after every dictation
key: g # press G to run it
prompt: Fix grammar and punctuation...
With offer: true, the step runs when you press G, not on every dictation. If the model call fails, the step leaves the text as it was. See Format dictation with a local model and the configuration reference.
Limits
On a 141-clip English benchmark (8 October 2026, M2 Pro), the full pipeline had 6.0% word error against 5.8% for raw Parakeet. Word error ignores punctuation and case, so it cannot see most of what sentence repair does. It can also count filler removal and number formatting against the pipeline.
Names are the weak spot. On that set, the pipeline spelled 11 of 62 vocabulary mentions right. Raw Parakeet spelled 9. Screen context was off for that replay, so context spelling did not run. Qwen3-ASR 1.7B, given a short vocabulary list, spelled 39 of 62. But it was worse on ordinary speech, 5.4% word error against 3.9%. With a long background text in its prompt, it returned empty transcripts for 57 of the 141 clips.
The other repairs still make mistakes. One real question in 111 lost its question mark. On a set of 45 labelled bare capitals, release 0.14.0 fixed all 40 wrong ones and lowercased 2 of the 5 that were right.
Most of these numbers come from one speaker’s dictation. The bare-capital repair, the slot check and the spaCy rule are English only.
Most of the measurements, and the approaches that were tried and rejected, are in docs/transcription.md.