fix: TTS "blocked" hardening — no-store page, first-interaction unlock, error detail

Operator still hit "TTS blocked" (play() refused). Three fixes:
- server.py: Cache-Control: no-store on GET / — an actively-iterated dev console must
  never serve a stale cached page (which silently hid the streaming / unlock updates
  through this debugging; likely the main cause after many :8765 relaunches).
- index.html: unlock the <audio> element on the FIRST user interaction anywhere
  (document pointerdown/keydown, capture), not just toggle/submit — so autoplay
  permission is granted however the operator first touches the page.
- index.html: the "blocked" ticker now names the DOMException (NotAllowedError=autoplay,
  NotSupportedError=browser refused the streaming WAV, AbortError=superseded) so the
  cause is visible instead of a generic "playback blocked".

Not COT: the spoken text is LIVE.resp (response only); chain-of-thought streams into a
separate LIVE.think buffer and never touches the audio path.
This commit is contained in:
vh
2026-08-02 07:13:04 -07:00
parent aecec87919
commit 677b03327d
2 changed files with 15 additions and 2 deletions
+6 -1
View File
@@ -112,11 +112,16 @@ def _static_dir() -> str:
def _root_endpoint(request: Request) -> FileResponse:
"""GET / → index.html. Per FN root_endpoint POST-001."""
"""GET / → index.html. Per FN root_endpoint POST-001.
no-store: this is an actively-iterated dev console — never let a browser serve a
stale cached page (which silently hid the streaming-TTS / autoplay-unlock updates
during debugging). The HTML is small; a re-fetch per load is free on the LAN."""
from pathlib import Path
return FileResponse(
Path(_static_dir()) / "index.html",
media_type="text/html",
headers={"Cache-Control": "no-store"},
)
+9 -1
View File
@@ -2031,6 +2031,12 @@ function _unlockTtsAudio() {
}).catch(() => {});
} catch (_) {}
}
// Unlock on the FIRST user interaction ANYWHERE on the page — not just toggle/submit —
// so the element has autoplay permission before the delayed speak-on-done play(), no
// matter how the operator first touches the page. _unlockTtsAudio self-guards, so these
// are no-ops once granted.
document.addEventListener("pointerdown", _unlockTtsAudio, true);
document.addEventListener("keydown", _unlockTtsAudio, true);
function cancelTts() {
// Stop + drop the current <audio> src; load() aborts the in-flight GET stream, which
// drops the server proxy (releasing its synth lock). No object URLs to revoke — the
@@ -2053,7 +2059,9 @@ function speakOnDone(text, agentId, pad) {
// play() may be refused by the autoplay policy in this async callback; the prompt/toggle
// gesture unlock (_unlockTtsAudio) satisfies it, and the catch keeps a refusal non-fatal.
a.play().then(() => tickerAdd("ok", "tts", "▶ voiced"))
.catch(() => tickerAdd("err", "tts", "playback blocked"));
.catch((e) => tickerAdd("err", "tts", "blocked: " + ((e && e.name) || "?")));
// NotAllowedError = autoplay (unlock didn't take); NotSupportedError = the browser
// refused the streaming WAV; AbortError = a new turn superseded it.
}
// ---- 🔊 toggle (mirrors theme / cot-toggle; default OFF, persisted) ----
(function () {