Generate Video with Google Veo 3.1 and the Gemini Python SDK
In this article
- Prerequisites
- Step 1: Acknowledge the billing requirement and install dependencies
- Step 2: Initialise the client
- Step 3: Select a model variant
- Step 4: Generate a video from a text prompt
- Prompt engineering for cinematic results
- Step 5: Register a webhook for production async handling
- Step 6: Generate video from an image (image-to-video)
- Step 7: Control start and end frames
- Step 8: Reference-to-video with multiple asset images
- Step 9: Extend an existing video
- What to watch out for
- Where to go next
Programmatic video generation has crossed from research curiosity into production reality. Google's Veo 3.1 family lets you call a REST-backed Python client, hand it a text prompt or a reference image, and receive a polished MP4 — complete with generated audio — without touching a video editor or a rendering farm. For engineers building content pipelines, ad-tech platforms, or rapid creative prototyping tools, this unlocks a capability that would otherwise require a dedicated VFX budget and significant turnaround time.
Who needs this? Anyone assembling automated content at scale: marketing teams running A/B tests on video creatives, game studios previewing cinematic sequences, social media tools generating short-form clips on demand. The Veo 3.1 Lite and Fast variants are explicitly designed for high-throughput backend workloads, while the full Veo 3.1 model handles quality-critical output and reference-image workflows. The tradeoff is cost — Veo is a paid-only feature with no free tier, so every call hits your billing account. Generation is asynchronous, so this is not a synchronous, user-facing operation; it demands an async architecture.
This guide is adapted from the Google Gemini Cookbook's Get_started_Veo.ipynb, published under the Apache-2.0 licence.
Prerequisites
- A Google AI Studio account with billing enabled. Veo does not run on the free tier.
- A Gemini API key stored as the environment variable or Colab Secret
GEMINI_API_KEY. - Python 3.9 or later.
google-genaiSDK version 2.9.0 or higher.- No GPU or specialised hardware is needed locally — generation runs entirely on Google's infrastructure.
Step 1: Acknowledge the billing requirement and install dependencies
The source notebook gates every generation cell behind a boolean flag. This is a deliberate friction mechanism — if you run the notebook top-to-bottom without thinking, nothing billable executes. In production code you would remove this guard, but keep the intent: fail loudly if the environment is not configured for paid use.
# Change that value if you want to run the colab
I_am_aware_that_veo_is_a_paid_feature = False # @param {type:"boolean"}
Install the SDK:
%pip install -U -q "google-genai>=2.9.0" # Minimum version 1.44 for video extension # 2.0 is needed to use the interactions API
Step 2: Initialise the client
from google.colab import userdata
from google import genai
from google.genai import types
GEMINI_API_KEY = userdata.get('GEMINI_API_KEY')
client = genai.Client(api_key=GEMINI_API_KEY)
Outside Colab, replace userdata.get('GEMINI_API_KEY') with os.environ["GEMINI_API_KEY"]. The client object is the single entry point for all Veo calls and for polling operations.
Step 3: Select a model variant
VEO_MODEL_ID = "veo-3.1-lite-generate-preview" # @param ["veo-3.1-lite-generate-preview", "veo-3.1-fast-generate-preview", "veo-3.1-generate-preview"] {"allow-input":true, isTemplate: true}
The three variants sit at different points on the cost/quality/speed curve:
| Model ID | Best for | Max resolution | Reference-to-video | Relative cost |
|---|---|---|---|---|
veo-3.1-lite-generate-preview |
High-throughput backends, social media clips | 1080p | No | Lowest |
veo-3.1-fast-generate-preview |
Rapid creative iteration, A/B testing | 1080p | No | Mid |
veo-3.1-generate-preview |
Quality-critical production output, reference-to-video | 1080p | Yes (16:9, 720p only) | Highest |
Step 4: Generate a video from a text prompt
This is the core pattern. Every subsequent capability — image-to-video, first/last frame control, extension — wraps the same client.models.generate_videos call with additional arguments.
if not I_am_aware_that_veo_is_a_paid_feature:
print("Veo is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
import time
prompt = "a close-up shot of a golden retriever playing in a field of sunflowers" # @param {type: "string"}
# Optional parameters
negative_prompt = "barking, woofing" # @param {type: "string"}
aspect_ratio = "16:9" # @param ["16:9","9:16"]
resolution = "1080p" # @param ["720p","1080p"]
operation = client.models.generate_videos(
model=VEO_MODEL_ID,
prompt=prompt,
config=types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
negative_prompt=negative_prompt,
),
)
# Waiting for the video(s) to be generated
while not operation.done:
time.sleep(20)
operation = client.operations.get(operation)
print(operation)
print(operation.result.generated_videos)
for n, generated_video in enumerate(operation.result.generated_videos):
client.files.download(file=generated_video.video)
generated_video.video.save(f'video{n}.mp4') # Saves the video(s)
display(generated_video.video.show()) # Displays the video(s) in a notebook
The polling loop (while not operation.done) is the key architectural detail. generate_videos returns immediately with an operation handle; the actual generation happens asynchronously on Google's infrastructure. The 20-second sleep between polls is appropriate — polling more aggressively wastes quota and rarely catches an earlier completion.
Prompt engineering for cinematic results
The model understands a rich vocabulary of cinematographic terms. The source provides worked examples that illustrate different control axes.
Lighting control — specify the quality and direction of light, colour temperature, and how it interacts with surfaces:
prompt = "a solitary, ancient oak tree silhouetted against a dramatic sunset. Emphasize the exquisite control over lighting: capture the deep, warm hues of the setting sun backlighting the tree, with subtle rays of light piercing through the branches, highlighting the texture of the bark and leaves with a golden glow. The sky should transition from fiery orange at the horizon to soft purples and blues overhead, with a single, faint star appearing as dusk deepens. Include the gentle sound of a breeze rustling through the leaves, and the distant call of an owl." # @param {type: "string"}
Camera movement — name specific moves like dolly zoom, tracking shot, and slow motion. The model interprets these faithfully:
prompt = "a realistic video of a futuristic red sportscar speeding down a winding coastal highway at dusk. Begin with a high-angle drone shot that slowly descends, transitioning into a close-up, low-angle tracking shot that perfectly follows the car as it rounds a curve, emphasizing its speed and the gleam of its paint under the fading light. Then, execute a smooth, rapid dolly zoom, making the background compress as the car remains the same size, conveying a sense of intense focus and speed. Finally, end with a perfectly stable, slow-motion shot from a fixed roadside perspective as the car blurs past, its taillights streaking across the frame. Include the immersive sound of the engine roaring, the tires gripping the asphalt, and the distant crash of waves." # @param {type: "string"}
Audio — Veo 3.1 generates audio by default. Reference it explicitly in the prompt to direct it:
prompt = "fireworks at a beautiful city skyline scene with many different fireworks colors and sounds. sounds from excited people enjoying the show surrounding the camera POV can be heard too." # @param {type: "string"}
Dialogue — you can script spoken lines directly in the prompt. Use negative_prompt to suppress on-screen captions if the model renders them:
prompt = "Two charismatic and funny raindrops like coloured characters. The first character start saying \"oh my god, it is really hot in here!\" - then the second answers \"right? I'm melting down! then it melts down completely." # @param {type: "string"}
# Optional parameters
negative_prompt = "texts, captions, subtitles" # @param {type: "string"}
Step 5: Register a webhook for production async handling
Polling inside a while loop works fine for a notebook, but it blocks a server thread for the entire generation duration. For production pipelines, register a webhook so the Gemini API pushes a notification to your endpoint when generation completes:
webhook = client.webhooks.create(
name="MyVideoGenerationWebhook",
subscribed_events=["video.generated"],
uri="https://my-api.com/gemini-callback",
)
print(f"Created webhook: {webhook}")
Your callback endpoint receives the operation result, and you can then call client.files.download on the generated video. This is the right fit for any production pipeline — the same architectural approach used in agent frameworks where long-running tasks are decoupled from the request lifecycle.
Step 6: Generate video from an image (image-to-video)
In a Colab notebook, upload your image first:
import os
from google.colab import files
uploaded = files.upload()
for fn in uploaded.keys():
os.rename(fn, 'image.jpg')
Then pass a PIL image converted to bytes alongside your prompt. If you leave the prompt empty, the model infers motion from the image content alone.
if not I_am_aware_that_veo_is_a_paid_feature:
print("Veo is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
import time
from PIL import Image
import io
prompt = "" # @param {"type":"string","placeholder":"Write your own prompt or leave empty to let the model decide"}
image_name = "" # @param {type: "string", "placeholder":"Enter the name of your image or leave empty for the one you just uploaded."}
# Optional parameters
negative_prompt = "ugly, low quality" # @param {type: "string"}
aspect_ratio = "9:16" # @param ["16:9","9:16"]
resolution = "720p" # @param ["720p","1080p"]
# Loading the image
if image_name=="":
image_name = "image.jpg"
im = Image.open(image_name)
# converting the image to bytes
image_bytes_io = io.BytesIO()
im.save(image_bytes_io, format=im.format)
image_bytes = image_bytes_io.getvalue()
operation = client.models.generate_videos(
model=VEO_MODEL_ID,
prompt=prompt,
image=types.Image(image_bytes=image_bytes, mime_type=im.format),
config=types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
negative_prompt=negative_prompt,
),
)
# Waiting for the video(s) to be generated
while not operation.done:
time.sleep(20)
operation = client.operations.get(operation)
print(operation)
print(operation.result.generated_videos)
for n, generated_video in enumerate(operation.result.generated_videos):
client.files.download(file=generated_video.video)
generated_video.video.save(f'video{n}.mp4') # Saves the video(s)
display(generated_video.video.show()) # Displays the video(s) in a notebook
The image parameter requires the bytes round-trip. When using PIL images, you must call im.save(image_bytes_io, format=im.format) before wrapping in types.Image. Passing a PIL image object directly will not work. Also note that im.format is None for images created in memory rather than opened from disk — always open from a saved file or set the format explicitly.
Step 7: Control start and end frames
You can anchor both the opening and closing frame of a clip by supplying last_frame in the config. This is useful when Veo-generated clips need to splice seamlessly into existing footage.
First, generate a starting image using gemini-2.5-flash-image:
if not I_am_aware_that_veo_is_a_paid_feature:
print("Image generation is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
prompt = "A high quality photorealistic front image of a ginger cat driving a red convertible racing car on the French riviera coast" # @param {type: "string"}
# Optional parameters
negative_prompt = "ugly, low quality, static, weird physics" # @param {type: "string"}
aspect_ratio = "16:9" # @param {type: "string"}
number_of_videos = 1
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=[prompt],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(
aspect_ratio=aspect_ratio,
)
)
)
for part in response.parts:
if part.inline_data:
gemini_image = part.as_image()
break
gemini_image.show()
gemini_image.save("cat_car.png")
Next, generate an ending image from the same scene at a later story moment:
import PIL
if not I_am_aware_that_veo_is_a_paid_feature:
print("Image generation is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
prompt = "Show what happens afterwards when the car take off from a cliff." # @param {type: "string"}
# Optional parameters
negative_prompt = "ugly, low quality, static, weird physics" # @param {type: "string"}
aspect_ratio = "16:9" # @param {type: "string"}
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=[prompt, PIL.Image.open("cat_car.png")],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(
aspect_ratio=aspect_ratio,
)
)
)
for part in response.parts:
if part.inline_data:
ending_image = part.as_image()
break
ending_image.show()
Now bridge the two with Veo using last_frame:
if not I_am_aware_that_veo_is_a_paid_feature:
print("Veo is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
import time
# Optional parameters
prompt = "" # @param {"type":"string","placeholder":"Prompt is optional here"}
negative_prompt = "ugly, low quality, static, weird physics" # @param {type: "string"}
aspect_ratio = "16:9" # @param ["16:9","9:16"]
resolution = "1080p" # @param ["720p","1080p"]
operation = client.models.generate_videos(
model=VEO_MODEL_ID,
prompt=prompt,
image=gemini_image,
config=types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
negative_prompt=negative_prompt,
last_frame=ending_image
),
)
# Waiting for the video(s) to be generated
while not operation.done:
time.sleep(20)
operation = client.operations.get(operation)
print(operation)
print(operation.result.generated_videos)
for n, generated_video in enumerate(operation.result.generated_videos):
client.files.download(file=generated_video.video)
generated_video.video.save(f'flying_car_{n}.mp4') # Saves the video(s)
display(generated_video.video.show()) # Display the video(s) in a notebook
A starting image is mandatory when using last_frame — you cannot supply only an ending frame.
Step 8: Reference-to-video with multiple asset images
Veo 3.1 (full, not Lite or Fast) supports reference images that define characters, objects, or props to appear in the generated video. Each image is wrapped in VideoGenerationReferenceImage with reference_type="asset". Note the hard constraints: only veo-3.1-generate-preview, landscape only (16:9), and 720p resolution.
First, generate the reference images using gemini-2.5-flash-image:
if not I_am_aware_that_veo_is_a_paid_feature:
print("Image generation is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=["A red and black backpack"],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(
aspect_ratio=aspect_ratio,
)
)
)
for part in response.parts:
if part.inline_data:
backpack_image = part.as_image()
break
backpack_image.show()
response = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=["Tanned guy in a leather jacket and a motorbike"],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(
aspect_ratio=aspect_ratio,
)
)
)
for part in response.parts:
if part.inline_data:
actor_image = part.as_image()
break
actor_image.show()
Then pass both as reference images to Veo:
if not I_am_aware_that_veo_is_a_paid_feature:
print("Veo is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
import time
prompt = "A caricatural commercial video for this backpack. The actor puts on the backpack, jumps on his motorbike and starts off with a bang and an epic music" # @param {"type":"string","placeholder":"Write your own prompt or leave empty to let the model decide"}
# Optional parameters
negative_prompt = "ugly, low quality, static, weird physics" # @param {type: "string"}
aspect_ratio = "16:9" # @param ["16:9"] # Only landscape is supported
resolution = "720p" # @param ["720p"] # Only 720p is supported
backpack_reference = types.VideoGenerationReferenceImage(
image=backpack_image,
reference_type="asset"
)
actor_reference = types.VideoGenerationReferenceImage(
image=actor_image,
reference_type="asset"
)
operation = client.models.generate_videos(
model="veo-3.1-generate-preview", # Reference to video only wirk with Veo 3.1 at the moment
prompt=prompt,
config=types.GenerateVideosConfig(
aspect_ratio=aspect_ratio,
resolution=resolution,
negative_prompt=negative_prompt,
reference_images=[backpack_reference, actor_reference],
),
)
# Waiting for the video(s) to be generated
while not operation.done:
time.sleep(20)
operation = client.operations.get(operation)
print(operation)
print(operation.result.generated_videos)
for n, generated_video in enumerate(operation.result.generated_videos):
client.files.download(file=generated_video.video)
generated_video.video.save(f'video{n}.mp4') # Saves the video(s)
display(generated_video.video.show()) # Display the video(s) in a notebook
Step 9: Extend an existing video
Pass a previously generated Veo video back to the API using the video parameter to append additional seconds to the clip. The input must be Veo-generated (not arbitrary uploaded footage) and must be at 720p.
if not I_am_aware_that_veo_is_a_paid_feature:
print("Veo is a paid feature. Please change the variable 'I_am_aware_that_veo_is_a_paid_feature' to True if you are okay with paying to run it.")
else:
import time
prompt = "" # @param {"type":"string","placeholder":"Prompt is optional."}
# Optional parameters
negative_prompt = "ugly, low quality, static, weird physics" # @param {type: "string"}
operation = client.models.generate_videos(
model=VEO_MODEL_ID,
video=generated_video.video,
prompt=prompt,
config=types.GenerateVideosConfig(
number_of_videos=1,
resolution="720p",
negative_prompt=negative_prompt,
),
)
# Waiting for the video(s) to be generated
while not operation.done:
time.sleep(20)
operation = client.operations.get(operation)
print(operation)
print(operation.result.generated_videos)
for n, generated_video in enumerate(operation.result.generated_videos):
client.files.download(file=generated_video.video)
generated_video.video.save(f'video{n}.mp4') # Saves the video(s)
display(generated_video.video.show()) # Display the video(s) in a notebook
What to watch out for
Every call is billable, including failures. If your prompt hits a safety filter or the operation errors partway through, you may still incur a charge. Build explicit error handling around operation.result before accessing generated_videos — treat a missing result as a cost event worth logging.
Polling at 20 seconds is a floor, not a ceiling. In a serverless function or worker process, a blocking while loop ties up the execution context for the entire generation duration. Use webhooks (Step 5) in any architecture where that matters. The event name to subscribe is "video.generated".
Resolution and aspect ratio constraints are model-specific. Requesting 1080p with veo-3.1-generate-preview for reference-to-video will fail — that combination is locked to 720p and 16:9. The API returns an error rather than silently downgrading, so invalid combinations cause a hard failure in your pipeline.
The image parameter requires the bytes round-trip. When using PIL images, you must call im.save(image_bytes_io, format=im.format) before wrapping in types.Image. Passing a PIL image object directly will not work. Also note that im.format is None for images created in memory rather than opened from disk — always open from a saved file or set the format explicitly.
Extension only works on Veo-generated source video. You cannot feed arbitrary MP4 footage from external sources into the extension call. The model validates provenance; passing non-Veo content returns an error.
SynthID watermarking is automatic and non-removable. All Veo output is digitally watermarked using Google DeepMind's SynthID technology. This is transparent to end users viewing the video normally, but it is detectable by analysis tools. Factor this into any deployment context where content authenticity verification matters.
Children are always blocked; adult content requires explicit person_generation configuration. The default policy blocks depictions of children entirely and restricts adult generation. If your pipeline requires human subjects, check the person_generation parameter in the API documentation before building assumptions about what will render.
The last_frame parameter cannot be used without a starting image. The API requires image when last_frame is present; omitting the starting frame raises a validation error.
Where to go next
- Veo documentation and prompt guide: The official prompt guide covers the full vocabulary of cinematographic terms the model understands, including shot composition, lens types, and style directives.
- Webhooks: For any production integration, replacing the polling loop with a webhook subscription is the correct architectural move. The Gemini webhooks documentation covers endpoint verification and event payloads.
- Pricing: Costs are per second of generated video and vary by model tier. Review the current pricing page before designing throughput estimates for your pipeline.
- Gemini image generation: The
gemini-2.5-flash-imagemodel used in Steps 7 and 8 to create reference frames is itself a capable generative tool. Understanding its output quality and cost characteristics is worthwhile if you plan to chain it with Veo in production. - Broader AI pipeline architecture: If you are integrating Veo into a larger agentic workflow, the considerations around structured output, memory, and tool orchestration discussed in structured output for local LLMs are directly applicable to building robust generation pipelines.
Frequently asked questions
Does Google Veo 3.1 have a free tier?
No. Veo is a paid-only feature — every generation call is billed to your Google AI account regardless of whether it succeeds or hits a safety filter. The source notebook gates all generation cells behind an explicit boolean flag to prevent accidental charges.
What is the difference between veo-3.1-lite, veo-3.1-fast, and veo-3.1?
All three support text-to-video at up to 1080p, but only `veo-3.1-generate-preview` supports reference-to-video with asset images. Lite and Fast are optimised for throughput and rapid iteration respectively; the full model is for quality-critical output. Reference-to-video is locked to 16:9 aspect ratio and 720p on the full model.
How do I avoid blocking a server thread while waiting for Veo to finish?
Register a webhook with `client.webhooks.create`, passing your callback URL and `subscribed_events=["video.generated"]`. The Gemini API will POST the operation result to your endpoint when generation completes, so no polling loop is needed in your server process.
Can I use any MP4 as input to Veo's video extension feature?
No. The extension call only accepts Veo-generated video — you cannot feed arbitrary uploaded footage. Passing non-Veo content returns an API error. The input must also be at 720p resolution.
How do I control the audio in a Veo-generated video?
Veo 3.1 generates audio by default based on scene content. To direct it, describe the sound explicitly in your text prompt — for example, naming ambient sounds, music style, or dialogue. Use `negative_prompt` to suppress unwanted audio elements or on-screen captions when generating dialogue.
Why does my image-to-video call fail with a PIL image object?
The API requires raw bytes, not a PIL Image object. You must convert with `io.BytesIO()`, call `im.save(image_bytes_io, format=im.format)`, then pass `image_bytes_io.getvalue()` to `types.Image(image_bytes=..., mime_type=im.format)`. Note that `im.format` is `None` for images created in memory — always open from a saved file on disk.
Related Guides
Reka Rho-1: 19B Model Cuts 13.8s Pipelines to 7.0s and Emits Robot Actions
Reka's 19B Rho-1 handles text, image, video, and robot actions in one context, cutting an illustrative 13.8s pipeline to a measured 7.0s first clip.

Generate Synthetic RAG Test Sets With Ragas
Build, save and reuse a Ragas knowledge graph to generate single-hop and multi-hop evaluation queries from Markdown documents.

Generate and Run a Python Expression with Outlines
Use Outlines and GPT-4o mini to turn a word problem into a Python expression, execute it, and understand the risks of eval.