Skip to content
ChristianGengPublic
forked from lmacan1/talktype

About

Push-to-talk voice typing for your terminal. Local Whisper, cross-platform.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

TalkType

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.

Demo

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 emacsclient and talktype.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 as XF86Tools; talktype --which-key names 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.

Why TalkType?

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

Features

  • 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

Installation

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.

Linux (Ubuntu/Debian)

sudo apt install xdotool xclip libportaudio2
uv tool install 'talktype[local] @ git+https://github.com/ChristianGeng/talktype'
talktype  # the setup wizard runs on first start

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

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

Windows

uv tool install "talktype[local] @ git+https://github.com/ChristianGeng/talktype"
talktype  # the setup wizard runs on first start

macOS

brew install portaudio
uv tool install 'talktype[local] @ git+https://github.com/ChristianGeng/talktype'
talktype  # the setup wizard runs on first start

From a clone (development)

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

With emacs and emacsclient installed, pytest also runs an end-to-end test against a throwaway headless Emacs server.

Usage

First Run — Setup Wizard

On first launch in a terminal, TalkType runs an interactive setup wizard:

talktype

The 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 name talktype --which-key prints
  • 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.

Basic Usage

talktype  # uses the saved config
  1. Press F9 to start recording (beep)
  2. Speak your text
  3. Press F9 again to stop (beep)
  4. Your words appear in the focused window

Recovery Hotkeys

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 least hold_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, 0x1008ff81

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

The 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-last with 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 with M-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.

Auto-stop

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, or auto held 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 length

Sounds

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

Replacements

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

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

Options

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

Every option can also be set in the config file; command-line flags win. talktype --help lists them all.

Configuration file

~/.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 Replacements

Streaming: type while you speak

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

Whisper (default engine)

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

Parakeet

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

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

Nemotron: native streaming

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.

How streamed words reach the window

--stream-output (config stream_output) picks the route, once per recording:

  • auto (default): emacsclient when 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 --eval calls into talktype.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-text into 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 through terminal-paste instead, without undo.
  • type: xdotool type keystrokes, 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 PATH

A socket under $XDG_RUNTIME_DIR is only reachable by you; an abstract unix:@… socket can be reached by any local user.

Emacs

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-begin opens a dictation region at point in the selected window's buffer; while it is open the words are underlined (face talktype-provisional).
  • talktype-append inserts each chunk at the region's end, before the tail; talktype-replace-region replaces the region's settled text (for corrections).
  • talktype-set-tail shows the words a Whisper or Parakeet pass heard but has not settled on yet, dimmed (face talktype-tentative) at the end of the region, and the next pass rewrites them; see the tail below.
  • talktype-end removes 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 --daemon

TalkType 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 that emacsclient reaches, and
  • kitty windows whose foreground process is emacsclient -nw on the same socket (-s/--socket-name, compared by name) or that server's own emacs -nw (from kitten @ 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).

OpenAI-Compatible APIs

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

TalkType auto-detects OpenAI-compatible endpoints by URL pattern. For custom servers, it uses a simpler format that works with most Whisper APIs.

Model Sizes

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.

Whisper API Server

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

Server Options

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

Running the Server as a Service (Linux)

cat > ~/.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-server

Adjust WorkingDirectory to your clone and the uv path to yours (command -v uv).

API Endpoints

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"

Running as a Service (Linux)

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 talktype

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

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

Using with Claude Code

TalkType works with Claude Code and similar terminal AI assistants, locally and over SSH:

  1. Run TalkType as a service (or in a separate terminal)
  2. Focus the terminal with Claude Code — also one where you are SSH'd into a remote host
  3. Press F9, describe what you want, press F9
  4. 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.

Using with Browsers

TalkType works in any browser text field:

  1. Focus a text field (Google Docs, ChatGPT, Slack, email composer, etc.)
  2. Press F9, speak, press F9
  3. 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.

Troubleshooting

Linux: Hotkey not working

pynput requires X11. If using Wayland, either:

  • Switch to an X11 session
  • Run with GDK_BACKEND=x11 environment variable

Linux: "No speech detected" although recording beeps

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

Linux: F9 does nothing right after installing

TalkType only listens for the hotkey once the model has loaded; the first start downloads it. Watch journalctl --user -u talktype -f for "Ready!".

Linux: Held hotkey starts and stops recording repeatedly

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: Accessibility permissions

macOS requires accessibility permissions for keyboard monitoring:

  1. Go to System Preferences → Security & Privacy → Privacy → Accessibility
  2. Add your terminal app (Terminal, iTerm, etc.)

Windows: No audio input

Make sure your microphone is set as the default input device in Windows Sound settings.

Transcription is slow

  • Try a smaller model: --model tiny or --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

How It Works

  1. Global hotkey capture (pynput) — works even when other apps are focused
  2. Audio recording (sounddevice) — captures from your microphone
  3. Local transcription (faster-whisper, Parakeet or Nemotron)
  4. 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

Contributing

Contributions welcome! Some ideas:

  • Voice activity detection (auto-stop on silence)
  • Wayland support (wtype instead of xdotool)
  • Tray icon / visual indicator
  • Custom vocabulary/prompts

License

MIT License — see LICENSE for details.

Acknowledgments

About

Push-to-talk voice typing for your terminal. Local Whisper, cross-platform.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages