Have you ever tried adding text-to-speech to your app, only to be disappointed by the robotic voice?
In April 2026, Google released two Gemini TTS models that changed the game: Flash TTS optimized for low latency, and Pro TTS optimized for audio quality. Flash TTS in particular combines real-time conversation speed with expressiveness that previous TTS engines couldn't match.
But simply calling the API and getting audio back won't unlock Flash TTS's full potential. This article covers the implementation patterns needed to ship a production-quality voice app.
Flash TTS vs Pro TTS: How to Choose
Let's start by understanding the characteristics of each model.
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# Flash TTS: Low latency priority (real-time dialogue)
flash_response = genai.generate_content(
model="gemini-2.5-flash-tts",
contents="Hello. Let's check your schedule for today.",
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice_config": {
"prebuilt_voice_config": {
"voice_name": "Kore"
}
}
}
}
)
# Pro TTS: High quality priority (narration, podcasts)
pro_response = genai.generate_content(
model="gemini-2.5-pro-tts",
contents="Hello. Let's check your schedule for today.",
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice_config": {
"prebuilt_voice_config": {
"voice_name": "Kore"
}
}
}
}
)In my measurements, Flash TTS returns its first response in about 200-400ms. Pro TTS takes 800ms-1.2s. Since the threshold for natural conversational flow is roughly 500ms, Flash is the only choice for real-time dialogue.
Pro TTS, however, clearly wins on intonation naturalness and emotional expressiveness. For use cases where audio quality directly impacts product value — podcast generation, audiobook narration — choose Pro.
My decision rule is simple: "Is the user staring at the screen while waiting for audio?" If they're waiting for a chatbot response, use Flash. If audio content is being generated in the background, use Pro.
Streaming Playback: Driving Perceived Latency to Near-Zero
Flash TTS's 200ms response time is impressive, but if you try to convert a long text all at once, playback can't start until the entire text is processed. Implementing streaming lets playback begin the moment the first chunk arrives.
import asyncio
import io
import wave
from collections import deque
class StreamingTTSPlayer:
"""Player that handles TTS stream reception and playback concurrently"""
def __init__(self, buffer_chunks: int = 3):
self.buffer: deque[bytes] = deque()
self.buffer_threshold = buffer_chunks
self.is_playing = False
self.is_complete = False
async def receive_stream(self, text: str) -> None:
"""Receive TTS stream and accumulate in buffer"""
response_stream = genai.generate_content(
model="gemini-2.5-flash-tts",
contents=text,
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice_config": {
"prebuilt_voice_config": {"voice_name": "Kore"}
}
}
},
stream=True,
)
for chunk in response_stream:
if hasattr(chunk, "audio") and chunk.audio:
self.buffer.append(chunk.audio.data)
# Start playback once buffer hits threshold
if not self.is_playing and len(self.buffer) >= self.buffer_threshold:
self.is_playing = True
self.is_complete = True
async def play_buffer(self, output_callback) -> None:
"""Dequeue chunks from buffer and play"""
while True:
if self.buffer and self.is_playing:
chunk = self.buffer.popleft()
await output_callback(chunk)
elif self.is_complete and not self.buffer:
break
else:
await asyncio.sleep(0.01) # Wait for buffer refill
async def run(self, text: str, output_callback) -> None:
"""Run reception and playback concurrently"""
await asyncio.gather(
self.receive_stream(text),
self.play_buffer(output_callback),
)The buffer_threshold value matters. Setting it to 1 means playback starts the instant the first chunk arrives, but network jitter can empty the buffer and cause audio dropouts. A 3-chunk buffer absorbs minor jitter while keeping playback smooth.
Adjust this based on network conditions: 5-8 chunks for mobile connections, 2-3 for WiFi or wired.
Controlling Voice Expressiveness
What surprised me most about Flash TTS is how much the voice expression changes just by adjusting how the text is written.
def enhance_text_for_tts(text: str, style: str = "conversational") -> str:
"""Optimize text for TTS output"""
style_prompts = {
"conversational": (
"Read the following text in a natural, warm tone "
"as if talking to a friend.\n\n"
),
"professional": (
"Read the following text in a clear, confident tone "
"as if delivering a presentation.\n\n"
),
"storytelling": (
"Read the following text in an expressive, captivating tone "
"as if narrating a story.\n\n"
),
}
prompt = style_prompts.get(style, style_prompts["conversational"])
return prompt + text
# Inserting natural pauses
def add_natural_pauses(text: str) -> str:
"""Insert natural pauses to improve rhythm"""
import re
# Add brief pauses after sentence-ending punctuation
text = re.sub(r"\.(?\!\.\.)(?\!\d)", ". ... ", text)
# Add pauses before emphasis words
emphasis_words = ["However", "But", "Actually", "In other words", "The key point is"]
for word in emphasis_words:
text = text.replace(word, f"... {word}")
return textThe style_prompts approach leverages Gemini's language understanding for speech synthesis — an approach unique to Gemini TTS. Traditional TTS engines required meticulous SSML tags to control pitch and rate, but Gemini TTS takes natural language instructions like "read this in a warm tone" and generates surprisingly appropriate intonation.
The add_natural_pauses function uses ellipses ("...") as pause markers. Gemini TTS interprets these as audio pauses. Speech with well-placed pauses sounds more listenable and professional than text with only punctuation.
Cost Optimization: Eliminating Unnecessary Synthesis
TTS API calls are pay-per-use. Naively converting all text to speech will cause costs to spike.
import hashlib
from functools import lru_cache
class TTSCacheManager:
"""Cache frequently used text-to-speech to reduce API calls"""
def __init__(self, cache_dir: str = "./tts_cache", max_cache_mb: int = 500):
self.cache_dir = cache_dir
self.max_cache_bytes = max_cache_mb * 1024 * 1024
self._ensure_cache_dir()
def _text_hash(self, text: str, voice: str) -> str:
return hashlib.sha256(f"{voice}:{text}".encode()).hexdigest()[:16]
async def get_or_generate(self, text: str, voice: str = "Kore") -> bytes:
cache_key = self._text_hash(text, voice)
cache_path = f"{self.cache_dir}/{cache_key}.wav"
# Cache hit
cached = self._read_cache(cache_path)
if cached:
return cached
# Cache miss — call API
audio_data = await self._generate_tts(text, voice)
self._write_cache(cache_path, audio_data)
return audio_data
def _read_cache(self, path: str) -> bytes | None:
try:
with open(path, "rb") as f:
return f.read()
except FileNotFoundError:
return None
def _write_cache(self, path: str, data: bytes) -> None:
self._evict_if_needed(len(data))
with open(path, "wb") as f:
f.write(data)
def _evict_if_needed(self, new_data_size: int) -> None:
"""Evict old entries using LRU policy"""
import os
import glob
files = glob.glob(f"{self.cache_dir}/*.wav")
total_size = sum(os.path.getsize(f) for f in files)
if total_size + new_data_size <= self.max_cache_bytes:
return
# Delete oldest files first
files.sort(key=lambda f: os.path.getatime(f))
for f in files:
os.remove(f)
total_size -= os.path.getsize(f)
if total_size + new_data_size <= self.max_cache_bytes:
break
async def _generate_tts(self, text: str, voice: str) -> bytes:
response = genai.generate_content(
model="gemini-2.5-flash-tts",
contents=text,
generation_config={
"response_modalities": ["AUDIO"],
"speech_config": {
"voice_config": {
"prebuilt_voice_config": {"voice_name": voice}
}
}
}
)
return response.audio.data
def _ensure_cache_dir(self) -> None:
import os
os.makedirs(self.cache_dir, exist_ok=True)Caching is most effective for UI boilerplate and system messages. Phrases like "You have a new message" or "Processing complete" can use the same audio every time, so generate once and serve from cache. This alone often reduces API calls by 30-50%.
Gemini 2.5 Flash TTS has dramatically lowered the barrier to voice app development. Low latency, high expressiveness, and natural language control — with all three in place, indie developers can build professional-quality voice experiences. Start by picking one piece of information in your app that's currently displayed as text but would work better as audio, and replace it with Flash TTS.
This article is published in full as a free sample of our premium content. Premium articles on Gemini Lab cover practical Gemini API patterns with working code examples. To access all premium articles, consider joining our membership.