#!/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 (its own usage prints `--host Listen address (default: # 127.0.0.1)`), and inside a container netns a loopback bind is reachable by # nothing outside that namespace — the façade dials this container by its bridge # address on the private voice network, which is why the address has to be # broad. The unit PUBLISHES NOTHING (see # dankclaw/voiceservice/quadlet/frank-tts-engine.container: no PublishPort at # all, not even 127.0.0.1:8880), so the only namespaces that can reach # 0.0.0.0:8880 are that bridge and this container's own loopback — and its own # loopback is exactly where the boot warmer and the health probe speak. : "${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"