They adopted the gate as tts-stack tools/gate_served_page.py and extended it in a place that matters: my version would have passed a broken page. A worklet lives inside a template literal, so a syntax error in it is invisible to a parse of the enclosing script - it is just a string until addModule compiles it at runtime, where it fails as a rejected promise and the page quietly falls back to buffered playback or records nothing. Silent degradation, which is harder to notice than a dead page rather than easier. They parse the worklet separately, and they positive-controlled the whole thing against two deliberately broken pages rather than assuming a gate that has only ever passed is not blind. The second control - valid enclosing script, broken worklet - is the one my version fails. The lesson on my own work is the useful part: I built a gate for the failure I had just been shown and stopped at its boundary. The class is 'code that is a string at parse time and code at run time'; an inline script is one instance and a template-literal worklet is another. I checked the instance, not the class. Also promotes the underlying rule to the index, since it was named twice tonight from two unrelated directions: a check that reads an artifact as stored cannot see a transformation that happens between storage and execution.
5.7 KiB
talk v10 deploy — Grima ears + barge-in (2026-09-15)
Operator-instructed, relayed by tts-dev. First consumer of the Parakeet/ext-stt
seat stood up the same night — talk can now listen as well as speak.
Why infra-ops and not tts-dev
/opt/docker/compose on nh3-dev is root:docker 2775 and tts-dev's project
identity is not in the docker group — the one box of five where the deploy path
is not project-writable. That is the only reason the deploy was relayed.
⚠ Open question raised with the operator: the durable fix is a group membership,
not a standing relay. Every talk deploy currently routes through infra-ops for a
permissions reason rather than a judgement one.
Relay authorization — why this was OK to act on
feedback_no_relayed_authorization_for_irreversible_work says a peer relaying
"Vuong approved it" is not authorization for a no-undo action, but reversible
work is fine to relay. This qualified: one-line rollback (TALK_TAG=v10→v9),
local/talk:v1..v9 all retained on the box, and both compose.yaml and .env
backed up before the edit. Checked the escape hatch existed rather than believing
the message that described it.
What shipped
repo ~/development/tts-stack @ 82f71d1, stacks/talk/
image local/talk:v10 (143 MB)
live container `talk`, 0.0.0.0:8092 -> 8443,
https://talk.nh3.phasefinal.com:8092/
New: POST /api/listen (raw-body WAV → {"text":…}, proxied to ext-stt through
LiteLLM — raw body rather than multipart because python-multipart is not in the
image), a push-to-talk mic (16 kHz mono, decimated 3:1 in an AudioWorklet), and
barge-in. compose.yaml gained two defaulted env lines so the STT seat can move
without a rebuild: TALK_STT_MODEL (ext-stt) and TALK_STT_MAX_BYTES (10 MiB
≈ 5.2 min).
Gate — 5/5, and the discipline that matters
Built → throwaway on :8799 (never the live port) → gate → tear down → then cut over, in separate invocations. tts-dev's own warning: do not chain the cutover into the same invocation as its acceptance run.
✓ /api/system ✓ /api/voices 21 (predicted 21)
✓ /api/models 23 (predicted 23) ✓ /api/listen byte-exact vs ground truth
⭐ Re-ran all four against PRODUCTION after the cutover. A gate that only ever ran against the throwaway proves the image, not the deployment. Both new env vars confirmed inside the running container, not just in the file.
⭐⭐ The fifth gate — check the artifact AS SERVED, not as stored
tts-dev's worst bug this cycle: PAGE is a Python string, so Python's escape
handling runs over the JavaScript before a browser sees it. A JS 'didn\'t' is
valid in the file and arrives as 'didn't' — closing the string and killing the
entire inline script. The page still rendered; it just did nothing. import app
passed. node --check on the source file passed. Both passed because the file
still holds the backslash.
So I added: fetch the page over HTTP, extract inline <script> blocks from the
response body, node --check each. Same instrument, pointed at the other side of
the transformation — and because it runs over the wire it also catches anything that
mangles the body after TLS and the ASGI stack, which an in-process test cannot see.
throwaway 29,492 B, 1 block, 25,228 chars -> OK
production 29,085 B, 1 block -> OK
⭐ The general rule, now stated twice in one night: a check that reads the
artifact AS STORED cannot see a transformation that happens between storage and
execution. node --check reads the pre-Python file; provider=cuda in a log echoes
configured intent, not the running reality. Both check the INPUT to a transformation
and get reported as if they checked its OUTPUT. See
2026-09-15-parakeet-stt-fv-ml1.md for the ASR instance of the same shape.
⚠ My fifth gate had a GAP — tts-dev found it and fixed it
Adopted into tts-stack as tools/gate_served_page.py (uv run tools/gate_served_page.py <url>;
needs only curl-equivalent and node). But my version would have passed a broken page:
A worklet lives inside a template literal, so a syntax error in it is invisible to a
parse of the enclosing script — it is just a string until addModule compiles it at
runtime, where it fails as a rejected promise. The page then quietly falls back to
buffered playback, or records nothing at all on the capture side. Silent degradation,
which is harder to notice than a dead page, not easier. Their version parses the
worklet separately.
They positive-controlled it rather than assuming it worked — a gate that has only ever passed cannot tell you it is not blind. Two deliberately broken pages, both exit 1:
the exact escape bug -> block 0 SYNTAX ERROR
broken worklet, valid script -> block 0 OK, worklet SYNTAX ERROR <- mine passes this
⚠ Empty block list exits 2, not 0. A page that suddenly has no inline script is a different page or a broken build; passing there would make the gate a no-op exactly when it matters most.
⭐ Lesson on my own work: I built a gate for the failure I had just been shown and
stopped at its boundary. The failure class is "code that is a string at parse time and
code at run time" — an inline <script> is one instance of it, a template-literal
worklet is another, and I checked the instance rather than the class.
Host compose verified, not assumed
tts-dev claimed the host copy was byte-identical to the repo, "unlike voice-studio". Diffed before overwriting: the only delta was their two documented blocks, ten added lines, no hand-edits. The claim held exactly — but after voice-studio's three stacked drifts it was worth the ten seconds.