DEV Community

Bank Gwen
Bank Gwen

Posted on

From Static to Cinematic: Building an Automated Image-to-Video Pipeline with Nano Banana and Veo 3.1

TL;DR — This tutorial shows how to build a two-stage "zero-studio" pipeline: first generate a high-fidelity static image using Gemini 3.1 Flash Image (community nickname: Nano Banana), then animate it with an AI video generator from image (Veo 3.1 or Gemini Omni Flash Preview). Includes working Python code, cost estimates, and failure handling.


Why a Two-Stage Pipeline?

Direct text-to-video models often struggle with:

  • Precise text rendering (e.g., product labels, UI mockups)
  • Spatial consistency across frames
  • Complex scene composition (lighting, reflections, depth)

By splitting the workflow:

  1. Stage 1 (Anchor): Use Nano Banana 2.0 (gemini-3.1-flash-image) to generate a structurally accurate, high-resolution image with exact text and stable composition.
  2. Stage 2 (Motion): Feed that image into an ai video generator from image (Veo 3.1 or Gemini Omni Flash Preview) to apply camera motion, lighting changes, or particle effects.

This approach trades a bit of latency for significantly higher control and consistency—especially useful for product demos, ad creatives, or UI animations.


Architecture Overview

[ Text Prompt ]
│
▼
┌─────────────────────────┐
│ Nano Banana             │
│ (gemini-3.1-flash-image)│
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Generated Image (PNG)   │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ai video generator      │
│ from image              │
│ (Veo 3.1 / Omni Flash)  │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Cinematic Video (.mp4)  │
│ 5–10 sec, 720p/1080p    │
└─────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Prerequisites

pip install google-genai pillow
export GEMINI_API_KEY="your_api_key_here"
Enter fullscreen mode Exit fullscreen mode

Note: As of August 2026:

  • gemini-3.1-flash-image is GA.
  • veo-3.1-generate-preview and gemini-omni-flash-preview are in preview (max 10 sec, 720p).
  • Veo 3.0 models were deprecated on 2026-06-30.

Step-by-Step Implementation

1. Generate Static Asset with Nano Banana

import os
from google import genai
from google.genai import types
from PIL import Image

client = genai.Client()

def generate_static_asset(prompt: str, output_path: str = "base_image.png"):
    """
    Generates a high-fidelity static image using Gemini 3.1 Flash Image.
    Community nickname: Nano Banana.
    Model: gemini-3.1-flash-image
    """
    print(f"Generating image with Nano Banana for: '{prompt}'...")

    response = client.models.generate_content(
        model="gemini-3.1-flash-image",
        contents=[prompt],
        config=types.GenerateContentConfig(
            response_modalities=["IMAGE"],
        ),
    )

    for part in response.candidates[0].content.parts:
        if part.inline_data is not None:
            image = Image.from_bytes(part.inline_data.data)
            image.save(output_path)
            print(f"✓ Base image saved to {output_path}")
            return output_path

    raise ValueError("Failed to generate image or retrieve inline data.")
Enter fullscreen mode Exit fullscreen mode

Tip: For consistent branding across multiple shots, pass up to 7 reference images via config.reference_images (Veo 3.1) or use Nano Banana's multi-image composition. openpaths


2. Animate with Veo 3.1 (Recommended) or Gemini Omni Flash

import time
from google.genai import types

def generate_video_from_image(
    image_path: str,
    motion_prompt: str,
    output_video_path: str = "output_cinematic.mp4",
    model: str = "veo-3.1-generate-preview"  # or "gemini-omni-flash-preview"
):

    Feeds the static image into an AI video generator from image.
    Uses async polling for production readiness.

    print(f"Animating image with motion prompt: '{motion_prompt}'...")

    # Load local image
    image = types.Image.from_file(image_path)

    # Submit async video generation job
    operation = client.models.generate_videos(
        model=model,
        prompt=motion_prompt,
        image=image,
        config=types.GenerateVideosConfig(
            duration_seconds=5,
            aspect_ratio="16:9",
            number_of_videos=1,
            enhance_prompt=True,
        ),
    )

    print(f"Video job submitted. Polling for completion...")

    # Poll until done
    while not operation.done:
        time.sleep(5)
        operation = client.operations.get(operation)

    if operation.response:
        video_uri = operation.result.generated_videos[0].video.uri
        print(f"✓ Video generated: {video_uri}")

        # Download if GCS URI
        if video_uri.startswith("gs://"):
            from google.cloud import storage
            client_gcs = storage.Client()
            bucket_name, blob_name = video_uri.replace("gs://", "").split("/", 1)
            bucket = client_gcs.bucket(bucket_name)
            blob = bucket.blob(blob_name)
            blob.download_to_filename(output_video_path)
            print(f"✓ Video saved to {output_video_path}")
        else:
            # For Omni Flash (direct URL)
            import requests
            resp = requests.get(video_uri)
            with open(output_video_path, "wb") as f:
                f.write(resp.content)
            print(f"✓ Video saved to {output_video_path}")
    else:
        raise RuntimeError(f"Video generation failed: {operation.error}")
Enter fullscreen mode Exit fullscreen mode

3. Full Pipeline Example

if __name__ == "__main__":
    base_prompt = (
        "A futuristic smart watch floating slightly above a sleek dark marble surface. "
        "The watch screen is glowing blue and displays legible text 'VIBE 2026'. "
        "Cyberpunk ambient lighting with neon reflection."
    )

    motion_prompt = (
        "A slow, steady orbital pan tracking right. Subtle dust particles in the air, "
        "natural light refraction across the metal casing of the watch."
    )

    try:
        img_path = generate_static_asset(base_prompt)
        generate_video_from_image(
            img_path,
            motion_prompt,
            model="veo-3.1-generate-preview"  # or "gemini-omni-flash-preview"
        )
    except Exception as e:
        print(f"An error occurred: {e}")
Enter fullscreen mode Exit fullscreen mode

Cost & Limits (as of August 2026)

Model Status Max Duration Resolution Est. Cost
gemini-3.1-flash-image GA N/A Up to 4K ~$0.02–0.05/image
veo-3.1-generate-preview Preview 10 sec 720p/1080p ~$0.11/sec
gemini-omni-flash-preview Preview 10 sec 720p ~$0.11/sec

Common Pitfalls & How to Avoid Them

  • Aspect Ratio Mismatch: Ensure your Nano Banana image uses the same aspect ratio (e.g., 16:9) as your video config to avoid cropping.
  • Prompt Vagueness: Avoid "make it cool". Use director-level terms: "slow handheld drift", "lens flare from top-left", "dust particles in volumetric light".
  • Async Timeouts: Add a max retry count (e.g., 60 polls × 5 sec = 5 min) to avoid infinite loops.
  • GCS Permissions: If using Veo, ensure your service account has storage.objects.get on the output bucket.

What's Next?

  • Batch Generation: Wrap the pipeline in a loop to test 10–20 prompt variations in parallel.
  • A/B Testing: Compare "Nano Banana + Veo" vs. "Veo direct text-to-video" for consistency and cost.
  • Audio Sync: Use Gemini Omni Flash's audio generation (preview) to add background music or SFX.

Join the Discussion

Have you tried this pipeline? What's your experience with Nano Banana for image generation or ai video generator from image workflows? Share your results, failures, or alternative architectures in the comments! 👇

Top comments (0)