Press a hotkey, talk, press again — your speech is transcribed with faster-whisper on CUDA and auto-pasted into the focused window. Works on Wayland where most existing dictation tools (nerd-dictation, xdotool-based scripts, whisper-input) either require X11 or fail silently.
Keywords: linux voice typing, wayland speech to text, KDE dictation hotkey, whisper global hotkey, faster-whisper dictation,
nerd-dictation wayland alternative, pipewire speech recognition,
ydotool whisper paste, Plasma Wayland voice input.
Tested on Ubuntu 24.04 + KDE Plasma Wayland + NVIDIA (CUDA 12). Should also
work on GNOME Wayland, Hyprland, sway, and any compositor that supports
system-tray icons and has ydotool working.
| Tool | Wayland | GPU | Parallel model load | Global hotkey | Auto-paste |
|---|---|---|---|---|---|
| this script | yes | CUDA | yes (model loads while you talk) | yes | yes |
| nerd-dictation | X11 only | CPU (VOSK) | n/a (streaming) | yes | via xdotool (X11) |
| whisper-input | partial | varies | no | yes | via pyautogui |
| Speech Note | yes | yes | no | no (GUI app) | manual |
| Plasma "Dictation" | yes | cloud | — | yes | depends |
1. Truly system-wide — no copy/paste dance. Focus any text field in any window (browser, editor, chat app, terminal, Slack in a Snap, your CAD tool, whatever), press the hotkey, talk, press again — the transcription appears in-place. No dedicated dictation window, no intermediate textarea, no "copy from here and paste there". The hotkey is registered at the compositor level, so it works regardless of which app is focused.
2. Clipboard as a safety net — you never lose a transcription.
Even when the auto-paste fails (window lost focus mid-recording, permission
issue, whatever), the text is always placed in the clipboard first via
wl-copy. So:
- Lost focus during recording? Click back into the text field and hit
Ctrl+V. Done. - Using a clipboard history manager (Klipper, clipman, CopyQ, Pano…)? Every transcription gets its own entry in history. You can scroll back to a transcription from ten minutes ago and paste it again.
- Multi-step workflow? Dictate a paragraph, then paste it into three different places without re-recording.
This has saved hours of re-dictation in real use — speak freely without worrying whether the target window is still active.
3. Parallel model load — transcription finishes almost instantly.
Pressing the hotkey launches pw-record AND faster-whisper model loading
at the same time, so by the time you stop talking the model is already
warm and transcription finishes in ~1 s even for large-v3-turbo. No
persistent GPU memory cost — the preloader exits after one transcription
and frees VRAM.
Everything runs locally. faster-whisper loads the model into your GPU's
VRAM and transcribes on-device. No audio, no text, no metadata ever leaves
your machine — there is no network code in this script. Works fully offline
after the initial model download.
Audio files are ephemeral. The recording is written to
/tmp/stt-recording.wav only for the duration of transcription, then
deleted on the same line (rm -f "$TMPFILE" ... in stt.sh) — whether the
transcription succeeded, returned empty, or hit an error. No leftover
voice recordings for anyone to replay later.
On most modern Linux distros /tmp is a tmpfs, i.e. RAM-backed, so the
audio never touches disk at all — not even for a moment. A reboot wipes
everything. (Check yours with mount | grep ' /tmp '; if it says tmpfs,
you're RAM-only. If it says ext4/btrfs, files do hit disk briefly but
are still deleted after transcription.)
Transcribed text does live in the clipboard after a successful run —
that's the "safety net" feature above. Anything in the clipboard is visible
to other apps that request it (including clipboard history managers like
Klipper). If you dictated something sensitive and want to purge it, either
copy something else over it or run wl-copy --clear.
Logs don't contain your speech or transcriptions. /tmp/stt-events.log
records only metadata (timestamps, audio file size, result text length, per-step
return codes) — never the text itself.
- Recording:
pw-record(PipeWire native) → follows system default mic. Switching mics in KDE audio settings orpavucontroljust works — no config file to edit. (This matters: soxrecon Ubuntu is ALSA-only and silently ignores PipeWire's default source.) - Transcription:
faster-whisper(CTranslate2 backend) withlarge-v3-turboon CUDA float16. ~6 GB VRAM. Polish, English, German, ~100 other languages — anything Whisper supports. - Clipboard:
wl-copy(wl-clipboard package). - Paste:
ydotool key ctrl+vvia theydotoolddaemon over/dev/uinput(virtual keyboard, no X11, no compositor-specific hacks). - Indicator: PyQt5
QSystemTrayIcon— chosen because GTK/yad tray icons render unreliably on Plasma Wayland. Red dot during recording, spinner during transcription, error flash on failure. - Locking:
flockon/tmp/stt.lockso rapid double-presses don't race.
| File | Purpose |
|---|---|
stt.sh |
Main hotkey entry point — toggles recording, pastes result |
stt-preload.py |
Preloads the Whisper model in parallel with recording, transcribes once, exits |
stt-tray.py |
Qt tray icon, driven via FIFO |
ydotoold.service |
systemd user unit for the ydotool daemon |
One-shot install command:
sudo apt install \
pipewire pipewire-bin \
wl-clipboard \
ydotool ydotoold \
python3 python3-venv python3-pyqt5Breakdown of what each package does and whether you probably already have it:
| Package | Role | Usually preinstalled? |
|---|---|---|
pipewire, pipewire-bin |
Audio server + pw-record for recording |
Yes on Ubuntu 22.10+, Fedora 34+, KDE Neon, most modern KDE/GNOME distros |
wl-clipboard |
wl-copy / wl-paste for clipboard on Wayland |
No — needs explicit install on Ubuntu |
ydotool |
CLI client that sends Ctrl+V to the focused window |
No — needs install |
ydotoold |
Daemon that opens /dev/uinput and runs queued keystrokes |
No — separate package from ydotool on Ubuntu, easy to miss |
python3 |
Python 3 interpreter | Yes |
python3-venv |
python3 -m venv support — without this the install step fails silently |
No on minimal Ubuntu / Debian installs (common gotcha) |
python3-pyqt5 |
Qt bindings for the tray icon script | No on most distros |
Verify everything is present:
command -v pw-record wl-copy ydotool ydotoold python3 && \
python3 -c "import PyQt5.QtWidgets" && \
echo "all system deps OK"- A working NVIDIA driver must be installed (Ubuntu:
sudo ubuntu-drivers install, or pick a specific version from Software & Updates → Additional Drivers). Verify withnvidia-smi— it should list your GPU. - You do NOT need to install
cuda-toolkitsystem-wide. Thefaster-whisperPython package pulls in the necessary CUDA runtime libraries (nvidia-cublas-cu12,nvidia-cudnn-cu12) as wheels into the venv.stt.shpointsLD_LIBRARY_PATHat them automatically.
Installed in step 2 of the Install section below:
faster-whisper— pulls in the NVIDIA CUDA runtime wheels as transitive dependencies. ~500 MB into the venv.
- CUDA-capable NVIDIA GPU (tested on RTX A3000 12 GB).
- The default model
large-v3-turbouses ~6 GB VRAM in float16. Smaller models (base,small,medium) work fine with less VRAM; set via theSTT_MODELenv var. - CPU-only inference is possible by editing
stt-preload.py(device="cpu",compute_type="int8") but will be ~20× slower — transcription of a 20 s clip may take 30 s instead of 1 s.
-
Clone the gist somewhere in your
$HOME, e.g.~/scripts/stt/. The scripts locate each other relative to their own directory — no hardcoded paths. -
Create the Python venv:
cd ~/scripts/stt python3 -m venv stt-venv ./stt-venv/bin/pip install faster-whisper chmod +x stt.sh stt-preload.py stt-tray.py
-
Pre-download the Whisper model (strongly recommended — otherwise the model silently downloads on first hotkey use, and you'll see nothing but a spinning icon for 5–10 minutes on a slow connection, which looks like a hang):
./stt-venv/bin/python3 -c "from faster_whisper import WhisperModel; WhisperModel('large-v3-turbo', device='cuda', compute_type='float16')"This runs the download in the foreground with a visible progress bar. Model sizes (cached under
~/.cache/huggingface/hub/):Model Size on disk Quality tiny~75 MB rough base~150 MB OK for English, weak on other languages small~500 MB good for casual dictation medium~1.5 GB very good large-v3~3 GB best quality, slower large-v3-turbo(default)~1.6 GB ~best quality, faster than large-v3Repeat with a different model name if you want to try alternatives — they're all cached independently.
-
Grant
/dev/uinputaccess to your user soydotoold(running as your user, not root) can create a virtual keyboard:sudo tee /etc/udev/rules.d/80-uinput.rules <<'EOF' KERNEL=="uinput", MODE="0660", TAG+="uaccess" EOF sudo udevadm control --reload && sudo udevadm trigger
Log out and back in, then verify:
ls -la /dev/uinputshould show a+at the end of the permission string andgetfacl /dev/uinputshould list your user withrw-. -
Enable ydotoold as a user service:
mkdir -p ~/.config/systemd/user cp ydotoold.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now ydotoold.service
Verify:
ls -la /tmp/.ydotool_socket— should be owned by your user. -
Bind
stt.shto a global hotkey in KDE:System Settings → Shortcuts → Custom Shortcuts → Edit → New → Global Shortcut → Command/URL. Set the action to the absolute path of
stt.sh.Recommended binding:
Meta+Z— ergonomic (both keys under the left hand, reachable without moving), and in default KDE it doesn't collide with anything (unlikeMeta+Dwhich toggles "show desktop" on some setups). Pick whatever suits you, but something involving Meta is wise because Meta shortcuts rarely clash with application-level bindings.On GNOME: Settings → Keyboard → Custom Shortcuts. On Hyprland / sway: add a
binddirective to the config.
Environment variables — set in the hotkey command or your shell init:
| Var | Default | Notes |
|---|---|---|
STT_MODEL |
large-v3-turbo |
Any faster-whisper model: tiny, base, small, medium, large-v2, large-v3, large-v3-turbo, or a HuggingFace path |
STT_LANG |
en |
ISO code: en, pl, de, fr, es, ja, … Leave empty for auto-detect |
Example custom-shortcut command for Polish dictation:
STT_LANG=pl /home/you/scripts/stt/stt.sh
- First press of the hotkey: a red-dot tray icon appears, recording starts, and the Whisper model begins loading in the background.
- Talk for up to 20 minutes (configurable via
MAX_WAIT_SECONDSinstt-preload.py). - Second press: icon switches to a spinner while the audio is
transcribed. The result is copied to the clipboard (via
wl-copy) and auto-pasted into the focused window (viaydotool key ctrl+v). The icon disappears on success. - On failure: red error icon flashes for 3 s. Even when auto-paste
fails, the text is still in the clipboard — hit
Ctrl+Vmanually. Check/tmp/stt-events.logfor what went wrong.
Whisper is multilingual by one single model — large-v3-turbo handles
~99 languages with no extra download needed (only the .en-suffixed model
variants are English-only).
The model also handles code-switching within a single utterance quite
well. If you naturally mix languages (e.g. Polish sentences with English
technical terms — "Ponglish"), it will usually transcribe both correctly as
long as the mix is intentional and the dominant language is set via
STT_LANG.
However, if you force one language (STT_LANG=pl) but speak entirely
in another (English), Whisper will try to interpret the English audio as
Polish, producing garbled phonetic transcriptions or partial translations.
Recommended setup for bilingual users — bind two hotkeys:
| Hotkey | Command | Use for |
|---|---|---|
Meta+Z |
/home/you/scripts/stt/stt.sh |
Your primary language (STT_LANG in the script, e.g. pl) |
Meta+Shift+Z |
env STT_LANG=en /home/you/scripts/stt/stt.sh |
Dictating purely in English (emails, Slack, GitHub) |
Add both via KDE System Settings → Custom Shortcuts → New → Global
Shortcut → Command/URL. Same script, different language prior —
cleaner output than relying on auto-detect for short utterances. Add more
hotkeys for more languages (German, Spanish, etc.) following the same
pattern with STT_LANG=de, STT_LANG=es, …
For maximum flexibility on unpredictable content, leave STT_LANG=""
(empty) — Whisper will auto-detect per recording. Works well on longer
recordings (30 s+), may misidentify the language on short/ambiguous clips.
All pipeline events log to /tmp/stt-events.log with per-step return codes.
Example of a healthy run:
2026-04-20 07:34:10 start: recording
2026-04-20 07:34:56 stop: requested, rec_duration=46s
2026-04-20 07:34:57 stop: audio_size=1458220B
2026-04-20 07:34:59 transcribe: wait=2s outcome=result_file
2026-04-20 07:34:59 transcribe: OK text_len=447
2026-04-20 07:34:59 paste: wl-copy rc=0 len=447
2026-04-20 07:35:00 paste: ydotool rc=0
Other logs: /tmp/stt-preload.log (model load + transcription) and
/tmp/stt-rec.log (pw-record stderr).
- "recording did not start" → default audio source is muted or missing.
Check
pactl get-default-sourceand your desktop's sound settings. - "no speech detected" → Whisper's VAD stripped everything as silence.
Usually the mic gain is too low, or you spoke into a different mic than
the system default. Confirm
pactl get-default-sourcematches the mic you actually used. - Clipboard has the text but nothing pastes →
ydotoold.serviceis not running, or your user has no access to/dev/uinput. Re-check install steps 3 and 4.systemctl --user status ydotooldshould sayactive. - Recording icon never appears →
stt-tray.pycrashed at launch. Run it manually:mkfifo /tmp/test.fifo && python3 stt-tray.py /tmp/test.fifo media-record testand look for Python errors. - Transcription takes forever → check that faster-whisper is actually
using CUDA.
/tmp/stt-preload.logshould show the model load without errors. The CPU fallback is ~20× slower.
Because it's ~250 lines that solves one problem well, depends on widely
available tools (pw-record, wl-copy, ydotool, faster-whisper), and
can be modified by anyone who reads bash. If you want it to be a proper app,
fork it and go wild.
CC0 / public domain. Do whatever you want.