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.

A Hermez product

$ 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 the model downloads Loading — the model arrives, with its gauge
The whisper-desk overlay while listening Listening — how to finish or cancel, below
The whisper-desk overlay showing the sentence just transcribed As you go — the last sentence, live
The whisper-desk overlay while transcribing Transcribing
The whisper-desk overlay after a successful dictation Done — the beginning of the text inserted
The whisper-desk overlay after a dictation that heard nothing Error — a short reason, and what to do
The whisper-desk overlay after a cancelled dictation Cancelled — Esc, a click or the shortcut
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.

Once installed, updates go through the command itself: whisper-desk update.

First launch

A first installation ends on the welcome window: the microphone with a gauge that must move, the language and the model, the download with its progress, then your shortcut and a first dictation. whisper-desk welcome opens it again; WD_NO_WELCOME=1 skips it.

Welcome window: the microphone gauge 1 — the microphone
Welcome window: language and model 2 — language and model
Welcome window: the model download 3 — the download, with its progress
Welcome window: the shortcut and a first dictation 4 — your shortcut, a first dictation

Check that everything is in place

$ whisper-desk doctor

Every line is ✓ (fine), ✗ (to fix) or ⚠ (optional), and every failure comes with the command or the setting that fixes it.

Updating

$ whisper-desk update

It says which version runs and what is available upstream, and stops there if there is nothing new. Otherwise it fetches the sources, brings the Python environment up to date, rewrites the command and the service, restarts the daemon, and hands back a summary of what changed. Your configuration is kept, and so is your keyboard shortcut — a binding you have made your own is never rewritten. A dictation under way is never cut off: the update refuses to run and asks you to come back once it is over.

$ whisper-desk update --check     # compares the versions and writes nothing

--check prints one line and exits 0 if the machine is up to date, 1 if something newer exists upstream, and 2 if the question could not be answered (offline, private repository, nonexistent branch) — enough to hang it on a cron or a status bar. whisper-desk --version and the doctor both name the commit installed: the project is distributed by main, where the commit says more than the version number.

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

The menu entries and the tray icon go with it. 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
Super + J (once more)cuts the dictation off: what is left is neither transcribed nor inserted
Super + J (again and again)gives up: the window closes, the microphone is released, the daemon is free
Esc, or a click on the overlaycancels the dictation — a click on a finished state dismisses it

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.

Correcting yourself as you speak

Change your mind mid-sentence and say so: “On se voit mardi, non, plutôt mercredi” is written On se voit mercredi., “Let’s meet at 2, actually 3” is written Let’s meet at 3. Local rules, no language model: corrections = false turns them off.

Hesitations left out

The euh, hum and um Whisper writes down and the words you stumble over stay out of the text: “Je je pense, euh, qu’on devrait livrer” is written Je pense qu’on devrait livrer, “We could, we could meet” is written We could meet. Nous nous voyons or had had stay as they are. hesitations = false keeps everything.

Snippets

Save the texts you keep retyping under a cue, in the [snippets] section of config.toml: say “Tu peux m’envoyer le colis à mon adresse perso” and Tu peux m’envoyer le colis à 12 rue des Lilas, 75011 Paris. is typed. Alone or inside a sentence, whatever the case or punctuation Whisper gives the cue; the longest cue wins. Applied on your machine, after the transcription.

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 update     # update the installation (--check to compare only)
$ whisper-desk history    # past dictations — see below
$ whisper-desk settings   # the settings window
$ whisper-desk welcome    # the first-launch window, again
$ whisper-desk tray       # the tray icon
$ whisper-desk pause      # the shortcut is ignored until whisper-desk resume
$ whisper-desk config     # open the configuration in $EDITOR
$ whisper-desk reload     # reload the configuration without restarting
$ whisper-desk quit       # stop the daemon

Tray icon

The icon says whether the service runs, with which model and which microphone, and holds a menu: start or finish a dictation, History…, Settings…, Pause dictation, Quit. It needs gir1.2-ayatanaappindicator3-0.1 for the system Python. GNOME shows no tray icon by itself: without the AppIndicator extension (gnome-shell-extension-appindicator, on by default on Ubuntu) the icon has nowhere to go, and whisper-desk doctor says so. Settings and History are then in the applications menu.

History

Every dictation is kept — date, listening time, model and text — in ~/.local/state/whisper-desk/history.jsonl. A text lost on the way can be found again, copied or typed again at the cursor. The history.log of earlier versions is still read.

$ whisper-desk history                  # the 20 latest, numbered from 1 (the latest)
$ whisper-desk history invoice friday   # search: every word, case and accents ignored
$ whisper-desk history --copy 3         # copies dictation 3 to the clipboard
$ whisper-desk history --delete 3       # deletes it
$ whisper-desk history --keep-days 30   # deletes dictations older than 30 days, from now on
$ whisper-desk history --window         # the history window
The whisper-desk history window: search, copy, insert again, delete Search, Copy, Insert again where the cursor was, delete — and the automatic purge
04 — Configuration

Everything fits in one file.

~/.config/whisper-desk/config.toml — or, for the essentials, a window.

$ whisper-desk settings

Language, model (with its size, its speed and whether it is already downloaded), microphone with a test gauge, shortcut captured from the keyboard, output mode, vocabulary, overlay position and colour. Save writes only what changed — your comments are kept — and reloads the daemon.

The whisper-desk settings window The settings window — no TOML to edit

The file holds everything else:

[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)
corrections = true            # "mardi, non, plutôt mercredi" is written "mercredi"
hesitations = true            # "euh", "um" and stuttered words left out

[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               # keeps every dictation: whisper-desk history
history_days = 0             # dictations older than this are deleted; 0 = kept forever

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

[snippets]
"mon adresse perso" = "12 rue des Lilas, 75011 Paris"   # cue said -> text typed

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
backtrack.pycorrections said aloud (“non, plutôt…”) carried out
hesitations.pyhesitations (“euh”, “um”) and stuttered words left out
snippets.pyspoken cues replaced by the text saved for them
inject.pysimulated keystroke: uinput, SendKeys, System Events
output.pyinsertion at the cursor, clipboard, notifications
overlay.pyX11 overlay (separate process)
overlay_protocol.pythe line protocol between the daemon and the overlay
history.pydictation history (JSON Lines), search, purge
history_window.pyhistory window (GTK3, separate process)
tomlwrite.pywriting config.toml while keeping its comments
settings_window.pysettings window (GTK3, separate process)
welcome_window.pyfirst-launch window (GTK3, separate process)
tray_window.pytray icon (AppIndicator, 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.
  • WSL — powershell.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].

Someone is talking next to me and the dictation never ends

In a shared office, a nearby voice keeps the microphone busy: the silence that ends a dictation never comes. The shortcut is a toggle and answers whatever the state — one press stops the listening, the next one cuts the transcription off, and the conversation caught along the way is dropped instead of being typed at the cursor.

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.

H
Hermez A Hermez product.