0367244d2adb9d246c073e0f6b4068477b5526ec
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ea2fc6440b |
docs(frank-tts): record the iGPU cutover, close the NPU question
The cutover PR merged the runtime; this carries the surface that has to be true afterwards. - Engine page: the shape is now `--device Vulkan0` on the gfx1151 as a co-tenant beside frank-dragoon; the registry/thread/streaming laws gain a fourth — the device law — whose numbers stay in the verdict report, and the deploy record gets the cutover steps including the `glslc` configure gate. The CPU-era arm/rollback prose keeps the qualitative argument and drops the dead numbers per the purpose test. - Endpoint page: "Where the engine computes" now says iGPU + co-tenancy, and states the NPU verdict in one paragraph instead of a research tour. - Quadlet comment: it claimed supplementary groups are what grant render access. On this host the udev rule world-writes the render nodes and the container's `id` shows no groups at all, so keep-groups is belt-and-braces; the RSS note now matches the device shape instead of the CPU trial. - Verdict report: gate 2 (contention) recorded as measured after the cutover, and the build-stage prediction corrected — the package is `glslc`, not `libvulkan-dev glslang-dev`. - index.md regenerated (one description changed). |
||
|
|
25df9d05a2 |
feat(frank-tts-engine): synthesize on the gfx1151 iGPU via Vulkan
The NPU path is a dead end for this payload (verdict: report/tts-npu-spike-verdict-2026-10-19.md); the iGPU is the accelerator that exists on this box today, and it is already shared with frank-dragoon. - Containerfile.engine: -DGGML_VULKAN=ON, plus the three packages the door needs to configure — libvulkan-dev, spirv-headers, and glslc, which Ubuntu's glslang-tools does NOT ship (find_package(Vulkan COMPONENTS glslc REQUIRED) fails without it) — and libvulkan1 + mesa-vulkan-drivers (the RADV ICD) at runtime. No NVIDIA ICD on purpose: one Vulkan device in the enumeration is what makes the NAME Vulkan0 safe to write down. - tts-engine-start.sh: QWENTTS_DEVICE -> --device, default Vulkan0. A value that is set but EMPTY passes no flag at all, which is the CPU rollback; that is why the default is written as an -v test and not with :=, because := also fires on an empty value and would silently undo the rollback. - quadlet: AddDevice=/dev/dri + GroupAdd=keep-groups (a rootless container that drops its supplementary groups enumerates no render device). The node is not named: this host is hybrid, renderD128 is the NVIDIA card and renderD129 the amdgpu iGPU. - env: QWENTTS_DEVICE=Vulkan0 plus the device law in the header, including the rollback line. - tests: the two CPU-only assertions now assert the Vulkan shape, and a new one pins the rollback (empty device => no --device in argv). 451/451 green. Measured before writing any of it: the configure in the digest-pinned base answers "Found Vulkan: ... found components: glslc" and "Including Vulkan backend"; gate 1 (RADV GFX1151 visible to a rootless container off this base) is recorded in the verdict report. |
||
|
|
2dc31ff09c |
docs(frank-tts): retire the 5.5 s cold-voice claim everywhere it still lived
PR #15 corrected the engine's page, the research brief's law bullet and the warmer's header, but the retired ladder had six more homes: both quadlet files ("6.2-8.0 s… 0.5 ms", "~6-8 s each", "paying the 5.5 s cold decode"), `scripts/frank-tts-engine.env` (the whole law paragraph and the WARM_TEXT note), `scripts/frank-tts.env`, the [[Frank TTS]] troubleshooting row that told an operator to expect ~5.5 s, the research brief's consequences paragraph, and one test comment. All re-keyed to what was measured on the deployed container 2026-10-18: the first synthesis of a freshly registered voice costs 1,883 ms against 1,226-1,283 ms afterwards, so the warm pass buys about 0.6 s — and the reason it exists at all, which the pages never led with, is that the registry is memory-only: without the hook every voice name is a 400 until somebody synthesizes. The trial's dated worklog entry stays as written; the correction points at it. No behavior change: comments, unit lore and docs only, so no cfhash move and no image rebuild. |
||
|
|
8fefe6e1a9 |
feat(frank-tts): deploy wiring for the 7772 voice endpoint, and voice naming written down
Stage 2 of the voice stack. Repo state only: nothing deployed, no host touched, no container started. - `frank-tts` is now registered everywhere a dankclaw service has to be: `SERVICE_ORDER` (after `frank-tts-engine`, before the consumers), the planner's dankclaw set, `deploy-remote.sh`'s allowlist + dispatch + a `deploy_frank_tts` handler, and `deploy.sh`'s dispatch regex — plus the `build_compose_image` arm naming `dankclaw/voiceservice/Containerfile`, without which the build silently returns 0 and the unit's `Pull=never` tag never exists. - The unit joins TWO networks, in order: `dankclaw_default` first (default route, where consumers and open-notebook live), then the voice bridge. `TTS_UPSTREAM` moves from `host.containers.internal:8880` to `http://frank-tts-engine:8880` — the actual fix for a shape that could never work, since a bridge container cannot reach a host-loopback bind and the engine publishes nothing at all. - `Wants=`/`After=` on the engine, never `Requires=`: the `llama-tts-cli` rollback backend needs no upstream, so a broken engine may delay this endpoint but must not take its unit down. - The handler installs the engine's quadlet pair too, because a referenced `Network=` file the generator cannot find means no façade unit is produced at all, and `--only frank-tts` has no other way to get it. It creates `/home/dank/voices` as the deploy user (rootless podman would auto-create a missing bind source root-owned, which reads as "no voices"), and gates on podman's recorded health. - Voice naming is now written down where it can be found: a voice is a lowercase directory name matching `^[a-z0-9][a-z0-9._-]*$`, at most 64 chars. That shape is the intersection of two rules that disagree — the shim would accept mixed case, the engine lowercases names — so a mixed-case pack resolves on one side only, and the boot warmer refuses it loudly. `narrator1/2/3` are load-bearing for open-notebook. - Homepage card for the pair, `wip: down` and hidden, probing `127.0.0.1:7772`; the engine gets no card because it publishes nothing and could never be probed. - **No Prometheus job, deliberately.** No `/metrics` exists on the shim, this Prometheus 3 refuses HTML/JSON bodies (the existing `comfyui` and `services` probes are documented down for exactly that), and `LlmEndpointDown` is scoped `up{job=~"frank-.*"}` — so a `/health` job would page on Signal for a healthy endpoint. Recorded in the page instead of quietly skipped. - Vault: new [[Frank TTS]] page linked from the index; [[Frank TTS Engine]] now points at it for the pair's public signal; log entry for the day. - Seam grows to 25 tests: the generator's rendering of the pair (both attachments, first-network order, one publish, Wants-not-Requires, bounded per-unit blocks), the env file's upstream checked as a network JOIN rather than a string, planner separation and order, the handler's steps, the card's gating, routing that selects both seams for either deployable. |
||
|
|
8d76854a1e |
feat(frank-tts-engine): the arm A engine as a boot-persistent quadlet unit
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. |
||
|
|
31266fb7f3 |
feat(frank-tts): cloned-voice speech endpoint — OpenAI-shaped facade, voice packs by name
Phase 1 of the voice-over goal (Gitea issue #2). One stdlib-only HTTP facade in front of whichever TTS engine wins the engine decision: POST /v1/audio/speech with voice=<name> answers audio in THAT voice, where a name is real exactly when /voices/<name>/reference.wav exists — an unknown name is a 400 listing the voices that are, never a silent fallback (a wrong-voice endpoint stays wrong for weeks). voice_id works as the alias the omp harness sends; response_format=mp3 is really mp3 (ffmpeg re-encode); oversized input is refused with 414 instead of half-narrated; /health reports the backend and the resolved voice list. Two backends behind the one surface, chosen by TTS_BACKEND: omni-http forwards to an OpenAI-shaped upstream carrying the voice as base64 reference_audio, and llama-tts-cli runs a local binary per request with --tts-speaker-file at nice 19 so narration never starves the text endpoints. Both are driven in the seam through stubs — no GPU, no containers. Named frank-tts, a first-class fleet model endpoint: port 7772 in the 7770/7771 family, alias naming per the StarCraft law, rootless boot-persistent self-healing quadlet with a read-only /home/dank/voices mount. Voice-pack law recorded in the env header: reference.wav PLUS the engine's own speaker artifact (required, so a voice is reusable in other runtimes), plus manifest.json provenance with offsets always past the first 10 s of the source file. Not deployed: the engine decision, the pack builder, deployer/homepage/Prometheus registration and the omp models.yml runner row are phase 2. |