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.
132 lines
6.6 KiB
Docker
132 lines
6.6 KiB
Docker
# frank-tts-engine — the arm A TTS engine: qwentts.cpp's resident `tts-server`
|
|
# (the CPU binary that frank-tts on :7772 forwards to). The runbook is
|
|
# [[Frank TTS Engine]]; the fleet-facing endpoint is [[Frank TTS]].
|
|
#
|
|
# WHAT THIS IS. The upstream source is NOT vendored here: the image clones
|
|
# ServeurpersoCom/qwentts.cpp at one pinned commit and builds it. Rebuilding is
|
|
# therefore a network operation (github.com plus objects.githubusercontent.com
|
|
# for the `ggml` submodule), and build_compose_image in scripts/deploy-remote.sh
|
|
# skips it whenever this Containerfile's sha256 label already matches the built
|
|
# image — the same offline-safe idiom as loki/alloy/comfyui, so a re-deploy of
|
|
# an unchanged engine needs no network.
|
|
#
|
|
# THE PINNED COMMIT IS THE CONTRACT. 5916bf7c79dbb4e66fc059cdc507a79a23a28bb4
|
|
# is the commit the 2026-10-18 CPU trial on Dankclaw measured (server-side TTFA
|
|
# 94-112 ms, warm RTF 0.55-0.67, run spread +-0.01 — the numbers that won the
|
|
# engine decision). Moving it means re-measuring, not just editing this line.
|
|
# `ggml` is a submodule of the repo (the only one; vendor/cpp-httplib and
|
|
# vendor/yyjson are ordinary directories in the tree), and the submodule commit
|
|
# is recorded in the parent tree, so cloning the parent commit recursively is
|
|
# reproducible without a second pin.
|
|
#
|
|
# THE BUILD LINE IS THE ONE PROVEN ON DANKCLAW, verbatim:
|
|
#
|
|
# cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CCACHE=OFF \
|
|
# -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_OPENMP=ON \
|
|
# -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=OFF \
|
|
# -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF \
|
|
# && cmake --build build -j 8
|
|
#
|
|
# Every flag is load-bearing, so do not "tidy" it:
|
|
# GGML_BLAS=OFF is FORCED, not a preference — Dankclaw has libopenblas.so.0
|
|
# but no BLAS dev headers, so the upstream default (BLAS on) fails to
|
|
# configure. Upstream's own Dockerfile and buildcpu.sh both pass BLAS on,
|
|
# which is why this file does not copy them.
|
|
# GGML_CCACHE=OFF because a container build layer has no persistent ccache
|
|
# directory to hit; on, it only writes a useless one.
|
|
# GGML_OPENMP=ON is what makes the runtime need libgomp1 (installed below).
|
|
# GGML_BACKEND_DL=OFF / GGML_CPU_ALL_VARIANTS=OFF: upstream's images build
|
|
# every x86 ISA as a loadable backend so one image runs anywhere. This one
|
|
# is built on the machine that runs it, so the single native CPU backend is
|
|
# what was measured. That is also why no -DGGML_NATIVE is passed: ggml
|
|
# defaults it ON outside wasm/cross-compiles, i.e. -march=native for THIS
|
|
# host — correct while build host == run host, wrong if anyone ever builds
|
|
# this image elsewhere, which is why the fleet builds it on Dankclaw.
|
|
# CUDA/HIP/VULKAN/METAL/SYCL/RPC all OFF: this is a CPU endpoint (the trial
|
|
# ran on CPU; the box's GPUs belong to the text endpoints).
|
|
#
|
|
# BUILD SHARED LIBS, RUNTIME SMALL. BUILD_SHARED_LIBS defaults ON on Linux, so
|
|
# tts-server links libggml*.so rather than swallowing them; the runtime stage
|
|
# copies the binary and its shared objects out of the build tree and points
|
|
# LD_LIBRARY_PATH at them, exactly as upstream's own Dockerfile does (the RPATH
|
|
# baked at link time names the build directory, which does not exist here).
|
|
#
|
|
# BASE PINNED BY DIGEST, like every other image this fleet builds: the digest
|
|
# is the multi-arch INDEX of ubuntu:24.04 as resolved from Docker Hub on
|
|
# 2026-10-18; Dankclaw takes the amd64 entry. One base for both stages so there
|
|
# is exactly one thing to re-resolve.
|
|
#
|
|
# NO HEALTHCHECK IN THIS FILE. The probe lives in the Quadlet unit
|
|
# (HealthCmd=), which is the fleet pattern for anything with a shell — see the
|
|
# alertmanager/prometheus units, and the opposite case in dankclaw/loki, whose
|
|
# distroless image has no /bin/sh for the generator's CMD-SHELL wrapping.
|
|
# `curl` is therefore installed in the runtime stage, for that probe only.
|
|
|
|
ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:68ee22004a9109b44973313b02ff7b1de250f1de0f5ecb628d73f579aaff33ac
|
|
|
|
# ------------------------------------------------------------------ build
|
|
FROM ${BASE_IMAGE} AS build
|
|
|
|
RUN apt-get update -qq \
|
|
&& apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates cmake g++ git make \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Checked out detached at a full SHA, then the recorded submodule. No branch
|
|
# name anywhere: a branch moves, this commit does not.
|
|
ARG QWENTTS_COMMIT=5916bf7c79dbb4e66fc059cdc507a79a23a28bb4
|
|
RUN git clone --no-checkout https://github.com/ServeurpersoCom/qwentts.cpp /src \
|
|
&& git -C /src checkout --detach "${QWENTTS_COMMIT}" \
|
|
&& git -C /src submodule update --init --depth 1 \
|
|
&& git -C /src rev-parse HEAD \
|
|
&& git -C /src submodule status
|
|
|
|
WORKDIR /src
|
|
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release \
|
|
-DGGML_CCACHE=OFF \
|
|
-DGGML_BACKEND_DL=OFF \
|
|
-DGGML_CPU_ALL_VARIANTS=OFF \
|
|
-DGGML_OPENMP=ON \
|
|
-DGGML_BLAS=OFF \
|
|
-DGGML_CUDA=OFF \
|
|
-DGGML_HIP=OFF \
|
|
-DGGML_VULKAN=OFF \
|
|
-DGGML_METAL=OFF \
|
|
-DGGML_SYCL=OFF \
|
|
-DGGML_RPC=OFF \
|
|
&& cmake --build build -j 8
|
|
|
|
# ---------------------------------------------------------------- runtime
|
|
FROM ${BASE_IMAGE}
|
|
|
|
# libgomp1: the OpenMP runtime the build above links against. curl: the unit's
|
|
# HealthCmd. ca-certificates: so that curl can speak https if anyone ever
|
|
# points a probe at a URL with a scheme this host terminates elsewhere.
|
|
RUN apt-get update -qq \
|
|
&& apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates curl libgomp1 \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Same unprivileged uid as the frank-tts façade (10000): the voice packs and the
|
|
# model directory are host state mounted read-only, and one uid across the pair
|
|
# means one thing to remember about what is world-readable.
|
|
RUN adduser --system --uid 10000 --home /app --shell /usr/sbin/nologin tts
|
|
|
|
WORKDIR /app
|
|
COPY --from=build /src/build/tts-server /src/build/*.so* /app/
|
|
COPY dankclaw/voiceservice/tts-engine-start.sh /app/start.sh
|
|
# The boot warmer lives INSIDE the image because the engine publishes no port:
|
|
# the unit reaches it with `podman exec`, and the script needs the same curl,
|
|
# base64 and /voices the engine's own namespace has. It is a sibling of
|
|
# start.sh, not a second entrypoint — CMD stays the server.
|
|
COPY dankclaw/voiceservice/tts-engine-warm.sh /app/warm-voices.sh
|
|
RUN chmod 755 /app/start.sh /app/warm-voices.sh
|
|
|
|
# The binary's RPATH points inside the build tree; this is what makes it find
|
|
# the libggml*.so copied beside it.
|
|
ENV LD_LIBRARY_PATH=/app
|
|
|
|
USER tts
|
|
EXPOSE 8880
|
|
CMD ["/app/start.sh"]
|