Files
command-center/agent/managed-skills/openwebui-tts-chain-audit/SKILL.md
T

2.8 KiB

name, description
name description
openwebui-tts-chain-audit Audit/repair OpenWebUI TTS on Dankclaw when cloned voices get rejected by a 29-name Kokoro enum or synthesis is silent; rootless DB + endpoint probes

Audit OpenWebUI's TTS/STT wiring on Dankclaw when a voice is rejected, synthesis is silent, or someone asks "is the cloned voice wired correctly". Not for model-picker/ACL issues (openwebui-picker-acl-repair owns those).

Shape of the stack

  • Container open-webui runs rootless as dank on Dankclaw: ssh dankclaw 'podman exec open-webui ...' (NO sudo: sudo podman sees an empty world). Host port 3005.
  • Server-side config = webui.db config table (admin), per-user overrides = user.settings JSON column. Settings changed via UI persist per-user; admin page persists to config.
  • Cloned voices live behind the frank-tts façade :7772 (/v1/audio/speech, /v1/audio/voices); voices resolve to /home/dank/voices/<name>/reference.wav.

The trap (2026-10-18 law)

Error: Voice "narrator1" not found. Should be one of: af_heart, ... (29 names) is OpenWebUI's bundled in-browser Kokoro (browser-kokoro) rejecting client-side — frank-tts never sees the request. Signature: container logs show zero audio/speech hits. Fix = the personal dropdown Settings -> Audio -> TTS Engine -> Default (empty string), not any deploy.

Probe sequence (read-only, safe)

  1. Admin truth: podman exec open-webui python -c "import sqlite3; c=sqlite3.connect('/app/backend/data/webui.db'); [print(k,'=',v) for k,v in sorted(dict(c.execute('SELECT key,value FROM config')).items()) if 'audio' in k or 'tts' in k.lower()]" Expect: audio.tts.engine=openai, audio.tts.openai.api_base_url=http://host.containers.internal:7772/v1, audio.tts.model=frank-tts, audio.tts.voice=<cloned name>.
  2. User overrides — a user pinned to browser-kokoro beats admin config. CAUTION: user.settings can be SQL NULL or the JSON string "null": ... for uid,s in c.execute('SELECT id,settings FROM user'): d=json.loads(s) if s else None; d=d if isinstance(d,dict) else {}; hits={k:v for k,v in d.items() if 'audio' in k or 'tts' in k.lower()} ...
  3. Endpoint truth: curl -s http://127.0.0.1:7772/v1/audio/voices (roster), then POST the exact OpenWebUI body {"model":"frank-tts","input":"probe","voice":"narrator1","response_format":"mp3","speed":1.0} to /v1/audio/speech — expect 200 audio/mpeg.
  4. Contrast logic: server-path failure leaves a container log line AND a façade error; browser-kokoro failure leaves neither.

Law

  • Full contract + diagnosis live in serverAppConfigs docs (Dankclaw/OpenWebUI.md § Text-to-speech, Dankclaw/Frank TTS.md troubleshooting). Docs are the source; this skill is just the probe route.
  • Rootless gotcha generalizes: any Dankclaw service probe through podman goes WITHOUT sudo unless the unit is a system quadlet.