Local-only toggle-to-talk dictation for Linux, macOS, and Windows, with local ASR and faithful cleanup for technical notes. Granite NAR is the default backend on Linux GPU, Granite autoregressive on Windows, and MLX on macOS Apple Silicon. TransClip is the product surface.
The default path is now the pure-Python dictation daemon:
shortcut -> transclip toggle-record --paste -> Python service -> clipboard -> paste
The runnable app lives in transclip/: Python inference service,
settings, audio capture, cleanup, paste injection, daemon install/status/log
commands, debug capture, platform tray UIs, and eval harness.
Platform-specific desktop integration is grouped under transclip/desktop/
(paste, hotkey, tray), with service lifecycle in transclip/daemon/ and
readiness checks in transclip/doctor/. See docs/package-layout.md
for the full package map and stable import paths.
TransClip is licensed under the Apache License, Version 2.0. Model weights and third-party dependencies are governed by their own licenses.
Create default config files:
uv run -m transclip.cli init-configThis writes settings.toml under the platform config directory.
Install the daemon and native shortcut:
uv run -m transclip.cli installOn Linux this writes ~/.config/systemd/user/transclip.service, enables
and starts it with systemctl --user, and installs the GNOME custom shortcut
TransClip Toggle. On this HP ZBook, wev reports the Copilot key as
<Super><Shift>XF86TouchpadOff; press once to start recording and again to
stop, transcribe, copy, and paste.
On macOS Apple Silicon, install writes the TransClip service LaunchAgent.
Install the native hotkey helper with install-macos-hotkey; it registers
Option+Space without relying on Shortcuts.app. Use the menu bar tray for
click-to-record after installing the optional UI extra:
uv sync --extra audio --extra mlx --extra macos-ui
transclip trayOn Windows, install registers a per-user logon autostart entry (an HKCU Run-key
value, no admin required). Global hotkey
ctrl+shift+space is registered when transclip tray is running (Windows tray
in transclip.desktop.tray.win32). Sync optional UI dependencies for the
system tray and in-process hotkey:
uv sync --extra audio --extra models --extra windows-ui
transclip trayInstall a CUDA-enabled PyTorch wheel before prefetching Granite AR models, then
run transclip models prefetch --model ibm-granite/granite-speech-4.1-2b.
Granite NAR (asr_backend = "granite_nar") is also selectable on Windows CUDA;
Granite AR is the default.
Check readiness and logs:
uv run -m transclip.cli status
uv run -m transclip.cli doctor
uv run -m transclip.cli smoke-test
uv run -m transclip.cli logsWith the service running, press the toggle shortcut once to start recording and again to stop. Ordinary speech is dictated normally. Start an utterance with one of these phrases to choose another mode:
clean up <text> -> Qwen model cleanup
trans cleanup <text> -> Qwen model cleanup
shell command <task> -> Bash command generation
bash command <task> -> Bash command generation
terminal command <task> -> Bash command generation
literal shell command <text> -> paste "shell command <text>"
literal bash command <text> -> paste "bash command <text>"
literal clean up <text> -> paste "clean up <text>"
Trigger matching is case-insensitive and only applies at the beginning of the
utterance, so a sentence that mentions "shell command" later is still normal
dictation. Use literal when you want to dictate the trigger words themselves
instead of activating cleanup or shell mode.
Run the Python tray:
transclip trayOn Linux this uses PyGObject/Ayatana AppIndicator (GTK tray in
transclip.desktop.tray.gtk). When running through uv, the command hands
off to system Python if the project virtual environment does not expose gi. Install the system bindings if missing:
sudo apt install -y python3-gi gir1.2-ayatanaappindicator3-0.1On macOS, transclip tray uses the native menu bar (transclip.desktop.tray.macos)
when macos-ui is installed (uv sync --extra macos-ui). The tray can copy
the hotkey setup command for Keyboard Shortcuts; global hotkeys are configured
manually in System Settings or Shortcuts.app.
Service controls:
uv run -m transclip.cli start
uv run -m transclip.cli stop
uv run -m transclip.cli restart
uv run -m transclip.cli uninstallTo run the service manually instead of using the service manager:
uv run -m transclip.cli serveRequirements: Apple Silicon, native ARM Python 3.12+, macOS 14+, and Xcode
Command Line Tools for swiftc:
For local edit/reinstall workflows, see docs/macos-local-development.md.
xcode-select --installuv sync --extra audio --extra mlx --extra macos-ui
uv run -m transclip.cli init-config
uv run -m transclip.cli models prefetch --model mlx-community/whisper-large-v3-turbo-asr-fp16
uv run -m transclip.cli install
uv run -m transclip.cli install-macos-hotkey
uv run -m transclip.cli status
uv run -m transclip.cli doctor
transclip trayinstall-macos-hotkey writes:
~/bin/transclip-toggle— robust start/stop wrapper with logging and stale lock cleanup. If stop/transcription hangs, the wrapper restarts the service after 75 seconds; a later press can clear a stale wrapper after 90 seconds.~/Applications/TransClipHotkey.app— a tiny native event-tap helper forOption+Spacewith a menu-bar status item.~/Library/LaunchAgents/com.paulbrav.transclip-hotkey.plist— starts the helper at login.
After installing or reinstalling TransClipHotkey.app, refresh Accessibility
after the final install-macos-hotkey run. A later rebuild can invalidate the
grant again. Stop the helper before changing the grant:
launchctl bootout gui/$(id -u)/com.paulbrav.transclip-hotkeyOpen System Settings > Privacy & Security > Accessibility, delete and re-add TransClipHotkey or toggle it off and back on, then start the helper again:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.paulbrav.transclip-hotkey.plistUsage: put the cursor in a text field, press Option+Space once to start
recording, speak, then press Option+Space again to stop, transcribe, copy, and
paste. Expect several seconds of transcription latency on the stop press.
Menu-bar transition:
| Phase | Label | Color | Meaning |
|---|---|---|---|
| Idle | TC |
System label color | Ready and doing nothing. |
| Shortcut received | TC... |
Yellow | Option+Space was detected and the helper is checking service state. |
| Recording | REC |
Orange | The microphone is recording. |
| Busy | TC... |
Yellow | A previous action is still running. |
| Transcribing | TXT... |
Purple | Speech is being converted to text; this is usually the longest wait. |
| Paste requested / pasting | PST... |
Teal | The transcript was copied and the helper is posting Command+V. |
| Finished | OK |
Green | The transcript was pasted; this resets to TC after a short delay. |
| Recovering | TC... |
Yellow | A stale wrapper is being cleared before trying again. |
| Error | TC! |
Red | The helper needs attention, such as Accessibility or service recovery. |
Shortcuts.app is only a fallback now. If you use it, bind the command printed by
install or copied from the tray menu (Copy hotkey setup command).
Supported MLX ASR models on macOS:
mlx-community/whisper-large-v3-turbo-asr-fp16(default)mlx-community/granite-4.0-1b-speech-8bit(asr_backend = "granite_mlx")
Granite Speech 4.1 NAR (asr_backend = "granite_nar") is also selectable on
Apple Silicon and runs via Torch/MPS; it and the optional Torch/MPS Granite AR
models require uv sync --extra audio --extra models.
| Action | Permission | Notes |
|---|---|---|
| Recording | Microphone | Grant when macOS prompts for the process that starts recording. |
| Hotkey | Accessibility | Required for TransClipHotkey.app to see and consume Option+Space. |
| Paste | Accessibility | TransClipHotkey.app posts Command+V after copying the transcript. |
The native helper path does not require Accessibility entries for Shortcuts.app,
AppleScript applet, osascript, or TransClipPaste. Those names are artifacts
of manual or older setup attempts and can be removed from Accessibility if
present.
To remove the native hotkey helper:
uv run -m transclip.cli uninstall-macos-hotkeyRequirements: Windows 10+, Python 3.12+, NVIDIA CUDA PyTorch for GPU inference.
uv sync --extra audio --extra models --extra windows-ui
# Match the CUDA build to your GPU: Blackwell (RTX 50-series) needs cu128+;
# older NVIDIA GPUs can use cu124/cu126.
uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
uv run -m transclip.cli init-config
uv run -m transclip.cli models prefetch --model ibm-granite/granite-speech-4.1-2b
uv run -m transclip.cli install
uv run -m transclip.cli status
uv run -m transclip.cli doctor
transclip traySupported ASR on Windows:
ibm-granite/granite-speech-4.1-2b(default Granite autoregressive)ibm-granite/granite-speech-4.1-2b-plus(speaker/timestamp features)ibm-granite/granite-speech-4.1-2b-nar(Granite NAR, lower latency; selectable on CUDA)
ROCm is not supported on Windows; Granite Speech 4.1 NAR is selectable on Windows
CUDA (Granite AR remains the default). Eval thresholds for Windows Granite AR are
in eval/windows/manifest.json (relaxed vs Linux NAR).
Windows laptops with an Intel CPU + integrated GPU (Iris Xe / Arc) and, on Core Ultra Series 1/2, an NPU can run accelerated ASR through OpenVINO instead of CUDA. IBM Granite Speech has no OpenVINO path, so the Intel build uses Whisper for ASR and an OpenVINO Qwen model for cleanup.
uv sync --extra audio --extra openvino --extra windows-ui
uv run -m transclip.cli init-config
uv run -m transclip.cli models prefetch --model OpenVINO/whisper-large-v3-int4-ov
uv run -m transclip.cli models prefetch --model OpenVINO/Qwen2.5-1.5B-Instruct-int4-ov
uv run -m transclip.cli doctor # reports detected OpenVINO devicesRequires the Intel GPU/NPU drivers to be installed at the OS level (the pip
wheels ship the runtime but not the device drivers); doctor surfaces missing
devices. When no NVIDIA CUDA is present but an Intel iGPU/NPU is detected,
init-config auto-selects the windows_openvino profile (asr_backend = "openvino_whisper", text_model_runtime = "openvino").
Selectable OpenVINO ASR models:
OpenVINO/whisper-large-v3-int4-ov(default — fast, int4)OpenVINO/whisper-large-v3-int8-ov(higher accuracy)OpenVINO/whisper-base-int8-ov(lightweight / quick smoke test)FluidInference/whisper-large-v3-turbo-int4-ov-npu(turbo, NPU-pre-exported)
asr_device accepts auto (OpenVINO AUTO picks NPU/GPU/CPU) or an explicit
openvino:GPU, openvino:NPU, or openvino:CPU (CPU works without Intel
drivers and is the way to test on any machine). Whisper does not support keyword
biasing, so keyword hints are ignored on this backend.
| Action | Permission | Notes |
|---|---|---|
| Recording | Microphone | Settings > Privacy & security > Microphone |
| Paste | Focused app | SendInput Ctrl+V; elevated apps may block injection (UIPI) |
Elevated (administrator) apps: Windows UIPI blocks a normal-integrity
process from injecting input into a window owned by a process running as
administrator. If the transcript is copied to the clipboard but paste does
nothing in an elevated app (some terminals, installers, or apps "Run as
administrator"), either run that app without elevation or run TransClip at the
same elevation. transclip doctor reports this under windows_elevated_paste.
For the portable CPU/CUDA path, install the model extras first:
uv pip install -e '.[models,audio]'On the current Linux gfx1151 workstation, the V1 latency profile uses AMD's
TheRock ROCm nightly index plus FlashAttention's Triton AMD backend. The
canonical runtime environment is .venv; the systemd service and GNOME
shortcut should point at .venv/bin/python3. Do not use the local custom wheel
for this app; it fails GPU tensor execution on this host.
Use the helper script (idempotent -- safe to re-run; it rebuilds .venv from the
pinned requirements-gfx1151.txt):
scripts/setup_gfx1151_env.shOr run the setup steps manually:
uv venv --clear --python 3.13 .venv
# --extra-index-url (NOT --index-url): the gfx1151 index only serves ROCm torch
# wheels, so replacing PyPI 404s on transformers etc. --pre allows ROCm prereleases.
uv pip install --python .venv/bin/python \
--extra-index-url https://rocm.nightlies.amd.com/v2/gfx1151/ --pre \
-r requirements-gfx1151.txt
uv pip install --python .venv/bin/python -e .
FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE MAX_JOBS=4 \
uv pip install --python .venv/bin/python --no-deps \
flash-attn==2.8.3 --no-build-isolation.venv (serving) vs .venv-dev (tooling). .venv holds the ROCm ML stack and
is built only by the script above. Lint/type/test tooling lives in a separate
.venv-dev (make dev-venv). Never run a bare uv run/uv sync against .venv:
torch is only on the ROCm index, so an unpinned sync prunes the whole ML stack and
breaks the service. make and .envrc pin UV_PROJECT_ENVIRONMENT=.venv-dev so the
serving env is never touched by tooling.
The default ASR backend is ibm-granite/granite-speech-4.1-2b-nar, selected
with asr_backend = "granite_nar", because it is the measured low-latency V1
path on gfx1151. The higher-accuracy autoregressive
ibm-granite/granite-speech-4.1-2b path remains available with
asr_backend = "granite". The current real-usage NAR run passes the V1 gate:
25 measured clips averaged 286 ms release-to-ready, with mean keyword
preservation at 0.952 and mean WER at 0.192.
On Linux GPU hosts the granite_nar backend can transcribe long recordings
incrementally while you are still speaking: once the uncommitted buffer
exceeds ~10 s, audio up to the most recent pause is transcribed in the
background and trimmed from the buffer, so releasing the key only has to
process the short residual tail. Measured on gfx1151
(eval/real-usage/long-recording-manifest.json, real-time paced): 38-50 s
recordings finalize in 287-708 ms versus 1.4-2.0 s for the one-shot batch
pass. Because committed segments are transcribed without later audio context,
quality may differ slightly from a single batch pass; a paired same-audio
comparison has not been run yet, so incremental mode is opt-in. Short
utterances are unaffected (still a single batch pass). Committed text never
changes because committed audio is physically removed from the buffer.
incremental_transcription = true # opt-in; defaults to false
incremental_commit_threshold_s = 10.0 # uncommitted audio that triggers a background pass
streaming_chunk_ms = 500 # mic chunk size fed to the session
warm_bucket_shapes_s = 16 # pre-warm NAR bucket shapes up to this length after startup (0 disables)After the service reports ready, the remaining NAR tensor-bucket shapes (4 s
through warm_bucket_shapes_s, in 2 s steps) are compiled in a background
thread that yields whenever a recording is active, so the first long utterance
after a restart does not pay a multi-second ROCm shape compile.
| Platform | Batch default | Incremental | Notes |
|---|---|---|---|
| Linux GPU (CUDA/ROCm) | Granite NAR | Opt-in | No extra dependencies |
| Linux/Windows CPU | Granite CPU/AR | No | Requires the granite_nar GPU backend |
| Windows CUDA | Granite AR (NAR selectable) | No | NAR selectable on CUDA; incremental pending validation |
| macOS MLX | MLX Whisper | Pending benchmark | Run scripts/bench_nar_mlx.py on an M-series (gate: warm 8 s pass <= 900 ms) |
While recording, GET /record/partial and the tray's Copy partial
transcript expose the committed text so far. The final text always runs the
normal post-ASR pipeline. Leave incremental_transcription unset (or set it
to false) for the default single-pass batch behavior.
GET /readyz (alias /healthz) reports ASR readiness: HTTP 200 with
{"ready": true, ...} once the model has loaded, or 503 with env_broken: true
and the error when the ML stack failed to import (e.g. torch was pruned from
.venv). Use it to tell a degraded service apart from a healthy one instead of
waiting for the first dictation to 500.
For fast local plumbing tests
without downloading a model, point asr_backend at a transcript file:
asr_backend = "file:/tmp/transcript.txt"Model loading is offline by default:
models_local_files_only = true
model_cache_dir = "/path/to/local/huggingface/cache"Populate the cache before running the service; the app should not download models during dictation. The helper commands are:
uv run -m transclip.cli models list
uv run -m transclip.cli models doctor
uv run -m transclip.cli models prefetch --model ibm-granite/granite-speech-4.1-2b-nar
uv run -m transclip.cli models prefetch --model Qwen/Qwen3.5-4BRun the helper through the same Python environment that runs the service. On
the current gfx1151 workstation, model downloads should use:
.venv/bin/python3 -m transclip.cli models prefetch --model ibm-granite/granite-speech-4.1-2b-nar
.venv/bin/python3 -m transclip.cli models prefetch --model Qwen/Qwen3.5-4BVoice mode routing runs after ASR and keyword restoration. Ordinary dictation
keeps the existing cleanup behavior unless a leading trigger phrase is spoken or
the tray setting enables model cleanup for all dictation. The CLI cleanup
command and POST /cleanup route follow that same dictation cleanup policy on
already-written text; they do not parse spoken trigger phrases. Shell mode validates
generated Bash with bash -n -c <command> when Bash is available and also uses
ShellCheck when installed and enabled. The shell prompt includes the user's
default shell from $SHELL, falling back to the login shell, while still asking
for Bash-compatible syntax. Invalid shell output is pasted as commented
diagnostic text. Valid shell commands are pasted for review only; TransClip
never presses Enter, executes the command, or auto-submits terminal input.
The tray menu includes Model cleanup always on. Enabling it persists
voice_model_cleanup_always_on = true and restarts the service so subsequent
ordinary dictation uses the shared Qwen text model:
voice_mode_routing_enabled = true
voice_model_cleanup_always_on = false
voice_mode_shell_enabled = true
text_model_runtime = "transformers"
text_model = "Qwen/Qwen3.5-4B"
shell_syntax_validation_enabled = true
shellcheck_enabled = trueThen transcribe a WAV:
uv run -m transclip.cli transcribe sample.wavInstall or refresh only the default GNOME shortcut for the Copilot key toggle workflow:
uv run -m transclip.cli install-gnome-shortcutThis creates or updates the same TransClip Toggle shortcut while
preserving unrelated custom shortcuts.
sudo apt update
sudo apt install -y \
libayatana-appindicator3-dev \
gir1.2-ayatanaappindicator3-0.1 \
python3-gi \
wl-clipboard \
wtype \
xdotool \
ydotoolLinux GNOME sessions use the native custom shortcut installed above. No
/dev/input group membership is required for the default toggle workflow.
On GNOME Wayland, clipboard copy/read requires wl-clipboard (wl-copy and
wl-paste). Paste injection uses wtype when the compositor supports the
virtual keyboard protocol, then ydotool if configured. xclip/xdotool are
X11-only fallbacks.
Create a JSON manifest:
{
"warmup_cases": [
{
"audio_path": "clips/warmup.wav",
"reference": "PyTorch on ROCm with gfx1151.",
"keywords": ["PyTorch", "ROCm", "gfx1151"]
}
],
"cases": [
{
"audio_path": "clips/example.wav",
"reference": "PyTorch on ROCm with gfx1151.",
"keywords": ["PyTorch", "ROCm", "gfx1151"]
}
]
}Run:
uv run -m transclip.cli eval eval-manifest.json --output eval-results.jsonThe output includes release-to-ready latency, WER when references exist, keyword preservation, and the number of warmup cases excluded from measured results.
For the required real-usage V1 eval, put 20 to 30 measured .wav clips and
matching reference .txt files in one folder. Optional per-clip keyword files
can use the same stem with .keywords.txt.
uv run scripts/record_real_eval_session.py ~/transclip-real-eval --manual-stopTo write the prompt list to a Markdown file first:
uv run scripts/record_real_eval_session.py ~/transclip-real-eval \
--prompt-sheet eval/real-usage/prompts.mdOr add individual custom clips:
uv run scripts/record_real_eval_clip.py ~/transclip-real-eval case_01 \
--duration 8 \
--reference "Use PyTorch on ROCm with gfx1151." \
--keywords PyTorch ROCm gfx1151Then build and run the eval:
TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 \
.venv/bin/python scripts/run_real_eval_pipeline.py \
~/transclip-real-evalmake test
make compile
.venv/bin/python scripts/check_v1_completion.pyContributors changing imports or adding platform code should read docs/package-layout.md for package boundaries and public entry points.
On Wayland, wtype is only usable when the compositor supports the virtual
keyboard protocol; GNOME Wayland may reject it. ydotool can be used as a
lower-level fallback when its daemon/uinput permissions are configured. On X11
or an XWayland-oriented session, use xdotool.
Check host readiness:
uv run -m transclip.cli doctor
uv run -m transclip.cli doctor --fix