82 lines
3.9 KiB
Markdown
82 lines
3.9 KiB
Markdown
---
|
||
name: frank-tts-add-cloned-voice
|
||
description: "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 A `qwentts.cpp tts-server`, container-internal `:8880`,
|
||
publishes **no port**. Reach it with `podman exec frank-tts-engine curl …`.
|
||
- `frank-tts` — OpenAI façade on host `:7772`; resolves voice **by name**.
|
||
- Packs: `/home/dank/voices/<name>/` with `reference.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
|
||
|
||
1. Build the pack (dry-run first — sizes are arithmetic and asserted).
|
||
2. 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).
|
||
3. 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.
|
||
4. Prove from the consumer side, not the config:
|
||
`curl -s localhost:7772/v1/audio/voices` then a real
|
||
`POST /v1/audio/speech` and 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` `COPY`s, 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 stop` **removes** the quadlet container — `podman start` then
|
||
finds nothing. To run the engine without its `ExecStartPost` hook, use a
|
||
separate container, not stop+start.
|
||
- `podman cp` of 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).
|