4.8 KiB
name, description
| name | description |
|---|---|
| frank-tts-consumer-voice-triage | Triage a consumer (OpenWebUI, omp, open-notebook) rejecting or ignoring a frank-tts voice name: prove whether the server or the browser-side Kokoro.js engine produced the error. |
frank-tts consumer voice triage
A consumer says a voice name is invalid, speaks in the wrong voice, or is
silent. frank-tts (192.168.0.143:7772, shim
dankclaw/voiceservice/app.py) resolves voice=<name> against
/home/dank/voices/<name>/reference.wav and answers 400 + the list of
voices that exist for a name it cannot resolve — never a silent fallback.
Any error naming a different voice list did not come from it.
Signature that decides the case immediately: an enum of ~29 names
af_heart, af_alloy, … am_*, bf_*, bm_* … bm_fable is Kokoro's fixed pack,
the voice list of Kokoro.js running in the browser (OpenWebUI ships it
in its own front end as TTS engine value browser-kokoro, with a personal
"Kokoro.js dtype" setting; model onnx-community/Kokoro-82M-v1.0-ONNX).
narrator1/2/3 are frank-tts cloned voices. Kokoro cannot clone.
Instrument ladder (stop at the first that answers)
- What the endpoint actually accepts (never infer from a consumer's text):
curl -s http://127.0.0.1:7772/v1/audio/voices→ JSON array of pack names. - Speak with the consumer's exact body, from inside the consumer's
container — same network, same shape. Write the JSON to a file and
--data-binary @file(nestedsh -cquoting silently sends an empty body and yields a bogus400 malformed request body):printf '%s' '{"model":"frank-tts","input":"probe","voice":"narrator1","response_format":"mp3","speed":1.0}' > /tmp/p.json podman cp /tmp/p.json <consumer>:/tmp/p.json podman exec <consumer> curl -sS -o /tmp/o.bin -w 'http=%{http_code} type=%{content_type} bytes=%{size_download}\n' \ -X POST http://host.containers.internal:7772/v1/audio/speech \ -H 'Content-Type: application/json' --data-binary @/tmp/p.json200 audio/mpeg|audio/wav⇒ the server path is healthy; the failure is elsewhere. - Did the request arrive at all?
podman logs --tail 400 <consumer> | grep -c audio/speech. A server-path failure leaves a log line and a frank-tts error body; a client-side engine leaves neither. This contrast is the discriminator. - Where the rejection string lives (proves the emitter without guessing):
Matched inside the front-end bundle ⇒ browser-side validator, not the API.
podman exec <consumer> sh -c 'grep -ho ".\{200\}Should be one of.\{80\}" /app/build/_app/immutable/chunks/*.js | head' podman exec <consumer> sh -c 'grep -ho ".\{60\}af_heart.\{200\}" /app/build/_app/immutable/chunks/*.js | head -2' - Config authority sweep (OpenWebUI): admin config is the
configtable (one key per row, JSONvalue; expectedaudio.tts.engine=openai,audio.tts.model,audio.tts.voice,audio.tts.openai.api_base_url); per-user overrides are thesettingsJSON column of tableuser(singular, nois_active; some rows hold the stringnull→ guard it).audio.stt.*andaudio.tts.*are separate. Nosqlite3CLI in the image or on the host:podman exec -i <c> python -with the stdlib module.podman execneeds-ito accept a heredoc script — without it the script is silently dropped and you get no output.
Known traps
- OpenWebUI is a rootless quadlet:
sudo podman …reports no such container. Read it rootless. podman psin this podman build has no.ImageNamefield — use{{.Image}}.- The in-browser engine choice is per-browser storage: empty
user.settings+ correct admin config + zero backend hits is the normal shape of this bug. Fix is UI: Settings → Audio → TTS Engine → Default. GET /api/configpublishes noaudioblock, and nonexistent/api/...paths return the SPA HTML with200— never conclude "config missing" from an HTML body.- TTS is memory-registry sensitive: the engine's voice registry is memory-only, so "every name returns 400" after a restart points at the warm pass, not at the caller (see frank-tts-add-cloned-voice).
Local playback side (omp /say, manual probes)
If synthesis succeeds (200, sane WAV bytes) but nothing is audible, the
fault is the PipeWire graph, not the endpoint:
wpctl status (default sink marked *, and Default Configured Devices —
a remembered-but-offline bluez_output.* there is the classic silent
route), wpctl get-volume @DEFAULT_AUDIO_SINK@, pw-play -v <file> (expect
stream drained + exit 0), and target a device explicitly with
pw-play --target <node-id> to identify which hardware is live. A one-word
clip (~0.7 s) is easy to miss — probe with a multi-second sentence.