Tensor LabsTENSORLABS

Build a Bulk Product-Image Generation Service with Google Nano Banana 2 Lit

On June 30, 2026, Google released Nano Banana 2 Lite, an image generation model that produces a finished image in about 4 seconds and costs $0.034 per 1,000 images.

July 13, 20268 min read12 sectionsBy Ahmed Abdullah
Build a Bulk Product-Image Generation Service with Google Nano Banana 2 Lit

Introduction

On June 30, 2026, Google released Nano Banana 2 Lite, an image generation model that produces a finished image in about 4 seconds and costs $0.034 per 1,000 images. In this tutorial you will build a batch service around it: a FastAPI app that takes product catalog rows and returns a full set of listing images per SKU, with an aspect-ratio matrix, a concurrency-controlled worker, and a budget guard that stops a runaway batch before it starts. At this price the interesting engineering problem is no longer the cost of generation. It is keeping a pipeline organized when generation is effectively free.

What Nano Banana 2 Lite Is

Nano Banana 2 Lite is the marketing name for Gemini 3.1 Flash-Lite Image, the fastest and cheapest member of Google's Gemini image family. Its API model ID is gemini-3.1-flash-lite-image, and it is available through Google AI Studio and the Gemini API. Three facts define where it fits. It only outputs 1K resolution, across 14 aspect ratios. It handles text-to-image, image editing, and multi-image composition through the same generate_content call. And every output carries an invisible SynthID watermark identifying it as AI-generated.

The bigger sibling, Nano Banana 2 (Gemini 3.1 Flash Image, released February 2026), generates up to 4K with stronger text rendering. Lite trades that ceiling for velocity: roughly 4 seconds per image and per-thousand pricing. That trade is exactly right for high-volume catalog work, where you generate twenty candidates and a human keeps three.

How Image Output Works in the Gemini API

The Gemini API returns images as parts inside a normal content response. You send a prompt with google-genai, the official Python SDK, and read inline_data bytes back from the response parts. There is no separate image endpoint, no job to poll for a single image, and no URL that expires. The bytes arrive in the response, and what you do with them is your problem, which is precisely what a pipeline is for.

Generate the First Product Image

Install the SDK and confirm the model responds. You need a Gemini API key from Google AI Studio in the GEMINI_API_KEY environment variable.

code
pip install google-genai fastapi uvicorn
export GEMINI_API_KEY="your-key-here"
python
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-3.1-flash-lite-image",
contents="Studio product photo of a matte black ceramic pour-over coffee dripper "
"on a light oak table, soft morning light, shallow depth of field",
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(aspect_ratio="1:1"),
),
)
for part in response.candidates[0].content.parts:
if part.inline_data is not None:
with open("dripper-1x1.png", "wb") as f:
f.write(part.inline_data.data)
print("saved dripper-1x1.png")

Run it. About four seconds later there is a PNG on disk. That latency number matters for everything that follows: at 4 seconds per image, a 500-image batch is 33 minutes sequential and about 4 minutes with eight workers.

Turn a Catalog Row into a Prompt

Bulk generation lives or dies on prompt consistency. A human typing prompts produces drift; a template does not. Build the prompt from structured catalog fields so every SKU gets the same photographic treatment.

python
def build_prompt(product: dict, scene: str) -> str:
scenes = {
"studio": "clean studio product photo, seamless white background, softbox lighting",
"lifestyle": "in-context lifestyle photo, natural setting matching the product's use, warm light",
"banner": "wide promotional banner composition with generous negative space on the left",
}
return (
f"{scenes[scene]}. Product: {product['name']}, "
f"{product['material']}, {product['color']}. "
f"Category: {product['category']}. No text, no logos, no people."
)
product = {
"sku": "KD-2201",
"name": "pour-over coffee dripper",
"material": "matte ceramic",
"color": "black",
"category": "kitchen",
}
print(build_prompt(product, "studio"))

The "no text, no logos" instruction is not decoration. Lite's text rendering is weaker than full Nano Banana 2, so the reliable move is to keep type out of the pixels and overlay copy downstream where it stays editable.

Generate the Variant Matrix

Each sales channel wants a different crop: square for the listing grid, 4:5 for social, 16:9 for the storefront banner. Nano Banana 2 Lite supports 14 aspect ratios natively, so generate at the target ratio instead of cropping a square and hoping the composition survives.

python
CHANNEL_RATIOS = {"listing": "1:1", "social": "4:5", "banner": "16:9"}
def generate_variant(product: dict, scene: str, channel: str) -> bytes:
response = client.models.generate_content(
model="gemini-3.1-flash-lite-image",
contents=build_prompt(product, scene),
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(aspect_ratio=CHANNEL_RATIOS[channel]),
),
)
for part in response.candidates[0].content.parts:
if part.inline_data is not None:
return part.inline_data.data
raise RuntimeError(f"no image returned for {product['sku']}/{channel}")

One product times three scenes times three channels is nine images per SKU. That multiplication is where per-thousand pricing stops being a curiosity and starts being an architecture decision.

Batch the Catalog with a Worker Pool

The SDK call is synchronous, so a thread pool is the honest concurrency model. Eight workers keeps you well inside preview rate limits while cutting wall-clock time by roughly 8x.

python
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
OUT = Path("generated")
OUT.mkdir(exist_ok=True)
def process_sku(product: dict) -> list[str]:
saved = []
for scene in ("studio", "lifestyle", "banner"):
for channel, _ in CHANNEL_RATIOS.items():
data = generate_variant(product, scene, channel)
path = OUT / f"{product['sku']}-{scene}-{channel}.png"
path.write_bytes(data)
saved.append(str(path))
return saved
def run_batch(catalog: list[dict], workers: int = 8) -> dict:
results, errors = {}, {}
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(process_sku, p): p["sku"] for p in catalog}
for future in as_completed(futures):
sku = futures[future]
try:
results[sku] = future.result()
except Exception as exc:
errors[sku] = str(exc)
return {"ok": results, "failed": errors}

Failures land in a dict instead of killing the batch. With generation this cheap, retrying a failed SKU costs less than a log line, so the error-handling posture is collect and re-run, not abort.

Add the Cost Meter and a Budget Guard

At $0.034 per 1,000 images, cost per image is $0.000034. Nobody bankrupts themselves on Lite generation. What actually explodes is review load, and the budget guard doubles as a sanity check on how many images you are about to ask a human to look at. (Four seconds is also about how long an art director spends on each one before rejecting it.)

python
PRICE_PER_IMAGE = 0.034 / 1000
MAX_BATCH_USD = 0.50 # also caps review load: ~14,700 images
def preflight(catalog: list[dict]) -> dict:
images = len(catalog) * 3 * len(CHANNEL_RATIOS)
cost = images * PRICE_PER_IMAGE
if cost > MAX_BATCH_USD:
raise ValueError(f"batch of {images} images (${cost:.4f}) exceeds cap ${MAX_BATCH_USD}")
return {"images": images, "estimated_usd": round(cost, 6)}

Nano Banana 2 Lite vs Nano Banana 2

| | Nano Banana 2 Lite | Nano Banana 2 | |---|---|---| | API model ID | gemini-3.1-flash-lite-image | gemini-3.1-flash-image | | Released | June 30, 2026 | February 2026 | | Max resolution | 1K only | Up to 4K | | Latency | ~4 seconds | Slower, quality-first | | Pricing | $0.034 per 1,000 images | Premium per-image tier | | Text rendering | Weak, avoid in-image type | Strong | | Aspect ratios | 14 | 14 | | Best for | High-volume variants, drafts, backgrounds | Hero images, 4K, in-image text |

The versus question answers itself by asking what the image is for. Grid thumbnails and A/B ad variants are Lite work. The homepage hero is not.

Limitations

The 1K ceiling rules out print and anything a designer will zoom into. Text inside images is unreliable, so overlay copy in your frontend instead. Generated images of a physical product are approximations, and if the image is the buying decision, as in apparel or jewelry, shipping a generated approximation as the product photo is lying to your customer; use Lite's image-editing mode on real photography for those, and keep pure generation for backgrounds, staging, and category art. Every output carries a SynthID watermark, so assume AI detection is possible and disclose accordingly. And the model is new enough that rate limits and quotas may shift under you; the worker-pool cap is your throttle, and HTTP 429 responses are the signal to lower it.

The Final Working Example

The full service, assembled and runnable end to end:

python
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from fastapi import FastAPI, HTTPException
from google import genai
from google.genai import types
from pydantic import BaseModel
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
app = FastAPI(title="nb2-lite-batch")
OUT = Path("generated")
OUT.mkdir(exist_ok=True)
MODEL = "gemini-3.1-flash-lite-image"
CHANNEL_RATIOS = {"listing": "1:1", "social": "4:5", "banner": "16:9"}
SCENES = {
"studio": "clean studio product photo, seamless white background, softbox lighting",
"lifestyle": "in-context lifestyle photo, natural setting matching the product's use, warm light",
"banner": "wide promotional banner composition with generous negative space on the left",
}
PRICE_PER_IMAGE = 0.034 / 1000
MAX_BATCH_USD = 0.50
class Product(BaseModel):
sku: str
name: str
material: str
color: str
category: str
def build_prompt(p: Product, scene: str) -> str:
return (f"{SCENES[scene]}. Product: {p.name}, {p.material}, {p.color}. "
f"Category: {p.category}. No text, no logos, no people.")
def generate_variant(p: Product, scene: str, channel: str) -> bytes:
response = client.models.generate_content(
model=MODEL,
contents=build_prompt(p, scene),
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(aspect_ratio=CHANNEL_RATIOS[channel]),
),
)
for part in response.candidates[0].content.parts:
if part.inline_data is not None:
return part.inline_data.data
raise RuntimeError(f"no image for {p.sku}/{scene}/{channel}")
def process_sku(p: Product) -> list[str]:
saved = []
for scene in SCENES:
for channel in CHANNEL_RATIOS:
path = OUT / f"{p.sku}-{scene}-{channel}.png"
path.write_bytes(generate_variant(p, scene, channel))
saved.append(str(path))
return saved
@app.post("/batches")
def run_batch(catalog: list[Product]):
images = len(catalog) * len(SCENES) * len(CHANNEL_RATIOS)
cost = images * PRICE_PER_IMAGE
if cost > MAX_BATCH_USD:
raise HTTPException(400, f"{images} images (${cost:.4f}) exceeds ${MAX_BATCH_USD} cap")
results, errors = {}, {}
with ThreadPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(process_sku, p): p.sku for p in catalog}
for future in as_completed(futures):
sku = futures[future]
try:
results[sku] = future.result()
except Exception as exc:
errors[sku] = str(exc)
return {"generated": results, "failed": errors,
"images": images, "estimated_usd": round(cost, 6)}

Start it with uvicorn app:app --reload, POST a JSON list of products to /batches, and collect the PNGs from generated/.

When to Use It

Pick Nano Banana 2 Lite whenever volume beats ceiling: marketplace listings, ad variant testing, category art, placeholder and concept imagery, seasonal re-skins of existing scenes. The per-thousand pricing means the constraint has moved from generation cost to curation capacity, so invest your next engineering hour in the review queue, not the generator. When a single image has to carry the brand, step up to full Nano Banana 2 and its 4K output. The models will keep leapfrogging each other; the batch scaffold you built here is the part that survives the next release.