Files
Frank e86e60af39 docs: close OKF conformance gaps found by night audit
- references/okf-spec.md: replace v0.2 note with the deltas verified
  against upstream SPEC.md 2026-10-19 (timestamp->generated.at and
  # Citations->sources are the two breaking changes; trust fields and
  root-index okf_version are additive; bundle stays v0.1).
- index.md: restore the agentic-web-research section lost in the
  2026-08-28 rebuild and link log.md (index-coverage doctrine).
- gen-keybinds.sh emits OKF frontmatter so its generated artifact
  (tools/niri/keybindings.md, regenerated, 120 bindings) passes kb-lint;
  test added red->green.
- fix the two broken [[niri]] wikilinks (handy, noctalia-shell).

Refs #29
2026-10-19 02:43:33 -07:00

234 lines
6.9 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]] for IPC integration patterns.
---
## See Also
- [[noctalia-shell|noctalia-shell.md]] — Desktop environment integration
- [niri](niri/index.md) — Compositor configuration
- [`handy --help`](https://github.com/cjpais/Handy) — CLI reference