diff --git a/docs/contracts/donut_voiced_interview.contract.md b/docs/contracts/donut_voiced_interview.contract.md index 24030cc..336f523 100644 --- a/docs/contracts/donut_voiced_interview.contract.md +++ b/docs/contracts/donut_voiced_interview.contract.md @@ -201,19 +201,26 @@ pin_kb_context(question: str, agent_id: str | None, *, client) -> list[dict] # searches in-voice natively. ``` -### FN client: speakOnDone (index.html — STREAMING, DEC-2) +### FN client: speakOnDone (index.html — Web Audio STREAMING, DEC-2) ``` on SSE `done`: if !ttsEnabled(): return # INV-TTS-2 - cancelTts() # INV-TTS-3: audio.pause()+removeAttribute(src)+load() - audio.src = "/api/tts?text=&agent_id=&p=&a=" # GET streaming URL (text sliced to the 2000 cap) - audio.play() -> ▶ voiced ; .catch -> "playback blocked" # progressive; failure non-fatal (INV-TTS-4) + cancelTts() # INV-TTS-3: abort fetch + stop scheduled nodes + fetch("/api/tts?text=&agent_id=&p=&a=") -> reader # chunked stream (text sliced to the 2000 cap) + loop: read chunk -> skip WAV header up to the data chunk -> int16 LE PCM -> Float32 -> AudioBuffer -> + BufferSource.start(playAt) scheduled GAPLESSLY -> playAt += buf.duration # progressive, TTFA ~0.5s + first scheduled node -> "▶ voiced"; any failure -> ticker + skip (INV-TTS-4) -AUTOPLAY UNLOCK (the load-bearing fix for "no audio"): speak fires play() in an async callback seconds after -the keypress, past the browser's transient-activation window, so a bare play() is blocked. _unlockTtsAudio() -plays a tiny silent WAV inside a REAL gesture (toggle-on + each prompt submit), which grants the