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.
Related Guides

Self-Consistency Voting with Outlines and gpt-4o-mini
Generate ten reasoning chains in one API call, extract integer answers with regex, and vote for the majority—reliably solving multi-step arithmetic.
How to Remove Claude Watermarks from Text, Code, and Files
A practical guide to handling Claude watermarks across prose, Python code, and C2PA-marked files, with rewrite and inspection code.

AI Web Scraping in Python: When an LLM Earns Its Cost
An LLM can read any page without a selector, and it bills you every time. Here is the decision rule, the Crawl4AI code for both paths, and the hybrid that pays for a model once and then runs free.