9041f1f402
Operator confirmed the "TTS blocked" was NotSupportedError on Safari — WebKit refuses a streaming 0xFFFFFFFF-length WAV via <audio src> (can't compute duration/seek), exactly as infra-ops warned. Replaced the <audio src> playback with a Web Audio path that works in all engines: - speakOnDone: fetch the chunked /api/tts stream, skip the WAV header to the data chunk, decode int16 LE PCM -> Float32, and schedule the samples GAPLESSLY into an AudioContext as they arrive (BufferSource per chunk, playAt += buf.duration). Progressive, TTFA ~0.5s. Decoding the raw PCM ourselves sidesteps every WAV-container quirk. - unlock: an AudioContext starts suspended; Safari + Chrome need resume() from a user gesture. _unlockTtsAudio() now resumes the ctx on the first interaction anywhere + toggle-on + submit, so it's running before the ~15s-delayed speak-on-done. - cancelTts: aborts the fetch + stops all scheduled BufferSource nodes. Validated in Chromium (Playwright, strict autoplay): 43 nodes scheduled, 5.1s of PCM decoded, ctx "running" 6.5s post-gesture, zero errors. Headless WebKit can't launch here (missing system libs — an infra-ops install), so the operator's live Safari is the final check; the code is standard Web Audio Safari has supported for years. Contract FN client:speakOnDone updated (Web Audio; the Safari NotSupportedError reason).