# 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"]