Files
serverAppConfigs/scripts/frank-tts.env
Frank 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.
2026-10-18 14:06:06 -07:00

72 lines
3.9 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 on the host (:8880),
# 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.
# 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 is host.containers.internal — the gateway name a bridge
# container uses for the host. It is NOT the LAN IP and it is NOT
# 127.0.0.1: a bridge netns has neither.
VOICES_DIR=/voices
TTS_BACKEND=omni-http
TTS_UPSTREAM=http://host.containers.internal: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