◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-06-25Intermediate

Turning Articles into Audio with the Gemini 3.1 Flash TTS Preview: Splitting Long Text, Stitching It Back, and What It Actually Costs

The Gemini 3.1 Flash TTS preview launched today. Here is a single-narrator pipeline that converts a written article into clean audio, including how to split long text, stitch PCM without ugly seams, keep one voice steady, and estimate the real per-article cost.

gemini-3.1-flash-ttstext-to-speech4audio8podcastpython108cost-optimization30

✦ Premium Article

The reason I kept putting off audio versions of my writing was always cost. I publish steadily on Dolice Labs, and running every single post through a text-to-speech model never balanced out between quality and money.

The Gemini 3.1 Flash TTS preview, which became available today, looks like it moves that break-even line. It keeps expression while staying cheap, and it is easy to steer. For a workload like article narration — long text, high frequency — those three traits compound. This is a walkthrough of converting a written article into a single-narrator voiceover and getting it ready for a channel like a podcast or stand.fm, looked at from both the implementation and the cost side.

Why a "preview model" shifts the economics of article narration

Narrating articles is not a one-off flashy demo. You pour 3,000 to 5,000 characters of body text through it almost daily, for months. That makes the unit cost per article the real weight on the project.

What used to stop me was a binary choice: models with expressive voices cost a lot, and cheap models read flatly. The arrival of a Flash-tier TTS preview puts a realistic option in the middle of that binary.

Boiled down to one card, there are three things to watch:

  • Characters per article (the billable volume)
  • How far prompt-level control holds (how rarely you re-record)
  • The cost of regeneration on failure (the leak in production)

Later in this piece I price the first item in real currency. The other two are mostly absorbed by how you build the pipeline, so let me clear those first.

You can't hand over long text in one shot

The first wall is that you cannot pass a whole article in a single request. TTS caps how much one synthesis call handles, and dumping several thousand characters in will cut off mid-way or wreck the prosody of the second half.

Split on sentence boundaries, sub-split only the long ones

My rule is never to split inside a sentence. Cutting mid-sentence makes the breath unnatural at the seam. For text, I use the period as the primary boundary and only sub-split overly long blocks at commas.

import re
 
def split_for_tts(text: str, max_chars: int = 280) -> list[str]:
    """Split article body into TTS-sized units.
    Never split inside a sentence; sub-split long sentences at commas."""
    raw = re.split(r"(?<=\.)\s+|\n+", text.strip())
    sentences = [s for s in raw if s]
 
    chunks: list[str] = []
    buf = ""
    for s in sentences:
        parts = re.split(r"(?<=,)\s", s) if len(s) > max_chars else [s]
        for p in parts:
            if len(buf) + len(p) <= max_chars:
                buf += (" " + p if buf else p)
            else:
                if buf:
                    chunks.append(buf)
                buf = p
    if buf:
        chunks.append(buf)
    return chunks
 
# A 4,000-character article lands as ~20 chunks of roughly 280 chars

I keep max_chars near 280 because longer makes the prosody monotone and shorter multiplies the seams — a compromise that surfaced through running it. The sweet spot moves with the material, so tune it.

Join PCM without a seam

Gemini TTS returns 24kHz, 16-bit, mono PCM. Receive audio per chunk, concatenate the raw PCM, and you get one track. The catch is to insert a short silence between chunks. Zero silence makes sentences collide; too much drags. I use 0.25 seconds between sentences.

import struct
 
SAMPLE_RATE = 24000  # Gemini TTS output sample rate
SILENCE_SEC = 0.25
 
def silence_pcm(seconds: float) -> bytes:
    n = int(SAMPLE_RATE * seconds)
    return struct.pack("<" + "h" * n, *([0] * n))  # 16-bit silence
 
def join_pcm(chunks_pcm: list[bytes]) -> bytes:
    gap = silence_pcm(SILENCE_SEC)
    out = bytearray()
    for i, pcm in enumerate(chunks_pcm):
        if i > 0:
            out += gap
        out += pcm
    return bytes(out)

With "split and join" as the skeleton, the pipeline survives any change in article length.

✦

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
✦If TTS pricing made you give up on audio versions of your posts, you'll be able to estimate the cost per article yourself
✦You'll get the code to split a multi-thousand-character article by sentence and join it into one seamless audio file
✦You'll learn how to stop a single narrator's voice from drifting across chunks using prompt-level control
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

◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.
◈ API / SDK2026-05-14
Controlling thinking_budget in Gemini 2.5 Pro — Cut Costs by 70% Without Sacrificing Reasoning Quality
Leaving thinking_budget unset in Gemini 2.5 Pro leads to unexpected costs. This guide covers task-level budget design, dynamic control, and production monitoring with working Python code.
◈ API / SDK2026-05-09
Why I Always Resize Images With Pillow Before Sending Them to the Gemini API
A practical look at why preprocessing images with Pillow before they reach the Gemini API quietly cuts both latency and token usage. Numbers from a real personal project, plus the helper function I now reuse everywhere.