Generate Video with Google Veo 3.1 and the Gemini Python SDK

August 12, 2026guides

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-genai SDK 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-image model 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