A hotkey-invoked desktop tool that takes text you type, dictate, or hold on the clipboard, optionally restyles it with an LLM, and delivers it by typing, pasting, or leaving it on the clipboard.
Its behaviour is five independent flags, so the command line states exactly what will happen:
--input keyboard|voice|clipboard where the text comes from (default keyboard)
--window | --no-window show a review window, or run unattended
--deliver type|paste|clipboard|stdout how the result leaves (stdout prints it)
--style[=NAME] apply a style pass (needs a configured AI provider)
--provider NAME pick a [provider.NAME] from config.ini
--quotes double|single|straight quotation style (default double)
--dialect off|british British spelling conversion (default off)
The style pass is the only feature that needs an API key. With no configuration and no key, scribe still runs as a dictation tool — the style pass and the styled review pane are simply absent.
Homebrew (Linux and macOS):
brew tap overseers-desk/od
brew install scribe
On macOS the formula pulls sox for audio capture; keystrokes and clipboard go
through the system's own osascript and pbcopy. On Linux the formula installs
scribe and Tcl/Tk only. For --input voice, install whisper.cpp separately
(brew install whisper-cpp provides whisper-cli) and supply a whisper model
file such as ggml-medium.en.bin, named in the [whisper] section of
config.ini or passed with --model. On Linux, a recorder (pw-record, or sox
as fallback) and dotool must also be on PATH (see Dependencies below).
Runtime commands (must be on PATH):
| Command | Provides | Needed for |
|---|---|---|
whisper-cli |
speech-to-text (whisper.cpp) | --input voice, local transcription |
curl |
POST audio to a whisper.cpp server | --input voice with a [whisper] server |
pw-record |
audio capture (PipeWire; preferred on Linux) | --input voice |
sox |
audio capture (macOS via coreaudio; Linux fallback when pw-record is absent) | --input voice |
dotool |
keystroke injection via uinput (Linux) | --deliver type, and the paste keystroke |
On macOS, keystrokes go through osascript (System Events) and the clipboard
through pbcopy/pbpaste, both of which ship with the OS. Grant the app that
launches scribe (e.g. your terminal) Accessibility permission for
typing/pasting and Microphone permission for recording, under System
Settings → Privacy & Security.
Other requirements:
-
Tcl/Tk 9 with a working
wish9.0andtk systray. The Tcl packageshttp,tls,json,yaml, andcsvmust be available to that interpreter. On Ubuntu those were provided by tcllib. With OS X brew they came with tcl9. -
A whisper model file (for example
ggml-medium.en.bin) for--input voice, named in the[whisper]section ofconfig.ini(model = ...) or passed with--model. There is no built-in default path. -
An AI provider in
config.ini, only for--style(optional; see below). -
dotoolneeds access to/dev/uinput(typically membership of theinputgroup). For non-ASCII characters (curly quotes, accented names) the--deliver typepath uses IBus Ctrl+Shift+U, so IBus (or fcitx) should be running.
-
(Optional, only for
--style) Copyconfig.example.inito~/.config/scribe/config.iniand fill in a provider:default_provider = deepseek [provider.deepseek] api_key = sk-your-key-here model = deepseek-chat api_base = https://api.deepseek.com
Add more
[provider.NAME]sections (e.g.claude,chatgpt, a local Ollama model) and pick one with--provider NAMEordefault_provider. Skip this entirely to run dictation only. A legacy single-providerdeepseek.jsonis still honoured if present. -
Bind the presets you want to global shortcuts (GNOME custom keyboard shortcuts, or your desktop's equivalent).
A second press of a
--input voiceshortcut stops the recording started by the first.For example, to bind dictation to the
Insertkey under GNOME, add a custom keybinding whose command is:code/scribe/scribe.tcl --input voice --deliver paste --dialect british \ --timeout 300 --window --model code/whisper.cpp/models/ggml-medium.en.bin \ --prompt-file ~/.whisper-prompt-filedir=/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ base=org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:$dir gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "['$dir']" gsettings set "$base" name 'Insert Voice Message' gsettings set "$base" binding 'Insert' gsettings set "$base" command '[the above launch command]'
| Goal | Command |
|---|---|
| Dictate straight into the focused window | scribe.tcl --input voice --no-window --deliver type |
| Dictate, review, then paste | scribe.tcl --input voice --window --style --auto-style-delay 1000 --deliver paste |
| Restyle the clipboard, review, copy back | scribe.tcl --input clipboard --window --style --auto-style-delay 1 --deliver clipboard |
With no --input, scribe defaults to keyboard: it opens an empty window for you
to type into. --no-window needs --input voice or --input clipboard, since
there is nothing to type into without a window.
When a window is shown it has two panes, the dictated text and the result, with one highlighted, and the history list down the left. The rewrite controls and the result pane appear only when a provider is configured. Both panes are editable: click into one to correct the text before rewriting or delivering.
A Listen button in the pane header records from the window itself: press,
dictate, and press again (or Escape) to stop. The transcript lands in the
pane, appended after any text already there. It covers windows opened without
--input voice, and the global shortcut's second press stops it like any
other recording.
Between the panes, two rows of radios pick what a Rewrite click does. Both
choices are remembered between runs, and unattended (--no-window --style)
runs use them too.
- Style: "No style" (the default) runs the clean-up alone; picking a style applies its guide on top of the clean-up. The clean-up repairs what composing in one take leaves behind: repeated versions of a point merged into the fullest one, mid-stream self-corrections resolved, and points reordered into the sequence the author would have chosen (a prerequisite recalled late moves ahead of what depends on it).
- Passes (greyed under "No style", where it is moot): 2 — clean up,
then style (the default) repairs first, then restyles the repaired text;
the source pane keeps the raw dictation, and the result pane shows the
repaired text until the styled text replaces it. 1 — merged prompt does
both in one call. Best on a reasoning model: set
thinking_modelin the provider's config section, otherwise the call goes to the provider's regularmodel.
The keys depend on focus. With the window itself focused (as it opens after voice or clipboard input), Space delivers, Enter delivers and then sends a return, and Up/Down switch the highlighted pane. Once you click into a pane to edit, Space and Enter type normally; deliver with Ctrl+Enter or the button. Escape closes without pasting and throws the text away; Shift+Escape closes without pasting and keeps the text in history. Closing the window (or the Copy button) copies to the clipboard first. In keyboard mode the window opens with the cursor already in the pane, ready to type.
Every text scribe delivers is kept. Shift+Escape keeps one without
delivering it: the window closes, nothing is pasted, and the entry is listed
with a * to say it has not been used yet. The list runs down the left of the
window, newest first, showing the time and the opening words of each entry.
Selecting one brings it back into the panes, both the dictation and its rewrite,
ready to edit or deliver. Deliver it and the * goes.
Entries live in history.tsv under ~/.local/state/scribe/, four
tab-separated columns: date, mark, original, rewrite. Line breaks inside an
entry are stored as carriage returns, so one entry is always one line and the
file reads in anything that reads TSV. The newest 999 entries are kept; when it
fills, the oldest entry without a * goes first, so text set aside outlasts the
ordinary deliveries piling up in front of it.
--quotesrewrites straight quotes:doublegives “ ” and ’,singlegives ‘ ’ and ’,straightleaves ASCII.--dialect britishmakessinglethe default unless--quotesis given.--dialect britishconverts US spelling to British usingdialect-us-to-british.tsvplus-ize/-isesuffix rules. There is noustarget on purpose; see the comment inscribe.tclfor why.
config.ini(~/.config/scribe/): AI providers for the style pass, and an optional[whisper]transcription backend. Optional;[provider.NAME]sections plusdefault_provider, and[whisper]server_url/fallback_local. Seeconfig.example.ini.styles/*.txt: style guides, one per file; the name is the--stylevalue.current-mode.conf: the last-used style name, used when--stylehas no name.system-prompts.yaml: the wrapper text around the style guide and user text.dialect-us-to-british.tsv: US to British spelling pairs.
By default scribe transcribes locally with whisper-cli. To offload transcription
to a whisper.cpp whisper-server (on this or another machine), add a [whisper]
section to config.ini, or pass --whisper-server URL:
[whisper]
model = /path/to/ggml-medium.en.bin # local transcription (or pass --model)
server_url = http://localhost:8080 # or offload to a server
fallback_local = true # if it is down, use whisper-cliYou run the server yourself (scribe only reaches the URL); server mode needs
curl. With fallback_local, keep model set (or pass --model) so the local
path can take over. For purely local transcription, set model and leave
server_url out.
To test the loop headlessly (e.g. over SSH, where there is no display), pair
--deliver stdout with a virtual display: xvfb-run -a scribe.tcl --input voice --test-file sample.wav --no-window --deliver stdout prints the transcript instead
of typing or pasting it. scribe is a Tk app, so it still needs a display; xvfb-run
supplies a throwaway one.
wish9.0 scribe.tcl --self-test
Runs the quote, dialect, injection, delivery, validation, rewrite-pipeline
(all three style/passes combinations, when a provider is configured),
second-press protocol, clipboard, and UI checks without a microphone, and
exits with the result.
--test-text "…" drives the window with fixed text instead of the mic.