Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc31ff09c | ||
|
|
0315a62d84 |
@@ -56,11 +56,14 @@
|
||||
# the registration body. The façade mounts the same host directory read-only for
|
||||
# its own name list, so one directory stays the single source of a voice name.
|
||||
#
|
||||
# COLD-VOICE LAW, THE REASON EXECSTARTPOST EXISTS. The FIRST speech request for
|
||||
# a registered voice decodes the whole reference clip to snapshot codec state:
|
||||
# 6.2-8.0 s, first audio byte at 5,530 ms. Every later request restores the
|
||||
# snapshot in 0.5 ms. So the unit warms every voice at boot, serially (~6-8 s
|
||||
# each), and `TimeoutStartSec=900` is what keeps a growing voice library from
|
||||
# REGISTRY LAW, THE REASON EXECSTARTPOST EXISTS. The voice registry is memory
|
||||
# only: a restart forgets every voice, and re-registering does not warm one —
|
||||
# only synthesizing does — so a boot without this hook leaves the façade
|
||||
# answering 400 for every name. The per-voice cold penalty is real but small, and
|
||||
# not the trial's 6.2-8.0 s: measured on the built container 2026-10-18 against an
|
||||
# empty registry, the first synthesis of a freshly registered voice cost 1,883 ms
|
||||
# against 1,226-1,283 ms for the requests after it. The warm pass runs serially
|
||||
# (two short syntheses per voice, ~5 s each) and `TimeoutStartSec=900` keeps a growing library from
|
||||
# tripping systemd's default 90 s start deadline. The leading `-` on
|
||||
# ExecStartPost is load-bearing: with `Restart=always`, a warmer that exits
|
||||
# non-zero (a pack missing its reference.wav, the engine still loading past the
|
||||
@@ -104,7 +107,7 @@ HealthStartPeriod=120s
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
# Model load (tens of seconds) plus one cold decode per registered voice.
|
||||
# Model load (tens of seconds) plus two short syntheses per registered voice.
|
||||
TimeoutStartSec=900
|
||||
ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
# has a second backend (`TTS_BACKEND=llama-tts-cli`) that needs no upstream, and
|
||||
# an engine that will not start must not take this endpoint's unit down with it.
|
||||
# When both boot, this comes up after the engine, whose ExecStartPost warm pass
|
||||
# is part of its start — so the first request of the day meets a warm voice
|
||||
# registry instead of paying the 5.5 s cold decode.
|
||||
# is part of its start — so the first request of the day meets a REGISTERED voice
|
||||
# (the registry is memory-only) rather than a 400, and skips the ~0.6 s penalty
|
||||
# the first synthesis of an unwarmed voice costs.
|
||||
#
|
||||
# VOICES. /home/dank/voices is host state, never repo state, and is mounted
|
||||
# READ-ONLY: this endpoint has no business writing voice packs. Because the
|
||||
|
||||
@@ -27,18 +27,17 @@
|
||||
# below stop describing the process you are running. Nothing in this fleet
|
||||
# pins this container; that is a decision, not an oversight.
|
||||
#
|
||||
# THE COLD-VOICE LAW (why the unit has an ExecStartPost at all). The first
|
||||
# speech request for a registered voice decodes the ENTIRE reference clip to
|
||||
# snapshot its codec state: 6.2-8.0 s total, first audio byte at 5,530 ms. Every
|
||||
# later request for that voice restores the snapshot in 0.5 ms. Registrations
|
||||
# live in memory only, so a restart loses them and re-registration alone does
|
||||
# NOT warm the voice — the warm pass has to synthesize something. So
|
||||
# dankclaw/voiceservice/tts-engine-warm.sh (in the image at
|
||||
# /app/warm-voices.sh) runs on every start and speaks one short sentence per
|
||||
# voice. Skip it and the first person to ask for audio that day waits 5.5 s in
|
||||
# silence. It runs INSIDE this container — `podman exec`, from the unit's
|
||||
# ExecStartPost — so it reads /voices here and the keys at the bottom of this
|
||||
# file are its knobs too.
|
||||
# THE REGISTRY LAW (why the unit has an ExecStartPost at all). Registrations live
|
||||
# in memory only, so a restart loses them, and re-registration alone does NOT warm
|
||||
# a voice — the warm pass has to synthesize something. Without it the façade
|
||||
# answers 400 for every name until somebody asks for audio. So
|
||||
# dankclaw/voiceservice/tts-engine-warm.sh (in the image at /app/warm-voices.sh)
|
||||
# runs on every start and speaks a short sentence twice per voice. The penalty it
|
||||
# removes is about 0.6 s, NOT the trial's 6.2-8.0 s: measured on the built
|
||||
# container 2026-10-18 against an empty registry, the first synthesis cost
|
||||
# 1,883 ms and the requests after it 1,226-1,283 ms. The hook runs INSIDE this
|
||||
# container — `podman exec`, from the unit's ExecStartPost — so it reads /voices
|
||||
# here and the keys at the bottom of this file are its knobs too.
|
||||
#
|
||||
# STREAMING IS NOT BUFFERED. The same seed gives audio that differs in 92.2 %
|
||||
# of samples (-41.6 dB RMS) between `response_format=pcm` (the streaming path,
|
||||
@@ -125,8 +124,8 @@ HEALTH_WAIT=300
|
||||
# The sentence each voice speaks to snapshot its codec state, and the text the
|
||||
# second (warm-timing) pass repeats. Fixed on purpose: the cold=/warm= numbers in
|
||||
# the journal are comparable across boots only if the input is the same. Kept
|
||||
# short because warming a library is serial — one cold decode per voice, 6-8 s
|
||||
# each, inside the unit's start deadline.
|
||||
# short because warming a library is serial — two syntheses per voice, ~5 s each,
|
||||
# inside the unit's start deadline.
|
||||
# QUOTED, and the quoting means two different things to the two readers of this
|
||||
# file, which is the trap here:
|
||||
# * bash (`set -a; source`, how the repo's seams and any human read it) needs
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
# 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.
|
||||
# and warms them at boot — see [[Frank TTS Engine]] for what that
|
||||
# boot warm is for (a memory-only registry: without it every name
|
||||
# is a 400) and what it saves (~0.6 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
|
||||
|
||||
@@ -148,5 +148,5 @@ gated `wip: down` until the deploy and probes `127.0.0.1:7772`.
|
||||
| `400 unknown voice 'x'; available voices: …` | the name is not a pack: case, spelling, missing `reference.wav`, or an empty/root-owned `/home/dank/voices` |
|
||||
| `(none: no voice pack with a reference.wav in /voices)` | the mount, not the voices: `podman inspect frank-tts --format '{{json .Mounts}}'`, then the directory owner |
|
||||
| Endpoint answers, synthesis fails | the engine: `podman healthcheck run frank-tts-engine`, then `systemctl --user status frank-tts-engine.service` |
|
||||
| First request of the day takes ~5.5 s | the warm pass did not run — cold-voice law in [[Frank TTS Engine]] |
|
||||
| First request of the day is ~0.6 s slower than the rest — or every name returns 400 | the warm pass did not run; it carries `-`, so it cannot fail the unit. `journalctl --user -u frank-tts-engine` for the warmer's lines, then `podman exec frank-tts-engine /app/warm-voices.sh` — the registry is memory-only (law 1 of [[Frank TTS Engine]]) |
|
||||
| The unit never appears after a deploy | the generator dropped it: `frank-tts-engine.network` is missing from `~/.config/containers/systemd/` |
|
||||
|
||||
@@ -328,9 +328,11 @@ a live stream mid-sentence.
|
||||
**Chosen: arm A, 1.7 B talker, resident `tts-server` in a `frank-tts-engine`
|
||||
container, with `TTS_BACKEND=llama-tts-cli` kept in the repo as the rollback.**
|
||||
Three consequences are configuration, not opinion: the unit **boot-warms every
|
||||
voice** (5.5 s cold first byte otherwise), it **does not pin a cpuset** casually
|
||||
(threads are `hardware_concurrency()/2`, no flag), and every pack registers with
|
||||
the **WAV** because the engine rejects a lone embedding.
|
||||
voice** — the registry is memory-only, so an unwarm'd name is a 400 and the first
|
||||
synthesis otherwise costs about 0.6 s more than the ones after it, as corrected
|
||||
above —, it **does not pin a cpuset** casually (threads are
|
||||
`hardware_concurrency()/2`, no flag), and every pack registers with the **WAV**
|
||||
because the engine rejects a lone embedding.
|
||||
|
||||
Two follow-ups stay open on purpose, and neither blocks the deploy:
|
||||
|
||||
|
||||
@@ -2682,3 +2682,11 @@ every restart since the cutover.
|
||||
probe for the voice pair (neither container exposes `/metrics`; the blackbox
|
||||
question is still open), and the podcast render — open-notebook is on `main` but
|
||||
not installed.
|
||||
|
||||
- Sweep afterwards: the retired ladder still lived in six places the first pass
|
||||
missed — both quadlet files, `scripts/frank-tts-engine.env`,
|
||||
`scripts/frank-tts.env`, the [[Frank TTS]] troubleshooting row, the research
|
||||
brief's consequences paragraph, and one test comment. All re-keyed to the
|
||||
measured claim: about **0.6 s**, and the 400-for-every-name reason the pages
|
||||
should have led with. The trial entry above is left as written — a dated worklog
|
||||
entry is a record — and this line is the correction that points at it.
|
||||
|
||||
@@ -510,8 +510,8 @@ PY
|
||||
# two GGUFs (1.2 GiB + 291 MB) are still being read and the HTTP thread has
|
||||
# not bound yet. The warmer is launched against an address with nothing on
|
||||
# it, the listener appears a beat later, and the requirement is that the
|
||||
# warmer notices and does the work — an early exit here is the day's first
|
||||
# caller paying 5.5 s of silence for a voice nobody warmed.
|
||||
# warmer notices and does the work — an early exit leaves every voice
|
||||
# unregistered, and the façade then answers 400 for all of them.
|
||||
port="$(free_port)"
|
||||
( ENGINE_URL="http://127.0.0.1:$port" VOICES_DIR="$VOICES" HEALTH_WAIT=60 \
|
||||
bash "$WARM" > "$TEST_TMP/race.log" 2>&1; echo "$?" > "$TEST_TMP/race.rc" ) &
|
||||
|
||||
Reference in New Issue
Block a user