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.
108 lines
5.7 KiB
Markdown
108 lines
5.7 KiB
Markdown
# 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.
|