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.
85 lines
4.2 KiB
Bash
Executable File
85 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# tts-engine-start.sh — the entrypoint of the frank-tts-engine image
|
|
# (dankclaw/voiceservice/Containerfile.engine). It exists for one reason: the
|
|
# knobs of the arm A engine are command-line flags, while the fleet's knobs
|
|
# live in scripts/frank-tts-engine.env and reach the container through
|
|
# EnvironmentFile=. This is the translation layer, and nothing else — no
|
|
# supervision, no retries, no logging of its own. PID 1 stays the server, so
|
|
# `podman stop` and the unit's Restart=always drive it directly.
|
|
#
|
|
# THE FLAGS THAT ARE NOT HERE, and must stay absent:
|
|
#
|
|
# --threads / -t does not exist in this build. src/backend.h computes the
|
|
# CPU thread count as std::thread::hardware_concurrency()/2
|
|
# (the comment there: GEMM shares SIMD units across
|
|
# hyperthreads), and there is no flag to override it. So a
|
|
# CPU affinity pin (CPUQuota=, taskset, a cpuset) silently
|
|
# changes the engine's thread count instead of just its
|
|
# placement — pinning is deliberately not done anywhere in
|
|
# this stack. See the env file header for the same warning
|
|
# aimed at the operator.
|
|
# --device left at its default. Every GPU backend is compiled out of
|
|
# the image, so the default device pick can only land on
|
|
# CPU; naming a device would add a way to be wrong.
|
|
#
|
|
# Everything defaults to the shape the 2026-10-18 trial measured, so an empty
|
|
# environment still boots the decided configuration: 1.7 B talker Q4_K_M with
|
|
# the Q8_0 tokenizer, one request at a time, on the private bridge.
|
|
set -euo pipefail
|
|
|
|
# The binary path. Overridable for one reason: the bats seam runs a stub
|
|
# instead of a 1.2 GiB model. Operators never set it — the image ships exactly
|
|
# one binary at /app/tts-server.
|
|
: "${QWENTTS_BIN:=/app/tts-server}"
|
|
|
|
# Paths are CONTAINER paths (the host directories they come from are named in
|
|
# scripts/frank-tts-engine.env and mounted by the Quadlet unit).
|
|
: "${QWENTTS_MODEL:=/models/qwen-talker-1.7b-base-Q4_K_M.gguf}"
|
|
: "${QWENTTS_CODEC:=/models/qwen-tokenizer-12hz-Q8_0.gguf}"
|
|
|
|
# Reported by GET /v1/models, and what scripts/frank-tts.env's
|
|
# TTS_UPSTREAM_MODEL names. A model id, not a filename: the file name would
|
|
# leak a quantization choice into every consumer's config.
|
|
: "${QWENTTS_ALIAS:=frank-tts-engine}"
|
|
|
|
# 0.0.0.0 inside the container is not "public". tts-server defaults to
|
|
# 127.0.0.1, which inside a container netns is reachable by nothing — the
|
|
# façade dials this container by its bridge address. The container publishes
|
|
# 8880 on the host's loopback only (diagnostics and the boot warmer), so the
|
|
# only namespace that can reach 0.0.0.0:8880 here is the private voice bridge.
|
|
: "${QWENTTS_HOST:=0.0.0.0}"
|
|
: "${QWENTTS_PORT:=8880}"
|
|
|
|
# The language label used when a request omits one. `auto` is the upstream
|
|
# default and what the trial ran.
|
|
: "${QWENTTS_LANG:=auto}"
|
|
|
|
# One request at a time. The server-side queue is a serialised FIFO at this
|
|
# setting, which is what keeps the measured per-voice timings true; batching
|
|
# on a CPU build splits the same cores between two syntheses and the warm RTF
|
|
# margin (0.55-0.67 against real time 1.0) disappears. The engine's own
|
|
# default is also 1; it is written here because it is a decision, not an
|
|
# accident.
|
|
: "${QWENTTS_MAX_BATCH:=1}"
|
|
|
|
# Fail loudly BEFORE binding, so the journal says which artifact is missing
|
|
# instead of the unit reporting a container that starts and dies. This is the
|
|
# self-healing path: Restart=always re-runs it every RestartSec, and the same
|
|
# sentence repeats until the operator fetches the weights
|
|
# (scripts/download-frank-tts-engine-qwentts.sh).
|
|
for artifact in "$QWENTTS_MODEL" "$QWENTTS_CODEC"; do
|
|
if [[ ! -r "$artifact" ]]; then
|
|
echo "frank-tts-engine: cannot read ${artifact}; run scripts/download-frank-tts-engine-qwentts.sh on the host (the /models mount is read-only and holds the two GGUF files)" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
exec "$QWENTTS_BIN" \
|
|
--model "$QWENTTS_MODEL" \
|
|
--codec "$QWENTTS_CODEC" \
|
|
--alias "$QWENTTS_ALIAS" \
|
|
--host "$QWENTTS_HOST" \
|
|
--port "$QWENTTS_PORT" \
|
|
--lang "$QWENTTS_LANG" \
|
|
--max-batch "$QWENTTS_MAX_BATCH"
|