# localflow
Talk, and get clean text where you type. A native dictation app for macOS,
Windows and Linux, written in Rust: its own speech engine (whisper.cpp,
in-process), its own small language model, `localflow-clean`, that turns what
you said into what you meant, and a pill at the bottom of the screen while you
talk. Nothing leaves your machine. English only.
| You say | You get |
| --- | --- |
| "let's meet in 3 hours wait in 2 hours" | Let's meet in 2 hours. |
| "um I think we should uh ship it on Monday" | I think we should ship it on Monday. |
| "my tasks today are first buy milk second call the bank third fix the login bug" | My tasks today are:
1. Buy milk
2. Call the bank
3. Fix the login bug |
| "send the deck to Priya *(mutters: yeah one sec I'm on a call)* and ask her to check the pricing slide" | Send the deck to Priya and ask her to check the pricing slide. |
| "generate some code using Claw and push it to GitHub" | …using Claude… (from your `words.txt`) |
It handles self-corrections ("wait", "actually", "sorry I mean", "scratch that",
"I just remembered"), filler words, punctuation, numbered and nested lists in
long rambling speech, paragraphs and `## ` section headings for long
multi-topic dictation, spoken commands ("new paragraph", "new line",
"heading …", "bullet point …"), misheard names, and
[asides](#asides-talking-to-yourself): what you say to yourself or to someone
else while dictating is left out. It never answers what you say: a dictated
question stays a question.
For coding, files you name become `@path` references
([Referring to files](#referring-to-files)), and in a terminal running Claude
Code, Codex and the like, "…and send it" sends the prompt while "stop",
"undo that" and "clear the chat" press the keys for you
([Talking to a coding agent](#talking-to-a-coding-agent)).
## Install
### Windows
Download `localflow--x64.msi` from
[Releases](https://github.com/radiumcoders/localflow/releases) and open it. It
installs localflow to Program Files, adds it to the Start menu and starts it
when you sign in. Open localflow from the Start menu: the first time, the
onboarding opens. Pick a speech model (it downloads while you go on), and
localflow downloads its own cleanup model (351 MB) too and runs it itself:
there's nothing else to install. Then try a dictation in the window. Open it
from the Start menu again to change the settings. Uninstall it from Settings > Apps;
that leaves your settings and vocabulary in `%APPDATA%\localflow` and the
models in `%LOCALAPPDATA%\localflow`, which you can delete.
The installer isn't signed yet, so SmartScreen warns about an unknown
publisher: click **More info**, then **Run anyway**.
For IT deployment: `msiexec /i localflow--x64.msi /qn` installs it
for everyone on the computer; then, as each user,
`"C:\Program Files\localflow\localflow.exe" setup` downloads their models
(see [`localflow setup`](#settings)). An upgrade closes the running localflow
and starts the new one.
### macOS
Download `localflow--arm64.dmg` (Apple Silicon) or `-x64.dmg` (Intel)
from [Releases](https://github.com/radiumcoders/localflow/releases), open it
and drag localflow into Applications. Open it from Applications: the
onboarding opens and adds localflow as a login item. It asks you to allow
localflow under Accessibility (the hotkey and pasting) and Microphone. Keep
it in Applications: the login item runs it from there.
To remove it: `localflow quit` (or
`launchctl bootout gui/$(id -u)/computer.localflow.dictate`), then delete the
app, `~/Library/LaunchAgents/computer.localflow.dictate.plist`, and if you
like `~/Library/Application Support/localflow` (the models),
`~/.config/localflow` (settings, vocabulary, history) and
`~/Library/Logs/localflow`. The app isn't signed with a Developer ID yet, so
to macOS each update is a new app: if the hotkey stops working after one,
switch localflow off and on again under Privacy & Security > Accessibility.
It isn't signed with an Apple Developer ID yet, so the first time macOS says
it can't check it: open **System Settings > Privacy & Security** and click
**Open Anyway** next to localflow.
### Linux
Download `localflow--x86_64.AppImage` from
[Releases](https://github.com/radiumcoders/localflow/releases), make it
executable and run it:
```sh
chmod +x localflow-*-x86_64.AppImage
./localflow-*-x86_64.AppImage
```
The onboarding opens. Once you've picked a speech model, it copies
localflow to `~/.local/share/localflow/app` (your key bindings run it from
there, so the AppImage can go anywhere, or away) and adds it to your app
launcher; then it shows the key bindings to add, with a Copy button, and lets
you try a dictation. `localflow onboarding` opens it again; running a newer
AppImage updates the copy. It needs glibc 2.39+ (Ubuntu 24.04, Fedora 40,
Debian 13, Arch), the Vulkan loader (`libvulkan1` / `vulkan-icd-loader`, on
every desktop with a GPU driver), PipeWire (with `pipewire-alsa`) or ALSA for
recording, and for pasting `wl-clipboard` + `wtype` on Wayland or `xclip` +
`xdotool` on X11 (Omarchy ships all of these). Transcription and cleanup run
on the GPU through Vulkan when there is one (Mesa's `vulkan-radeon` /
`vulkan-intel`, or NVIDIA's driver), else on the CPU. Without FUSE, run it
with `APPIMAGE_EXTRACT_AND_RUN=1`.
### From source
```sh
git submodule update --init --depth 1 # llama.cpp, for the cleanup model
cargo build --release # needs Rust and cmake (whisper.cpp, llama.cpp); on Linux
# the ALSA, Wayland, X11, xkbcommon and fontconfig headers
target/release/localflow # the settings window (Linux) or the background app
cargo xtask package --version 0.2.0 [--features vulkan] # the installer, in build/
```
## Dictating
| Platform | Hotkey |
| --- | --- |
| macOS | hold `Option+Shift+Space` and talk, or tap it, talk, tap again |
| Windows | hold `Ctrl+Shift+Space` and talk, or tap it, talk, tap again |
| Linux | keys you bind to `localflow start` / `localflow stop` (below) |
`Esc` cancels on macOS and Windows; `localflow cancel` on Linux.
A pill at the bottom of the screen shows a red dot and your microphone's level
while you talk, then "Transcribing…" and "Cleaning up…". The text is pasted
into the window you were typing in; the pill never takes focus (on Wayland
it's a layer-shell surface, so no window rules are needed).
### Linux (Hyprland / Omarchy)
Wayland apps can't grab global keys, so the compositor owns them, and nothing
runs between dictations. In `~/.config/hypr/bindings.lua` (the settings window
shows these with your path, and a Copy button):
```lua
hl.unbind("SUPER + CTRL + X")
hl.unbind("F9")
o.bind("SUPER + CTRL + X", "Toggle dictation", "~/.local/share/localflow/app/localflow toggle")
o.bind("F9", "Start dictation (push-to-talk)", "~/.local/share/localflow/app/localflow start")
o.bind("F9", "Stop dictation (push-to-talk)", "~/.local/share/localflow/app/localflow stop", { release = true })
o.bind("F10", "Start raw dictation (push-to-talk)", "~/.local/share/localflow/app/localflow start raw")
o.bind("F10", "Stop raw dictation (push-to-talk)", "~/.local/share/localflow/app/localflow stop", { release = true })
o.bind("F8", "Learn from a dictation correction", "~/.local/share/localflow/app/localflow learn")
```
Raw dictation pastes the transcript as heard, without cleanup. On other
desktops, add custom shortcuts running `localflow toggle` (or `start` on
press and `stop` on release). It pastes with Ctrl+V in apps, Shift+Insert in
terminals and VS Code-family editors (their chat panels ignore a synthetic
Ctrl+V), restoring your clipboard after. Where the paste keys can't be sent
(GNOME's Wayland session doesn't allow `wtype`), the text stays on the
clipboard and the pill says so: press Ctrl+V.
## How it works
Each dictation is a short-lived process: it starts recording at once, shows
the pill, and exits once it has pasted. On macOS and Windows a background app
of ~6 MB waits for the hotkey and starts it; on Linux your key bindings do.
1. **Speech to text**: whisper.cpp runs inside localflow. The speech model
loads only while it transcribes (40–200 ms to load) and is dropped right
after. Silero voice detection skips silence first, and whisper encodes a
window as long as your dictation instead of 30 s, which makes a short
dictation several times faster. Your vocabulary (`words.txt`) primes the
spelling of names (on Small and larger models). Each segment's loudness is
measured, for [asides](#asides-talking-to-yourself).
2. **Cleanup**: localflow's own model, `localflow-clean`, cleans and formats
the transcript, with the spellings in your `words.txt`. It learned the
rules from examples, so it gets no long prompt: it loads while you speak
and answers in ~0.06 s on a GPU.
If the cleanup model can't run or answers with something much longer than you
said, you get the transcript as heard. You never lose a dictation.
## Asides: talking to yourself
While dictating you think aloud ("hmm, where was I", "let me think"), answer
someone in the room ("one sec, I'm on a call"), or mutter. None of that is
meant for the text, and localflow leaves it out:
- **Muttered**: the speech engine measures each segment's loudness; anything
9 dB or more below your main voice (muttering, someone in the background) is
marked quiet and left out unless it clearly continues your sentence.
- **Said to yourself or someone else**: the cleanup recognises thinking aloud,
remarks to people around you and comments about the dictation ("is this
recording") and leaves them out. When unsure, it keeps your words.
- If a dictation was nothing but an aside, nothing is pasted ("Left out an
aside").
## Requirements & resource use
Measured on Linux (Ryzen 5 5600, 6 cores; Radeon RX 9070 XT) with the default
speech model, Small (English):
| State | Memory |
| --- | --- |
| Between dictations | **nothing** on Linux; **~6 MB** on macOS / Windows (the hotkey listener) |
| While you talk | ~40 MB of its own (the pill and the recorder; the GPU libraries it shares with other apps not counted) |
| Transcribing (~0.1–0.8 s) | ~100 MB RAM + 450 MB GPU memory with Vulkan / Metal, ~330 MB RAM on the CPU |
| 9 s of speech | RAM | GPU memory | Time |
| --- | --- | --- | --- |
| tiny.en, CPU | 104 MB | – | 0.18 s |
| base.en, CPU | 164 MB | – | 0.27 s |
| **small.en, CPU** | 330 MB | – | 0.78 s |
| **small.en, Vulkan** | ~100 MB | 448 MB | 0.11–0.21 s |
| large-v3-turbo, Vulkan | 118 MB | 846 MB | 0.26–0.51 s |
| (before: Handy's Parakeet, Vulkan) | 183 MB | 752 MB | 0.16–0.45 s |
Small heard the same words as Handy's Parakeet on real recordings, at about
half the memory. On a machine without a GPU, Base (82 MB) is faster still.
Accuracy, on 100 LibriSpeech test clips each (word error rate, lower is
better; time is the median per clip):
| | test-clean | test-other (harder) |
| --- | --- | --- |
| **large-v3-turbo**, RX 9070 | 1.6%, 0.18 s | 4.8%, 0.16 s |
| small.en, RX 9070 | 2.2%, 0.13 s | 8.4%, 0.10 s |
| small.en, CPU (Ryzen 5 5600) | 2.1%, 0.71 s | 8.4%, 0.52 s |
| tiny.en, CPU | 4.8%, 0.11 s | 16.1%, 0.10 s |
| large-v3-turbo, Apple M4 | 1.6%, 1.38 s | 4.8%, 2.13 s |
| **small.en**, Apple M4 | 2.0%, 0.16 s | 8.7%, 0.17 s |
So with a graphics card, setup picks Large: half the mistakes of Small on
hard speech for a few hundredths of a second. On Apple Silicon and the CPU it
picks Small: nearly as good on clear speech, and many times faster there.
whisper encodes 30 s at a time; a shorter dictation can encode just its own
length, several times faster on a CPU or Apple Silicon. That shortcut can send
whisper round in a loop ("She passed. Cotton. She passed. Cotton."), and it
costs Large accuracy on hard speech (8.8% instead of 4.8%). So localflow
uses the full window on a graphics card, and for Large on any GPU; elsewhere it
takes the shortcut, and a transcript that loops goes again with the full
window. `cargo run -p localflow-speech --release --example bench` measures it
all on your machine.
Cleanup on this computer runs localflow's own model, `localflow-clean`: Qwen3.5
0.8B fine-tuned on the cleanup rules alone, with an English-only vocabulary, in
a 351 MB file. localflow runs it in its own process with llama.cpp (Vulkan,
Metal, or the CPU), loads it while you speak (~0.1 s) and frees it after the
dictation, so it uses no memory between dictations. While dictating, speech and
cleanup together peak at ~460 MB.
| Cleanup | Under 30 words | 100+ words | cases.txt | Memory |
| --- | --- | --- | --- | --- |
| localflow-clean, GPU (RX 9070 XT) | ~0.06 s | ~0.34 s | 32/41 | ~530 MB GPU, freed after |
| localflow-clean, CPU only | ~0.25 s | ~1.8 s | 32/41 | ~510 MB, freed after |
| (before: qwen3.5:4b in Ollama, GPU) | ~0.33 s | ~1.8 s | 35/41 | ~4.3 GB GPU, kept loaded |
| (before: Claude Haiku with your API key) | ~1.0–1.3 s | ~2 s | 41/41 | – |
On 142 held-out examples written to the rules, localflow-clean matched the
expected text exactly in 51% (qwen3.5:4b: 27%).
## Referring to files
In a coding app, say the file: "can you refer to the install file and check
why it fails on Mac" pastes "Can you refer to @install.py and check why it
fails on Mac?", with the reference put in the way the app takes it:
| You're dictating into | A file you name becomes |
| --- | --- |
| Cursor, VS Code, Windsurf | `@path`, relative to the project |
| A terminal (Claude Code, Codex, opencode…) | `@path`, relative to where the terminal is |
| Anything else (a browser, Slack, mail) | the file itself, pasted as an attachment |
While you talk, localflow works out the project: the folder open in the
editor window (from its title and the editor's list of open folders), or the
terminal's working directory (from its title, else the coding agent or newest
program running in it). Elsewhere, it's the project you last had open in your
editor. When what you said mentions a file ("file", "folder", "script", "dot
py", "read me", "refer", "attach"…), the project's best-matching paths go to
the cleanup with your words, and it writes `@path` for the ones you meant.
Only those paths are sent, never file contents; raw dictation skips all this.
On Windows, a terminal's own folder can't be read, so terminals there use the
editor's project.
## Talking to a coding agent
When the terminal you dictate into runs a coding agent (Claude Code, Codex,
opencode, Gemini CLI, aider, Amp, Goose…), localflow notices it (from the
programs running in that window) and a few things change:
| You say | It does |
| --- | --- |
| "…and send it" at the end ("send it", "submit it", "and send") | pastes the prompt without those words, then presses Enter |
| just "stop" ("cancel", "hold on", "wait") | Esc: interrupts the agent |
| just "undo that" ("go back", "rewind") | Esc Esc: Claude Code's rewind |
| just "clear the chat" ("start over", "new chat") | `/clear` (`/new` in Codex and opencode) |
| just "compact" ("compact the context") | `/compact` (`/compress` in Gemini CLI) |
| just "send it" | Enter: sends what's in the prompt already |
A command only counts when it's the whole dictation: "stop using the old API"
is a prompt. Without "send it", the prompt is pasted and left for you to read
over (and fix: the learn key learns from that). "Send prompts to coding agents"
in the settings (`LOCALFLOW_SUBMIT`): `said` (default), `always`, `never`.
Anywhere else, what you say is pasted as always and nothing is pressed.
## History
Every dictation is kept on your computer: when, where it went, what was heard
and what went out (pasted, sent, a snippet, or a command's keys). The last 200, readable
only by you, next to the log; "Keep a history" in the settings
(`LOCALFLOW_HISTORY=off`) turns it off.
```sh
localflow history # the last 10 (or: localflow history 50)
localflow history --last | wl-copy # the last text again, e.g. when a paste went astray
localflow history --clear
```
## Learning from your fixes
When localflow gets a word wrong, fix it, select the fixed sentence (or copy
it) and press the learn key: `Option+Shift+L` (macOS), `Ctrl+Alt+Space`
(Windows), or a key bound to `localflow learn` (Linux). The words you changed
("Claw" to "Claude", "omar key" to "Omarchy") are saved to your vocabulary,
`~/.config/localflow/words.txt` (`%APPDATA%\localflow\words.txt` on Windows),
as `Claude: often heard as "Claw"`, and from then on it's spelled right. It
compares your fix with the last dictation, which is kept on your machine,
readable only by you. Nothing is logged.
## Your vocabulary
`words.txt` lists names and terms you say, one per line, optionally with a hint
about how they get misheard:
```
Claude: the AI assistant, often heard as "Claw" or "clawed"
Pine Script: TradingView's scripting language, often heard as "Py script"
```
Your own list, `~/.config/localflow/words.txt`, is read too: learning adds to
it, and you can edit it. Both are read on every dictation.
## Snippets
Say "my email" and get your email address. `snippets.txt`, next to your
`words.txt` (`~/.config/localflow/snippets.txt`,
`%APPDATA%\localflow\snippets.txt` on Windows), holds what you'd rather not
spell out, one per line, with `\n` for a new line:
```
# trigger => what's pasted
my email => jay@example.com
sign off => Thanks,\nJay
```
A snippet only counts when it's the whole dictation: case and punctuation
don't matter, and "insert", "paste" or "type" in front is fine, but "my email
is broken" is a dictation as always. The text is pasted exactly as written,
without the cleanup, so it's quicker too. In a coding agent it's only pasted,
never sent. The file is read on every dictation; `localflow snippets` shows
what it read. Raw dictation pastes a trigger as heard.
## Settings
The settings window writes `~/.config/localflow/env` (`%APPDATA%\localflow\env`
on Windows); a variable set in the environment wins. `localflow setup` does the
same without the window (`--model`, `--speech-model`), downloads the speech
and cleanup models and puts the app in place.
| Variable | What |
| --- | --- |
| `LOCALFLOW_SPEECH_MODEL` | `tiny.en`, `base.en`, `small.en`, `large-v3-turbo` (see `localflow models`). Setup picks `large-v3-turbo` with a graphics card, else `small.en` |
| `LOCALFLOW_SPEECH_GPU` | `off` to transcribe on the CPU (`on` to try the GPU on an Intel Mac, where the CPU is the default) |
| `LOCALFLOW_MIC` | the microphone to record, as `localflow mics` lists it: on Linux with PipeWire a source's node name (recorded through PipeWire), elsewhere the device's name; the system default when unset, or when it isn't connected |
| `LOCALFLOW_MODEL` | the cleanup model: `localflow-clean` (default) or a `.gguf` file |
| `LOCALFLOW_CLEANUP_GPU` | `off` to clean up on the CPU (the default on an Intel Mac) |
| `LOCALFLOW_SUBMIT` | in a coding agent, press Enter after pasting: `said` (when you end with "send it", default), `always`, `never` |
| `LOCALFLOW_HISTORY` | `off` to keep no history of your dictations |
Logs (never your words; those are in the [history](#history)) go to `~/.local/state/localflow/dictate.log`,
`~/Library/Logs/localflow/dictate.log` or `%LOCALAPPDATA%\localflow\dictate.log`.
## Tools and tuning
```sh
localflow models [download ID] # speech models and the cleanup model (localflow-clean)
localflow mics # the microphones, with the ids LOCALFLOW_MIC takes
localflow history [N] # what you said and what went out
localflow snippets # your snippets, as read from snippets.txt
localflow listen 5 # record 5 s, transcribe, clean up, print (segments and levels on stderr)
localflow transcribe talk.wav # the same from a WAV file
echo "call mom tomorrow actually tonight" | localflow -
localflow --check # does the cleanup model load?
localflow --eval # cases.txt against the cleanup model
localflow --selftest # each piece that doesn't need a person
localflow quit # stop the background app (macOS, Windows)
```
The model is trained in a repository of its own (localflow-model);
`crates/core/src/local.rs` has the prompt it was trained with. `cases.txt`
holds `raw => expected` cases, compared ignoring case and punctuation (list
cases compare the numbered items; `? +must; -must not; order: a < b` checks
properties of long dictation). After a new model, run `localflow --eval`.
## Testing
`cargo test --workspace` needs no model, microphone or network. CI
(`.github/workflows/ci.yml`) runs it, clippy, and `cargo xtask e2e` on fresh
Linux, macOS and Windows machines: it speaks a sentence with the OS's
text-to-speech and dictates it, through the hotkey (Linux: `start`/`stop`),
the pill, our speech engine, the cleanup model and the paste, into a
text box in another process; only the microphone is replaced by the WAV. The
package workflow builds the `.msi`, both `.dmg`s and the AppImage, and
`cargo xtask installed` installs each on a fresh machine, checks it, dictates
with it and removes it. CI machines have no microphone, so recording itself is
what `localflow --selftest` checks on yours.
## Known limitations
- The installers aren't signed yet (SmartScreen and Gatekeeper ask once).
- On Hyprland, apps running through XWayland get garbled keys from `wtype`,
so pasting into them fails; native Wayland apps are fine.
- File references are found by name: "the install file" finds `install.py`,
but a description ("the file with the hotkey code") doesn't.
- Misheard words are fixed reliably only when they're in `words.txt` with a
hint.
- Windows transcribes on the CPU (the GPU build needs the Vulkan SDK; Small
takes ~1 s there). Intel Macs use the CPU too, by default: Metal on their
integrated GPUs is slower than the CPU, or crashes.
- On Windows, the hotkey and the paste don't reach windows running as
administrator (an elevated terminal, Task Manager): nothing happens there.
Ctrl+Shift+Space is taken from other programs while localflow runs.
- Only text on the clipboard is put back after a paste; a copied image or
file is gone.
- On Wayland desktops other than Hyprland, localflow can't see which window
has focus, so terminals there get Ctrl+V (not Shift+Insert) and coding
agents aren't recognised.
## Layout
| Path | What |
| --- | --- |
| `crates/core` | the cleanup with localflow's own model; your vocabulary, snippets and learning |
| `crates/llm` | llama.cpp in a library of its own, embedded in the binary, that runs the cleanup model |
| `crates/speech` | the microphone, the speech engine (whisper.cpp), its models, quiet segments |
| `crates/desktop` | where you're dictating, paste and clipboard, hotkeys, the learn key |
| `crates/app` | `localflow`: the GPUI pill and settings window, sessions, setup, the CLI |
| `crates/xtask` | `cargo xtask package / e2e / installed` |
| `crates/testbox` | the text box the end-to-end test dictates into |
| `pkg/localflow.wxs` | the Windows installer |
| `words.txt`, `cases.txt` | the shipped vocabulary, the eval cases |
## License
Copyright (c) 2026 radiumcoders. All rights reserved.
The installers are free to download and use. The source code is not licensed
for copying, modification or redistribution.
The software localflow includes (whisper.cpp, llama.cpp, the Geist fonts, the
Rust crates) and the models it downloads are listed with their licenses in
[THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md), which every installer
includes too.