GEMINI LABJP
FLASH35 — Gemini 3.5 Flash is now GA and powers gemini-flash-latest, making everyday generation faster and more affordableAGENTS — Managed Agents launch in public preview in the Gemini API, running in secure, isolated Google-hosted Linux sandboxesMEDIA — Nano Banana 2 Lite and Gemini Omni Flash bring faster image and high-quality video generation across AI Studio and the APITTS — Streaming speech generation is now supported for gemini-3.1-flash-tts-preview via streamGenerateContentTRANSLATE — A new audio model detects 70+ languages for live speech-to-speech translation while preserving natural intonationSPENDCAP — Project-level spend caps for billing have been added in Google AI Studio to keep costs under controlFLASH35 — Gemini 3.5 Flash is now GA and powers gemini-flash-latest, making everyday generation faster and more affordableAGENTS — Managed Agents launch in public preview in the Gemini API, running in secure, isolated Google-hosted Linux sandboxesMEDIA — Nano Banana 2 Lite and Gemini Omni Flash bring faster image and high-quality video generation across AI Studio and the APITTS — Streaming speech generation is now supported for gemini-3.1-flash-tts-preview via streamGenerateContentTRANSLATE — A new audio model detects 70+ languages for live speech-to-speech translation while preserving natural intonationSPENDCAP — Project-level spend caps for billing have been added in Google AI Studio to keep costs under control
Articles/Dev Tools
Dev Tools/2026-07-14Advanced

Making TTS seams inaudible: prosody continuity and silence boundaries for long-form narration

Splitting a long article by sentence and synthesizing each piece leaves audible seams: prosody resets at every sentence start, clicks appear at PCM joints, and pauses feel uneven. Four layers -- context priming, zero-crossing edge fades, punctuation-aware silence, and an idempotent seam ledger -- make the joins disappear, with full code.

Gemini TTS2Speech synthesisNarrationSolo development

Premium Article

I was preparing the guidance voice for a calming app I run as an indie developer, using Gemini's TTS. I had synthesized it sentence by sentence, stitched the pieces together, and sat down to hear the whole thing for the first time. And I stopped.

At every boundary between sentences, the speaker seemed to swap for a person for an instant. Each individual clip sounded clean, yet the moment they were joined the result felt restless. A voice meant to ease someone toward sleep tensed up, faintly, at each seam.

There was not one cause but three. A reset in intonation, a small click at each PCM joint, and uneven pauses. Together they produced a recording that announced its own seams. This article separates those three, and gives working code for each. Not merely splitting and joining -- taking it all the way to where the ear stops noticing the joins.

Why seams catch the ear

You cannot voice a long article in a single request. There is a ceiling on how much one call can synthesize, and a few thousand characters will cut off partway. So you split by sentence. That much is unavoidable.

The problem is what happens when you join the pieces back. In my own runs, the discontinuity came in three flavors, each with a different character.

Seam symptomWhat you hearLayer that fixes it
Prosody resetA stiffness, as if re-reading from the top at each sentenceContext priming
Boundary clickA brief pop at the joinZero-crossing snap and edge fade
Uneven pausesSentences collide, or dragPunctuation-aware silence

Naive "split by sentence, insert a fixed pause, concatenate raw PCM" leaves the reset and the click intact. A constant pause length keeps the pacing uneven. From here, we peel the problem apart one layer at a time.

Passing the previous ending into the next call (context priming)

The most obvious offender is intonation snapping back to blank at each sentence start. The model has no idea how the previous sentence ended; each request arrives as a fresh introduction.

So we hand it a short tail of the prior sentence and ask it to continue in the same manner. Only the current sentence is spoken. The previous one is nothing more than a cue for carrying the cadence forward.

from google import genai
from google.genai import types
 
client = genai.Client()
MODEL = "gemini-3.1-flash-tts-preview"
VOICE = "Kore"
STYLE = "Read in a calm, low voice at a steady, unhurried pace."
 
def build_prompt(text: str, prev_tail: str) -> str:
    if prev_tail:
        return (
            f"{STYLE}\n"
            f"You just finished reading: \"{prev_tail}\". Keep the same timbre and the flow of intonation.\n"
            f"Now read only the following sentence:\n{text}"
        )
    return f"{STYLE}\nRead the following sentence:\n{text}"
 
def synth(text: str, prev_tail: str = "") -> bytes:
    resp = client.models.generate_content(
        model=MODEL,
        contents=build_prompt(text, prev_tail),
        config=types.GenerateContentConfig(
            response_modalities=["AUDIO"],
            speech_config=types.SpeechConfig(
                voice_config=types.VoiceConfig(
                    prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=VOICE)
                )
            ),
        ),
    )
    cand = resp.candidates[0]
    if cand.finish_reason and cand.finish_reason.name != "STOP":
        raise RuntimeError(f"unexpected finish_reason: {cand.finish_reason.name}")
    return cand.content.parts[0].inline_data.data  # 24kHz 16bit mono PCM

It does not line up perfectly. Priming is a nudge, not a guarantee. Even so, the stiffness at sentence starts visibly dropped in my runs. I keep the tail to around 20 to 24 characters. Hand over more and the model will occasionally read the cue itself. Keep the cue short -- just enough to convey the flow.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Separate the three distinct causes of seams at sentence boundaries and take away code that addresses each one
Context priming carries the previous sentence's cadence into the next call, while zero-crossing edge fades remove the click at the join
Take home an idempotent seam ledger -- JSON schema and regeneration code -- so re-rendering one chunk never rebuilds the whole track
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-11
Never Generate the Same Narration Twice: Cache-Key Design and Invalidation for Gemini TTS Output
For apps that replay the same audio over and over—meditation, language learning, storytelling—caching the Gemini TTS output itself drives the variable cost to near zero. This is a working design: how to build a cache key that text alone can't cover, a two-tier server-plus-device cache, and an invalidation policy that survives model shutdowns like the August 17 image-model deprecation.
Dev Tools2026-07-09
Closing the Failures That Never Throw: Normalizing Gemini API Responses into a Discriminated Union
An HTTP 200 with an empty body will never reach your catch block. Here is how I normalize finishReason and blockReason into a discriminated union, and let a never check turn missed cases into compile errors.
Dev Tools2026-07-03
Stop Making Listeners Wait for the Whole File — Wiring Gemini TTS Streaming into Your Delivery Path
gemini-3.1-flash-tts-preview now streams audio via streamGenerateContent. A delivery path with 1.8s to first sound, covering PCM boundary handling, sentence-level resume, and a fallback for preview shutdown.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →