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.
81 lines
4.6 KiB
Bash
81 lines
4.6 KiB
Bash
# frank-tts.env — tunables for **frank-tts**, the fleet's cloned-voice speech
|
|
# endpoint on Dankclaw, host port **7772** (the LLM port family: 7770
|
|
# frank-dragoon, 7771 frank-zealot, 7772 frank-tts). Read by the Quadlet unit
|
|
# (EnvironmentFile=), which is why the paths below are CONTAINER paths.
|
|
#
|
|
# WHAT THIS IS. One OpenAI-compatible /v1/audio/speech façade in front of
|
|
# whichever TTS engine wins the engine decision (dankclaw/voiceservice/app.py
|
|
# is a single stdlib-only file in a locally built image). Consumers — omp's
|
|
# TTS device, OpenWebUI, open-notebook — address a VOICE BY NAME
|
|
# (voice=narrator1, or the harness' voice_id=narrator1) and never see a path:
|
|
# a name is real exactly when /voices/<name>/reference.wav exists, and
|
|
# anything else is a 400 that lists the names that are.
|
|
#
|
|
# THE PACKS. /home/dank/voices on the host, mounted read-only at /voices. One
|
|
# directory per voice, and a directory is a PACK only when it carries BOTH
|
|
# required members:
|
|
# reference.wav 24 kHz mono PCM, 18-25 s of clean single-speaker audio,
|
|
# plus the 16 kHz variant some engines want;
|
|
# the engine artifact the pinned backend's own speaker file (embedding /
|
|
# speaker weights). REQUIRED, beside the WAV and never
|
|
# replacing it — a voice that cannot be reused by another
|
|
# runtime is not a saved voice.
|
|
# plus manifest.json for provenance: source file, offsets (always past the
|
|
# first 10 s of that file, the source-material law), engine, sha256. Packs are
|
|
# host state and stay out of this repo; they must be world-readable because the
|
|
# container runs as uid 10000. Who enforces what: the shim gates on
|
|
# reference.wav because that is the only engine-agnostic term, so the pack
|
|
# builder is what refuses to emit a pack without its engine artifact — until a
|
|
# winning engine pins the format and this gate can name the file.
|
|
#
|
|
# BACKEND SWITCH. TTS_BACKEND picks the shape and nothing else changes for the
|
|
# consumer:
|
|
# omni-http forward to an OpenAI-shaped upstream, carrying the voice along
|
|
# as base64 reference_audio. The engine does the inference; the
|
|
# shim resolves the voice and re-encodes to mp3 when asked. The
|
|
# shipped engine (frank-tts-engine) speaks a voice it has
|
|
# REGISTERED on itself, from the same /home/dank/voices packs,
|
|
# and warms them at boot — see [[Frank TTS Engine]] for why an
|
|
# unwarmed voice costs the first caller 5.5 s.
|
|
# llama-tts-cli run LLAMA_TTS_BIN once per request with
|
|
# --tts-speaker-file <reference.wav>, nice 19 so a narration
|
|
# never starves the text endpoints on this box. Needs
|
|
# LLAMA_TTS_BIN + LLAMA_TTS_MODEL filled in; LLAMA_TTS_MMPROJ
|
|
# only if that binary takes a projector.
|
|
|
|
# THE UPSTREAM ADDRESS. The engine is a container on the private bridge this
|
|
# unit also joins (dankclaw/voiceservice/quadlet/frank-tts-engine.network), so
|
|
# this is its container DNS name. It used to read
|
|
# `http://host.containers.internal:8880`, which is a shape that cannot work: a
|
|
# bridge container reaches the host's network namespace, netavark DNATs PUBLISHED
|
|
# ports only, and an engine on the host's loopback is published to nobody
|
|
# ([[Container Network Namespaces]] — the trap that 502'd hermes). Inert under
|
|
# TTS_BACKEND=llama-tts-cli, which needs no upstream at all.
|
|
VOICES_DIR=/voices
|
|
TTS_BACKEND=omni-http
|
|
TTS_UPSTREAM=http://frank-tts-engine:8880
|
|
TTS_UPSTREAM_MODEL=voxcpm2
|
|
|
|
# CLI-backend knobs, inert while TTS_BACKEND=omni-http. Empty means "not
|
|
# configured": the endpoint answers a 500 that says so instead of running
|
|
# `nice env -m ...` with an empty binary name.
|
|
LLAMA_TTS_BIN=
|
|
LLAMA_TTS_MODEL=
|
|
LLAMA_TTS_MMPROJ=
|
|
LLAMA_TTS_LANG=english
|
|
LLAMA_TTS_THREADS=8
|
|
|
|
# The wire shape. 7772 is frank-tts's host port (the container's too), the next
|
|
# slot in the fleet's model-endpoint family; the TTS engine upstream of it
|
|
# keeps 8880, which is not a fleet endpoint.
|
|
SHIM_PORT=7772
|
|
# Longest single narration, in characters of `input`. Podcast-length text is
|
|
# the consumer's job to chunk; one request over this limit is refused with 414
|
|
# rather than silently truncated into a half-sentence of audio.
|
|
SHIM_MAX_CHARS=15000
|
|
# Seconds one request may take, covering both an upstream synthesis and a CLI
|
|
# run. Voice is slow compared to text — a long paragraph on a CPU engine is
|
|
# minutes, not milliseconds — and a client that gave up earlier would only
|
|
# retry into a second synthesis competing for the same CPU.
|
|
SHIM_TIMEOUT=600
|