Context (2026-10-12): reading redacted write-ups aloud with gemini-3.8-flash-tts via POST /v1beta/interactions, one call per paragraph with speech_metadata.style, 88 renders of 0.6-6 min.
What happened: two renders of the same yap (different voices: en-au-tutor-5, en-us-tutor-25) ran ~50 s long. Re-transcribing showed the model had continued speaking after paragraph 1 ('…I personally've never been, been meaning to go.'): 88 and 138 extra words of on-topic, specific, fabricated content (a 'Persian Center', a restaurant 'Daryoush', Walnut Creek) that exist nowhere in the input or the source recording. The audio was otherwise normal, in the target voice. Word drift 10% and 16.5% vs a 0-4.4% baseline.
Gate that caught both:
heard = transcribe(render) # any decent STT; we used gemini-3.8-flash
drift = wer(words(sent_text), words(heard))
if drift > 0.06: # baseline drift is ASR normalization (names, numerals)
set_aside(render); render = rerender() # re-sampling fixed both on the first retry (1.8%)Run it per paragraph in production so a retry re-renders one paragraph, not the whole read. Duration is a cheap pre-signal (the bad takes were ~15-20% longer than siblings).
Second trap in the gate itself: the scoring STT (gemini-3.8-flash, 'Transcribe this audio verbatim') returned its own reasoning ('the user wants a verbatim transcription…') or a looped transcript in 3 of 88 calls, producing WER > 100%. Re-transcribe when the output contains that phrase or has > 1.4x the expected words, or you'll gate good takes / miss bad ones.
Why it matters: for TTS that speaks as a person (narration, anonymized voice, accessibility reads), an invented sentence is attributed to them. Treat LLM-based TTS like any generative model: verify output against input.