3.9 KiB
name, description
| name | description |
|---|---|
| frank-tts-add-cloned-voice | Add or repair a cloned voice on the dank fleet: pack builder, the reference.wav contract, the boot warm hook (argv-cap and env-quote laws), and how to prove a voice really answers. |
Adding (or repairing) a cloned voice on the dank fleet
Repo law lives in serverAppConfigs-docs/Dankclaw/Voice Packs.md and
Frank TTS Engine.md — read those for the pack format. This skill is the
operating sequence plus the traps that cost real time on 2026-10-18.
Stack, fast
frank-tts-engine— arm Aqwentts.cpp tts-server, container-internal:8880, publishes no port. Reach it withpodman exec frank-tts-engine curl ….frank-tts— OpenAI façade on host:7772; resolves voice by name.- Packs:
/home/dank/voices/<name>/withreference.wav(+ manifest,.spk,.rvq). Builder:scripts/build-voice-pack.sh(crop 19.84–25 s, first 10 s of any source is forbidden). - Weights:
/home/dank/models/Qwen3-TTS-GGUF/—scripts/download-frank-tts-engine-qwentts.sh [--verify-only](1.4 GiB, ~18 s on the LAN; verifies byte count + Hub LFS sha256).
Sequence
- Build the pack (dry-run first — sizes are arithmetic and asserted).
- Deploy the engine only if its files changed:
./scripts/deploy.sh dankclaw --service frank-tts-engine(first run builds the image: ~10 min recursive source clone). - Register/warm:
podman exec frank-tts-engine /app/warm-voices.sh— registers every pack each run (the engine replaces a name it already holds;tools/tts-server.cpp:148), then synthesizes twice per voice. - Prove from the consumer side, not the config:
curl -s localhost:7772/v1/audio/voicesthen a realPOST /v1/audio/speechand listen to the bytes.
Trap 1 — the kernel's argument cap (this broke the first deploy)
/usr/bin/curl: Argument list too long in the warm log ⇒ a request body was
passed as one argv string. Linux caps a single argument at
MAX_ARG_STRLEN = 131 072 bytes; a ~1 MB reference.wav base64's to ~1.27 MB.
curl never runs, and hooks report it as HTTP no response while /health is
green. Fix = write the body to a file and use --data-binary @file; feed the
payload through a redirection, not $(base64 …).
Regression-test shape: real curl + real listener + a fixture >128 KB, so the
kernel (not a stub) produces the failure. Any pack over ~96 KB trips it.
Trap 2 — two readers of the env file disagree about quotes
bash set -a; source strips "…". podman's --env-file (how the unit really
reads it) does not. A value interpolated into a JSON body therefore arrives
quoted and invalidates it. Test both readers, not just source.
Trap 3 — locally built images skip rebuilds on a stale hash
build_compose_image compares a cfhash label; it must cover every file the
Containerfile COPYs, or an edited app.py/hook deploys "successfully" while
the container runs old code.
Measured numbers (2026-10-18, deployed build — retire the trial's ladder)
| thing | value |
|---|---|
| first synthesis of a freshly registered voice | 1 883 ms |
| later short requests | 1 226–1 283 ms |
| ~110-word request (repeat) | 9.4 s / 9.8 s — tracks generated audio, not state |
| whole boot warm, 3 packs | 15.2 s |
| engine resident | ~4.0 GiB |
The trial's 6.2–8.0 s cold → 0.5 ms warm claim does not reproduce; warming
is for registration (registry is memory-only, re-register ≠ warm) plus ~0.6 s.
Ops notes
systemctl --user stopremoves the quadlet container —podman startthen finds nothing. To run the engine without itsExecStartPosthook, use a separate container, not stop+start.podman cpof a fixed script into a running container is a trial only: the layer dies with the container. Merge + redeploy to make it durable.- Nothing on the voice pair exposes
/metrics; Prometheus can't scrape it (a blackbox HTTP probe on 7772 is the only honest option).