100 % local · Linux · WSL · macOS

whisper‑desk.

You press Super + J, you speak, and the text is typed straight where your cursor is — sentence after sentence, while you speak. No data leaves the machine.

Written and maintained by Salvador Cardona · cardona.digital

$ curl -LsSf https://raw.githubusercontent.com/SalvadorCardona/whisper-desk/main/install.sh | sh
The source code
your editor, your browser, your chat client…

The real overlay: a microphone, and an equalizer where each bar tracks a frequency band, from lows on the left to highs on the right.

01 — What it is

Dictation that never leaves your machine.

Transcription runs on your GPU (or your CPU) with faster-whisper. No account, no API, no network — the model is loaded once, and it stays in memory.

Offline, really

Not a byte of audio leaves the computer. Cut the network: it still works.

Inserted at the cursor

The text lands in the focused application. Your clipboard is handed back untouched.

As you go

A short pause is enough: the sentence is transcribed and inserted while you dictate the next one.

Three hosts, one tool

Linux, WSL and macOS. The install script recognises the host and adapts every step.

The whisper-desk overlay while listening While listening
The whisper-desk overlay while transcribing While transcribing
02 — Installation

One command, and that’s all.

$ curl -LsSf https://raw.githubusercontent.com/SalvadorCardona/whisper-desk/main/install.sh | sh

The script recognises the host — Linux, WSL or macOS — and adapts every step:

  1. it checks the system dependencies (microphone, clipboard, notifications, GTK) and offers to install them, with apt or brew as appropriate;
  2. it creates an isolated Python environment and installs faster-whisper in it — plus the CUDA libraries if an NVIDIA card is detected;
  3. it installs the whisper-desk command in ~/.local/bin;
  4. it enables the user service — systemd on Linux and WSL, launchd on macOS — started at login;
  5. it registers the global shortcut with the host’s shortcut manager.

The Whisper model (a few hundred MB) is downloaded the first time the service starts.

Running the same command again updates the installation: the code is replaced, the service restarted, and your configuration and models are kept.

Check that everything is in place

$ whisper-desk doctor

What each host uses

LinuxWSLmacOS
Microphone capturearecord (ALSA)parec (WSLg)rec (sox) or ffmpeg
Clipboardwl-copy / xclipclip.exepbcopy
Paste keystroke/dev/uinputSendKeys (PowerShell)System Events
Global shortcutGNOME (gsettings)Start menuskhd, or by hand
Servicesystemdsystemd, otherwise on demandlaunchd
Notificationsnotify-sendnotify-send (WSLg)osascript

None of these choices are set in stone: backend, keyboard and paste_shortcut can be forced in the configuration.

Requirements

  • Linux — GNOME (Wayland or X11), systemd in the user session.
  • WSL — WSL 2 with WSLg (Windows 11, or an up-to-date Windows 10) for the microphone, and Windows interoperability enabled. Text is inserted into Windows windows.
  • macOS — 12 or newer. The first paste asks for accessibility permission, and the first dictation asks for microphone permission.

An NVIDIA GPU is a bonus, not a requirement; on macOS transcription runs on the CPU, since CTranslate2 does not use Metal.

Uninstalling

$ curl -LsSf https://raw.githubusercontent.com/SalvadorCardona/whisper-desk/main/uninstall.sh | sh

Add WD_PURGE=1 to remove the configuration and the history as well. Downloaded models stay in ~/.cache/huggingface.

03 — Usage

A shortcut, a voice, some text.

GestureEffect
Super + Jstarts listening — the overlay appears
a short pause (~0.6 s)the sentence is transcribed and inserted at the cursor, listening continues
2 s of silenceend of dictation
Super + J (again)stops listening immediately

The default shortcut follows the host: Super + J on Linux and macOS (Cmd + J), Ctrl + Alt + J on WSL — Windows reserves the Windows key for itself.

From the command line

$ whisper-desk record     # dictate and write the text to standard output
$ whisper-desk toggle     # same as the keyboard shortcut
$ whisper-desk status     # daemon state, loaded model, GPU or CPU
$ whisper-desk doctor     # full diagnostic
$ whisper-desk config     # open the configuration in $EDITOR
$ whisper-desk reload     # reload the configuration without restarting
$ whisper-desk quit       # stop the daemon
04 — Configuration

Everything fits in one file.

~/.config/whisper-desk/config.toml

[hotkey]
binding = "auto"              # "auto", or <Ctrl>/<Alt>/<Shift>/<Super> + a key

[model]
name = "auto"                 # auto | tiny | base | small | medium | large-v3 | large-v3-turbo
device = "auto"               # auto | cuda | cpu
language = "fr"               # ISO code, or "auto" for detection
initial_prompt = ""           # vocabulary to favour (proper nouns, jargon)

[recording]
backend = "auto"              # auto | arecord | parec | rec | sox | ffmpeg
streaming = true              # insertion as you go, sentence by sentence
segment_silence_seconds = 0.6 # pause that splits a sentence
silence_seconds = 2.0         # silence that ends the dictation
max_seconds = 120

[output]
mode = "cursor"               # cursor | clipboard | stdout — combinable: "cursor+stdout"
paste_shortcut = "auto"       # "shift+insert" if you mostly dictate in a terminal
keyboard = "auto"             # auto | uinput | windows | applescript | none
restore_clipboard = true      # hands your original clipboard back at the end
notify = false
history = true               # log in ~/.local/state/whisper-desk/history.log

[overlay]
enabled = true
accent = "#e46212"
position = "bottom-center"    # bottom-center | top-center | center
margin = 96

After a change:

$ whisper-desk reload            # for everything but the shortcut
$ whisper-desk hotkey install    # to apply a new shortcut

Choosing a model

ModelVRAMSpeedQuality
small~1 GBvery fastdecent — default without a GPU
medium~2.5 GBfastgood
large-v3-turbo~2 GBfastexcellent — default with a GPU
large-v3~4.5 GBslowerthe best

To shorten the delay between the end of a sentence and its insertion even further, lower beam_size to 1 in [model].

05 — How it works

Three Wayland constraints shaped this architecture.

Super + J  ─→  whisper-desk toggle  ─→  Unix socket  ─→  daemon (model in memory)
                                                              │
                              X11 overlay ←── audio level ─────┤
                                                              │
 mic capture ─→ silence detection ─→ sentence ─→ faster-whisper ─→ text
                                                              │
                          clipboard + Ctrl+V (simulated keystroke) ─→ cursor

The daemon keeps the model loaded at all times, and transcribes one sentence while the microphone is already recording the next. Only the two ends of that chain — capture and keystroke — change from one host to another; the rest is shared.

A client cannot type into another client’s window

The virtual-keyboard protocol (the one wtype uses) is not implemented by GNOME. So we go through a kernel virtual keyboard (/dev/uinput, reachable without privileges thanks to the ACL set by systemd) that sends a plain paste shortcut.

Sending the text key by key would mean knowing the active XKB layout

On an AZERTY keyboard, accents and half the letters would land on the wrong key; the paste shortcut, on the other hand, sits on the same physical key everywhere. The text therefore travels through the clipboard, which is restored afterwards.

A Wayland window can neither refuse focus nor position itself

A Wayland overlay would catch the paste instead of your application. The overlay is therefore an X11 client (via Xwayland) of type NOTIFICATION: never focused, and positionable.

The two other hosts follow the same idea with their own tools: on WSL, the keystroke goes to Windows through SendKeys (a Linux virtual keyboard would only reach WSLg windows); on macOS, it goes through System Events, which is what earns the program the accessibility permission prompt.

The code, module by module

FileRole
daemon.pyservice, Unix socket, listening and transcription in parallel
host.pyhost detection, PowerShell bridge on WSL
capture.pychoice of capture tool (arecord, parec, rec, ffmpeg)
recorder.pysilence detection, splitting into sentences
transcriber.pyfaster-whisper, GPU/CPU selection
inject.pysimulated keystroke: uinput, SendKeys, System Events
output.pyinsertion at the cursor, clipboard, notifications
overlay.pyX11 overlay (separate process)
hotkey.pyglobal shortcut: GNOME, Start menu, skhd
service.pydaemon startup: systemd, launchd or direct

Development

$ git clone https://github.com/SalvadorCardona/whisper-desk
$ cd whisper-desk
$ WD_SRC="$PWD" sh install.sh     # installs from the local clone, without network access

The test suite depends on the standard library alone — no virtual environment, no model to download. The WD_HOST variable (linux, wsl, macos) forces the detection, which makes it possible to test all three hosts from any of them.

$ python3 -m unittest discover -s tests -t .
06 — Troubleshooting

Always start with the doctor.

whisper-desk doctor names the detected host, then checks one by one the pieces it uses: it is the first thing to try for everything below.

The shortcut does nothing
$ whisper-desk status                      # is the daemon answering?
$ whisper-desk hotkey show                 # is the shortcut registered?
$ journalctl --user -u whisper-desk -f     # the logs (systemd)
$ tail -f ~/.local/state/whisper-desk/daemon.log   # the logs (launchd)
The text stays in the clipboard instead of being inserted

The simulated keystroke did not get through. whisper-desk doctor says which one is at fault:

  • Linux/dev/uinput must be writable. In a local session, systemd grants you access automatically; over SSH or in a remote session, it does not.
  • macOS — allow accessibility for the terminal (or for whisper-desk) in System Settings → Privacy & Security → Accessibility, then restart the daemon.
  • WSLpowershell.exe must be reachable from WSL, and the target window must be a Windows window in the foreground.
I dictate in a terminal and get ^V

Ctrl + V is not paste there. Set paste_shortcut = "shift+insert" in [output], then run whisper-desk reload.

The overlay opens but nothing is written

Nine times out of ten, the default microphone is the wrong one — an empty jack socket often stays the default source, and returns nothing but silence. whisper-desk doctor measures the level actually captured:

$ whisper-desk doctor          # "the microphone picks up sound" must be ticked
$ wpctl status                 # lists the sources; spot the real microphone
$ wpctl set-default <id>       # switch to it
Sentences are cut too early, or too late

Adjust segment_silence_seconds (the splitting) and silence_seconds (the end of dictation) in [recording].

Transcription runs on the CPU even though I have a GPU

whisper-desk status reports the selected device, and journalctl --user -u whisper-desk the reason for the fallback — often missing CUDA libraries, or not enough VRAM.

The cases specific to WSL and macOS (no sound captured, the shortcut without skhd, microphone gain…) are detailed in the repository README.

07 — The author

Handmade, by a developer.

SC
Salvador Cardona Thirteen years of web development — and the other projects at the same address.
cardona.digital