Skip to content

Repository files navigation

talk-rs

⚠️ 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.

Features

  • 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 speak command synthesizes text and plays it (or saves a WAV). Two providers mirror the transcription side: kokoro (local, offline, multi-language via sherpa-onnx) and mistral (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-pause is used. Fully configurable or disabled.
  • Bluetooth headset auto-switch – when a Bluetooth headset is connected in A2DP mode (high-quality stereo, no microphone), talk-rs automatically 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.json is 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 standard device.form_factor property, not vendor-specific identifiers). Disable per-invocation with --no-bt-auto-switch or globally via the audio.bt_auto_switch config 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-rs processes 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 browserrecord --ui opens a GTK4 window listing all recordings (OGG from output_dir) and dictation cache (OGG). Imported audio dropped into output_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-yaml includes per-segment start/end timestamps in the metadata sidecar, suitable for subtitles and post-processing.
  • Standalone commandsrecord and transcribe can be used independently for scripting.
  • Environment overrides – every config value can be set via TALK_RS_* environment variables.

Prerequisites

Build dependencies

# 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-devel

libpulse-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.

Runtime dependencies

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:

Installation

If building from a git clone, run ./autogen.sh first to resolve version placeholders in Cargo.toml:

./autogen.sh
cargo build --release

The binary is at target/release/talk-rs. Copy it somewhere in your $PATH:

cp target/release/talk-rs ~/.local/bin/

Configuration

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.yaml

Minimal working configuration:

output_dir: ~/talk-rs-output

providers:
  mistral:
    api_key: YOUR_MISTRAL_API_KEY

Required fields

FieldDescription
output_dirAbsolute path to a writable directory for recordings (a leading ~~~ is expanded to your home)
providers.mistral.api_keyMistral API key (if using Mistral)
providers.openai.api_keyOpenAI API key (if using OpenAI)

Optional fields

FieldDefaultDescription
providers.mistral.urlhttps://api.mistral.aiMistral API base URL
providers.mistral.modelvoxtral-mini-2507Mistral transcription model
providers.mistral.context_biasnoneComma-separated words for accuracy
providers.mistral.tts_modelvoxtral-mini-tts-latestVoxtral TTS model for speak --provider mistral (shares the STT api_key)
providers.mistral.tts_voicenoneDefault Mistral preset voice id for speak (else pass --voice)
providers.openai.urlhttps://api.openai.comOpenAI API base URL
providers.openai.modelgpt-transcribeOpenAI batch model
providers.openai.realtime_modelgpt-live-transcribeOpenAI realtime model
providers.openai.promptnonePrompt for spelling, punctuation, and context hints
providers.openai.keywordsnoneExpected vocabulary list for the new models
providers.openai.languagesnoneOrdered expected-language list
providers.openai.realtime_delaynoneRealtime delay: minimal, low, medium, high, or xhigh
providers.kokoro.model_dirXDG data dirKokoro TTS model cache dir (auto-downloaded on first speak)
providers.kokoro.voiceper-language defaultDefault Kokoro voice name (af_heart, ff_siwis, …)
providers.kokoro.num_threads4Kokoro inference threads
providers.kokoro.langen (model-baked)Default Kokoro phonemization language
transcription.default_providermistralDefault transcription provider when unspecified
speak.default_providerkokoro-if-configuredDefault speak provider (kokoro or mistral)
indicators.boop_interval_ms5000Periodic boop interval in ms (0 disables boops; also --no-boop)
indicators.visual_overlaytrueShow X11 overlay badge
indicators.viznoneIn-badge visualizer: waterfall, amplitude, or spectrum (also --viz; env TALK_RS_INDICATORS_VIZ)
indicators.monofalseMonochrome visualizer (also --mono)
paste.chunk_chars150Max chars per paste chunk (0 disables chunking; also --no-chunk-paste)
audio.bt_auto_switchtrueAuto-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_rate48000Sample rate (Hz) of the record command’s .ogg output (env TALK_RS_RECORDING_SAMPLE_RATE)
recording.channels1Channels for record output: 1 mono, 2 stereo (env TALK_RS_RECORDING_CHANNELS)
recording.bitrate128000Opus 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.

OpenAI model migration

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-transcribe sends prompt, repeated keywords[], repeated languages[], and response_format=json to the batch endpoint.
  • gpt-live-transcribe nests model, prompt, keywords, languages, and delay under session.audio.input.transcription in the realtime session.update.
  • whisper-1 remains available for segment/word timestamps, subtitles, and translation workflows. It retains response_format=verbose_json, accepts prompt and one unambiguous singular language, and does not accept keywords or multiple languages.
  • gpt-4o-transcribe and gpt-4o-mini-transcribe remain available as legacy batch choices. gpt-realtime-whisper remains available as a legacy realtime choice, using singular language for one expected language and rejecting keywords, multiple languages, and realtime_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.

Environment overrides

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.

Usage

Global options

FlagEffect
-vIncrease 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

Dictate (main workflow)

Record, transcribe, and paste into the focused application:

talk-rs dictate

Toggle mode (ideal for keyboard shortcuts):

talk-rs dictate --toggle

First call starts a background daemon that records. Second call stops recording, transcribes, and pastes the result.

Options:

FlagEffect
--toggleDaemon toggle mode
--providerChoose mistral or openai
--modelOverride model for this invocation
--diarizeEnable speaker diarization (batch mode only)
--realtimeStream audio via WebSocket (incremental text)
--pickShow multi-candidate picker (GTK window)
--retry-lastRe-transcribe the last cached recording
--replace-last-pasteDelete 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
--monitorMix system audio (monitor) with mic input
--no-soundsDisable audio indicators
--no-boopDisable periodic boop sounds (keep start/stop)
--no-chunk-pastePaste all text in one shot (disable chunking)
--no-overlayDisable visual overlay
--no-auto-pauseDisable auto-pause during silence (forward all audio)
--no-pasteSkip 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
--monoMonochrome visualizer (theme-aware)
--no-bt-auto-switchDisable Bluetooth headset HFP auto-switch (overrides audio.bt_auto_switch)

Record

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 filename

Options:

FlagEffect
--monitorMix system audio (monitor) with microphone input
--uiOpen GTK4 recordings browser (play, delete, open folder)
--no-bt-auto-switchDisable Bluetooth headset HFP auto-switch (overrides audio.bt_auto_switch)

Transcribe

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 works

Options:

FlagEffect
--providerChoose mistral or openai
--modelOverride model for this invocation
--diarizeEnable speaker diarization (tag by speaker)

Speak (text-to-speech)

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:

FlagEffect
--providerChoose kokoro (local) or mistral (remote Voxtral)
--voiceKokoro voice name (af_heart, am_michael, ff_siwis, …) or a Mistral preset voice id
--langPhonemization language for Kokoro (en, fr, …); ignored by Mistral
--speedSpeech rate multiplier for Kokoro (1.0 = normal)
-f, --fileRead the text to speak from a file
-o, --outputSave 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.

Supported input audio formats

Commands that read audio from disk – transcribe, dictate --input-audio-file, dictate --retry-last, and the record --ui recordings browser – accept the following formats:

ExtensionContainer / codecHow it is handled
.oggOgg OpusNative format written by talk-rs record
.m4a / .mp4MP4 / AACDecoded via symphonia; ideal for iPhone voice memos and similar imports
.aacRaw AAC streamDecoded via symphonia
.wav16-bit PCMDecoded 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.

GNOME keyboard shortcut

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.

Development

cargo fmt                     # format
cargo clippy --all-targets    # lint
cargo test                    # test
cargo build                   # build

Cargo feature flags

The default build enables everything (full dictation CLI). Library consumers can opt out of the desktop stack with default-features = false:

FeatureDefaultPulls inProvides
(core)alwaysreqwest, tokio-tungstenite, opus, rubato, symphonia, …Transcription providers (Mistral/OpenAI batch + realtime WS), Mistral Voxtral TTS, resampler, Opus/OGG encode, file decode, config, cache
parakeetyessherpa-onnx (static C++), tar, bzip2Local ASR backend (Parakeet TDT, CPU)
kokoroyessherpa-onnx (static C++), tar, bzip2Local TTS backend (Kokoro multi-lang, CPU) for speak
playbackyescpalLocal audio playback (the speak command’s speakers output; shared player)
captureyespipewire, cpal (via playback), libpulse-bindingLive mic/monitor capture, BT headset HFP switch, indicator tones
uiyesgtk4, 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 }

License

MIT – see LICENSE.

About

Voice dictation CLI for Linux (Rust, GTK4, PipeWire, Whisper)

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages