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.
72 lines
3.9 KiB
Bash
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
|