When you build apps on your own, there's a day when you suddenly think, "this would land better with a voice." For me it was a screen that read out a guidance script. The text was there, but having an off-the-shelf TTS read it flatly felt clerical — there was no warmth in it.
That nagging lack of warmth is what pushed me to try Gemini's TTS. Here are the calling patterns I settled on while wiring it in, plus the snags that the docs don't print in bold.
How it differs from old-school readout
Traditional TTS was a tool that turned text into sound. Gemini's TTS leans toward reading text with an understanding of its intent.
Concretely, the same sentence shifts in tone just by telling it "calmly" or "with a lift" in the prompt. Compared with the days of hand-writing SSML tags to shape intonation, the entry point for tweaking has moved much closer.
A news script you want read evenly, versus a story narration where the pauses do the work — being able to split those with natural language instead of tags genuinely lightens the implementation load.
The shortest path to one audible clip
Current Gemini uses the google-genai package (from google import genai). Plenty of samples online still use the older google.generativeai line, but that one is being deprecated in stages, so for new work it's safer to stay on the new SDK.
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_API_KEY")
response = client.models.generate_content(
model="gemini-2.5-flash-preview-tts",
contents="Read the following calmly: It's a clear day today. Perfect weather for a walk.",
config=types.GenerateContentConfig(
response_modalities=["AUDIO"],
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name="Kore"
)
)
),
),
)
pcm = response.candidates[0].content.parts[0].inline_data.dataModel names shifted a few times during the preview period. The basic split is a Pro-tier model (gemini-2.5-pro-preview-tts) when quality matters and a Flash-tier model when you want speed and lower cost — but the exact string moves over time. Always confirm the current name in the official changelog before you build. Preview models can be shut off on a given date, and a hard-coded name is a seed for future outages.
The first snag — you get raw PCM, not a WAV
This is where I lost the better part of an hour.
If you write the inline_data.data from the code above straight to a file named output.wav, most players won't play it. What Gemini's TTS returns isn't a WAV file — it's headerless raw PCM (24kHz, 16-bit, mono). Slapping a .wav extension on bare waveform data leaves the player confused.
The right move is to wrap the PCM in a proper WAV header before saving.
import wave
def save_as_wav(pcm_bytes, path, rate=24000, channels=1, width=2):
# Gemini TTS output is 24kHz, 16-bit, mono raw PCM
with wave.open(path, "wb") as wf:
wf.setnchannels(channels)
wf.setsampwidth(width) # 16-bit = 2 bytes
wf.setframerate(rate)
wf.writeframes(pcm_bytes)
save_as_wav(pcm, "output.wav")The moment I added that one step, the silent output finally became a voice. The official samples do mention it, but the failure shows up as "a silent WAV gets generated" with no error, so it takes a surprisingly long time to trace the cause.
Decide voice and pacing in two layers — prompt plus timbre
The impression of a readout is decided by the combination of the timbre (voice_name) and the prompt instructions. At first I tried voice after voice and despaired that "none of them feel right" — but half the cause was on the prompt side.
Think of timbre as the base character and the prompt as the performance in the moment; separating them makes tuning faster.
contents = """Read the following guidance in a tone that gently leads the listener.
Take a slightly longer pause at full stops, and a beat before technical terms:
Now, let's walk through the first setup together.
Press the gear icon in the top-right of the screen."""Just adding performance cues like "a beat before" or "gently" in plain text changes the impression even with the same timbre. If you want to read multi-speaker dialogue distinctly, multi_speaker_voice_config lets you assign a voice per speaker — but starting with a single voice and getting the warmth right first makes the tuning instincts easier to build.
Running cost lands by the second, not the character
Audio has a different cost shape from text generation. Synthesizing a long script in full on every call adds up steadily, even when the per-character figure is small.
The measure I took in practice was caching the generated audio. Fixed-wording lines like guidance prompts get synthesized once, saved, and reused; only the parts that change dynamically get synthesized on demand. Drawing that line alone brought my monthly API cost down noticeably. For a solo indie developer running apps on AdMob revenue, that gap feeds straight into how much breathing room the project has.
A readout that doesn't need to be real-time doesn't need to be synthesized every time a user arrives. Building "when do we synthesize" into the design is the dividing line for whether you can keep TTS in your app long-term.
When not to add TTS
Finally, a note on the cases where I chose not to.
For very short notification sounds, or text that changes character by character, Gemini TTS wasn't a fit. Synthesis latency turns directly into perceived sluggishness, and the cost doesn't pay off. For those, the device's built-in OS TTS was plenty.
Gemini TTS shows its strengths exactly where expressiveness is needed — narration, guidance, story readouts. Rather than replacing everything, narrowing it to "places that need warmth" is my own realistic way of using it.
If you try it next
Start by passing a single sentence to gemini-2.5-flash-preview-tts, wrapping the raw PCM into a WAV, and playing it through. Once you have sound, swap the timbre two or three times and listen to how the same sentence changes. Once that minimal loop is turning, the rest is just adding performance cues in the prompt and dialing in a voice that fits your app.
I hope this helps anyone who, like me, has felt that "this would be better with a voice here" — as a first step.