How to Generate Ad Voiceovers With Gemini 3.8 Flash TTS
By the end of this you'll have a script that turns one ad line into a batch of voiceover files, a two-person dialogue spot, and a budget number you can defend to finance. We'll use Gemini 3.8 Flash TTS (gemini-3.8-flash-tts), which Google made generally available on September 22, 2026. Text goes in, a WAV comes out, and nobody has to rebook a studio for the third round of copy tweaks.
Set up Gemini 3.8 Flash TTS
You need Python, an API key from Google AI Studio, and google-genai 2.25.0 or later, the minimum version in the speech generation docs. The Gemini TTS API is called through the Interactions API.
pip install "google-genai>=2.25.0"
export GEMINI_API_KEY="<your-api-key>"
genai.Client() reads the key from the environment, so nothing gets hardcoded.
1. Generate a single-speaker ad voiceover
This wraps the documented call in a helper so you can reuse it for every variant.
import base64
from google import genai
client = genai.Client()
def voiceover(text, style, voice, path):
interaction = client.interactions.create(
model="gemini-3.8-flash-tts",
input=[{
"type": "user_input",
"content": [{
"type": "text",
"text": text,
"annotations": [{
"type": "speech_metadata",
"style": style,
}],
}],
}],
response_format={"type": "audio"},
generation_config={"speech_config": [{"voice": voice}]},
)
with open(path, "wb") as f:
f.write(base64.b64decode(interaction.output_audio.data))
voiceover("Meet the sneaker that finally fits.", "cheerful and friendly", "Kore", "ad_v1.wav")
The audio arrives base64-encoded, so decode it before writing. Kore is one of 30 prebuilt voices.
2. Make ad voiceover variants with style and inline tags
Marketing runs on variants: same copy, different mood. Put sustained delivery (emotion, pace, volume) in speech_metadata.style. Put one-off moments in angle-bracket tags such as <short pause>, <sigh> or <whispers>.
The catch: the model treats text as a verbatim transcript. Anything you put there gets spoken, so a stage direction like "(excitedly)" belongs in style, never in the script.
script = "Meet the sneaker that finally fits. <short pause> Free returns, ALWAYS."
styles = {
"upbeat": "bright and energetic",
"calm": "warm, calm and reassuring",
"urgent": "fast and excited, like a flash sale",
}
for name, style in styles.items():
voiceover(script, style, "Kore", f"ad_{name}.wav")
That renders three moods of one script with the same voice. Capital letters add emphasis.
3. Pick a voice with the voices list endpoint
Browse the library instead of guessing. Values inside one filter are OR-ed; different filters are AND-ed.
from google import genai
client = genai.Client()
response = client.voices.list(
language_code=["en-US"],
gender=["female"],
contexts=["Conversational"],
search="warm",
page_size=20,
)
for voice in response.voices or []:
print(f"{voice.id} | {voice.display_name} ({voice.language_code}, {voice.accent}): {voice.description}")
Audition a few candidates with the helper from step 1, then keep the winner fixed across the campaign so the brand doesn't change voice between ads.
4. Write a two-speaker dialogue ad
Each turn is its own text item, and every turn needs a speaker. For dialogue, speech_config becomes an object with a speakers list instead of the list used for one voice.
import base64
from google import genai
client = genai.Client()
interaction = client.interactions.create(
model="gemini-3.8-flash-tts",
input=[{
"type": "user_input",
"content": [
{
"type": "text",
"text": "Jane, have you tried the new sneakers yet?",
"annotations": [{
"type": "speech_metadata",
"speaker": "Joe",
"style": "curious and upbeat",
}],
},
{
"type": "text",
"text": "I have. They finally fit, and returns are free.",
"annotations": [{
"type": "speech_metadata",
"speaker": "Jane",
"style": "warm and confident",
}],
},
],
}],
response_format={"type": "audio"},
generation_config={
"speech_config": {
"speakers": [
{"speaker": "Joe", "voice": "Puck"},
{"speaker": "Jane", "voice": "Kore"},
],
}
},
)
with open("ad_dialogue.wav", "wb") as f:
f.write(base64.b64decode(interaction.output_audio.data))
Prebuilt voices allow two speakers per request. If the mood shifts mid-spot, split it into separate turns with their own style.
5. Handle the WAV output for your editor
By default you get audio/wav: a RIFF header, 24 kHz, mono, 16-bit PCM, so the bytes are a playable file as they are. Most video editors expect 48 kHz, so resample:
ffmpeg -i ad_upbeat.wav -ar 48000 ad_upbeat_48k.wav
With stream=True you get headerless raw PCM instead, which won't open as a WAV. For phone or IVR spots, response_format also accepts audio/l16, audio/mulaw and audio/alaw, plus sample rates of 24000, 16000 or 8000.
Check Gemini TTS pricing before you plan the budget
The free tier charges nothing for input or output, but Google's pricing page says free-plan content is used to improve its products and paid-plan content is not. Keep unreleased campaign copy on the paid tier.
Paid Standard rates are introductory and double on January 1, 2027:
| Per 1M tokens | Through Dec 31, 2026 | From Jan 1, 2027 | | --- | --- | --- | | Text input | $0.50 | $1.00 | | Audio output | $9.00 | $18.00 | | Batch audio output | $4.50 | $9.00 |
The page lists audio output at $0.00225 per 10 seconds, so a 30-second spot costs about $0.0068 now and $0.0135 after the change. Two hundred variants is roughly $1.35 today and $2.70 in 2027. Tiny numbers, but a rate that doubles on a date belongs in the plan for any evergreen pipeline.
Gotchas
- Keep immutable traits (age, gender, names, permanent accents) out of
style. Choose a voice instead, and skip meta-instructions like "keep the same timbre", which the docs say add voice drift. - The voices endpoint lists what exists, but the docs send you to a separate page for supported regions. Check it before you build.
- The pricing page gives no rate limits for TTS models. Read the rate limits page before a big batch run.
Cheap, repeatable voiceovers let you test copy and delivery like code, provided you pin the voice, version the scripts and redo the budget math when January's price change lands.