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:
@@ -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"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user