`[[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
234 lines
7.0 KiB
Markdown
234 lines
7.0 KiB
Markdown
---
|
|
type: reference
|
|
title: Handy
|
|
description: Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI.
|
|
resource: https://github.com/cjpais/Handy
|
|
tags:
|
|
- handy
|
|
- stt
|
|
- speech-to-text
|
|
- transcription
|
|
- wayland
|
|
- tauri
|
|
timestamp: 2026-10-09
|
|
verified: 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`:
|
|
|
|
```json
|
|
{ "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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`
|
|
|
|
```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:**
|
|
|
|
```bash
|
|
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
|
handy
|
|
```
|
|
|
|
### SIGUSR1 Crash
|
|
|
|
**Trap:** The upstream SIGUSR1 handler was **REMOVED** (WebKitGTK now owns it).
|
|
|
|
```bash
|
|
# WRONG (may crash the app):
|
|
pkill -USR1 handy
|
|
|
|
# CORRECT (toggle transcription):
|
|
pkill -USR2 -n handy
|
|
# or use CLI:
|
|
handy --toggle-transcription
|
|
```
|
|
|
|
### Debug Mode
|
|
|
|
```bash
|
|
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 with `allow_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`.
|
|
|
|
```bash
|
|
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](/tools/noctalia-shell.md) for IPC integration patterns.
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [noctalia-shell.md](/tools/noctalia-shell.md) — Desktop environment integration
|
|
- [niri](niri/index.md) — Compositor configuration
|
|
- [`handy --help`](https://github.com/cjpais/Handy) — CLI reference
|