Press a hotkey. Speak. Your words appear in whatever window has focus.
Local, GPU-accelerated voice dictation for Linux — a free, open-source alternative to paid dictation apps, built for the platform they all skipped: GNOME on Wayland. Whisper runs on your hardware. No cloud, no subscription, no audio ever leaving your machine.
The on-screen preview while you speak — it updates every second and never steals focus. Release the hotkey and the final, full-context transcription is pasted where your cursor is.
Pick your operating system — the commands are not interchangeable.
Open a terminal and run:
curl -fsSL https://raw.githubusercontent.com/AveJaPl/voiceflow/main/install.sh | bashThe only step that asks for sudo installs ydotool + a udev rule (the script
explains why). Updating later: run the same command again.
Prefer to read before you run?
curl -fsSL …/install.sh > install.sh, open it, thenbash install.sh.
Easiest — no terminal at all:
- Download
voiceflow-install.batfrom the latest release (Assets section) - Double-click it; if SmartScreen warns, choose More info → Run anyway
Or from a terminal — this exact command works in both PowerShell and cmd, from any drive or directory:
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/AveJaPl/voiceflow/main/windows/install.ps1 | iex"
Updating later: same file / same command again. A signed .exe installer is
planned and will appear on the Releases
page when ready. Details and current limitations: docs/WINDOWS.md.
⚠ Do not run the Linux command on Windows: if WSL is installed,
| bashsilently executes it inside WSL, where dictation cannot work (the installer now detects this and stops, but save yourself the round trip).
The installer downloads the speech model (~1.6 GB) with a visible progress
bar as its final step — so once it says done, everything works immediately.
Press the hotkey (Super+G on Linux, Ctrl+Shift+Space on Windows),
speak, press it again. The text lands in whatever window has focus.
Manual install from source (Linux)
git clone https://github.com/AveJaPl/voiceflow && cd voiceflow
sudo bash scripts/install-system-deps.sh # ydotool, wl-clipboard, python3-gi-cairo, udev
uv sync # pinned Python 3.13 + dependencies
mkdir -p ~/.local/bin
printf '#!/usr/bin/env bash\nexec "%s/.venv/bin/voiceflow" "$@"\n' "$PWD" > ~/.local/bin/voiceflow
chmod +x ~/.local/bin/voiceflow
systemctl --user enable --now ydotool.service
cp systemd/voiceflow.service ~/.config/systemd/user/
systemctl --user daemon-reload && systemctl --user enable --now voiceflow.service
bash scripts/install-hotkey.sh # Super+G by defaultDictating prompts to AI assistants beats typing them — but the good dictation apps are subscription cloud services, and none of them run on Linux at all. voiceflow does the same job with a local Whisper model:
| voiceflow | typical paid dictation app | |
|---|---|---|
| price | free, MIT | ~$15/month |
| audio leaves your machine | never | always |
| Linux / GNOME / Wayland | native | unsupported |
| latency after you stop speaking | ~0.1 s on a GPU | network round-trip |
| works offline | yes | no |
- Fast. A user daemon keeps the model warm in VRAM: on an RTX 3070,
large-v3-turbotranscribes 4 s of speech in ~0.04 s. No GPU? It falls back to CPU (int8) automatically — slower, still private. - Live preview. A minimal always-on-top card shows what the model hears while you are still talking, with a pulsing recording indicator. The pasted text is a second, full-context pass — accuracy is never sacrificed for the preview.
- Voice-chat aware. Dictating while on Discord? Your mic stream to the call is muted (they never hear your prompts) and the call's audio is ducked to 40% (it stops derailing your sentence). Both restored the instant recording ends; your manual mute is respected.
- Any language. Whatever Whisper speaks — set
model.language(en,de,pl, … ornullto auto-detect). - Custom vocabulary. Your product names and jargon, biased into the decoder so it
stops mangling them (
model.vocabulary). It biases only — never rewrites your words. - Honest with your clipboard. Text is injected via paste (the only non-ASCII-safe path on GNOME Wayland) and your previous clipboard is put back afterwards.
- Tested without hardware. 82 tests, none need a GPU, microphone, or display — they run in CI on every commit.
| OS | Linux with PipeWire (developed on Ubuntu 26.04) · Windows 10/11 beta — docs/WINDOWS.md |
| Desktop | GNOME on Wayland (developed on GNOME 50) — other compositors: see roadmap |
| System packages | ydotool, wl-clipboard, python3-gi-cairo (the last package enables charts in the GTK application) |
| GPU | optional; NVIDIA with ~2.5 GB free VRAM for large-v3-turbo (CUDA libraries come from pip — no CUDA Toolkit install needed) |
| Disk | ~1.6 GB model + ~2.7 GB environment |
| dictate | Super+G → speak → Super+G |
| cancel without pasting | voiceflow cancel |
| health check & diagnostics | voiceflow status |
| logs | journalctl --user -u voiceflow -f |
| change the hotkey | VOICEFLOW_BINDING='<Control><Alt>space' bash scripts/install-hotkey.sh |
| free the VRAM (before gaming) | systemctl --user stop voiceflow |
| stress-test / leak check | bash scripts/audit.sh |
~/.config/voiceflow/config.yaml — created with commented defaults on first run;
restart the daemon after editing.
model:
name: large-v3-turbo # any faster-whisper model
device: cuda # cuda | cpu | auto
language: pl # ISO 639-1; null = auto-detect
vocabulary: [] # names the decoder should lean towards
inject:
method: clipboard # clipboard | ydotool | auto
paste_key: ctrl+shift+v # terminals paste with shift; GUI apps may want ctrl+v
mute_apps:
apps: [WEBRTC VoiceEngine] # Discord's mic stream; find others via pw-dump
duck_volume: 0.4 # duck the call to 40% while dictatingFull configuration reference
| key | default | meaning |
|---|---|---|
model.name |
large-v3-turbo |
any faster-whisper model id |
model.device |
cuda |
cuda / cpu / auto; CUDA failure falls back to CPU |
model.compute_type |
float16 |
precision on GPU; CPU uses int8 |
model.language |
pl |
ISO 639-1 code or null for auto-detect |
model.beam_size |
5 |
decoder beam width |
model.vocabulary |
[] |
terms biased into decoding |
audio.source |
null |
PipeWire source; null = default mic |
audio.max_seconds |
300 |
safety cap on one recording |
inject.method |
clipboard |
ydotool types ASCII only; clipboard is safe for all languages |
inject.paste_key |
ctrl+shift+v |
the paste chord that gets sent |
inject.restore_clipboard |
true |
put the previous clipboard back |
preview.enabled |
true |
live preview while speaking |
preview.interval_seconds |
1.0 |
preview refresh rate |
mute_apps.enabled |
true |
mute configured apps' mic streams while recording |
mute_apps.apps |
[WEBRTC VoiceEngine] |
PipeWire application.name values |
mute_apps.duck_enabled |
true |
also duck those apps' playback |
mute_apps.duck_volume |
0.4 |
duck target as a fraction of full volume |
mute_apps.duck_rules |
{} |
per-application playback targets keyed by PipeWire application.name |
presence.enabled |
false |
show Discord Rich Presence while dictating |
presence.client_id |
"" |
Discord developer application ID used for Rich Presence |
overlay.enabled |
true |
the on-screen indicator card |
The config file is generated once and not migrated — delete it to regenerate with current defaults.
hotkey ─▶ voiceflow toggle ──unix socket──▶ voiceflow daemon (systemd --user)
├─ recorder pw-record, 16 kHz WAV
├─ transcriber faster-whisper, warm in VRAM
├─ preview re-transcribes the tail every 1 s
├─ overlay separate GTK3 process (X11 popup)
├─ micmute mutes/ducks voice chats (wpctl)
└─ injector wl-copy + paste keystroke
A thin client (~0.1 s startup) talks to a persistent daemon, so the model loads once per login, not once per dictation. The modules do not import each other — the daemon composes them — which is what keeps the test suite hardware-free and the platform ports tractable.
Why GNOME/Wayland needed all this (field notes)
Constraints discovered the hard way — read this before porting:
- You cannot type into another window. Wayland forbids input injection and GNOME
implements no
virtual-keyboardprotocol, sowtypeis out.ydotoolworks at the kernel level via/dev/uinput, but itstypecommand silently drops non-ASCII characters. Hence: clipboard + a real paste keystroke, clipboard restored after. - A window cannot refuse focus — a normal preview window would swallow the paste. The overlay is an X11 override-redirect popup via XWayland: the one window type the compositor neither focuses nor repositions.
pw-recordexits 1 after SIGINT even when the WAV is complete. Success must be judged by parsing the WAV header, not the exit code.wl-copyforks, and its child owns the Wayland selection; piping its stdio and waiting for EOF deadlocks every paste. No pipes to self-daemonizing processes.- User services race the login. From
default.targetthe daemon gets noDISPLAY/XAUTHORITY; the unit binds tographical-session.target, with a runtime fallback that locates Mutter's XWayland auth cookie.
Ordered by how much of the codebase carries over — the daemon, transcriber, preview
logic, vocabulary, and config are already platform-neutral; only recorder,
injector, overlay, and micmute touch the OS:
- Native settings app (GTK4/libadwaita) — status, model & language, vocabulary editor, voice-chat ducking, all without touching YAML (in progress)
- Other Wayland compositors (KDE, Hyprland, Sway) — easiest port: they
implement
virtual-keyboard/layer-shell, so injection and overlay get simpler (#1) - Prebuilt packages — .deb, AUR, Flatpak/AppImage (the
/dev/uinputaccess needs design work in sandboxed formats) (#4) - Windows (beta) — same core, Win32 backends: in-daemon global hotkey,
PortAudio recording, clipboard+
SendInputinjection, no-focus tkinter overlay, Core Audio ducking. See docs/WINDOWS.md; feedback in #2 - macOS — AVFoundation recorder, CGEvent paste, NSPanel overlay, Accessibility permissions; Apple Silicon inference via CPU or an mlx/whisper.cpp backend (#3)
- Android (in progress — early alpha in
android/) — a custom keyboard (IME) with on-device whisper.cpp, so dictation works in every app (#6) - iOS — keyboard extension, sibling of the Android app (#7)
- i18n of user-facing strings (currently Polish — the author dictates in Polish) (#5)
Each item has a tracking issue with implementation notes — grab one, comment, and go.
PRs and issues welcome. Ground rules are short:
uv run pytestmust stay green and hardware-free — mock the OS, not the logic.- Module boundaries are load-bearing:
recorder/transcriber/injector/overlay/micmutemust not import each other. The daemon is the only composer. - One platform assumption per module, documented in its docstring.
MIT © Filip Piątek
