Push-to-talk voice typing for your terminal.
Press a hotkey, speak, press again — your words appear wherever you're typing. Works with any terminal, IDE, or text field. Local transcription, no cloud services required.
This is ChristianGeng/talktype, a fork of lmacan1/talktype. It adds:
- Streaming: words appear while you are still speaking, with Whisper, NVIDIA Parakeet, or the natively streaming NVIDIA Nemotron.
- Text straight into kitty via
kitten @ send-text: no clipboard, Unicode intact, and it passes through SSH to remote hosts.- Text straight into Emacs via
emacsclientandtalktype.el: the buffer is edited by position, so evil's normal state or the minibuffer cannot turn words into commands, and one undo removes a dictation.- Any key as hotkey (
pause,menu,f13, and on Linux any X key name such asXF86Tools;talktype --which-keynames the key you press), with held keys no longer toggling recording on auto-repeat.- A setup wizard and systemd service that work in more setups, and tests in CI.
When you type, you self-edit and truncate. When you speak, you explain naturally and fully. TalkType bridges that gap — letting you talk to your terminal, your AI assistant, or any app, and have your words appear instantly.
Built for developers who want:
- Voice input for CLI tools like Claude Code, aider, or any terminal app
- System-wide dictation that works anywhere — terminals, IDEs, browsers
- Local, private transcription — your voice never leaves your machine
- Low latency — with streaming, text follows your speech
- Push-to-talk: press F9 to start, speak, press F9 to stop
- Streaming: optionally type while you speak (Whisper, Parakeet, Nemotron)
- Works everywhere: browsers, IDEs, terminals, and kitty directly
- Cross-platform: Linux, Windows, macOS
- Local models: faster-whisper, Parakeet and Nemotron run on your machine
- API mode: connect to any Whisper-compatible API server
- Smart paste: detects terminals vs other apps (Ctrl+Shift+V vs Ctrl+V)
- Window focus: remembers where you started — switch apps while speaking
- Configurable: hotkeys, model, language, streaming engine and route
TalkType is installed as a uv tool straight
from the fork; this puts the talktype command on your PATH
(~/.local/bin). The local extra brings faster-whisper for local
transcription.
sudo apt install xdotool xclip libportaudio2
uv tool install 'talktype[local] @ git+https://github.com/ChristianGeng/talktype'
talktype # the setup wizard runs on first startOr let the installer do all of it on Debian/Ubuntu, Fedora, Arch or
openSUSE: it installs the system packages, uv if it is missing, and
TalkType as a uv tool. Extras go in TALKTYPE_EXTRAS or --extras
(default local); INSTALL_DRY_RUN=1 only prints the commands.
curl -LsSf https://raw.githubusercontent.com/ChristianGeng/talktype/main/install.sh | bash
TALKTYPE_EXTRAS=local,nemotron ./install.sh # from a cloneOptional engines for streaming (see Streaming):
uv tool install --force 'talktype[local,parakeet] @ git+https://github.com/ChristianGeng/talktype'
uv tool install --force 'talktype[local,nemotron] @ git+https://github.com/ChristianGeng/talktype'The nemotron extra needs Python 3.11 or newer (onnxruntime-genai has no
3.10 wheels); on 3.10 it installs nothing.
uv tool install "talktype[local] @ git+https://github.com/ChristianGeng/talktype"
talktype # the setup wizard runs on first startbrew install portaudio
uv tool install 'talktype[local] @ git+https://github.com/ChristianGeng/talktype'
talktype # the setup wizard runs on first startgit clone https://github.com/ChristianGeng/talktype.git && cd talktype
uv run --extra local talktype # run from the checkout
xvfb-run -a uv run pytest -q # tests (talktype imports pynput, which needs X)
emacs --batch -Q -L . -l tests/talktype-test.el -f ert-run-tests-batch-and-exit # talktype.elWith emacs and emacsclient installed, pytest also runs an end-to-end test
against a throwaway headless Emacs server.
On first launch in a terminal, TalkType runs an interactive setup wizard:
talktypeThe wizard lets you:
- Choose transcription mode (local server, cloud API, or local model)
- Set the hotkey by pressing it (Enter keeps the default); on Linux also
keys that only have an X name, such as a media key (
XF86Tools). It saves the nametalktype --which-keyprints - Select Whisper model and language
- Optionally install a systemd user service (runs on login)
Config is saved to ~/.config/talktype/config.yaml. Re-run it with
talktype --setup. talktype --help and starts without a terminal (e.g.
under systemd) skip the wizard.
talktype # uses the saved config- Press F9 to start recording (beep)
- Speak your text
- Press F9 again to stop (beep)
- Your words appear in the focused window
| Key | What it does |
|---|---|
| F9 | Record / Stop & Paste |
| unbound | Re-paste last transcription (if paste failed): hotkeys.recovery |
| unbound | Retry transcription (if API timed out): hotkeys.retry |
| unbound | Remove the last dictation (Emacs, kitty prompt): hotkeys.undo |
The record key has three modes (hotkeys.record_mode, or --record-mode):
toggle(default): press to start, press again to stop.hold: hold the key to talk, release to stop (push-to-talk).auto: a tap toggles as above; holding the key for at leasthold_ms(default 500) records until you let go. Recording starts at the press, so there is no delay either way. The hold counts from when recording has started (the start beep), so a microphone that is slow to open doesn't turn a tap into a hold.
Use a key that does not type (a function key, Pause, Scroll Lock, Right Ctrl): holding a typing key such as Space would put repeated characters into the focused window.
TalkType listens for its keys but does not grab them, so the focused program
gets the key as well: F10 also opens Emacs' and Chrome's menu, for example.
If your function keys are taken, Pause is the most portable choice: full-size
and tenkeyless keyboards have it as a key of its own, compact ones on an Fn
layer, and neither programs nor desktops bind it. What other keys do in the
focused program, as observed on the author's private laptop (Ubuntu with GNOME on X11, Doom
Emacs with evil, Chrome, kitty with byobu over SSH). These are suggestions,
not rules: other keyboards, layouts, desktops and configurations bind keys
differently, so check a key with talktype --which-key and by pressing it
in the programs you use:
Key (hotkeys.record) |
Emacs | Browser (Chrome) | Terminal (kitty, byobu) | Desktop (GNOME) | Notes |
|---|---|---|---|---|---|
pause |
unbound | nothing | nothing, but ESC[57362u once a program turns on the kitty keyboard protocol (see below) |
nothing | recommended; on compact keyboards Fn + a key marked Pause/Break |
scroll_lock |
unbound | nothing | nothing, but ESC[57359u if a program asks the kitty keyboard protocol for all keys |
nothing | as good as Pause where it exists; toggles the Scroll Lock LED |
f13 … f20 |
unbound | nothing | escape sequence | nothing | few keyboards have them; a key remapper (e.g. keyd) can map a key to one |
f9, f10, f11, f12 |
F10 opens the menu | F11 full screen, F12 developer tools | byobu: F9 menu, F12 prefix | nothing | the default F9; kitty can drop a key with map f9 discard_event |
insert |
toggles overwrite mode | nothing | escape sequence | nothing | |
menu |
M-x |
context menu | nothing | nothing | not recommended |
ctrl_r, alt_r |
modifier | modifier | modifier | modifier | breaks shortcuts typed with that key; AltGr types @, { on many layouts |
XF86Tools, XF86Mail, XF86Calculator, XF86Explorer (X names) |
unbound | nothing | nothing | GNOME opens Settings, mail, calculator, files | only on keyboards with those media keys |
| Space, letters, numpad | type text | type text | type text | never: a held key types repeated characters |
"Nothing" in the terminal column holds for the classic key encoding. Some
programs, Claude Code among them, switch kitty to the
kitty keyboard protocol,
and then kitty sends an escape sequence for keys like Pause as well. Passed
through byobu or tmux, Emacs and vterm, part of it can end up as text: Claude
Code in vterm on a remote host showed 62uHello62u (the tail of
ESC[57362u, once at the start and once at the stop). Since TalkType sees its
key at the X server before kitty, let kitty drop it:
# kitty.conf
map pause discard_event
On Linux (X11) a key can also be any X key name, as xev or
xmodmap -pke print it, besides pynput's names (f10, pause,
scroll_lock, ...). Media keys only exist on some keyboards, and desktops
often bind them: GNOME opens Settings on XF86Tools
(gsettings get org.gnome.settings-daemon.plugins.media-keys control-center-static),
the calculator on XF86Calculator, mail on XF86Mail. Check that nothing
happens when you press one before you use it:
hotkeys:
record: pause # or an X key name: XF86Tools, xf86tools, XF86_Tools, 0x1008ff81To find a key's name, run talktype --which-key and press the key: it
prints the name to put in hotkeys.record and exits (it works while
TalkType is running). Windows and macOS take pynput's names only.
Only recording has a key by default; every bound key is taken away from the
focused window. Give the others a key in the config file if you want
them, and use null to leave any action unbound:
hotkeys:
record: f9
record_mode: auto # toggle | hold | auto
hold_ms: 500
recovery: f8 # default: null
retry: null
undo: null # e.g. scroll_lockThe undo key (hotkeys.undo, or --undo-hotkey) removes the last
dictation, for when it came out wrong and you would rather say it again.
It works only while TalkType is idle, once per dictation (a second press
logs [undo] nothing to undo), and only where the text went:
- Emacs: it runs
talktype-undo-lastwith the text TalkType wrote (see Emacs), which deletes the dictation if its text is unchanged and refuses otherwise, also when the last dictation Emacs knows is another one (say one you made withM-x talktype-begin). - kitty: it sends one DEL per character TalkType wrote (after
replacements) into the same kitty window, through
kitten @ send-text. It refuses if another window has the focus, if any other key was pressed since the recording started (a moved cursor or typed text would make DEL delete the wrong characters), or if the dictation wrote a line break. Both checks run again right before the DELs are sent, but a key typed while kitty takes them (some 50 ms) can still mix in, so don't type while undoing. It is meant for line-editing prompts (a shell, Claude Code, the same behind byobu or tmux); elsewhere a DEL is just a key, so use the undo key only where Backspace would remove the text. - Other routes (
terminal-paste,type,paste): nothing, it logs[undo] not supported for route <route>.
The log says what happened: [undo] emacs: removed,
[undo] kitty: removed 42 chars or [undo] refused: <reason>. If
emacsclient or kitten fails or times out (after 1 s), part of the
text may be gone already, so TalkType forgets the dictation rather than
try again: [undo] failed, not retried: <reason>. A record key pressed
during the undo waits for it to finish.
A recording that nobody stops ends by itself, as if you had pressed the key (final pass, text written, history entry):
- after 60 s without speech (
recording.silence_stop_s, or--silence-stop-s). Speech is any 50 ms with a level above the one the "no speech" check uses; background noise or music can count as speech. A warning beep plays 10 s before this stop (at 50 s), and speaking again restarts the count. While the record key is held (hold, orautoheld down), there is no silence stop. - after 10 min in any case (
recording.max_s, or--max-recording-s), also while the key is held.
0 turns either limit off. An auto-stop plays its own tone, lower and
longer than the stop beep, and the log says why:
[stream] warn: silence 50 s, [stream] auto-stop: silence 60 s or
[stream] auto-stop: max length 600 s.
recording:
silence_stop_s: 60 # 0: never stop on silence
max_s: 600 # 0: no maximum lengthTalkType can beep on six events: start (recording starts, 880 Hz),
stop (it stops, 440 Hz), success (the text is in, 660 Hz), error (no
speech, or a failure, 220 Hz), warn (a silent recording will stop in
10 s, a short high 1320 Hz) and auto_stop (it stopped by itself, a longer
330 Hz; see Auto-stop). By default you hear one beep to start and one to stop:
success is off, since it followed stop a moment later and sounded like
a double beep; error, warn and auto_stop are on and only play when
something went wrong or a recording stops by itself. Switch single beeps
on or off, all of them with sounds: true / sounds: false:
sounds:
success: true # the others keep their defaultsEngines mishear some words the same way every time, mostly technical terms: "onnx" comes out as "onyx", "Claude Code" as "cloud code". Nemotron and Parakeet take no hints, so TalkType fixes these itself, for every engine, before the text is typed, sent or pasted. List them in the config, heard on the left, written on the right:
replacements: # heard -> written
onyx: onnx
onix: onnx
cloud code: Claude CodeA word or phrase matches only as a whole, in any case ("Onyx", "ONYX"),
also next to punctuation ("onyx," becomes "onnx,"); the longest phrase
wins, and the replacement keeps its own spelling. While streaming, the last
word of a chunk waits only if it may still become a listed word ("on" +
"yx"). A word that may begin a listed phrase ("cloud" before "code") also
waits for the next chunk or the stop. Other words appear immediately.
The history keeps the replaced text under text and, when it differs, the
engine's own under raw, to find more candidates for the list.
# Use a different model (tiny, base, small, medium, large-v3)
talktype --model small
# Use a different hotkey: any key name pynput knows, e.g. f10, pause, scroll_lock, menu,
# or on Linux any X key name, e.g. XF86Tools
talktype --hotkey pause
# Print the name of the next key you press, for hotkeys.record
talktype --which-key
# Connect to a Whisper API server (if you have one running)
talktype --api http://localhost:8002/transcribe
# Change language
talktype --language es # Spanish
# Type while you are still speaking (see Streaming below)
talktype --streamEvery option can also be set in the config file; command-line flags win.
talktype --help lists them all.
~/.config/talktype/config.yaml, with every key and its default. Keys whose
default depends on something else are commented out; leave them unset to
keep that default.
hotkeys:
record: f9 # pynput key name (f10, pause, scroll_lock, menu, ...) or X key name (XF86Tools)
record_mode: toggle # toggle | hold | auto
hold_ms: 500 # auto mode: hold at least this long to stop on release
recovery: null # re-paste the last transcription; e.g. f8
retry: null # re-transcribe the last saved audio; e.g. f7
undo: null # remove the last dictation (Emacs, kitty prompt); e.g. scroll_lock
recording:
silence_stop_s: 60 # stop after this long without speech; 0: never
max_s: 600 # stop after this long in any case; 0: no limit
sounds: # true: all six beeps; false: none
start: true
stop: true
success: false
error: true
warn: true # 10 s before a silence stop
auto_stop: true # instead of stop, when a recording stops by itself
transcription:
mode: local # local | api
api_url: http://localhost:8002/transcribe # used with mode: api
api_model: whisper-1 # used with OpenAI-compatible APIs; Groq: whisper-large-v3
model: base # Whisper: tiny, base, small, medium, large-v3
# language: en # default: auto-detect
# cpu_threads: 8 # default: min(8, CPU count); local model
streaming: false # true: type while you speak (local model only)
stream_engine: whisper # whisper | parakeet | nemotron
# final_engine: whisper # engine after you stop; default: the streaming engine
stream_interval: 1.0 # seconds between passes (whisper, parakeet)
stream_tail: true # Emacs: show unsettled words dimmed (whisper, parakeet)
parakeet_model: nemo-parakeet-tdt-0.6b-v3
nemotron_model: onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4
nemotron_threads: 4
stream_output: auto # auto | emacs | kitty | type | paste
kitty_socket: unix:@kitty # may contain {kitty_pid}
kitten: kitten # path of kitty's kitten command; default: found on PATH
emacsclient: emacsclient # path of emacsclient; default: found on PATH
# emacs_socket: server # Emacs server socket name or path (emacsclient -s); default: emacsclient's
ui:
minimal: false # show only the status
history:
limit: 100 # transcriptions kept for re-paste
replacements: {} # heard -> written, e.g. onyx: onnx; see ReplacementsWith --stream (or streaming: true under transcription:), words appear
while you are still talking instead of all at once when you stop. Streaming
needs a local model (not --api). Three engines are available.
When the extra of the configured stream_engine or final_engine is not
installed, TalkType does not exit: it warns in the start-up lines (and the
journal, as a service), plays the error beep once and runs on an engine
that is installed. Streaming falls back to nemotron, else parakeet, else
whisper; nemotron only stands in when final_engine is unset or nemotron,
since it runs no final pass of another engine. For the same reason
final_engine does not apply while nemotron streams, and a missing one is
not warned about then. An unset final_engine follows the engine that
streams, a set one falls back to whisper. The warning names the command
that installs what the config needs, keeping the extras already installed,
for example
uv tool install --force 'talktype[local,parakeet] @ git+https://github.com/ChristianGeng/talktype'.
An engine whose extra is there but fails to load (a dependency missing
after an interrupted install) falls back the same way. Only without
faster-whisper (the local extra) does TalkType stop.
Every --stream-interval seconds (default 1.0) the recording so far is
transcribed again, and a word is typed once two consecutive transcripts
agree on it; words Whisper is still changing its mind about stay back. When
you stop, a final transcription of the whole recording adds the rest.
Nothing is typed twice.
Each pass costs about a second of CPU with the base model, so text trails
your speech by a few seconds; --cpu-threads (default up to 8) matters more
for this than the model size.
transcription:
mode: local
model: base
streaming: true
stream_interval: 1.0
cpu_threads: 8--stream-engine parakeet (config stream_engine: parakeet) streams with
NVIDIA's Parakeet TDT
0.6B v3 instead
of Whisper, run locally through
onnx-asr with int8 weights. It
covers English and 24 other European languages, German included, and
detects the language itself (--language does not apply to it). It needs
the parakeet extra; the model (about 640 MB) downloads on first use.
transcription:
streaming: true
stream_engine: parakeet
# final_engine: parakeet # default: same as stream_engineParakeet's cost grows with the audio length (about 0.2 s per second of
speech on a laptop CPU), while Whisper pads everything to 30 s and costs
the same up to there. Both report when each word starts (Whisper's word
times add about 0.02–0.06 s to a pass of 5–20 s with base on a CPU), so
audio up to the last typed sentence end is dropped from later passes, and
the pass after you stop only reads what is left, with Parakeet or Whisper
as --final-engine; passes then stay about as long as a sentence however
long you talk. The journal shows how
long that pass took ([stream] final pass: 0.4 s for 3.9 s of audio).
--final-engine picks the engine for the pass after you stop. It defaults
to the streaming engine: typed words and the final text then come from the
same model. Mixing them works, but the final text can word something
differently from what is already typed ("wanna" typed, "want to" final
gives "wanna to").
Measured on a 12-core laptop CPU, base Whisper vs Parakeet int8, 8
threads, one pass per second, speech played back in real time:
| Clip | Engine | First word | Words while speaking | Last text after stop |
|---|---|---|---|---|
| English, 9 s | Whisper | 5.2 s | 19/28 | 2.0 s |
| Parakeet | 3.6 s | 20/28 | 2.7 s | |
| German, 9 s | Whisper | 3.0 s | 14/21 | 1.7 s |
| Parakeet | 4.7 s | 17/21 | 2.7 s | |
| English, 34 s | Whisper | 3.0 s | 77/93 | 6.2 s |
| Parakeet | 4.3 s | 84/93 | 2.3 s |
Past 30 s Whisper needs two windows per pass, so it falls behind; on the 34 s clip Parakeet also got every word right where Whisper wrote "pan" for "plan". For short dictation the two are close. Parakeet also writes fillers such as "uh" that Whisper drops.
Whisper and Parakeet are offline models, so streaming with them means
transcribing again and again and waiting for two passes to agree; the first
word then lags speech by 3-5 s whatever the model. --stream-engine nemotron uses NVIDIA's Nemotron 3.5 ASR Streaming
0.6B
instead, a cache-aware model: it keeps its state from one 560 ms chunk to
the next, so each chunk is decoded once and its words are typed right away.
There is no re-transcription and no pass after you stop, only the last
chunk. It covers 40 locales, English and German among them; --language
picks one, otherwise the model detects it.
It runs on the CPU through
onnxruntime-genai with the
INT4 ONNX export
(about 790 MB, downloaded on first use). It needs the nemotron extra.
transcription:
streaming: true
stream_engine: nemotron
language: en # or de, ...; omit to auto-detect
nemotron_threads: 4 # see below--nemotron-threads defaults to 4. On a laptop with 2 performance and 8
efficiency cores (i7-1255U) a chunk took 320 ms with 4 threads, but 850 ms
with 8 and 1280 ms with 12: extra threads land on the slow cores and hold
the fast ones back. With fewer than about 560 ms per chunk it keeps up with
speech; try 3-6 on other CPUs.
Same laptop and clips as above, speech played back in real time, and here including a simulated 0.4 s paste (the Whisper and Parakeet numbers above paste instantly):
| Clip | First word on screen | Words while speaking | Last text after stop | WER |
|---|---|---|---|---|
| English, 9 s | 2.7 s | 29/29 | none left | – |
| German, 9 s | 2.5 s | 21/21 | none left | 0 % |
| English, 34 s | 1.6 s | 90/93 | 0.3 s | 0 % |
The model itself emits its first word about 1.4 s into speech; the rest is the paste and setting up the recording. It writes fillers such as "uh", and it always punctuates.
--stream-output (config stream_output) picks the route, once per
recording:
auto(default):emacsclientwhen the focused window is Emacs (see below); kitty remote control when the focused window is a kitty that answers on its socket; one paste per chunk (Ctrl+Shift+V) in the Linux terminals listed below, such as GNOME Terminal, Tilix or Alacritty; otherwise keystrokes, pasting only chunks with characters that are not on the keyboard (ü, ß, €).emacs:emacsclient --evalcalls intotalktype.el, whenever an Emacs server answers, whatever window is focused. Without a server the recording writes nothing while streaming (the text is in the history).kitty:kitten @ send-textinto the kitty window that had the focus when the first words came, by its id: the whole dictation lands in that window, even if you switch tabs meanwhile. No clipboard, no synthetic keys, no focus change, and Unicode arrives intact; over SSH it reaches the remote shell like typed input. If kitty can't take the words (the window was closed), they are pasted instead and the undo key leaves that dictation alone. If kitty doesn't report the focused window's id, the dictation goes throughterminal-pasteinstead, without undo.type:xdotool typekeystrokes, as nerd-dictation does. xdotool makes missing characters by remapping a spare key, which kitty misses, so German umlauts get lost there.paste: the clipboard and Ctrl+V per chunk. Each chunk hands the clipboard to a new xclip, re-activates the window, and makes kitty read the clipboard synchronously (up to 2 s, stalling all its windows); on a GNOME desktop the terminal stayed blocked until recording stopped.
In GNOME Terminal, Tilix, Terminator, Alacritty, foot, Konsole, Xfce
Terminal, MATE Terminal, LXTerminal, sakura, Terminology, WezTerm and st,
auto pastes each chunk with Ctrl+Shift+V instead of typing it: a program that redraws on every key, such as Claude
Code, falls behind typed keys, so the text showed up only after the
recording and the terminal stopped taking input. A paste arrives as one
block (bracketed paste), in any language, and nothing adds a newline, so a
prompt is never submitted. The clipboard is saved before the first chunk
and restored once after the recording; if it can't be set (no xclip), the
recording types instead. The blocking described for paste
was kitty reading the clipboard synchronously; a kitty without remote
control therefore keeps keystrokes, and so do xterm and urxvt, which don't
paste on Ctrl+Shift+V, and every window whose WM_CLASS is not exactly one
of these terminals (Steam or Postman, say, though their names contain
"st").
The kitty route needs kitty's remote control socket. In kitty.conf
(read at kitty start, so restart kitty once):
allow_remote_control yes
listen_on unix:${XDG_RUNTIME_DIR}/kitty-{kitty_pid}.sock
and the same socket for TalkType; {kitty_pid} is filled in from the
focused window, since kitty otherwise appends its PID to the name:
transcription:
streaming: true
stream_engine: nemotron
stream_output: auto
kitty_socket: unix:/run/user/1000/kitty-{kitty_pid}.sock # your XDG_RUNTIME_DIR
kitten: /home/me/.local/kitty.app/bin/kitten # if not on the service's PATHA socket under $XDG_RUNTIME_DIR is only reachable by you; an abstract
unix:@… socket can be reached by any local user.
In Emacs, keys are commands: with evil in normal state DEL is a motion,
and the minibuffer, isearch or org-agenda bind keys of their own. The
emacs route therefore edits the buffer itself, through emacsclient and
the functions in talktype.el:
talktype-beginopens a dictation region at point in the selected window's buffer; while it is open the words are underlined (facetalktype-provisional).talktype-appendinserts each chunk at the region's end, before the tail;talktype-replace-regionreplaces the region's settled text (for corrections).talktype-set-tailshows the words a Whisper or Parakeet pass heard but has not settled on yet, dimmed (facetalktype-tentative) at the end of the region, and the next pass rewrites them; see the tail below.talktype-endremoves the tail and the underline and closes the region. The whole dictation is one undo step, unless you edited the buffer yourself meanwhile: then its chunks stay separate undo steps, so undoing the dictation never takes your own edits along.
With the whisper and parakeet stream engines, a word is written only
once two passes agree on it, so the settled text trails your speech by a
pass or two. In Emacs the rest of each pass appears right away as the
dimmed tail: the next pass replaces it, words that settle move into the
dictation's text, and when you stop, the final transcript replaces
whatever tail is left. The tail is never part of the dictation's text: it
leaves no undo step of its own, and talktype-undo-last and the undo key
see only the settled words. Each pass changes the tail in the same
emacsclient call as its settled words, and not at all if it is
unchanged. --no-stream-tail (config stream_tail: false) writes only
the settled words, as before; Nemotron's words are final as they come and
have no tail, and the other routes never get one.
The evil state and the mark stay as they are, and point moves along only if
it was at the end of the dictation. Selecting another buffer meanwhile does
not matter: the words keep going into the buffer the dictation started in.
In a read-only buffer, the minibuffer or during isearch, talktype-begin
refuses and that recording writes nothing into Emacs (never keys, never
Ctrl+V); the text is still in the history for the recovery key. The same
holds when Emacs stops taking words halfway (server gone, buffer killed):
the rest of that recording is not written, to leave no gaps. A region that
could not be closed is closed by the next dictation.
Either way TalkType plays the error beep once, and when talktype.el is the
one refusing, Emacs shows why in the echo area (and *Messages*), e.g.
TalkType: *daemons* is read-only. Instead of ✅ DONE the status then says
⚠️ NOT WRITTEN (nothing reached Emacs) or ⚠️ PARTLY WRITTEN (the words
before the stop did), and there is no success beep.
While a dictation is open the mode line shows a red ● REC (face
talktype-recording, in global-mode-string, so doom-modeline shows it
too); (setq talktype-mode-line nil) turns the indicator off.
M-x talktype-undo-last removes the last dictation again: if its text is
still exactly as TalkType wrote it, the command deletes just it — even
after you typed elsewhere in that buffer — as one undo step, and it works
from any buffer. If the text was edited, the buffer is read-only or was
killed, or a dictation is still running, it refuses and changes nothing.
TalkType's undo key (hotkeys.undo, see Recovery Hotkeys)
runs the same command. A Doom binding for it:
(map! :leader :desc "Undo last dictation" "t U" #'talktype-undo-last)The transcript reaches Emacs as data, never as code: it is passed as a Lisp
string literal with \ and " escaped and everything outside printable
ASCII written as \uXXXX, so quotes, backslashes or parentheses in speech
are only ever inserted.
Setup: start a server in the Emacs you dictate into and load talktype.el
from the clone, e.g. in Doom's config.el:
(add-to-list 'load-path "~/src/talktype") ; your clone
(require 'talktype)
(global-set-key [f9] #'ignore) ; TalkType's record key (hotkeys.record)
(server-start) ; unless already running, or emacs --daemonTalkType does not grab its record key, so the focused Emacs receives it
too; bind whatever key hotkeys.record is set to (F9 by default) to
ignore. With F10, Emacs would otherwise run menu-bar-open on every
dictation. A record key Emacs does not bind, such as Pause, needs no such
line.
With Doom or straight.el, install only talktype.el from this repository
instead of using a clone: in Doom's packages.el
(package! talktype
:recipe (:host github :repo "ChristianGeng/talktype" :files ("talktype.el")))and (use-package! talktype) in config.el, then doom sync and restart
Emacs (doom/reload does not add a newly installed package to the
load-path).
The five functions TalkType calls are also commands, so M-x talktype-begin, talktype-append, talktype-set-tail,
talktype-replace-region and talktype-end try the region by hand; M-x talktype-undo-last is the
command above.
stream_output: auto then picks emacs for
- GUI Emacs frames (
WM_CLASS"emacs", "Emacs") of the Emacs server thatemacsclientreaches, and - kitty windows whose foreground process is
emacsclient -nwon the same socket (-s/--socket-name, compared by name) or that server's ownemacs -nw(fromkitten @ ls, so kitty's remote control must be set up as above).
An emacs -nw without a server, or with a different one, gets the kitty
route. Set emacs_socket if your server does not use emacsclient's default
socket (server-name other than server).
TalkType supports any OpenAI-compatible transcription API:
# OpenAI API
talktype --api https://api.openai.com/v1/audio/transcriptions --api-model whisper-1
# Groq (super fast)
talktype --api https://api.groq.com/openai/v1/audio/transcriptions --api-model whisper-large-v3
# Local OpenAI-compatible server (e.g., faster-whisper-server, whisper.cpp)
talktype --api http://localhost:8080/v1/audio/transcriptions --api-model whisper-1
# Any custom server
talktype --api http://localhost:8002/transcribeTalkType auto-detects OpenAI-compatible endpoints by URL pattern. For custom servers, it uses a simpler format that works with most Whisper APIs.
| Model | Size | Speed | Accuracy | VRAM |
|---|---|---|---|---|
| tiny | ~75MB | Fastest | Basic | ~1GB |
| base | ~150MB | Fast | Good | ~1GB |
| small | ~500MB | Medium | Better | ~2GB |
| medium | ~1.5GB | Slow | Great | ~5GB |
| large-v3 | ~3GB | Slowest | Best | ~10GB |
For most use cases, base or small is the sweet spot.
For faster startup, run the included Whisper API server: the model stays loaded in memory, so TalkType connects instantly.
| Mode | Startup | Memory | Best for |
|---|---|---|---|
Direct (talktype) |
~3-5 s (loads model) | Uses RAM while running | Occasional use |
Server (whisper_server.py) |
Instant | Server keeps model loaded | Heavy use, multiple apps |
The server runs from a clone, with uv:
git clone https://github.com/ChristianGeng/talktype.git && cd talktype
# Terminal 1 — the server (defaults to CUDA; add --device cpu without a GPU)
uv run --extra local --extra server whisper_server.py --model base
# Terminal 2 — TalkType against it
talktype --api http://localhost:8002/transcribeuv run --extra local --extra server whisper_server.py --help
# Examples:
uv run --extra local --extra server whisper_server.py --model small # Better accuracy
uv run --extra local --extra server whisper_server.py --port 8080 # Different port
uv run --extra local --extra server whisper_server.py --device cpu # Force CPU
# Environment variables also work:
WHISPER_MODEL=large-v3 WHISPER_DEVICE=cuda uv run --extra local --extra server whisper_server.pycat > ~/.config/systemd/user/whisper-server.service << 'EOF'
[Unit]
Description=Whisper API Server
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/talktype
ExecStart=%h/.local/bin/uv run --extra local --extra server whisper_server.py --model base
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now whisper-serverAdjust WorkingDirectory to your clone and the uv path to yours
(command -v uv).
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Check server status |
/transcribe |
POST | Transcribe audio file |
/docs |
GET | Interactive web UI — test transcription right in your browser |
curl -X POST http://localhost:8002/transcribe \
-F "file=@audio.wav" \
-F "language=en"The setup wizard can install TalkType as a systemd user service — just
select "Run at startup" when prompted. It writes the unit below, then runs
systemctl --user daemon-reload, reenable talktype and restart talktype,
so running the wizard again applies its new settings to a running service.
Or write the unit yourself:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/talktype.service << 'EOF'
[Unit]
Description=TalkType Voice Typing
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=%h/.local/bin/talktype
Restart=on-failure
RestartSec=5
# Status lines appear in journalctl as they happen.
Environment=PYTHONUNBUFFERED=1
[Install]
# Start with the desktop: under default.target it starts before the
# desktop has set DISPLAY and crashes once per login.
WantedBy=graphical-session.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now talktypeQuote an ExecStart path that contains spaces in double quotes. If your
unit still says WantedBy=default.target, change it and run
systemctl --user daemon-reload && systemctl --user reenable talktype to
move the link.
Manage with:
systemctl --user status talktype # Check status
systemctl --user restart talktype # After editing the config
journalctl --user -u talktype -f # Watch its outputDo not set DISPLAY in the unit: it is inherited from the desktop session,
and a hard-coded DISPLAY=:0 crash-loops the service on a session that runs
on another display. If the service cannot reach X at all, import the
session's variables once with
systemctl --user import-environment DISPLAY XAUTHORITY. All settings come
from ~/.config/talktype/config.yaml, so after editing it a restart is
enough. Use systemctl --user without sudo; under sudo it cannot find
your session.
If the first model download stalls at 0 bytes (seen on some networks),
add Environment=HF_HUB_DISABLE_XET=1 to the unit: it makes the Hugging
Face download use plain HTTPS.
TalkType works with Claude Code and similar terminal AI assistants, locally and over SSH:
- Run TalkType as a service (or in a separate terminal)
- Focus the terminal with Claude Code — also one where you are SSH'd into a remote host
- Press F9, describe what you want, press F9
- Your prompt appears in Claude Code; with streaming it grows while you speak
Nothing needs installing on the remote host: the text travels as typed
input. In kitty, the kitty route delivers it without the clipboard.
TalkType works in any browser text field:
- Focus a text field (Google Docs, ChatGPT, Slack, email composer, etc.)
- Press F9, speak, press F9
- Your words appear in the browser
Outside kitty it uses the clipboard and the standard paste shortcut (Ctrl+V / Cmd+V), or keystrokes while streaming, so it works anywhere that accepts text.
pynput requires X11. If using Wayland, either:
- Switch to an X11 session
- Run with
GDK_BACKEND=x11environment variable
The default input is probably not your microphone (a virtual device, a
muted mic, or a headset that is switched off). Check with
pactl get-default-source and pactl list short sources, and set the right
one with pactl set-default-source <name>.
TalkType only listens for the hotkey once the model has loaded; the first
start downloads it. Watch journalctl --user -u talktype -f for
"Ready!".
Fixed in this fork: auto-repeat of a held key is ignored. If you see it,
update with uv tool install --force 'talktype[local] @ git+https://github.com/ChristianGeng/talktype'.
macOS requires accessibility permissions for keyboard monitoring:
- Go to System Preferences → Security & Privacy → Privacy → Accessibility
- Add your terminal app (Terminal, iTerm, etc.)
Make sure your microphone is set as the default input device in Windows Sound settings.
- Try a smaller model:
--model tinyor--model base - If you have an NVIDIA GPU, ensure CUDA is installed for GPU acceleration
- Consider running a separate Whisper API server and using
--api - For streaming, try
--stream-engine nemotron
- Global hotkey capture (pynput) — works even when other apps are focused
- Audio recording (sounddevice) — captures from your microphone
- Local transcription (faster-whisper, Parakeet or Nemotron)
- Delivery — emacsclient into Emacs, kitty remote control, keystrokes, or smart paste
[F9 Press] → Start Recording → [Speak] → [F9 Press] → Stop Recording
↓ (streaming) ↓
words typed as you speak Transcribe the rest
↓
Focus Original Window
↓
Paste / type the text
Contributions welcome! Some ideas:
- Voice activity detection (auto-stop on silence)
- Wayland support (wtype instead of xdotool)
- Tray icon / visual indicator
- Custom vocabulary/prompts
MIT License — see LICENSE for details.
- lmacan1/talktype — the original TalkType this fork builds on
- faster-whisper — CTranslate2-based Whisper
- OpenAI Whisper — the model itself
- onnx-asr and onnxruntime-genai — Parakeet and Nemotron on the CPU
- pynput — cross-platform input monitoring
