◉GEMINI LABJP
●TTS — Gemini 3.8 Flash TTS and 3.8 Flash-Lite TTS became generally available on September 22. Flash-Lite TTS replaces the 3.1 Flash TTS preview●GEMINI2.5 — Access to the 2.5 models is now limited to people who already used them. They are not deprecated; new projects are pointed to 3.5 Flash-Lite or 3.8 Flash●9/30 — gemini-omni-flash-preview is deprecated on September 30, five days from now●SANDBOX — Users report that since v0.60.0, a sandboxed Gemini CLI asks for sign-in every time and can no longer resume past sessions●NEW — Review only the orange sentences before sending a Gemini draft●CONFIG — Antigravity 2.17.0 now reads repository settings from .gemini/config.json, the same place Gemini CLI looks●TTS — Gemini 3.8 Flash TTS and 3.8 Flash-Lite TTS became generally available on September 22. Flash-Lite TTS replaces the 3.1 Flash TTS preview●GEMINI2.5 — Access to the 2.5 models is now limited to people who already used them. They are not deprecated; new projects are pointed to 3.5 Flash-Lite or 3.8 Flash●9/30 — gemini-omni-flash-preview is deprecated on September 30, five days from now●SANDBOX — Users report that since v0.60.0, a sandboxed Gemini CLI asks for sign-in every time and can no longer resume past sessions●NEW — Review only the orange sentences before sending a Gemini draft●CONFIG — Antigravity 2.17.0 now reads repository settings from .gemini/config.json, the same place Gemini CLI looks
Articles/Updates
◎ Updates/2026-09-26Intermediate

Swapping a Preview TTS Model for 3.8 Flash-Lite TTS, Then Re-choosing a Voice From 150+ Candidates

On September 22, gemini-3.8-flash-lite-tts went GA as the replacement for the 3.1 Flash TTS preview. Here is what I did over one week: replacing the preview model name behind my healing app's guidance audio and re-choosing a voice from the new /v1beta/voices endpoint.

Gemini API243TTS4Gemini 3.8indie development27operations19

Late on September 22 I was re-reading the Gemini API changelog when one line made me stop. gemini-3.8-flash-tts and gemini-3.8-flash-lite-tts had gone GA, and the changelog said plainly that Flash-Lite TTS is the replacement for gemini-3.1-flash-tts-preview.

I knew that preview name. It lives in a small script behind the healing-sounds app I run as an indie developer — the one that generates the short spoken guidance you hear before playback starts, a few dozen seconds of "turn the screen face down and follow your breath." I had picked a voice half a year ago and hadn't touched the script since.

Preview models eventually disappear, name and all. A week in which the official replacement is spelled out felt like reason enough to move something that was still working. What follows is what I actually did over that week, including the two places where I turned back.

The replacement was not "the same name, now GA"

The first thing I'd like to pass on is the mapping. The successor to the 3.1 Flash TTS preview is Flash-Lite TTS, not 3.8 Flash TTS. The larger 3.8 Flash TTS carries the new capabilities — voice design, and voice cloning with a consent step.

All my guidance audio needs is a calm voice reading a fixed sentence the same way every time. There is nobody whose voice I want to imitate, and no emotion to modulate. Lite is enough for this job — and choosing the model with more features would mostly mean more decisions handed back to me.

So I drew a line at my desk before writing any code: short, fixed guidance lines go to Flash-Lite TTS; long reads and anything that needs expression go to 3.8 Flash TTS. I don't intend to move that line for a while.

UseModel I choseWhy
In-app guidance audio (tens of seconds, fixed text)gemini-3.8-flash-lite-ttsThe official replacement for the 3.1 Flash TTS preview, with everything this use needs
Article narration, multiple speakers, styled voicesgemini-3.8-flash-ttsVoice design and cloning live only here

Before changing the model name, I counted the call sites

The change itself is one line. Precisely because I knew it was one line, I made myself count first. I don't trust my six-months-ago self to remember where he wrote things.

# Find every place the preview name still lives in the guidance-audio repo
grep -rn "flash-tts-preview\|flash-preview-tts" --include="*.py" --include="*.json" --include="*.env*" .

Two hits. The constant in the generation script, and the sample value in .env.example. The sample is the one that's easy to miss, and it's a trap: the next time I rebuild an environment, I'd copy the stale name straight in. I updated both to gemini-3.8-flash-lite-tts and moved the model name into an environment variable.

import os
 
# Keep the model name out of the code so the next swap is a config change
TTS_MODEL = os.environ.get("TTS_MODEL", "gemini-3.8-flash-lite-tts")

Why an environment variable? So that the next replacement doesn't begin with grep. I'd like this grep to have been the last one.

I looked at /v1beta/voices raw before filtering anything

If I had only changed the model, the old voice name would probably have kept working. But this release also brought more than 150 prebuilt voices and a /v1beta/voices endpoint to list them. Half a year ago I chose from a handful. I couldn't think of a better moment to choose again.

Before reading the documentation's table, I saved the list as JSON. Code written against guessed field names usually greets me with a KeyError on the first run.

import json
import os
import urllib.request
 
# Save the raw list and read the field names with my own eyes before writing a filter
API_KEY = os.environ["GEMINI_API_KEY"]  # real value comes from the environment, never the code
URL = "https://generativelanguage.googleapis.com/v1beta/voices"
 
req = urllib.request.Request(URL, headers={"x-goog-api-key": API_KEY})
with urllib.request.urlopen(req, timeout=30) as res:
    raw = json.load(res)
 
with open("voices_raw.json", "w", encoding="utf-8") as f:
    json.dump(raw, f, ensure_ascii=False, indent=2)
 
voices = raw.get("voices", [])
print(f"{len(voices)} voices")
for v in voices:
    # Don't assume the schema; skim one line per voice to see what fields exist
    print(json.dumps(v, ensure_ascii=False)[:160])

Once the list was in hand, I wanted to filter it, and here I made my first mistake. I mechanically kept only voices whose description mentioned "calm" or "warm." The list shrank nicely, but when I listened, several of the survivors had a stiff edge on Japanese sentence endings. A word tag is not a substitute for an ear.

Then I played the same sentence through each candidate

So I changed the approach. Tags would only do the first rough cut. Every remaining candidate would read the same guidance line, get written to a WAV file, and be judged by listening. The line I used is the one the app actually plays.

import base64
import os
import wave
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
TTS_MODEL = os.environ.get("TTS_MODEL", "gemini-3.8-flash-lite-tts")
 
# The guidance line the app actually plays; every candidate voice reads this same text
GUIDE_TEXT = "Turn the screen face down and slowly follow your breath."
CANDIDATES = ["Kore", "Charon", "Aoede", "Leda"]  # whatever survived the first rough cut
 
 
def rate_from_mime(mime: str, default: int = 24000) -> int:
    # e.g. "audio/L16;codec=pcm;rate=24000" — take the sample rate from the response
    for part in mime.split(";"):
        part = part.strip()
        if part.startswith("rate="):
            return int(part.split("=", 1)[1])
    return default
 
 
def synth_to_wav(voice: str, text: str, path: str) -> None:
    res = client.models.generate_content(
        model=TTS_MODEL,
        contents=text,
        config=types.GenerateContentConfig(
            response_modalities=["AUDIO"],
            speech_config=types.SpeechConfig(
                voice_config=types.VoiceConfig(
                    prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=voice)
                )
            ),
        ),
    )
    part = res.candidates[0].content.parts[0]
    pcm = part.inline_data.data
    if isinstance(pcm, str):  # some SDK versions hand back base64 text instead of bytes
        pcm = base64.b64decode(pcm)
    rate = rate_from_mime(part.inline_data.mime_type or "")
    with wave.open(path, "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)  # 16-bit
        w.setframerate(rate)
        w.writeframes(pcm)
 
 
for name in CANDIDATES:
    out = f"guide_{name}.wav"
    synth_to_wav(name, GUIDE_TEXT, out)
    print("wrote", out)

Reading the sample rate from mime_type is a note to my past self. TTS output is 24 kHz, 16-bit, mono PCM, and I once wrote a WAV header assuming 44.1 kHz — the voice came back as a fast-talking stranger. The ordering of chunks and headers when you stream the audio is something I wrote up in Receiving Gemini TTS via streamGenerateContent and playing from the first chunk.

The listening itself happened late at night with headphones, the same sentence looping through each file. With a line this short, the way a sentence ending falls changes the whole impression. The voice that remained at the end was not the one I had been using.

Two places where I turned back

The first was voice cloning. 3.8 Flash TTS can now clone a voice with a consent step, but I had no reason to borrow anyone's voice for guidance audio. Bringing a new capability into a place where a stock voice already does the job only adds one more thing I'd have to explain.

The second was timing. Even after deciding I preferred the new voice, I didn't swap the audio that's already playing in the app that same day. A voice becomes part of how people remember an app, so the swap will ride along with the next app update, with one line in the release notes. The model swap happens today; the voice swap happens on the next release day. Separating the two took a weight off my shoulders.

One next step

If gemini-3.1-flash-tts-preview is still somewhere in your code, I'd suggest starting with grep and a count. The replacement is Flash-Lite TTS, and you can re-choose the voice from /v1beta/voices — there is more room to choose than there was six months ago.

The other side of this — splitting a long article into pieces and joining them into one narration — is something I wrote during the preview era in Turning articles into audio with the Gemini 3.1 Flash TTS preview, cost estimates included. Read the model name as the new one and the setup should carry over as it is.

I may be missing something, but starting from a few dozen seconds of guidance text is what let me carry the listening test all the way to the end. Small was the point.

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 $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◎ Updates2026-09-10
iOS 27 arrives September 14, and my Gemini API calls stay exactly as they are — what changes is the menu
iOS 27 ships on September 14. Even with the new Siri built on Gemini, the Gemini API calls inside your own app do not change. Here is how I separate the three layers at the call site, and the four things I finish before release day.
◎ Updates2026-08-18
The Assistant Switch Is One-Way, So the Baseline Has to Be Captured Now
From September 4, Google Assistant is replaced by Gemini, and a device that has switched cannot go back. Here is how I started recording voice-originated launches inside my own apps, and the assumption about splitting before and after by date that turned out to be wrong.
◎ Updates2026-07-04
Before the August 17 Gemini Image Model Shutdown: Inventory Where You Actually Call Them First
Some Gemini image generation models retire on August 17. Before choosing a replacement, here is how to inventory which models are actually being called, and from where, using your request logs.
📚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