From 2dc31ff09c59dd745611f0bfb656636c8dd2d0a5 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Oct 2026 18:52:50 -0700 Subject: [PATCH] docs(frank-tts): retire the 5.5 s cold-voice claim everywhere it still lived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../quadlet/frank-tts-engine.container | 15 ++++++----- .../voiceservice/quadlet/frank-tts.container | 5 ++-- scripts/frank-tts-engine.env | 27 +++++++++---------- scripts/frank-tts.env | 5 ++-- serverAppConfigs-docs/Dankclaw/Frank TTS.md | 2 +- .../Research/Local TTS and Voice Cloning.md | 8 +++--- serverAppConfigs-docs/log.md | 8 ++++++ tests/frank_tts_engine.bats | 4 +-- 8 files changed, 44 insertions(+), 30 deletions(-) diff --git a/dankclaw/voiceservice/quadlet/frank-tts-engine.container b/dankclaw/voiceservice/quadlet/frank-tts-engine.container index c2f806d..f424444 100644 --- a/dankclaw/voiceservice/quadlet/frank-tts-engine.container +++ b/dankclaw/voiceservice/quadlet/frank-tts-engine.container @@ -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 diff --git a/dankclaw/voiceservice/quadlet/frank-tts.container b/dankclaw/voiceservice/quadlet/frank-tts.container index 6cdcd72..083919d 100644 --- a/dankclaw/voiceservice/quadlet/frank-tts.container +++ b/dankclaw/voiceservice/quadlet/frank-tts.container @@ -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 diff --git a/scripts/frank-tts-engine.env b/scripts/frank-tts-engine.env index 605c1b4..6f9a133 100644 --- a/scripts/frank-tts-engine.env +++ b/scripts/frank-tts-engine.env @@ -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 diff --git a/scripts/frank-tts.env b/scripts/frank-tts.env index 2fc61ef..d836fc8 100644 --- a/scripts/frank-tts.env +++ b/scripts/frank-tts.env @@ -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 , nice 19 so a narration # never starves the text endpoints on this box. Needs diff --git a/serverAppConfigs-docs/Dankclaw/Frank TTS.md b/serverAppConfigs-docs/Dankclaw/Frank TTS.md index 578e53e..72fca9b 100644 --- a/serverAppConfigs-docs/Dankclaw/Frank TTS.md +++ b/serverAppConfigs-docs/Dankclaw/Frank TTS.md @@ -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/` | diff --git a/serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md b/serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md index 93fe561..bcb111a 100644 --- a/serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md +++ b/serverAppConfigs-docs/Research/Local TTS and Voice Cloning.md @@ -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: diff --git a/serverAppConfigs-docs/log.md b/serverAppConfigs-docs/log.md index 388b71d..df8e711 100644 --- a/serverAppConfigs-docs/log.md +++ b/serverAppConfigs-docs/log.md @@ -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. diff --git a/tests/frank_tts_engine.bats b/tests/frank_tts_engine.bats index 4290b31..4ce4153 100755 --- a/tests/frank_tts_engine.bats +++ b/tests/frank_tts_engine.bats @@ -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" ) & -- 2.54.0