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.
53 lines
2.9 KiB
SYSTEMD
53 lines
2.9 KiB
SYSTEMD
# Quadlet network: the private bridge of the voice pair (rootless).
|
|
#
|
|
# Install path on dankclaw: ~/.config/containers/systemd/frank-tts-engine.network
|
|
# Unit name: frank-tts-engine-network.service (podman network name:
|
|
# systemd-frank-tts-engine — quadlet prefixes "systemd-", so probe the runtime
|
|
# name, never the filename; see [[Container Network Namespaces]]).
|
|
#
|
|
# WHY A PRIVATE BRIDGE FOR A PAUSE THAT LOOKS PUBLIC-FACING. frank-tts (:7772,
|
|
# published, the one endpoint consumers address) and frank-tts-engine (:8880,
|
|
# never published, the arm A qwentts.cpp server behind it) could both have
|
|
# stayed on the fleet-wide dankclaw_default and resolved each other by name.
|
|
# They are split off anyway, for two reasons:
|
|
#
|
|
# 1. THE HOST-LOOPBACK TRAP. The phase-1 shape in `scripts/frank-tts.env` still
|
|
# names `host.containers.internal:8880` as the upstream, on the assumption the
|
|
# engine runs on the host and publishes `127.0.0.1:8880`. That cannot work:
|
|
# netavark DNATs published ports only, a published port is not on any podman
|
|
# bridge, and a container cannot reach the host's own loopback bind — the same
|
|
# shape that 502'd hermes, and the reason the signal-relay hop needs a host LAN
|
|
# bind instead ([[Container Network Namespaces]], [[Homepage Status Probes]]).
|
|
# Putting both containers on one committed network removes the guess: the
|
|
# façade dials the engine's container DNS name on the network that owns it, and
|
|
# the engine publishes nothing at all.
|
|
#
|
|
# 2. THE ENGINE HAS NO AUTH AND NO QUEUE LIMIT WORTH THE NAME. `tts-server`
|
|
# answers anyone who can open a socket, and at `--max-batch 1` a second
|
|
# caller simply waits behind the first. Left on dankclaw_default every
|
|
# container on that net — open-webui, hermes, the *arr proxies — could drive
|
|
# a CPU synthesiser that occupies every other core on a box that also serves
|
|
# two text endpoints. On this bridge the only dialer is frank-tts: the engine
|
|
# publishes no port, so not even the host has an address to reach it at, and
|
|
# diagnostics go through `podman exec` instead.
|
|
#
|
|
# Kernel bridge, not slirp4netns: the 2026-08-22 open-webui incident (a dead
|
|
# per-container userspace helper left a container that accepted TCP and reset
|
|
# every request for two days, logging nothing). A bridge has no such helper.
|
|
#
|
|
# Subnet registry on dankclaw (rootless):
|
|
# 10.88.0.0/16 default "podman" bridge
|
|
# 10.89.0.0/24 gitea (systemd-gitea)
|
|
# 10.89.1.0/24 compose stack (dankclaw_default)
|
|
# 10.89.2.0/24 searxng (systemd-searxng)
|
|
# 10.89.3.0/24 open-webui (systemd-open-webui)
|
|
# 10.89.4.0/24 open-notebook (systemd-open-notebook)
|
|
# 10.89.5.0/24 voice pair (systemd-frank-tts-engine) <- this file
|
|
# Pinned so the two container IPs stay deterministic across reboots, like the
|
|
# other pinned nets; 10.89.5.0/24 was the next unclaimed /24 in this registry.
|
|
|
|
[Network]
|
|
Driver=bridge
|
|
Subnet=10.89.5.0/24
|
|
Gateway=10.89.5.1
|