`[[page|Label]]` is Obsidian syntax, not OKF. `kb-lint` resolves it Obsidian-style — by filename, anywhere in the bundle — so it has never tripped the link gate, while every other consumer of the bundle (an OKF renderer, a Gitea wiki export, `grep` for reachability) sees bracketed text where a link should be. That is how two generated pages ended up with no inbound link at all and nobody noticed: the my-first link census of the bundle reported them as orphans because their only inbound edges were wikilinks. Mechanical conversion, 52 links across 17 pages, each resolved against the same page set `kb-lint` uses: an explicit `|Label` is kept verbatim, an unlabelled link takes the target's frontmatter `title`. Targets are emitted bundle-absolute (`/tools/niri/tips.md`), the form the spec recommends and the form 99 existing links in this bundle already use. Two links were `[[index]]`, which resolves by filename to 21 pages — the ambiguity that filename-resolution invites. Both meant their own directory's index, so both are now written out in full: `/tools/gitea/index.md`, `/tools/niri/index.md`. The linter still tolerates wikilinks; whether it should reject them outright is an open question on #64, because that would replace the two tests pinning today's tolerance. Refs #64
7.0 KiB
type, title, description, resource, tags, timestamp, verified
| type | title | description | resource | tags | timestamp | verified | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| reference | Handy | Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI. | https://github.com/cjpais/Handy |
|
2026-10-09 | 2026-10-09 |
Handy
Handy is an offline speech-to-text (STT) transcription tool built with Tauri v2 and WebKitGTK. It runs entirely offline, supports multiple GPU backends, and provides real-time transcription with optional post-processing.
Installed version: 0.9.6-1 (local/handy-bin, /usr/bin/handy) — 0.9.8 upgrade pending: yay -Syu handy-bin (root work is operator-owned; agents never sudo).
Upstream latest: 0.9.8 (as of 2026-10-03)
Binary: /usr/bin/handy
Config: ~/.config/com.pais.handy/ (upstream docs) / ~/.local/share/com.pais.handy/settings_store.json (live observed)
Architecture
Handy uses Tauri v2 for its UI (WebKitGTK for rendering) and a native Rust backend for STT processing. On Wayland, it requires compositor-level global shortcuts.
Live settings snapshot (blankpad):
| Setting | Value |
|---|---|
paste_method |
none |
copy_to_clipboard |
true |
push_to_talk |
true |
bindings |
transcribe=ctrl+space, transcribe_with_post_process=ctrl+shift+space, cancel=escape |
keyboard_implementation |
handy_keys |
overlay |
bottom (live), true (config) |
model |
handy-computer/canary-180m-flash-gguf (Q8_0) |
transcribe_gpu |
vulkan (PCI 07:00.0) |
autostart_enabled |
false (niri-managed) |
experimental |
true |
filler_word_removal |
true |
post_process_enabled |
true (fleet-wired) |
clipboard_handling |
copy_to_clipboard |
provider |
custom (base_url=http://192.168.0.143:7771/v1, model=frank-secondary) |
prompt |
default_improve_transcriptions |
Configuration
Config Location
Upstream docs: ~/.config/com.pais.handy/
Live observed: ~/.local/share/com.pais.handy/settings_store.json
Both locations may contain settings; the live data directory is the authoritative runtime store.
Live Settings (blankpad)
The store is nested: everything lives under a top-level settings object.
Post-processing wiring (applied 2026-10-09) touches five keys plus the
custom entry inside post_process_providers:
{ "settings": {
"post_process_enabled": true,
"post_process_provider_id": "custom",
"post_process_models": { "custom": "frank-secondary", "openai": "", "...": "" },
"post_process_selected_prompt_id": "default_improve_transcriptions",
"post_process_providers": [
{ "id": "custom", "base_url": "http://192.168.0.143:7771/v1",
"allow_base_url_edit": true, "models_endpoint": "/models", "...": "" }
],
"post_process_api_keys": { "custom": "", "...": "" },
"autostart_enabled": false,
"push_to_talk": true,
"paste_method": "none",
"clipboard_handling": "copy_to_clipboard",
"keyboard_implementation": "handy_keys",
"overlay_position": "bottom",
"overlay_style": "live"
} }
Repo-side merge patch + apply procedure:
dotfiles/zz-blankpad/.local/share/com.pais.handy/ (README.md).
Command-Line Interface
Startup Flags
handy --start-hidden # Start without showing the UI
handy --no-tray # Hide the system tray icon
handy --debug # Enable debug mode (Ctrl+Shift+D)
Remote Control
Send commands to a running instance:
handy --toggle-transcription # Toggle transcription on/off
handy --toggle-post-process # Toggle post-processing on/off
handy --cancel # Cancel the current transcription
Wayland Integration
Global Shortcuts
On Wayland, global shortcuts must be defined in the compositor (niri), not in Handy itself.
File: ~/.config/niri/cfg/keybinds.kdl
// Live (cfg/keybinds.kdl):
Mod+Shift+Space hotkey-overlay-title="Handy: toggle post-process recording" allow-when-locked=true { spawn-sh "handy --toggle-post-process"; }
// Transcription itself stays on Handy's own ctrl+space (keyboard_implementation="handy_keys").
Text Input
Requirement: wtype or dotool for text input (NOT installed on blankpad).
Current config: paste_method=none + manual Ctrl+V paste.
Overlay
Default: Disabled on Linux (can steal focus and break paste).
Live config: mode=bottom, live=true.
Troubleshooting
WebKit Renderer Issue
Symptom: Blank or corrupted rendering.
Fix:
export WEBKIT_DISABLE_DMABUF_RENDERER=1
handy
SIGUSR1 Crash
Trap: The upstream SIGUSR1 handler was REMOVED (WebKitGTK now owns it).
# WRONG (may crash the app):
pkill -USR1 handy
# CORRECT (toggle transcription):
pkill -USR2 -n handy
# or use CLI:
handy --toggle-transcription
Debug Mode
handy --debug
# or press Ctrl+Shift+D in a running instance
Dependencies
| Package | Purpose |
|---|---|
handy |
Main application |
tauri-apps |
Tauri v2 runtime |
webkit2gtk |
WebKitGTK rendering |
gtk-layer-shell |
Wayland layer-shell support |
vulkan |
GPU-accelerated STT |
ffmpeg |
Audio processing |
Fleet LLM Endpoint
Post-processing pipeline: handy → http://192.168.0.143:7771/v1, model id
frank-secondary (the served id; harness callsign for the same unit is
frank-zealot — same llama.cpp server, also the fleet compaction endpoint).
Endpoint configuration:
- Provider:
custom(the one preset withallow_base_url_edit: true) - Base URL:
http://192.168.0.143:7771/v1 - Model:
frank-secondary - Prompt:
default_improve_transcriptions
Known issues
Thinking-model empty content (2026-10-09, open). frank-secondary is a
thinking model. Default chat completions return content: "" with the answer
trapped in reasoning_content — Handy reads content, so post-processed
transcripts arrive empty. Verified per-request cure:
"chat_template_kwargs": {"enable_thinking": false} (probe below returns
cleaned text in content). Handy has no field to send extra body keys, and
the cure CANNOT be set server-global on :7771 without degrading fleet
compaction (same server). Proposed fix (not yet implemented): a dedicated
no-think unit/port for Handy traffic, e.g. llama-server on :7772 with
thinking disabled at the template level, then repoint custom.base_url.
curl -s http://192.168.0.143:7771/v1/chat/completions -d '{
"model": "frank-secondary",
"messages": [{"role":"user","content":"Clean this transcript: twenty five items cost ten dollars um ok."}],
"max_tokens": 200,
"chat_template_kwargs": {"enable_thinking": false}}'
See Noctalia Shell for IPC integration patterns.
See Also
- noctalia-shell.md — Desktop environment integration
- niri — Compositor configuration
handy --help— CLI reference