Skip to content
denelson1-dotPublic

About

System-wide dictation for Linux. One service, one model, every application. Pluggable speech engines, types via uinput so it works on X11 and Wayland alike. Pure Go.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vox

System-wide dictation for Linux. One service, one model, every application.

Written in Go. Pure static binary, no runtime dependencies beyond a speech engine of your choosing.

Status: early. The service, engines, capture and injection all work. See Roadmap.


Why one service

Most dictation setups end up per-project: a virtualenv here, a model there, several hundred megabytes duplicated for each thing that wants to hear you.

vox is a single long-lived user service. The model loads once. Your editor, your browser, your window manager keybinding and your tablet's microphone button all talk to the same thing, and there is exactly one copy of the model on disk.

                   ┌──────────────┐
  keybinding  ───► │              │
  tablet UI   ───► │  vox service │ ──► types into whatever has focus
  script      ───► │              │
                   └──────────────┘
                    one model, loaded once

Quick start

./install.sh
vox doctor                          # what is present, what is missing
systemctl --user enable --now vox
vox toggle                          # speak; run again to stop and type

Bind vox-hotkey to a key for push-to-talk, and you will see the on-screen indicator from your next login (or run vox-overlay & now).

Hotkey and indicator

Two small companions are installed alongside the service.

vox-hotkey is vox toggle for a keyboard shortcut. Stopping types the transcript within a second, and a shortcut such as Super+H usually still has Super held down at that moment, which turns every typed letter into a Super+letter shortcut. vox-hotkey waits (for at most two seconds) until the modifiers are released. The wait needs X11 and python3-xlib; without them it is a plain toggle.

vox-overlay is a small pill near the bottom of the screen: red with a running timer while listening, amber while the last words are typed, grey if the service is down, and gone while idle. In the last thirty seconds before the recording limit it turns orange and counts down. It never takes focus and clicks pass straight through it, so it cannot steal the window you are dictating into. It follows the subscribe stream, so it does nothing while idle. It needs GTK 3 with PyGObject and cairo (python3-gi, python3-gi-cairo, gir1.2-gtk-3.0), and on Wayland it runs through XWayland.

Speech engines

vox drives every engine as an external command, and ships profiles for the common ones. That means a new engine is a config entry rather than a release, and you pick your own accuracy/size/latency tradeoff instead of inheriting someone else's.

Profile Needs Notes
faster-whisper the faster-whisper Python package Fast and accurate; a wrapper ships with vox
whisper-cpp whisper-cli Single binary, ggml-*.bin models
whisper the reference whisper CLI Slowest
vosk vosk-transcriber Small and fully offline, lower accuracy

vox daemon autodetects the first usable one. Override with -engine NAME -model NAME.

Accuracy

Defaults favour accuracy over speed, because dictation has latency to spare. Measured on an i5-8250U, transcribing a 22-second utterance:

Model Decoding Speed
base.en greedy (beam=1) 20x realtime fast and noticeably worse
base.en beam=5 10x realtime default
small.en beam=5 4x realtime better; still imperceptible for dictation

beam_size=5 is faster-whisper's own default and cut word error rate by about a fifth in testing against greedy decoding. Running 10x faster than realtime means a ten-second utterance transcribes in one second, so there is no reason to spend that headroom on speed you cannot feel.

If accuracy is not good enough, get a larger model -- that is the bigger lever:

vox models get small.en
vox daemon -model small.en

A caveat on the numbers above: they are speed measurements. Attempts to score accuracy with synthetic speech were not meaningful -- every configuration transcribed espeak output perfectly, because synthetic speech is far more regular than the human speech these models are trained on. Only the same-model beam comparison is a real accuracy result. Judge models with your own voice; nothing else generalises.

Latency

What you wait for is a chunk's fixed cost, not its length: Whisper pads every input to a thirty-second window, so two seconds of speech take nearly as long as six. On the i5-8250U above, base.en:

per chunk, and after tapping stop
one process per chunk (vox before serving) 2.2 s
model kept loaded, physical cores only 0.75 s

Two things make the difference. vox keeps the model loaded in one long-lived process (vox-faster-whisper --serve), started and warmed when the service starts, instead of paying Python's imports and a model load -- well over a second -- on every chunk. That costs about 240 MB of memory for as long as the service runs. And it uses one thread per physical core: Whisper's arithmetic saturates a core, so hyperthreads only contend, and 4 threads beat 8 on a 4-core laptop. Override with VOX_WHISPER_THREADS.

If the engine cannot serve (an engine without a serve mode, or an older wrapper), vox says so in its log and starts a process per chunk as before.

Already have faster-whisper?

vox looks for an existing virtualenv before asking you to make a new one — it checks $VOX_WHISPER_VENV, then ~/.local/share/vox/venv, then ~/.local/share/voice-dictation/venv. Point it at an existing model cache with:

export VOX_MODEL_DIR=~/.cache/faster-whisper

Nothing is re-downloaded.

Models live in one place

vox models        # shows the directory and its contents

$XDG_DATA_HOME/vox/models, overridable with VOX_MODEL_DIR. Bare model names resolve there; absolute paths are used as given. Downloading a model once and having every tool on the machine use it is the entire point.

How text gets typed

By default vox creates a uinput virtual keyboard. This is worth explaining, because most tools reach for xdotool or wtype.

A virtual keyboard is a kernel input device, so text arrives by the same path as a real keypress. It works identically on X11 and on every Wayland compositor, needs no per-compositor protocol, and does not care which toolkit owns the focused window. By contrast xdotool is X11-only, and wtype needs virtual-keyboard-unstable-v1, which GNOME and KDE do not implement.

The cost is that a keyboard emits keycodes, not characters, so characters are mapped through a US-QWERTY layout. On a different layout the keycodes are still valid but the characters differ. vox reports characters it cannot type rather than typing something else, and wtype/ydotool/xdotool remain available as fallbacks. Reading the active keymap is on the roadmap.

Security, plainly: write access to /dev/uinput is the ability to synthesize arbitrary keystrokes into your session. That is inherent to what dictation does, but it is a real capability — the shipped udev rule scopes it to the active local session via uaccess rather than granting it to a group permanently. Skip it with install.sh --no-udev and vox falls back to an external tool.

Streaming

Text appears while you speak, the way phone and desktop dictation does, rather than arriving all at once when you stop. The wait after tapping stop is a second or two regardless of how long you spoke.

This is the default. To transcribe in a single pass instead:

vox daemon -stream=false

Pauses never stop the recording. Silence is used to decide where to cut a chunk, never whether to keep listening. People pause to think, and dictation that shuts off when they do is worse than dictation that is slow. Tap stop when you are finished.

The one exception is a recording someone forgot about. After five minutes vox stops and types what it has, exactly as if you had tapped stop: left running, it would keep typing into whatever has focus, and room noise that clears the silence threshold invites Whisper to invent phrases. Change the limit with vox daemon -max-record 10m, or remove it with -max-record 0.

Your pauses are what make it work well: a chunk is cut at the quietest moment near the target length, so a natural pause becomes a clean boundary. Talk without pausing and it still cuts, just at the least-bad point it can find.

Making the seams invisible

Chunking is an implementation detail and should not be visible in the transcript. Three things would otherwise give it away:

Ellipses. Whisper writes ... when speech trails off or starts mid-utterance, which is exactly what a chunk boundary looks like to it. Left alone this litters dictation with pauses the speaker never made — and they land precisely where the speaker was most fluent, since chunks are cut at the quietest moment. They are stripped.

Restarted sentences. Every chunk looks like the beginning of an utterance, so the engine capitalises it and re-punctuates. Each chunk is given the preceding text as an initial_prompt, so the model knows it is continuing, and a leading capital mid-sentence is lowered again — except for I, acronyms and mixed-case names.

False full stops. Audio that stops after a word sounds like an ending, so Whisper closes almost every chunk with a full stop — measured, about half of all cuts that fell mid-sentence. vox measures the pause each cut landed in: between words it is tens of milliseconds, between sentences it is longer. Under 0.3 s, or after a word no sentence ends on ("the", "to", "and"), the closing . ? or ! is dropped and the next chunk carries on in lowercase. The engine still sees its own punctuation as context, so its accuracy is untouched; only what is typed changes. Each chunk's pause is in the log (pause=, end_dropped=), which is what to look at if it guesses wrong for your voice.

The remaining tradeoff is real: Whisper is more accurate with more context, so a six-second chunk is slightly worse than the same words inside a thirty-second utterance. Longer chunks recover accuracy and give back latency; -chunk-seconds 10 is a reasonable middle.

Audio capture

Detected, not assumed: pw-record (PipeWire), then parec (PulseAudio), then arecord (ALSA). Recording is 16 kHz mono s16, which is what the engines want.

The recorder is stopped with SIGINT rather than SIGKILL so it can finish writing a valid WAV header — a truncated file makes every engine fail with a confusing error.

API

A Unix socket at $XDG_RUNTIME_DIR/vox.sock, newline-delimited. No D-Bus dependency, works with no session bus, and is trivial to drive from a script.

Command
toggle Start, or stop and type
start / stop Explicit control
cancel Abandon without transcribing
state ready | listening | transcribing
subscribe Stream state changes, for building a UI
key NAME Press a single key: Return, Tab, BackSpace, Escape, arrows
info Which engine, recorder and injector are in use, and the recording limit
echo toggle | nc -U $XDG_RUNTIME_DIR/vox.sock

subscribe is what a microphone button, or vox-overlay, uses to show listening and transcribing without polling.

key exists because a touch panel has no physical keyboard to submit with. Pressing a key and typing text are the same capability -- synthetic input into the focused window -- so it reuses the virtual keyboard rather than growing a second mechanism with its own device and permissions.

Roadmap

  • Pluggable engines, autodetection, shared model directory
  • uinput injection working on X11 and Wayland alike
  • Socket API with state subscription
  • Read the active keymap instead of assuming US-QWERTY
  • Streaming transcription, so text appears while you speak
  • Voice commands (punctuation, newline, corrections)
  • A model downloader, so vox models get base.en just works

Related

  • hinged — tablet mode for Linux convertibles. Its on-screen input panel calls vox for dictation, which is what a tablet with no keyboard needs.

License

MIT

About

System-wide dictation for Linux. One service, one model, every application. Pluggable speech engines, types via uinput so it works on X11 and Wayland alike. Pure Go.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages