Files
serverAppConfigs/scripts/download-frank-tts-engine-qwentts.sh
Frank 8d76854a1e feat(frank-tts-engine): the arm A engine as a boot-persistent quadlet unit
Repo state only: nothing deployed, no host touched, no container started.

- `frank-tts-engine` on Dankclaw: `qwentts.cpp` commit
  5916bf7c79dbb4e66fc059cdc507a79a23a28bb4, resident `tts-server`, 1.7 B base
  talker Q4_K_M + Q8_0 tokenizer, container port 8880. It is deliberately NOT a
  fleet endpoint — `frank-tts` on 7772 is the endpoint consumers address.
- Publishes NOTHING. A host-loopback publish would have been useless to the
  façade (netavark DNATs published ports only, so a bridge container cannot
  reach a host loopback bind — the shape that 502'd hermes) and would have been
  the one LAN-reachable surface of an unauthenticated synthesiser. Both voice
  containers sit on one committed quadlet network, `frank-tts-engine.network`
  pinned to 10.89.5.0/24 (next free /24 after open-notebook's 10.89.4.0/24), and
  peers address the engine by container name. Diagnostics and the deployer's
  health gate run inside the netns (`podman exec`, `wait_container_healthy`).
- Cold-voice law honoured in the unit: the first request for a registered voice
  decodes its whole reference to snapshot codec state (6.2-8.0 s, first audio
  byte 5 530 ms), later ones restore in 0.5 ms, and the registry is memory-only.
  `ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh`
  with `TimeoutStartSec=900`; the `-` prefix keeps a failed warm from
  crash-looping the endpoint under `Restart=always`. It logs cold=/warm= per
  voice, so the under-a-second second request is proven on every boot.
- Thread law recorded: `tts-server` has no `--threads`; src/backend.h uses
  hardware_concurrency()/2, so a cpuset pin silently changes the thread count.
- Image builds from the pinned commit with the proven line (GGML_BLAS=OFF
  forced by this host, every GPU backend off, -j 8, no -DGGML_NATIVE because
  ggml defaults to -march=native for the building host). Two stages; the runtime
  stage COPYs both the entrypoint and the warmer, because a container that
  publishes nothing is reached with `podman exec`, not a port.
- `scripts/download-frank-tts-engine-qwentts.sh` fetches the two GGUFs with byte
  and LFS-oid checks (talker 1 219 245 248 B, tokenizer 291 150 624 B); the
  deployer gates on --verify-only instead of pulling 1.4 GiB mid-deploy.
- Registered in SERVICE_ORDER, DANKCLAW_SERVICES, the deploy-remote.sh dispatch,
  its pre-deploy requirement, deploy.sh's regex, the planner's path map (the
  whole `dankclaw/voiceservice/` tree mapped to nothing before this), and
  test-changed.sh ABOVE the `dankclaw/*.container` case whose `*` crosses `/`.
- New seam tests/frank_tts_engine.bats (19): the Quadlet generator's own output
  for network/publish/boot/warm shape, entrypoint argv driven from the tracked
  env file, the warmer against a stub tts-server over a real socket
  (registration keys, ref_text absent, late engine, hung synthesis, name
  refusal), the fetcher's verifier, the pinned cmake line, and both routing
  tables. Suite: 422 passed.
- Vault: [[Frank TTS Engine]] linked from the index; log.md entry for the day.
2026-10-18 17:26:50 -07:00

94 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Download the frank-tts-engine payload: the two GGUF files the arm A engine
# (qwentts.cpp `tts-server`, dankclaw/voiceservice/Containerfile.engine) loads
# once at startup — Serveurperso/Qwen3-TTS-GGUF's **1.7 B base talker at
# Q4_K_M** and the **12 Hz tokenizer at Q8_0**. Public repo — no auth header,
# and never a token on the command line (world-readable in /proc/<pid>/cmdline).
#
# THE BASE VARIANT IS MANDATORY, NOT A PREFERENCE. The engine refuses every
# voice registration unless the loaded model reports type "base"
# (tools/tts-server.cpp, register_voice): on a customvoice or voicedesign talker
# the cloned-voice product does not exist. The 0.6 B talker (628,905,056 B for
# its Q4_K_M) also exists and is NOT the decided artifact — 1.7 B is what live
# output was measured with on 2026-10-18.
#
# Fresh downloads, no resume (the same rule the frank-zealot fetcher learned the
# hard way: `curl -C -` once produced an oversized file on a multi-GB payload);
# -f/--fail-with-body so an HTTP error is never a success; then exact byte count
# and sha256 against the Hub's own LFS oid. This repo IS LFS, so the sha values
# below are the pointer oids the Hub publishes — verified at fetch, not
# computed locally. Idempotent: a complete, correctly-hashed file is skipped,
# and `--verify-only` reports without downloading (that is the mode the deployer
# and the runbook use to answer "are the weights there?").
#
# Both files together are 1.4 GiB and the engine mmaps them: peak RSS measured
# 5.7 GiB with both resident. Nothing here is deleted on a swap — the previous
# talker stays on disk, because the rollback for a bad engine build is to point
# QWENTTS_MODEL at a different file, not to re-download 1.4 GiB under pressure.
#
# Usage:
# scripts/download-frank-tts-engine-qwentts.sh [DEST_DIR] [--verify-only]
# Default DEST_DIR is the host directory the frank-tts-engine unit mounts
# read-only at /models.
set -euo pipefail
DEST=""
VERIFY_ONLY=0
for arg in "$@"; do
case "$arg" in
--verify-only) VERIFY_ONLY=1 ;;
*) DEST="$arg" ;;
esac
done
DEST="${DEST:-/home/dank/models/Qwen3-TTS-GGUF}"
# repo | hub-name | dest-name | bytes | sha256 (Hub LFS oid, resolved 2026-10-18)
FILES=(
"Serveurperso/Qwen3-TTS-GGUF|qwen-talker-1.7b-base-Q4_K_M.gguf|qwen-talker-1.7b-base-Q4_K_M.gguf|1219245248|ea393ebaf2167ea23ce9fc18b093822851358a950d7075cd47ab4f6ce23e887d"
"Serveurperso/Qwen3-TTS-GGUF|qwen-tokenizer-12hz-Q8_0.gguf|qwen-tokenizer-12hz-Q8_0.gguf|291150624|1883beeed99348fc35e23dd225e9082f93f6f8c109330a33d935baa8acdbfd94"
)
mkdir -p "$DEST"
status=0
for entry in "${FILES[@]}"; do
IFS='|' read -r repo hubname destname size sha <<<"$entry"
url="https://huggingface.co/${repo}/resolve/main/${hubname}"
f="$DEST/$destname"
have=0
if [[ -f "$f" ]] && [[ "$(stat -c%s "$f")" == "$size" ]]; then
if sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
have=1
fi
fi
if (( have )); then
echo "OK $destname"
continue
fi
if (( VERIFY_ONLY )); then
echo "MISSING $destname"
status=1
continue
fi
echo "FETCHING $destname (from $repo)"
tmp="$f.part"
curl -fsSL --fail-with-body -o "$tmp" "$url"
mv "$tmp" "$f"
if [[ "$(stat -c%s "$f")" != "$size" ]]; then
echo "SIZE FAIL $destname ($(stat -c%s "$f") != $size)"
status=1
continue
fi
if ! sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
echo "SHA FAIL $destname"
status=1
continue
fi
echo "VERIFIED $destname"
done
if (( status == 0 )); then
echo "DOWNLOAD_DONE $DEST"
else
echo "DOWNLOAD_FAILED $DEST" >&2
fi
exit "$status"