WHISRS(1)

πŸ“¦ version 0.1.30, in nixpkgs unstable

πŸ’– WHISRS(1) Β· User Commands Β· Linux-first voice-to-text dictation CLI πŸ’–

✨ NAME ✨

whisrs β€” Linux-first voice-to-text dictation CLI. 🎀✨

🌸 SYNOPSIS 🌸

whisrs subcommand

πŸ¦‹ DESCRIPTION πŸ¦‹

Okay so whisrs is a thin CLI client, and it talks to whisrsd(1) over a Unix domain socket. It sends commands (toggle, cancel, status) and prints the daemon's response. πŸ’…

The setup subcommand runs an interactive onboarding flow and does not require the daemon. 🫢


πŸ’… SUBCOMMANDS πŸ’…

setup

Interactive onboarding β€” select a transcription backend, enter an API key, test the microphone, and write config.toml. Does not require the daemon to be running. πŸŽ€

config

Open an interactive menu to edit any section of config.toml β€” backend, language, hotkeys, overlay, command-mode LLM, and more. On save, writes the file and restarts the daemon via systemd. Unlike setup, this does not touch udev rules, the systemd unit, or compositor keybindings. ✨

toggle

Start or stop dictation. If idle, it begins audio capture. If recording, it stops capture and sends the audio for transcription. Text is typed at the cursor in the previously focused window. 🎀

  • -l, --language CODE β€” override the transcription language for this session only β€” no config edit or daemon restart. CODE is an ISO 639-1 code (e.g. en, pl), optionally with a region (e.g. en-US), or auto for automatic detection. Applies to the toggle that starts recording; the override ends with the session. A plain toggle keeps using the configured general.language, so an existing hotkey is unaffected. Bind a separate key per language to dictate in each without switching settings. πŸ’–

cancel

Cancel the current dictation and discard captured audio. Returns the daemon to the idle state. Has no effect if the daemon is not recording. πŸ™ˆ

log

Show recent transcription history. Nothing is recorded when [general] history is false. πŸ“œ

  • -n, --limit N β€” number of entries to show. Defaults to 20.
  • --clear β€” clear all history instead of showing it.

command

Command mode: select text, speak an instruction, and the LLM rewrites it in place. Requires a selection. To write text where there is none, configure an [[llm_commands]] entry with a generic β€œtreat the following text as a request and output only what is asked” instruction β€” that path never reads the selection. See https://github.com/y0sif/whisrs/blob/main/docs/configuration.md for the recipe. πŸͺ„

llm-command NAME

Toggle a named custom LLM command (see [[llm_commands]] in config.toml): dictate, the LLM applies the configured instruction, and the result is typed at the cursor. Press again to stop recording, same as toggle. πŸ”

llm-command-set NAME

Reprogram a named LLM command from the current selection: highlight the new instruction text, then run this β€” it is saved to config.toml and applied live. Pairs with an entry's set_hotkey. πŸ’Ύ

speak

Read the current text selection aloud via text-to-speech. A second speak or a cancel stops playback. Alias: read. πŸ”Š

status

Query the dictation state (for a recording, see whisrs recording status). Prints one of: idle, recording, transcribing, synthesizing, or speaking. πŸ‘‘

restart

Restart the whisrs daemon. Uses systemctl --user restart whisrs.service on systemd systems and rc-service --user whisrs restart on OpenRC systems, when the corresponding user service is installed; otherwise prints guidance for manual restart. Does not require the daemon to be running: restart will start a stopped service. πŸ”„

recording

Record the microphone and the system audio (the default output's monitor) as two separate tracks and turn them into a transcript, for a meeting or call, a lecture, a video or language practice. πŸŽ™οΈ The tracks are transcribed after stop into a Markdown transcript whose speaker labels are your name ([recording] user_name) and β€œOthers”, or, when [recording] diarization tells the voices on the system audio apart, β€œSpeaker 1”, β€œSpeaker 2” and so on (a single voice keeps β€œOthers”), unless a voice is named: by the people given with --with, or, with [recording] name_speakers (off by default; it sends transcript excerpts to the [llm] endpoint), by the LLM from a quote of the transcript that shows the name. With [recording] summary (off by default; it sends the whole transcript to the [llm] endpoint), each transcript also gets a summary from the LLM, and a recording you gave no title gets one. Each track is transcribed in its own language: [recording] mic_language and system_language, which default to [recording] language and then to [general] language. A recording keeps the languages it started with. While a recording is in progress, whisrs toggle, whisrs command and whisrs llm-command refuse to start, since the microphone is shared with the recording. Each recording lives in its own folder under the recordings directory (see FILES), named by its id (YYYY-MM-DD_HHMM, plus a slug of the title). Wherever an ID is taken, it may be a full id, a unique id prefix, or last for the newest recording.

Requires an action: πŸ’«

start

start [-t, --title TITLE] [-l, --language LANG] [--mic-language LANG] [--system-language LANG] [--others NAME] [--shared-mic[=BOOL]] [--num-speakers N] [--with NAMES]
  • -t, --title TITLE β€” the optional title goes into the transcript and the folder name. 🏷️
  • -l, --language LANG β€” sets the language of both tracks for this recording (an ISO 639 code such as de, optionally with a region, or auto).
  • --mic-language LANG β€” sets one track's language, over -l.
  • --system-language LANG β€” sets one track's language, over -l.
  • --others NAME β€” names the system track in the transcript instead of β€œOthers”.
  • --shared-mic[=BOOL] β€” says several people share your mic, so its voices are told apart too (--shared-mic=false turns [recording] shared_mic off for this recording).
  • --num-speakers N β€” tells a diarization sidecar ([recording] diarization_url) how many voices the system audio holds (your mic, when there is no system audio), from 0 (let it decide) to 20.
  • --with NAMES β€” says who the recording is with, names separated by commas (at most 20), to name the voices on the system audio: one voice and one name, or the last voice left once the others have names. 🫢

stop

stop β€” stop the recording and queue it for transcription. πŸ›‘

toggle

toggle [-t TITLE] [-l LANG] [--mic-language LANG] [--system-language LANG] [--others NAME] [--shared-mic[=BOOL]] [--num-speakers N] [--with NAMES]

Start a recording when none is in progress, otherwise stop the one that is. Meant for a compositor hotkey. The options are those of start and apply when this starts a recording. πŸŽ›οΈ

cancel

cancel β€” stop the recording and delete it. πŸ—‘οΈ

status

status β€” print whether a recording is in progress or processing, and which recordings are waiting for the network. πŸ“‘

list

list [--json]

List recordings, newest first, with id, start time, duration, status and title.

  • --json β€” prints a JSON array instead.

show

show ID [-f, --format md|json|txt]

Print a transcript.

  • -f, --format md|json|txt β€” md (the default) prints the Markdown file, json prints recording.json, and txt prints plain text. πŸ“„

path

path ID β€” print the recording's folder. πŸ“

copy

copy ID β€” copy the transcript to the clipboard. πŸ“‹

name

name ID SPEAKER NAME

Name a speaker of a transcribed recording. SPEAKER is its id (such as S2), β€œSpeaker 2”, or the name it has now; NAME may be several words, and an empty one ("") clears the name you gave. The transcript and recording.json are rewritten at once, nothing is transcribed again, and retry keeps the name, which works while a summary is being made; a summary made before the name is marked as such. Refused while the recording is queued or being transcribed. 🏷️

retry

retry ID [-l LANG] [--mic-language LANG] [--system-language LANG] [--shared-mic[=BOOL]] [--num-speakers N] [--with NAMES]

Transcribe the recording again, for example after a failure or in another language. πŸ”₯

  • -l, --language LANG β€” a language given here replaces the one the recording saved; as for start, -l covers both tracks; a track left out keeps its own.
  • --mic-language LANG β€” a language given here replaces the saved one; a track left out keeps its own.
  • --system-language LANG β€” a language given here replaces the saved one; a track left out keeps its own.
  • --shared-mic[=BOOL] β€” replaces the saved setting; leaving it out keeps it.
  • --num-speakers N β€” replaces the saved setting; leaving it out keeps it.
  • --with NAMES β€” replaces the saved setting; leaving it out keeps it (--with "" clears the names).

A recording whose transcription was interrupted three times (by a crash each time) is marked failed instead of being resumed at startup; retry resets that count and queues it again. After a failure, chunks already transcribed with the same backend and settings, language included, are reused; a recording that finished keeps none, so its retry sends both tracks again. With [recording] summary on, a retry makes a new summary; with it off, the summary is kept and marked as from an earlier transcript. 🌈

✨ summarize ID ✨

Okay so here's the vibe: this one summarizes a recording you already transcribed, using the [llm] right now β€” whatever [recording] summary happens to say, it does not transcribe the thing again. Iconic, honestly.

Where does the summary go? Two places, bestie:

  • In the Markdown file, right before the transcript πŸ’–
  • Into recording.json, under summary

Also, a recording you never gave a title to gets one β€” a whole brand-new title of 3 to 6 words. And a title you gave? Never touched. Never changed. Periodt. 🫢

The default rules it follows are:

  • a TL;DR
  • sections by topic
  • decisions and next steps, only when there are any

...and you can replace all of that with [recording] summary_instruction if you want your own flavor. 🌸

Important tea: a transcript of 50 words or fewer is not sent at all. Nope. Not happening.

The job waits behind any recording that's being transcribed, and a notification tells you when it's done. If it fails? The recording keeps the summary it already had and notes why it failed. A summary interrupted by a shutdown gets made at the next start β€” it comes back for you, lowkey loyal. 🧚

One job runs at a time: a running summary delays the next transcription until it ends. So yeah, patience, babe.

✨ mcp ✨

Serve your recordings to an AI agent, because why gatekeep πŸŽ€ It's a read-only MCP (Model Context Protocol) server on standard input and output, for a client such as Claude Code:

claude mcp add whisrs -- whisrs mcp

Its tools can:

  • list recordings
  • give one recording's details and summary
  • page through a transcript
  • search titles, summaries and transcripts

It reads the recordings directory directly, works fine without the daemon, and never, ever changes a recording. The mcp feature needs the recording feature, and a build without mcp says so and exits 1.

Also β€” quick reminder from the girls: list, show and path read the recordings directory directly and work without the daemon. A build without the recording feature says so and exits 1, whatever the action. πŸ’…


✨ LLM OUTPUT ✨

This whole section applies to command and to every [[llm_commands]] entry. Both of them type the model's reply right at your cursor. πŸ’–

The fence thing. A reply wrapped entirely in a Markdown code fence gets unwrapped before it's typed β€” a fence is never what you actually wanted sitting at your cursor, let's be real. But a reply with several fenced blocks? That's prose about code, not a wrapper, so it's left alone. πŸ™ˆ

Stripping has no opt-out. So an entry whose instruction asks for a fenced block literally cannot produce one. That's just how it is, bestie.

Multi-line replies. A reply spanning several lines is typed normally β€” translating a paragraph or drafting an email is exactly what these commands are for ✨

The one exception: a terminal. In a terminal, a line break is an Enter that runs a command you haven't even read yet. Spooky. So a multi-line reply aimed at a terminal is refused rather than typed 😭

  • It gets written to the history, where whisrs log can recover it.
  • With [general] history set to false there's no copy, and it's just discarded.
  • Ask for a one-liner if you want an injected result. 🎯

The refusal also applies with [input] paste set, since bracketed paste is the foreground program's choice and cannot be relied on. And its notification fires even with [general] notify turned off, because nothing was typed β€” so it has to tell you somehow, right? πŸ’€

With [input] clipboard_only set, there is no refusal: whisrs types and pastes nothing, so the reply gets copied to the clipboard like any other result. Pasting it into a shell yourself carries the same caveat about bracketed paste, though. You've been warned. πŸ‘‘

Terminal detection needs WindowTracker::get_focused_window_class, which Hyprland, Niri, Sway and X11 implement. On GNOME and KDE it reports nothing, so a terminal there is treated as an ordinary target and multi-line replies are typed. So it goes. πŸ¦‹


✨ ENVIRONMENT ✨

  • WHISRS_GROQ_API_KEY β€” your Groq API key. Overrides the value in config.toml.
  • WHISRS_OPENAI_API_KEY β€” your OpenAI API key. Overrides the value in config.toml.
  • XDG_RUNTIME_DIR β€” directory for the Unix socket. The socket gets created at $XDG_RUNTIME_DIR/whisrs.sock. Falls back to /tmp/whisrs-<uid>.sock.
  • XDG_CONFIG_HOME β€” directory for configuration. The config file is read from $XDG_CONFIG_HOME/whisrs/config.toml. Defaults to ~/.config/whisrs/config.toml.

✨ FILES ✨

  • ~/.config/whisrs/config.toml β€” the main configuration file, created by whisrs setup. Contains backend selection, API keys, language, audio settings, and hooks.
Permissions should be 0600 since it may contain API keys. Lock it down, babe πŸ”’
  • $XDG_RUNTIME_DIR/whisrs.sock β€” the Unix domain socket for CLI-daemon IPC.
  • ~/.cache/whisrs/recovery/ β€” the recovery directory for audio that failed transcription. 😭
  • $XDG_DATA_HOME/whisrs/recordings/ β€” recordings and their transcripts, one folder per recording. Defaults to ~/.local/share/whisrs/recordings/; override with [recording] dir in config.toml. πŸ’–

✨ EXIT STATUS ✨

  • 0 β€” Success. Slay. ✨
  • 1 β€” Error: daemon not running, invalid state transition, or setup failure. πŸ’€

✨ EXAMPLES ✨

Run interactive setup:

whisrs setup

Start dictation (bind this to a hotkey):

whisrs toggle

Start dictation in Polish for this session only:

whisrs toggle -l pl

Cancel a dictation:

whisrs cancel

Check daemon state:

whisrs status

✨ SEE ALSO ✨

whisrsd(1) β€” go say hi to the daemon's own page 🫢

✨ AUTHORS ✨

whisrs contributors β€” the whole squad πŸ‘‘

✨ LICENSE ✨

MIT 🌈


Okay that's the whole page, bestie β€” you ate that. This was whisrs version 0.1.30, dated 2026-10-20. Go forth and dictate, mwah πŸ’‹

πŸ“¦ where this page comes from