Migrating from Sora or DALL-E? Use promo code DALLE1000 for $10 in free API credits!
Restaurant Tutorial

Bulk Menu Image Generation from CSV

Transform your entire restaurant menu into AI-generated food photography in minutes. This script reads a CSV of menu items and generates appetizing images using Seedream V1.5 with automatic negative prompts for realistic results.

~2 min
per menu item
4 concurrent
parallel requests
CSV input
easy spreadsheet
Auto retry
built-in resilience
1

Prepare Your Menu CSV

Create a CSV file with your menu items. The script expects these columns:

ColumnRequiredDescription
item_nameYesName of the dish
categoryYesEntree, Sides, Dessert, etc.
descriptionYesDetailed description for prompt
styleOptionalCustom photography style

Example CSV:

csv
item_name,category,description,style
Artisan Burger,Entree,Gourmet smash burger with aged cheddar and caramelized onions on brioche,casual dining overhead shot
Truffle Fries,Sides,Crispy fries drizzled with truffle oil and parmesan,rustic wooden board
Grilled Salmon,Entree,Atlantic salmon with lemon butter sauce and seasonal vegetables,fine dining plating
Caesar Salad,Starter,Crisp romaine with house-made dressing and shaved parmesan,bright natural lighting
Chocolate Lava Cake,Dessert,Warm chocolate cake with molten center and vanilla ice cream,elegant plating dark background
2

Customize Your Brand Template

Edit the BRAND_TEMPLATE in the script to match your restaurant's visual identity. This ensures all images have consistent branding.

Template Options:

lighting

warm golden side lighting, bright natural light, dramatic moody lighting

angle

overhead shot, 45-degree angle, close-up detail shot

background

dark slate surface, rustic wooden table, white seamless, marble counter

plate

ceramic plate, rustic board, slate platter, minimalist white dish

3

Run the Generation Script

Install dependencies and run:

pip install openai httpx
export CREATIVEAI_API_KEY=your_key_here
python menu_batch_generator.py menu.csv

Full Script:

python
#!/usr/bin/env python3
"""
Bulk Restaurant Menu Image Generator
Transforms a CSV of menu items into AI-generated food photography.

Requirements:
  pip install openai httpx

Usage:
  export CREATIVEAI_API_KEY=your_key_here
  python menu_batch_generator.py menu.csv
"""

import asyncio
import csv
import json
import os
import sys
from dataclasses import dataclass
from datetime import datetime

import httpx

# Configuration
API_KEY = os.environ.get("CREATIVEAI_API_KEY", "")
BASE_URL = "https://api.creativeai.run/v1/images/generations"
MODEL = "seedream-v1.5"  # Best for food photography
MAX_CONCURRENCY = 4  # Parallel requests
OUTPUT_DIR = "menu_images"

# Food photography defaults
DEFAULT_STYLE = "professional food photography, appetizing, warm lighting, shallow depth of field"
DEFAULT_NEGATIVE = "plastic, artificial, cartoon, illustration, text, watermark, blurry, oversaturated, neon colors"

# Brand template - customize for your restaurant
BRAND_TEMPLATE = {
    "lighting": "warm golden side lighting",
    "angle": "45-degree angle",
    "background": "dark slate surface",
    "plate": "ceramic plate with subtle texture",
    "garnish_style": "fresh herbs, minimal garnish",
}


@dataclass
class MenuItem:
    item_name: str
    category: str
    description: str
    style: str = ""


def build_food_prompt(item: MenuItem) -> str:
    """Build a detailed food photography prompt."""
    parts = [
        f"Appetizing photo of {item.item_name}",
        item.description,
        item.style or DEFAULT_STYLE,
        BRAND_TEMPLATE["lighting"],
        BRAND_TEMPLATE["angle"],
        f"on {BRAND_TEMPLATE['background']}",
        f"served on {BRAND_TEMPLATE['plate']}",
        BRAND_TEMPLATE["garnish_style"],
        "photorealistic, high detail, commercial food photography",
    ]
    return ", ".join(parts)


async def generate_menu_image(
    client: httpx.AsyncClient,
    semaphore: asyncio.Semaphore,
    item: MenuItem,
    index: int
) -> dict:
    """Generate a single menu image with retry logic."""
    prompt = build_food_prompt(item)
    
    payload = {
        "model": MODEL,
        "prompt": prompt,
        "negative_prompt": DEFAULT_NEGATIVE,
        "size": "1024x1024",
        "n": 1,
    }
    
    async with semaphore:
        for attempt in range(3):  # Max 3 retries
            try:
                resp = await client.post(BASE_URL, json=payload)
                if resp.status_code == 429:  # Rate limited
                    await asyncio.sleep(2 ** attempt)
                    continue
                resp.raise_for_status()
                data = resp.json()
                
                if data.get("data") and data["data"][0].get("url"):
                    return {
                        "item_name": item.item_name,
                        "category": item.category,
                        "status": "success",
                        "image_url": data["data"][0]["url"],
                        "prompt_used": prompt,
                        "index": index,
                    }
                
                return {
                    "item_name": item.item_name,
                    "status": "failed",
                    "error": "No image URL in response",
                    "index": index,
                }
                
            except Exception as e:
                if attempt == 2:
                    return {
                        "item_name": item.item_name,
                        "status": "failed",
                        "error": str(e),
                        "index": index,
                    }
                await asyncio.sleep(1.5 ** (attempt + 1))
    
    return {
        "item_name": item.item_name,
        "status": "failed",
        "error": "Max retries exceeded",
        "index": index,
    }


def load_menu_csv(path: str) -> list[MenuItem]:
    """Load menu items from CSV file."""
    items = []
    with open(path, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            items.append(MenuItem(
                item_name=row.get("item_name", "").strip(),
                category=row.get("category", "").strip(),
                description=row.get("description", "").strip(),
                style=row.get("style", "").strip(),
            ))
    return [i for i in items if i.item_name]  # Filter empty rows


async def generate_all_images(items: list[MenuItem]) -> list[dict]:
    """Generate images for all menu items with controlled concurrency."""
    semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    
    async with httpx.AsyncClient(headers=headers, timeout=60.0) as client:
        tasks = [
            generate_menu_image(client, semaphore, item, idx)
            for idx, item in enumerate(items)
        ]
        return await asyncio.gather(*tasks)


async def main():
    if not API_KEY:
        print("Error: Set CREATIVEAI_API_KEY environment variable")
        sys.exit(1)
    
    if len(sys.argv) < 2:
        print("Usage: python menu_batch_generator.py <menu.csv>")
        sys.exit(1)
    
    csv_path = sys.argv[1]
    print(f"Loading menu from {csv_path}...")
    
    items = load_menu_csv(csv_path)
    print(f"Found {len(items)} menu items")
    
    if not items:
        print("No valid menu items found in CSV")
        sys.exit(1)
    
    print(f"Generating images with {MODEL}...")
    start_time = datetime.now()
    
    results = await generate_all_images(items)
    
    # Create output directory
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    
    # Save results
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_file = os.path.join(OUTPUT_DIR, f"menu_images_{timestamp}.json")
    
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump({
            "generated_at": datetime.now().isoformat(),
            "model": MODEL,
            "total_items": len(items),
            "results": results,
        }, f, indent=2)
    
    # Summary
    success = sum(1 for r in results if r["status"] == "success")
    failed = sum(1 for r in results if r["status"] == "failed")
    elapsed = (datetime.now() - start_time).total_seconds()
    
    print(f"\n{'='*50}")
    print(f"Generation Complete!")
    print(f"{'='*50}")
    print(f"Success: {success}/{len(items)} images")
    print(f"Failed: {failed}")
    print(f"Time: {elapsed:.1f} seconds")
    print(f"Output: {output_file}")
    
    if success > 0:
        print(f"\nGenerated Image URLs:")
        for r in results:
            if r["status"] == "success":
                print(f"  [{r['category']}] {r['item_name']}: {r['image_url']}")


if __name__ == "__main__":
    asyncio.run(main()))

Quick Test (Single Image)

Test the API with a single dish before running the full batch:

python
# Quick test with 3 menu items
import os
from openai import OpenAI

os.environ["CREATIVEAI_API_KEY"] = "your_key_here"

client = OpenAI(
    api_key=os.environ["CREATIVEAI_API_KEY"],
    base_url="https://api.creativeai.run/v1"
)

# Test with a single dish
result = client.images.generate(
    model="seedream-v1.5",
    prompt="""Artisan Burger with aged cheddar, caramelized onions,
    on brioche bun, professional food photography, warm golden
    lighting, 45-degree angle, appetizing, photorealistic""",
    negative_prompt="plastic, artificial, cartoon, text, watermark",
    size="1024x1024",
    n=1
)

print(f"Image URL: {result.data[0].url}")

Output Format

The script saves results to menu_images/menu_images_YYYYMMDD_HHMMSS.json:

{
  "generated_at": "2026-03-16T15:30:00",
  "model": "seedream-v1.5",
  "total_items": 5,
  "results": [
    {
      "item_name": "Artisan Burger",
      "category": "Entree",
      "status": "success",
      "image_url": "https://cdn.creativeai.run/...",
      "prompt_used": "Appetizing photo of Artisan Burger..."
    }
  ]
}

Next Steps

  • β€’ Download images and upload to your POS or delivery app
  • β€’ Use the URLs directly in your website or ordering system
  • β€’ Generate promo videos from top images using the video API
  • β€’ Integrate into n8n/Make/Zapier for automated workflows

Ready to Automate Your Menu Photography?

Get 50 free credits to test the API. No credit card required.