talk-rs 0.7.0

Voice dictation for Linux -- record, transcribe, and paste
docs.rs failed to build talk-rs-0.7.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

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 browser -- record --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 commands -- record 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.87+). 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

| 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) |

Optional fields

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

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

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

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:

| 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) |

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
talk-rs record --toggle              # first call starts, second call stops
talk-rs record --toggle meeting.ogg  # toggle with an explicit output path

Toggle mode starts a background recorder on the first call. The second call sends SIGINT; the recorder stops capture, finalizes and syncs the audio file, then exits. Its PID remains published during finalization, so repeated stop calls cannot start a second writer for the same toggle slot. Toggle mode conflicts with --ui.

Foreground and toggle recording use the same feedback as dictation: the start tone finishes before capture begins, an optional X11 badge visualizes the captured PCM, and the periodic boop sounds only while the badge detects silence. On stop, the boop and badge are torn down before capture closes. The stop tone sounds only after the encoder has finalized the file and sync_all() has made it durable. If X11 is unavailable, recording and sound feedback continue without the badge.

Options:

| Flag | Effect | | --toggle | Toggle background recording on the first/second call | | --monitor | Mix system audio (monitor) with microphone input | | --no-sounds | Disable start, stop, and boop sounds | | --no-boop | Disable silence-gated boops (keep start/stop) | | --no-overlay | Disable the recording badge | | --viz <MODE> | In-badge visualizer: waterfall, amplitude, or spectrum | | --mono | Monochrome visualizer (theme-aware) | | --ui | Open GTK4 recordings browser (play, delete, open folder) | | --no-bt-auto-switch | Disable Bluetooth headset HFP auto-switch (overrides audio.bt_auto_switch) |

Toggle state is stored under $XDG_CACHE_HOME/talk-rs/ (normally ~~/.cache/talk-rs/~). Dictation keeps its existing daemon.pid, daemon.lock, and daemon.log files. Standalone recording uses the separate record.pid, record.lock, and record.log files, so the two commands cannot stop or overwrite each other's toggle state.

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:

| Flag | Effect | | --provider | Choose mistral or openai | | --model | Override model for this invocation | | --diarize | Enable speaker diarization (tag by speaker) |

Provider overload

Cloud providers periodically answer 503 high load or 429 backend_out_of_capacity, especially on long recordings. talk-rs treats every 5xx and 429 as "busy, try again later" and waits between attempts: 5 s, 15 s, 30 s, 60 s, 120 s, 120 s (about 5.5 minutes in total, seven attempts). A Retry-After header from the provider overrides the schedule slot (capped at 120 s). Each wait is logged at -v as server retry N/6. Other 4xx answers are permanent and fail immediately.

Connection failures (DNS, unreachable host, TLS timeout) use a separate, shorter schedule and never consume the server-retry budget. Large uploads are given the whole request time budget on every attempt, so an 80-minute recording is not cut off by the early connection slots.

If all seven attempts fail the recording is kept in the cache; re-run transcribe on the .ogg (or dictate --retry-last) once the provider has recovered.

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:

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

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:

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

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:

| 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 }

License

MIT -- see LICENSE.