docs(frank-tts): retire the 5.5 s cold-voice claim everywhere it still lived
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.
This commit is contained in:
@@ -56,11 +56,14 @@
|
|||||||
# the registration body. The façade mounts the same host directory read-only for
|
# 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.
|
# 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
|
# REGISTRY LAW, THE REASON EXECSTARTPOST EXISTS. The voice registry is memory
|
||||||
# a registered voice decodes the whole reference clip to snapshot codec state:
|
# only: a restart forgets every voice, and re-registering does not warm one —
|
||||||
# 6.2-8.0 s, first audio byte at 5,530 ms. Every later request restores the
|
# only synthesizing does — so a boot without this hook leaves the façade
|
||||||
# snapshot in 0.5 ms. So the unit warms every voice at boot, serially (~6-8 s
|
# answering 400 for every name. The per-voice cold penalty is real but small, and
|
||||||
# each), and `TimeoutStartSec=900` is what keeps a growing voice library from
|
# 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
|
# tripping systemd's default 90 s start deadline. The leading `-` on
|
||||||
# ExecStartPost is load-bearing: with `Restart=always`, a warmer that exits
|
# 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
|
# non-zero (a pack missing its reference.wav, the engine still loading past the
|
||||||
@@ -104,7 +107,7 @@ HealthStartPeriod=120s
|
|||||||
[Service]
|
[Service]
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
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
|
TimeoutStartSec=900
|
||||||
ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh
|
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
|
# 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.
|
# 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
|
# 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
|
# is part of its start — so the first request of the day meets a REGISTERED voice
|
||||||
# registry instead of paying the 5.5 s cold decode.
|
# (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
|
# 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
|
# 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
|
# below stop describing the process you are running. Nothing in this fleet
|
||||||
# pins this container; that is a decision, not an oversight.
|
# pins this container; that is a decision, not an oversight.
|
||||||
#
|
#
|
||||||
# THE COLD-VOICE LAW (why the unit has an ExecStartPost at all). The first
|
# THE REGISTRY LAW (why the unit has an ExecStartPost at all). Registrations live
|
||||||
# speech request for a registered voice decodes the ENTIRE reference clip to
|
# in memory only, so a restart loses them, and re-registration alone does NOT warm
|
||||||
# snapshot its codec state: 6.2-8.0 s total, first audio byte at 5,530 ms. Every
|
# a voice — the warm pass has to synthesize something. Without it the façade
|
||||||
# later request for that voice restores the snapshot in 0.5 ms. Registrations
|
# answers 400 for every name until somebody asks for audio. So
|
||||||
# live in memory only, so a restart loses them and re-registration alone does
|
# dankclaw/voiceservice/tts-engine-warm.sh (in the image at /app/warm-voices.sh)
|
||||||
# NOT warm the voice — the warm pass has to synthesize something. So
|
# runs on every start and speaks a short sentence twice per voice. The penalty it
|
||||||
# dankclaw/voiceservice/tts-engine-warm.sh (in the image at
|
# removes is about 0.6 s, NOT the trial's 6.2-8.0 s: measured on the built
|
||||||
# /app/warm-voices.sh) runs on every start and speaks one short sentence per
|
# container 2026-10-18 against an empty registry, the first synthesis cost
|
||||||
# voice. Skip it and the first person to ask for audio that day waits 5.5 s in
|
# 1,883 ms and the requests after it 1,226-1,283 ms. The hook runs INSIDE this
|
||||||
# silence. It runs INSIDE this container — `podman exec`, from the unit's
|
# container — `podman exec`, from the unit's ExecStartPost — so it reads /voices
|
||||||
# ExecStartPost — so it reads /voices here and the keys at the bottom of this
|
# here and the keys at the bottom of this file are its knobs too.
|
||||||
# file are its knobs too.
|
|
||||||
#
|
#
|
||||||
# STREAMING IS NOT BUFFERED. The same seed gives audio that differs in 92.2 %
|
# 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,
|
# 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
|
# 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
|
# 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
|
# 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
|
# short because warming a library is serial — two syntheses per voice, ~5 s each,
|
||||||
# each, inside the unit's start deadline.
|
# inside the unit's start deadline.
|
||||||
# QUOTED, and the quoting means two different things to the two readers of this
|
# QUOTED, and the quoting means two different things to the two readers of this
|
||||||
# file, which is the trap here:
|
# file, which is the trap here:
|
||||||
# * bash (`set -a; source`, how the repo's seams and any human read it) needs
|
# * 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
|
# shim resolves the voice and re-encodes to mp3 when asked. The
|
||||||
# shipped engine (frank-tts-engine) speaks a voice it has
|
# shipped engine (frank-tts-engine) speaks a voice it has
|
||||||
# REGISTERED on itself, from the same /home/dank/voices packs,
|
# REGISTERED on itself, from the same /home/dank/voices packs,
|
||||||
# and warms them at boot — see [[Frank TTS Engine]] for why an
|
# and warms them at boot — see [[Frank TTS Engine]] for what that
|
||||||
# unwarmed voice costs the first caller 5.5 s.
|
# 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
|
# llama-tts-cli run LLAMA_TTS_BIN once per request with
|
||||||
# --tts-speaker-file <reference.wav>, nice 19 so a narration
|
# --tts-speaker-file <reference.wav>, nice 19 so a narration
|
||||||
# never starves the text endpoints on this box. Needs
|
# 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` |
|
| `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 |
|
| `(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` |
|
| 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/` |
|
| 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`
|
**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.**
|
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
|
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
|
voice** — the registry is memory-only, so an unwarm'd name is a 400 and the first
|
||||||
(threads are `hardware_concurrency()/2`, no flag), and every pack registers with
|
synthesis otherwise costs about 0.6 s more than the ones after it, as corrected
|
||||||
the **WAV** because the engine rejects a lone embedding.
|
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:
|
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
|
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
|
question is still open), and the podcast render — open-notebook is on `main` but
|
||||||
not installed.
|
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
|
# 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
|
# 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
|
# 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
|
# warmer notices and does the work — an early exit leaves every voice
|
||||||
# caller paying 5.5 s of silence for a voice nobody warmed.
|
# unregistered, and the façade then answers 400 for all of them.
|
||||||
port="$(free_port)"
|
port="$(free_port)"
|
||||||
( ENGINE_URL="http://127.0.0.1:$port" VOICES_DIR="$VOICES" HEALTH_WAIT=60 \
|
( 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" ) &
|
bash "$WARM" > "$TEST_TMP/race.log" 2>&1; echo "$?" > "$TEST_TMP/race.rc" ) &
|
||||||
|
|||||||
Reference in New Issue
Block a user