⚠️ Disclaimer: this project is entirely “vibe coded”.Every line of code, test, and documentation in this repository was produced by an AI coding agent under human direction. No part of it has been written, line-by-line audited, or formally reviewed by a human engineer. It works on the author’s machine and ships with tests, but treat it accordingly: read the source before trusting it with anything important, and expect rough edges, dead code paths, and the occasional architectural oddity that a human would have caught.
Voice dictation for Linux. Record, transcribe, and paste text into any application – all from a single keyboard shortcut.
talk-rs captures audio from your microphone, sends it to a
transcription API (Mistral Voxtral or OpenAI Whisper), and types the
result into the focused window. A small X11 overlay badge shows the
current state (recording / transcribing) so you always know what is
happening.
- Dictation workflow – press a key to start recording, press again to stop; transcribed text is pasted automatically.
- Multiple providers – Mistral (Voxtral) and OpenAI (Whisper / GPT-4o) for batch transcription; Mistral and OpenAI realtime streaming via WebSocket.
- Text-to-speech – the
speakcommand synthesizes text and plays it (or saves a WAV). Two providers mirror the transcription side:kokoro(local, offline, multi-language via sherpa-onnx) andmistral(remote Voxtral TTS). Language handling is config-driven and agnostic. - Speaker diarization – identify who is speaking (
--diarize); output is tagged with speaker labels. Currently supported with Mistral V2 models in batch mode. - Multi-candidate picker – with
--pick, run several providers in parallel and choose the best transcription from a GTK picker window. A waterfall spectrogram of the recording is shown above the candidate list; it loads asynchronously and adapts to window width. Non-default models are transcribed on demand via a per-candidate button to avoid unnecessary API calls. - Visual overlay – non-intrusive X11 badge at the top of the screen (works without a compositor).
- Live transcription overlay – a dynamic phase-coloured waterfall replaces the static “transcribing” badge; three independent throughput tracks (upload bytes, download bytes, paste characters) show progress through the pipeline, with a time-grid layer over the spectrogram.
- Dead audio detection – overlay badge shows a red prohibit icon and “NO SOUND” warning when no real microphone is detected (e.g. headset unplugged); a centered full-screen overlay and repeating alert tone reinforce the signal, and a notification also appears on the text panel.
- Auto-pause – automatically pauses audio forwarding during
silence, trimming dead air from transcription input. Resumes
instantly with a 300 ms lookback buffer to preserve speech onset.
The badge shows yellow pause bars and “LISTENING” during pauses.
Disable with
--no-auto-pause. - Audio visualizers – optional in-badge visualization during
recording (
--viz waterfall,--viz amplitude,--viz spectrum); monochrome mode with--mono. - Audio feedback – start/stop tones plus a periodic boop while
the badge shows
LISTENING(i.e. during auto-pause silence); the boop is silent while you are actively speaking, and is never heard when--no-auto-pauseis used. Fully configurable or disabled. - Bluetooth headset auto-switch – when a Bluetooth headset is
connected in A2DP mode (high-quality stereo, no microphone),
talk-rsautomatically switches it to its Hands-Free Profile (HFP) for the duration of the recording so the headset microphone is available, then restores the original profile on stop. Survives unclean termination: a state file at$XDG_RUNTIME_DIR/talk-rs/card-profile.jsonis written before the switch, and the next invocation restores the original profile from that file before starting a new recording. Works with any HFP-capable headset (uses PulseAudio’s standarddevice.form_factorproperty, not vendor-specific identifiers). Disable per-invocation with--no-bt-auto-switchor globally via theaudio.bt_auto_switchconfig key. - Context bias – supply domain-specific vocabulary to improve transcription accuracy.
- Daemon toggle mode – first invocation spawns a short-lived daemon that records; second invocation signals it to stop, transcribe, paste, and exit. Ideal for global shortcuts.
- No idle daemon – between dictations, zero
talk-rsprocesses stay resident. The toggle daemon spawns on press, records, and exits after pasting. No memory footprint when idle. - Retry last – re-transcribe the last cached recording without
speaking again (
--retry-last). - Recordings browser –
record --uiopens a GTK4 window listing all recordings (OGG fromoutput_dir) and dictation cache (OGG). Imported audio dropped intooutput_dir–.m4a,.mp4, or.aac(e.g. iPhone voice memos) – is listed and playable alongside native OGG recordings, with the same waterfall and transcription workflow. Play, delete, open in the file manager, or transcribe on demand. A waterfall spectrogram is shown for every recording, even those without transcripts; sections auto-refresh via inotify when files change externally. - Per-segment timing export –
--output-yamlincludes per-segment start/end timestamps in the metadata sidecar, suitable for subtitles and post-processing. - Standalone commands –
recordandtranscribecan be used independently for scripting. - Environment overrides – every config value can be set via
TALK_RS_*environment variables.
# Debian / Ubuntu
sudo apt install build-essential pkg-config libasound2-dev libopus-dev \
libgtk-4-dev libpipewire-0.3-dev libspa-0.2-dev libclang-dev \
libpulse-dev
# Fedora
sudo dnf install alsa-lib-devel opus-devel pkg-config \
gtk4-devel pipewire-devel spa-devel clang-devel \
pulseaudio-libs-devellibpulse-dev / pulseaudio-libs-devel provides the PulseAudio client
library used to switch a Bluetooth headset’s PulseAudio card profile
between A2DP (high-quality stereo) and HFP (microphone-enabled) when
recording. On PipeWire systems, pipewire-pulse supplies the
libpulse.so shared object at runtime, so no PulseAudio daemon is
needed – only the development headers for compilation.
A working Rust toolchain is required (1.70+). Install via rustup if needed.
PipeWire must be running (used for audio capture). Most modern Linux
desktops ship with PipeWire by default.
An API key for at least one transcription provider is required:
If building from a git clone, run ./autogen.sh first to resolve
version placeholders in Cargo.toml:
./autogen.sh
cargo build --releaseThe binary is at target/release/talk-rs. Copy it somewhere in your
$PATH:
cp target/release/talk-rs ~/.local/bin/talk-rs reads $XDG_CONFIG_HOME/talk-rs/config.yaml (typically
~~/.config/talk-rs/config.yaml~).
Copy the example and fill in your values:
mkdir -p ~/.config/talk-rs
cp config.example.yaml ~/.config/talk-rs/config.yamlMinimal working configuration:
output_dir: ~/talk-rs-output
providers:
mistral:
api_key: YOUR_MISTRAL_API_KEY| Field | Description |
|---|---|
output_dir | Absolute path to a writable directory for recordings (a leading ~~~ is expanded to your home) |
providers.mistral.api_key | Mistral API key (if using Mistral) |
providers.openai.api_key | OpenAI API key (if using OpenAI) |
| Field | Default | Description |
|---|---|---|
providers.mistral.url | https://api.mistral.ai | Mistral API base URL |
providers.mistral.model | voxtral-mini-2507 | Mistral transcription model |
providers.mistral.context_bias | none | Comma-separated words for accuracy |
providers.mistral.tts_model | voxtral-mini-tts-latest | Voxtral TTS model for speak --provider mistral (shares the STT api_key) |
providers.mistral.tts_voice | none | Default Mistral preset voice id for speak (else pass --voice) |
providers.openai.url | https://api.openai.com | OpenAI API base URL |
providers.openai.model | gpt-transcribe | OpenAI batch model |
providers.openai.realtime_model | gpt-live-transcribe | OpenAI realtime model |
providers.openai.prompt | none | Prompt for spelling, punctuation, and context hints |
providers.openai.keywords | none | Expected vocabulary list for the new models |
providers.openai.languages | none | Ordered expected-language list |
providers.openai.realtime_delay | none | Realtime delay: minimal, low, medium, high, or xhigh |
providers.kokoro.model_dir | XDG data dir | Kokoro TTS model cache dir (auto-downloaded on first speak) |
providers.kokoro.voice | per-language default | Default Kokoro voice name (af_heart, ff_siwis, …) |
providers.kokoro.num_threads | 4 | Kokoro inference threads |
providers.kokoro.lang | en (model-baked) | Default Kokoro phonemization language |
transcription.default_provider | mistral | Default transcription provider when unspecified |
speak.default_provider | kokoro-if-configured | Default speak provider (kokoro or mistral) |
indicators.boop_interval_ms | 5000 | Periodic boop interval in ms (0 disables boops; also --no-boop) |
indicators.visual_overlay | true | Show X11 overlay badge |
indicators.viz | none | In-badge visualizer: waterfall, amplitude, or spectrum (also --viz; env TALK_RS_INDICATORS_VIZ) |
indicators.mono | false | Monochrome visualizer (also --mono) |
paste.chunk_chars | 150 | Max chars per paste chunk (0 disables chunking; also --no-chunk-paste) |
audio.bt_auto_switch | true | Auto-switch a connected Bluetooth headset to HFP for the duration of a recording, then restore (also --no-bt-auto-switch; env TALK_RS_AUDIO_BT_AUTO_SWITCH) |
recording.sample_rate | 48000 | Sample rate (Hz) of the record command’s .ogg output (env TALK_RS_RECORDING_SAMPLE_RATE) |
recording.channels | 1 | Channels for record output: 1 mono, 2 stereo (env TALK_RS_RECORDING_CHANNELS) |
recording.bitrate | 128000 | Opus bitrate (bps) of the record command’s .ogg output (env TALK_RS_RECORDING_BITRATE) |
The recording.* settings control the quality of recordings meant
for a human to listen to or share. They are independent of
transcription: audio sent to the providers is always downsampled to
16 kHz mono internally (both Voxtral and Whisper operate at 16 kHz),
so these knobs do not affect transcription accuracy or upload size.
The OpenAI batch default is exactly gpt-transcribe, and the realtime
default is exactly gpt-live-transcribe. Existing configuration files
remain valid: prompt, keywords, languages, and realtime_delay are
optional, and omitting them sends no hint fields.
gpt-transcribesendsprompt, repeatedkeywords[], repeatedlanguages[], andresponse_format=jsonto the batch endpoint.gpt-live-transcribenestsmodel,prompt,keywords,languages, anddelayundersession.audio.input.transcriptionin the realtimesession.update.whisper-1remains available for segment/word timestamps, subtitles, and translation workflows. It retainsresponse_format=verbose_json, acceptspromptand one unambiguous singularlanguage, and does not acceptkeywordsor multiple languages.gpt-4o-transcribeandgpt-4o-mini-transcriberemain available as legacy batch choices.gpt-realtime-whisperremains available as a legacy realtime choice, using singularlanguagefor one expected language and rejectingkeywords, multiple languages, andrealtime_delay.
Configured hints that the selected model cannot represent fail locally before HTTP or WebSocket traffic; talk-rs never silently drops them. There is no structured previous-turn configuration field in the official schema. Earlier-turn context is service-managed where supported.
Direct-OpenAI prices checked 2026-07-31: gpt-transcribe costs
$0.0045/min ($0.27/hr), while whisper-1 costs $0.006/min ($0.36/hr).
The new batch default is 25% lower, a $0.09 per hour savings.
Every config value can be overridden via environment variables:
export TALK_RS_PROVIDERS_MISTRAL_API_KEY="sk-..."
export TALK_RS_PROVIDERS_OPENAI_API_KEY="sk-..."
export TALK_RS_PROVIDERS_OPENAI_PROMPT="Preserve punctuation and casing."
export TALK_RS_PROVIDERS_OPENAI_KEYWORDS="Kalysto, talk-rs"
export TALK_RS_PROVIDERS_OPENAI_LANGUAGES="fr, en"
export TALK_RS_PROVIDERS_OPENAI_REALTIME_DELAY="low"TALK_RS_PROVIDERS_OPENAI_KEYWORDS and
TALK_RS_PROVIDERS_OPENAI_LANGUAGES are comma-separated lists. talk-rs
trims whitespace and ignores empty entries deterministically; environment
values override YAML in both existing and environment-created OpenAI
sections.
See config.example.yaml for the full list.
| Flag | Effect |
|---|---|
-v | Increase logging verbosity (-vv debug, -vvv trace) |
--log-file <PATH> | Write logs to a file in addition to stderr (env TALK_RS_LOG_FILE); propagated to the toggle daemon |
Record, transcribe, and paste into the focused application:
talk-rs dictateToggle mode (ideal for keyboard shortcuts):
talk-rs dictate --toggleFirst call starts a background daemon that records. Second call stops recording, transcribes, and pastes the result.
Options:
| Flag | Effect |
|---|---|
--toggle | Daemon toggle mode |
--provider | Choose mistral or openai |
--model | Override model for this invocation |
--diarize | Enable speaker diarization (batch mode only) |
--realtime | Stream audio via WebSocket (incremental text) |
--pick | Show multi-candidate picker (GTK window) |
--retry-last | Re-transcribe the last cached recording |
--replace-last-paste | Delete previous paste before inserting new text |
--save <PATH> | Save audio recording to a file |
--output-yaml <FILE> | Write transcription metadata YAML |
--input-audio-file <FILE> | Feed a pre-recorded audio file instead of live mic |
--monitor | Mix system audio (monitor) with mic input |
--no-sounds | Disable audio indicators |
--no-boop | Disable periodic boop sounds (keep start/stop) |
--no-chunk-paste | Paste all text in one shot (disable chunking) |
--no-overlay | Disable visual overlay |
--no-auto-pause | Disable auto-pause during silence (forward all audio) |
--no-paste | Skip pasting transcription into the focused application |
--upload-format <FORMAT> | Audio format for batch uploads: wav (default) or ogg |
--viz <MODE> | In-badge visualizer: waterfall, amplitude, or spectrum |
--mono | Monochrome visualizer (theme-aware) |
--no-bt-auto-switch | Disable Bluetooth headset HFP auto-switch (overrides audio.bt_auto_switch) |
Capture audio to an OGG/Opus file:
talk-rs record # auto-named <output_dir>/YYYY/MM/YYYY-MM-DDTHH-MM-SS±ZZZZ.ogg
talk-rs record meeting-notes.ogg # custom filenameOptions:
| Flag | Effect |
|---|---|
--monitor | Mix system audio (monitor) with microphone input |
--ui | Open GTK4 recordings browser (play, delete, open folder) |
--no-bt-auto-switch | Disable Bluetooth headset HFP auto-switch (overrides audio.bt_auto_switch) |
Transcribe an existing audio file:
talk-rs transcribe recording.ogg # print to stdout
talk-rs transcribe recording.ogg output.txt # write to file
talk-rs transcribe recording.ogg --provider openai
talk-rs transcribe voice-memo.m4a # imported m4a also worksOptions:
| Flag | Effect |
|---|---|
--provider | Choose mistral or openai |
--model | Override model for this invocation |
--diarize | Enable speaker diarization (tag by speaker) |
The architectural mirror of transcribe: text in, speech out.
Synthesize text and play it through the speakers, or save it to a
WAV file. Two providers – kokoro (local, offline, on-device via
sherpa-onnx) and mistral (remote Voxtral TTS).
talk-rs speak "hello world" # synthesize + play
talk-rs speak --provider mistral --voice <id> "hi" # remote Voxtral TTS
talk-rs speak -o out.wav "save me to a file" # write WAV, don't play
echo "from a pipe" | talk-rs speak # read text from stdin
talk-rs speak -f message.txt # read text from a file
talk-rs speak --lang fr --voice ff_siwis "Bonjour" # French (Kokoro)Text is resolved in priority order: the positional argument, then
--file, then stdin (when stdin is not a TTY).
The default provider is resolved as --provider >
speak.default_provider (config) > the local kokoro backend when a
providers.kokoro section exists, else mistral. On first use the
Kokoro model (~350 MB) is downloaded after an interactive consent
prompt (or a non-interactive proceed-with-log, exactly like the
Parakeet ASR model) into
~~/.local/share/talk-rs/models/kokoro-multi-lang-v1_0/~.
Options:
| Flag | Effect |
|---|---|
--provider | Choose kokoro (local) or mistral (remote Voxtral) |
--voice | Kokoro voice name (af_heart, am_michael, ff_siwis, …) or a Mistral preset voice id |
--lang | Phonemization language for Kokoro (en, fr, …); ignored by Mistral |
--speed | Speech rate multiplier for Kokoro (1.0 = normal) |
-f, --file | Read the text to speak from a file |
-o, --output | Save synthesized audio to a WAV file instead of playing it |
Language handling is config-driven and agnostic: the requested
language selects the phonemizer. The stock Kokoro model ships one
baked language; other languages are derived on demand by patching the
model’s ONNX voice metadata, cached as model-<lang>.onnx next to
the stock model.
Commands that read audio from disk – transcribe,
dictate --input-audio-file, dictate --retry-last, and the
record --ui recordings browser – accept the following formats:
| Extension | Container / codec | How it is handled |
|---|---|---|
.ogg | Ogg Opus | Native format written by talk-rs record |
.m4a / .mp4 | MP4 / AAC | Decoded via symphonia; ideal for iPhone voice memos and similar imports |
.aac | Raw AAC stream | Decoded via symphonia |
.wav | 16-bit PCM | Decoded directly; primarily used for legacy cache entries |
Imported files keep their original extension on upload, so the
transcribe command sends .m4a to the provider as-is – both
Mistral Voxtral and OpenAI Whisper accept these natively. Playback
and waterfall spectrograms in the records browser work for every
listed format.
Bind talk-rs dictate --toggle to a key (e.g. Super+/):
BASE="org.gnome.settings-daemon.plugins.media-keys"
BPATH="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings"
CURRENT=$(gsettings get "$BASE" custom-keybindings)
N=0
while echo "$CURRENT" | grep -q "custom${N}/"; do
N=$((N + 1))
done
SLOT="${BPATH}/custom${N}/"
SCHEMA="${BASE}.custom-keybinding:${SLOT}"
if [ "$CURRENT" = "@as []" ]; then
gsettings set "$BASE" custom-keybindings "['${SLOT}']"
else
gsettings set "$BASE" custom-keybindings "$(echo "$CURRENT" | sed "s|]$|, '${SLOT}']|")"
fi
gsettings set "$SCHEMA" name 'talk-rs dictate'
gsettings set "$SCHEMA" command 'talk-rs dictate --toggle --viz waterfall'
gsettings set "$SCHEMA" binding '<Super>slash'First press starts recording, second press stops, transcribes, and pastes into the focused application.
To change the key, replace <Super>slash with the desired binding
(e.g. <Super>semicolon, <Super>d). Add --realtime to use
streaming transcription instead of batch mode.
cargo fmt # format
cargo clippy --all-targets # lint
cargo test # test
cargo build # buildThe default build enables everything (full dictation CLI). Library
consumers can opt out of the desktop stack with
default-features = false:
| Feature | Default | Pulls in | Provides |
|---|---|---|---|
| (core) | always | reqwest, tokio-tungstenite, opus, rubato, symphonia, … | Transcription providers (Mistral/OpenAI batch + realtime WS), Mistral Voxtral TTS, resampler, Opus/OGG encode, file decode, config, cache |
parakeet | yes | sherpa-onnx (static C++), tar, bzip2 | Local ASR backend (Parakeet TDT, CPU) |
kokoro | yes | sherpa-onnx (static C++), tar, bzip2 | Local TTS backend (Kokoro multi-lang, CPU) for speak |
playback | yes | cpal | Local audio playback (the speak command’s speakers output; shared player) |
capture | yes | pipewire, cpal (via playback), libpulse-binding | Live mic/monitor capture, BT headset HFP switch, indicator tones |
ui | yes | gtk4, x11rb, png, fontdue, dark-light, … (+ playback) | X11 overlay, visualizers, clipboard/paste, picker, recordings browser |
Example headless consumer (cloud transcription only):
talk-rs = { path = "../talk-rs", default-features = false }MIT – see LICENSE.