Skill: teebs4140/gemini-image (version 1.0.0)

--- name: gemini-image description: Generate or edit images using Gemini API with multiple reference image support. Use for image generation, style transfer, combining references, UI mockups, or any visual creation task. homepage: https://ai.google.dev/gemini-api/docs/image-generation metadata: {"clawdbot":{"emoji":"🎨","requires":{"bins":["uv"],"env":["GEMINI_API_KEY"]},"primaryEnv":"GEMINI_API_KEY","install":[{"id":"uv-brew","kind":"brew","formula":"uv","bins":["uv"],"label":"Install uv (brew)"}]}} --- # Gemini Image Generation Generate images with support for multiple reference images (up to 14). ## Setup **Required:** `GEMINI_API_KEY` environment variable Get your API key at: https://aistudio.google.com/apikey Set it: ```bash export GEMINI_API_KEY="your-key-here" ``` Or in your OpenClaw config (`~/.openclaw/openclaw.json`): ```json { "skills": { "entries": { "gemini-image": { "env": { "GEMINI_API_KEY": "your-key-here" } } } } } ``` ## Usage ```bash # Generate from prompt uv run {baseDir}/scripts/generate.py -p "a sunset over mountains" -o sunset.png # Edit/transform one image uv run {baseDir}/scripts/generate.py -p "make this pop art style" -i photo.png -o popart.png # Combine multiple references (up to 14) uv run {baseDir}/scripts/generate.py -p "combine style of first with content of second" \ -i style-ref.png -i content.png -o combined.png # With aspect ratio uv run {baseDir}/scripts/generate.py -p "wide landscape" -a "16:9" -o wide.png # Higher resolution uv run {baseDir}/scripts/generate.py -p "detailed portrait" -r 2K -o portrait.png ``` ## Options | Flag | Description | |------|-------------| | `-p, --prompt` | Image prompt (required) | | `-o, --output` | Output filename (required) | | `-i, --input-image` | Reference image (repeatable, up to 14) | | `-m, --model` | `pro` (default), `flash2`, `flash`, or `exp` | | `-r, --resolution` | `1K` (default), `512`, `2K`, `4K` (pro/flash2) | | `-a, --aspect-ratio` | See ratios below (pro/flash2) | | `-t, --thinking` | `minimal` (default), `high`, `dynamic` (pro/flash2) | ## Models - **pro** (`gemini-3-pro-image-preview`) — Highest quality, thinking mode, up to 14 refs, aspect ratio + resolution - **flash2** (`gemini-3.1-flash-image-preview`) — **NEW: Nano Banana 2.** Best price/perf, thinking levels, great text rendering, localization, aspect ratio + resolution + 512px - **flash** (`gemini-2.5-flash-image`) — Older Flash, simpler config, no aspect ratio/resolution control - **exp** (`gemini-2.0-flash-exp`) — Experimental, good for edits **When to use flash2 vs pro:** - **flash2** — Fast iterations, text-heavy images, localization, bulk generation, great quality at lower cost - **pro** — Maximum quality, complex multi-reference compositions, highest fidelity ## Aspect Ratios (pro/flash2) `1:1` · `2:3` · `3:2` · `3:4` · `4:3` · `4:5` · `5:4` · `9:16` · `16:9` · `21:9` · `4:1` · `1:4` · `8:1` · `1:8` ## Thinking Levels (pro/flash2) Control how much the model reasons before generating: - **minimal** (default) — Fast, good for simple prompts - **high** — Better quality for complex multi-element prompts - **dynamic** — Model decides how much thinking is needed ```bash # Complex scene with high thinking uv run {baseDir}/scripts/generate.py -p "detailed city scene with specific text overlay" -m flash2 -t high -o city.png ``` ## Reference Image Limits | Model | Max Images | Notes | |-------|------------|-------| | **pro** | 14 total | 5 high-fidelity, 6 objects, 5 humans | | **flash** | 3 | Best with ≤3 inputs | | **exp** | varies | Experimental | ## Batch Generation (50% cheaper) For bulk jobs that can wait 24hr, use the batch API. ### "Batch Submit" Workflow When user says **"batch submit"**, follow this automated workflow: 1. **Submit batch job** — Create JSONL, run `batch.py submit` 2. **Create hourly cron** — Check batch status every hour 3. **On completion** — Download images to specified output directory 4. **Deliver to originating channel** — Send all images to the channel where the request was made (Telegram, Discord, Signal, etc.) 5. **Self-disable cron** — Remove the cron job after successful delivery **Cron job text template:** ``` Check gemini-image batch [BATCH_ID]. If complete: download to [OUTPUT_DIR], send all images to [CHANNEL], then disable this cron job. ``` ### Manual Batch Commands **1. Create requests JSONL** (one per line): ```json {"key": "sunset", "prompt": "a sunset over mountains", "aspect_ratio": "16:9", "resolution": "2K"} {"key": "portrait", "prompt": "corporate headshot", "input_images": ["/path/to/ref.png"]} ``` **2. Submit batch:** ```bash uv run {baseDir}/scripts/batch.py submit requests.jsonl ``` **3. Check status** (typically ready within 24hr): ```bash uv run {baseDir}/scripts/batch.py status ``` **4. Download when done:** ```bash uv run {baseDir}/scripts/batch.py download -o ./images/ ``` **When to use batch vs instant:** - Need it now → `generate.py` (instant, full price) - Bulk generation, can wait → `batch.py` (24hr, 50% off) ## Prompting Guide See [references/prompting.md](references/prompting.md) for detailed strategies: **Generation:** - Photorealistic scenes (camera/lens/lighting terms) - Stylized illustrations & stickers (transparent backgrounds) - Text rendering (logos, typography) - Product mockups (commercial photography) - Minimalist/negative space design - Sequential art (comics, storyboards) - Google Search grounding (real-time info) - iOS wireframes & UI mockups **Editing:** - Adding/removing elements - Inpainting (semantic masking) - Style transfer - Combining multiple images - High-fidelity detail preservation - Sketch → finished image - Character consistency (360 views) **Core principle:** Describe the scene narratively, don't just list keywords. ## Tips - Images go first in context, prompt last — order `-i` flags intentionally - For style transfer: first image = style reference, second = content - Pro model uses "Thinking" phase to plan composition before generating - Timestamps in filenames: `2026-01-26-sunset.png` - Script outputs `MEDIA:` line for Clawdbot auto-attach ## Draft Mode (Preview Before Generate) For complex generations — especially UI mockups and wireframes — use a two-step workflow: ### Step 1: Draft the JSON Prompt When user asks for a wireframe or complex image, **show the structured prompt first** instead of generating immediately: ``` Here's the prompt I'll use: { "image_type": "UI mockup", "device": {"frame": "iPhone 16 Pro", "orientation": "portrait"}, "design_system": { "style": "iOS 18 native", "corners": "rounded, 16px radius", "shadows": "soft drop shadows", "spacing": "8pt grid", "font": "SF Pro" }, "layout": { "header": "Navigation bar with back button and title 'Settings'", "content": "List of settings items with icons and toggles", "bottom": "Tab bar with 4 items" } } Want me to generate this, or should I adjust anything first? ``` ### Step 2: User Approves or Tweaks - **"looks good"** / **"generate"** → Run the generation - **"make the header bigger"** / **"add a search bar"** → Update JSON, show again - **"use flash model"** → Note the model preference ### Step 3: Generate Convert JSON to narrative prompt and run: ```bash uv run {baseDir}/scripts/generate.py \ -p "[narrative version of JSON]" \ -o mockup.png \ -a "9:16" \ -m pro ``` ### When to Use Draft Mode | Use Draft Mode | Skip Draft (Generate Direct) | |----------------|------------------------------| | UI mockups / wireframes (new) | Simple images ("sunset over mountains") | | Complex multi-element scenes | Style transfer with clear reference | | User says "let me see the prompt first" | User says "just make it" | | Previous generation failed/missed the mark | Simple edits (color swap, minor tweaks) | | **Structural changes to existing image** | — | **Trigger phrases for draft mode:** - "wireframe", "mockup", "UI", "screen design" - "let me review the prompt" - "show me what you'll generate" - "draft first" **Edit iterations:** - **Simple tweaks** (color change, font swap) → Generate directly, no approval needed - **Structural changes** (new elements, layout changes, removing sections) → Show change summary for approval ## iOS Wireframes Quick example: ```bash uv run {baseDir}/scripts/generate.py \ -p "Convert this wireframe to high-fidelity iOS 18 UI. iPhone 16 Pro frame, rounded corners, soft shadows, SF Pro font. Interpret: scribbles→images, rectangles→buttons, lines→text" \ -i wireframe-sketch.png \ -o mockup.png \ -a "9:16" ``` See [references/prompting.md](references/prompting.md) for detailed templates including JSON-structured prompts and multi-screen flows.

path: README.md
content: # 🎨 Gemini Image

An [OpenClaw](https://github.com/openclaw/openclaw) skill for generating and editing images with Google's Gemini API. Supports multiple reference images (up to 14), batch generation at 50% off, thinking levels, and 4 model tiers.

## Features

- **4 models** — Pro (highest quality), Flash 2 / Nano Banana 2 (best price/perf), Flash (simple), Exp (experimental)
- **Up to 14 reference images** — style transfer, multi-ref composition, character consistency
- **Batch API** — submit bulk jobs at 50% cost, results within 24hr
- **Thinking levels** — minimal, high, dynamic — control quality vs speed
- **Aspect ratios** — 14 options from 1:1 to 8:1
- **Resolution control** — 512px to 4K
- **Draft mode** — preview complex prompts before generating
- **Zero dependencies** beyond `uv` and a Gemini API key

## Install

### As an OpenClaw Skill

```bash
git clone https://github.com/teebs4140/gemini-image.git
cd gemini-image
./install-skill.sh
```

Then add your API key to your OpenClaw config (`~/.openclaw/openclaw.json`):

```json
{
  "skills": {
    "entries": {
      "gemini-image": {
        "env": {
          "GEMINI_API_KEY": "your-key-here"
        }
      }
    }
  }
}
```

Restart your gateway (`openclaw gateway restart`) and the skill is live.

### Standalone (No OpenClaw)

Just clone and run the scripts directly with `uv`:

```bash
git clone https://github.com/teebs4140/gemini-image.git
cd gemini-image
export GEMINI_API_KEY="your-key"
uv run scripts/generate.py -p "a sunset over mountains" -o sunset.png
```

## Requirements

1. A Gemini API key (free) — [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
2. [uv](https://docs.astral.sh/uv/) — `brew install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh`

## Usage

### Generate from a prompt

```bash
uv run scripts/generate.py -p "a watercolor painting of a coastal town" -o town.png
```

### Edit an existing image

```bash
uv run scripts/generate.py -p "make this pop art style" -i photo.png -o popart.png
```

### Combine multiple reference images

```bash
uv run scripts/generate.py \
  -p "combine the style of the first image with the content of the second" \
  -i style-ref.png -i content.png -o combined.png
```

### Control aspect ratio and resolution

```bash
uv run scripts/generate.py -p "wide cinematic landscape" -a "16:9" -r 2K -o landscape.png
```

### Use thinking for complex prompts

```bash
uv run scripts/generate.py -p "detailed city scene with neon signs reading 'OPEN 24/7'" \
  -m flash2 -t high -o city.png
```

## Options

| Flag | Description |
|------|-------------|
| `-p, --prompt` | Image prompt (required) |
| `-o, --output` | Output filename (required) |
| `-i, --input-image` | Reference image (repeatable, up to 14) |
| `-m, --model` | `pro` (default), `flash2`, `flash`, `exp` |
| `-r, --resolution` | `512`, `1K` (default), `2K`, `4K` |
| `-a, --aspect-ratio` | `1:1`, `16:9`, `9:16`, `3:2`, etc. |
| `-t, --thinking` | `minimal` (default), `high`, `dynamic` |

## Models

| Model | Best For | Max Refs | Aspect/Resolution |
|-------|----------|----------|-------------------|
| **pro** | Maximum quality, complex compositions | 14 | ✅ |
| **flash2** | Fast iterations, text rendering, bulk work | 14 | ✅ |
| **flash** | Simple generations | 3 | ❌ |
| **exp** | Experimental edits | varies | ❌ |

## Batch API (50% Cheaper)

For bulk generation that can wait up to 24 hours:

**1. Create a requests file** (`requests.jsonl`):
```json
{"key": "sunset", "prompt": "a sunset over mountains", "aspect_ratio": "16:9", "resolution": "2K"}
{"key": "portrait", "prompt": "corporate headshot", "input_images": ["/path/to/ref.png"]}
```

**2. Submit, check, download:**
```bash
uv run scripts/batch.py submit requests.jsonl
uv run scripts/batch.py status
uv run scripts/batch.py download -o ./images/
```

## Prompting Guide

See [references/prompting.md](references/prompting.md) for detailed strategies covering:

- Photorealistic scenes (camera/lens/lighting terminology)
- Stylized illustrations & stickers
- Text rendering (logos, typography)
- Product mockups
- iOS wireframes & UI mockups
- Style transfer & multi-image composition
- Character consistency

**Core principle:** Describe the scene narratively. Paragraphs beat keyword lists.

## Companion Skills

- **nano-triple** by [@mvanhorn](https://x.com/mvanhorn) — generates 3 variations of the same prompt so you can pick the best one. Great UX pattern for iterative image work.

## License

MIT


path: install-skill.sh
content: #!/usr/bin/env bash
# Install gemini-image as an OpenClaw / Clawdbot skill
set -euo pipefail

SKILL_NAME="gemini-image"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Detect skill directory
if [ -d "$HOME/.openclaw/skills" ]; then
    SKILL_DIR="$HOME/.openclaw/skills/$SKILL_NAME"
elif [ -d "$HOME/.clawdbot/skills" ]; then
    SKILL_DIR="$HOME/.clawdbot/skills/$SKILL_NAME"
else
    # Default to openclaw
    SKILL_DIR="$HOME/.openclaw/skills/$SKILL_NAME"
    mkdir -p "$HOME/.openclaw/skills"
fi

echo "Installing $SKILL_NAME to $SKILL_DIR..."

# Create skill directory
mkdir -p "$SKILL_DIR/scripts" "$SKILL_DIR/references"

# Copy files
cp "$SCRIPT_DIR/SKILL.md" "$SKILL_DIR/SKILL.md"
cp "$SCRIPT_DIR/scripts/generate.py" "$SKILL_DIR/scripts/generate.py"
cp "$SCRIPT_DIR/scripts/batch.py" "$SKILL_DIR/scripts/batch.py"
cp "$SCRIPT_DIR/references/prompting.md" "$SKILL_DIR/references/prompting.md"

echo "✓ Installed to $SKILL_DIR"
echo ""
echo "Setup:"
echo "  1. Get a Gemini API key: https://aistudio.google.com/apikey"
echo "  2. Set it: export GEMINI_API_KEY=\"your-key\""
echo "     Or add to your OpenClaw config under skills.gemini-image.env"
echo "  3. Make sure 'uv' is installed: brew install uv"
echo ""
echo "Quick test:"
echo "  uv run $SKILL_DIR/scripts/generate.py -p \"a cat in a top hat\" -o test.png"


path: references/prompting.md
content: # Gemini Image Prompting Guide

**Core principle:** Describe the scene, don't just list keywords. Narrative paragraphs beat disconnected words.

---

## 1. Photorealistic Scenes

Use photography terms: camera angles, lens types, lighting, fine details.

**Template:**
```
A photorealistic [shot type] of [subject], [action or expression], set in [environment]. 
The scene is illuminated by [lighting description], creating a [mood] atmosphere. 
Captured with a [camera/lens details], emphasizing [key textures and details]. 
The image should be in a [aspect ratio] format.
```

**Example:**
```
A photorealistic close-up portrait of an elderly Japanese ceramicist, focused intently 
on shaping clay on a traditional wheel, set in a rustic, sunlit workshop. The scene is 
illuminated by soft, diffused natural light from a large window, creating a warm, 
contemplative atmosphere. Captured with a 85mm f/1.4 lens, emphasizing the deep lines 
on his hands and the smooth, wet texture of the clay.
```

---

## 2. Stylized Illustrations & Stickers

Be explicit about style and request transparent background for assets.

**Template:**
```
A [style] sticker of a [subject], featuring [key characteristics] and a [color palette]. 
The design should have [line style] and [shading style]. The background must be transparent.
```

**Example:**
```
A kawaii-style sticker of a happy red panda holding a bamboo shoot, featuring oversized 
sparkling eyes and rosy cheeks with a pastel pink and green color palette. The design 
should have thick black outlines and soft cel-shading. The background must be transparent.
```

---

## 3. Accurate Text in Images

Gemini excels at rendering text. Be clear about text content, font style, and design. Use **Gemini 3 Pro Image** for best results.

**Template:**
```
Create a [image type] for [brand/concept] with the text "[text to render]" in a [font style]. 
The design should be [style description], with a [color scheme].
```

**Example:**
```
Create a modern, minimalist logo for a coffee shop called 'The Daily Grind' with the text 
"The Daily Grind" in a clean, sans-serif font with a subtle coffee bean replacing the 'o'. 
The design should be flat and geometric, with a warm brown and cream color scheme.
```

---

## 4. Product Mockups & Commercial Photography

Clean, professional product shots for ecommerce, advertising, or branding.

**Template:**
```
A high-resolution, studio-lit product photograph of a [product description] on a 
[background surface/description]. The lighting is a [lighting setup] to [lighting purpose]. 
The camera angle is a [angle type] to showcase [specific feature]. Ultra-realistic, 
with sharp focus on [key detail]. [Aspect ratio].
```

**Example:**
```
A high-resolution, studio-lit product photograph of a minimalist ceramic coffee mug 
with a matte terracotta finish on a raw concrete slab. The lighting is a three-point 
softbox setup to create soft shadows and highlight the mug's curves. The camera angle 
is a 45-degree hero shot to showcase the handle and interior. Ultra-realistic, with 
sharp focus on the subtle glaze texture. 4:5 aspect ratio.
```

---

## 5. Minimalist & Negative Space Design

Great for backgrounds, presentations, or marketing materials with text overlay.

**Template:**
```
A minimalist composition featuring a single [subject] positioned in the [position] of the frame. 
The background is a vast, empty [color] canvas, creating significant negative space. 
Soft, subtle lighting. [Aspect ratio].
```

**Example:**
```
A minimalist composition featuring a single, delicate red maple leaf positioned in the 
bottom-right of the frame. The background is a vast, empty off-white canvas, creating 
significant negative space for text. Soft, subtle lighting. 16:9 aspect ratio.
```

---

## 6. Sequential Art (Comics / Storyboards)

Character consistency + scene description for visual storytelling. Best with **Gemini 3 Pro Image**.

**Template:**
```
Make a 3 panel comic in a [style]. Put the character in a [type of scene].
```

**Example:**
```
Make a 3 panel comic in a noir detective style. Panel 1: A detective in a trench coat 
enters a dimly lit bar. Panel 2: Close-up of his weathered face as he orders whiskey. 
Panel 3: He notices a mysterious woman at the end of the bar watching him.
```

---

## 7. Grounding with Google Search

Generate images based on recent/real-time information (news, weather, sports).

**Example:**
```
Make a simple but stylish graphic of last night's Arsenal game in the Champion's League
```

---

## 8. iOS Wireframes & UI Mockups

Convert sketches to high-fidelity iOS UI. Use reference-first workflow with design system specs.

**Template:**
```
Convert this wireframe to high-fidelity iOS 18 UI. 
Device: [iPhone model] frame, [orientation].
Design system: [style specs].
Interpret sketch elements: scribbles→images, rectangles→buttons, lines→text.
```

**Example:**
```
Convert this wireframe sketch to a high-fidelity iOS 18 UI mockup.
Device: iPhone 16 Pro frame, portrait orientation.
Design system: iOS 18 native aesthetic, rounded corners (16px radius), soft drop shadows, 
SF Pro font, 8pt spacing grid, vibrant primary color (#007AFF).
Interpret the sketch: scribbles become placeholder images, rectangles become buttons, 
horizontal lines become text labels. Add a standard iOS status bar and home indicator.
```

**JSON-Structured Prompt (for complex layouts):**
```json
{
  "image_type": "UI mockup",
  "device": {"frame": "iPhone 16 Pro", "orientation": "portrait"},
  "design_system": {
    "style": "iOS 18 native",
    "corners": "rounded, 16px radius",
    "shadows": "soft drop shadows",
    "spacing": "8pt grid",
    "font": "SF Pro"
  },
  "layout": {
    "header": "Navigation bar with back button and title",
    "hero": "Large image card with rounded corners",
    "content": "List of items with icons and chevrons",
    "bottom_nav": "5 tab icons - Home, Search, Add, Activity, Profile"
  }
}
```

**Multi-Screen User Journeys:**
Request exactly N screens showing left-to-right flow:
```
Create a 4-screen iOS onboarding flow, arranged left to right:
1. Welcome screen with app logo and "Get Started" button
2. Feature highlight with illustration and "Next" button  
3. Permission request for notifications
4. Success screen with "Enter App" button
Consistent iOS 18 styling across all screens, iPhone 16 frames.
```

**Key Tips:**
- Specify device frame explicitly (iPhone 16, iPhone 16 Pro, etc.)
- Include design system details (corner radius, shadows, fonts)
- Tell model how to interpret sketch elements
- For multi-screen, generate separately or request specific grid layout
- Use `9:16` aspect ratio for portrait phone screens

---

# Editing Images

Provide images alongside text prompts for editing, composition, and style transfer.

---

## 1. Adding and Removing Elements

Provide an image and describe your change. Model matches original style, lighting, perspective.

**Template:**
```
Using the provided image of [subject], please [add/remove/modify] [element] to/from the scene. 
Ensure the change is [description of how the change should integrate].
```

**Example:**
```
Using the provided image of my cat, please add a small, knitted wizard hat perched 
on its head. Ensure the hat casts a subtle shadow and matches the soft lighting of 
the original photo.
```

---

## 2. Inpainting (Semantic Masking)

Conversationally define a "mask" to edit a specific part while leaving the rest untouched.

**Template:**
```
Using the provided image, change only the [specific element] to [new element/description]. 
Keep everything else in the image exactly the same, preserving the original style, 
lighting, and composition.
```

**Example:**
```
Using the provided image of a living room, change only the blue sofa to be a vintage, 
brown leather chesterfield sofa. Keep everything else exactly the same.
```

---

## 3. Style Transfer

Provide an image and recreate its content in a different artistic style.

**Template:**
```
Transform the provided photograph of [subject] into the artistic style of [artist/art style]. 
Preserve the original composition but render it with [description of stylistic elements].
```

**Example:**
```
Transform the provided photograph of a modern city street at night into the artistic 
style of a 1980s synthwave poster. Preserve the composition but render it with neon 
pink and cyan colors, chrome reflections, and a grid perspective.
```

---

## 4. Advanced Composition: Combining Multiple Images

Provide multiple images to create a new composite scene. Perfect for product mockups or creative collages.

**Template:**
```
Create a new image by combining the elements from the provided images. 
Take the [element from image 1] and place it with/on the [element from image 2]. 
The final image should be a [description of the final scene].
```

**Example:**
```
Create a professional e-commerce fashion photo. Take the blue floral dress from image 1 
and show it being worn by the model from image 2. The final image should be a full-body 
shot in a clean white studio setting.
```

---

## 5. High-Fidelity Detail Preservation

Ensure critical details (face, logo) are preserved during edits by describing them explicitly.

**Template:**
```
Using the provided images, place [element from image 2] onto [element from image 1]. 
Ensure that the features of [element from image 1] remain completely unchanged. 
The added element should [description of how the element should integrate].
```

**Example:**
```
Using the provided images, place the sunglasses from image 2 onto the person's face 
in image 1. Ensure that the person's facial features remain completely unchanged. 
The sunglasses should sit naturally on the bridge of the nose with realistic reflections.
```

---

## 6. Bring Something to Life

Upload a rough sketch and refine it into a finished image.

**Template:**
```
Turn this rough [medium] sketch of a [subject] into a [style description] photo. 
Keep the [specific features] from the sketch but add [new details/materials].
```

**Example:**
```
Turn this rough pencil sketch of a sports car into a photorealistic studio photograph. 
Keep the aggressive body lines and proportions from the sketch but add glossy red paint, 
chrome details, and dramatic studio lighting.
```

---

## 7. Character Consistency: 360 View

Generate 360-degree views by iteratively prompting for different angles. Include previously generated images in subsequent prompts for consistency.

**Template:**
```
A studio portrait of [person] against [background], [looking forward/in profile looking right/etc.]
```

**Workflow:**
1. Generate initial view: "A studio portrait of a man with white glasses against a gray background, looking forward"
2. Include that image + prompt: "Same person, now in profile looking right"
3. Include both images + prompt: "Same person, three-quarter view looking left"

---

# Best Practices

**Be Hyper-Specific:**
Instead of "fantasy armor," describe it: "ornate elven plate armor, etched with silver leaf patterns, with a high collar and pauldrons shaped like falcon wings."

**Provide Context and Intent:**
Explain the purpose. "Create a logo for a high-end, minimalist skincare brand" beats "Create a logo."

**Iterate and Refine:**
Use conversation: "That's great, but can you make the lighting warmer?" or "Keep everything the same, but change the expression to more serious."

**Use Step-by-Step Instructions:**
For complex scenes: "First, create a background of a misty forest at dawn. Then, add a moss-covered stone altar in the foreground. Finally, place a glowing sword on top."

**Use Semantic Negative Prompts:**
Instead of "no cars," describe positively: "an empty, deserted street with no signs of traffic."

**Control the Camera:**
Use photographic terms: wide-angle shot, macro shot, low-angle perspective, 85mm portrait lens, three-point lighting.

**Generate Text First:**
For images with text, first generate the text content, then ask for an image containing it.

---

# Limitations

- **Languages:** Best in EN, ar-EG, de-DE, es-MX, fr-FR, hi-IN, id-ID, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, ua-UA, vi-VN, zh-CN
- **No audio/video inputs** for image generation
- **Image count may vary** from what you request
- **Input limits:**
  - `gemini-2.5-flash-image`: Up to 3 images
  - `gemini-3-pro-image-preview`: 5 images high-fidelity, up to 14 total
- **SynthID watermark** included in all generated images

---

# Quick Tips

- **Narrative > Keywords** — "A cat sleeping on a sunny windowsill" beats "cat, window, sun, sleeping"
- **Be specific** — Mention colors, materials, lighting, mood
- **Use photography terms** for realism — lens type, f-stop, lighting setup
- **Request transparent background** for stickers/icons
- **Specify aspect ratio** when composition matters
- **Use Gemini 3 Pro** for text rendering and complex scenes
- **For edits** — Describe what to preserve, not just what to change


path: scripts/batch.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "google-genai>=1.0.0",
#     "pillow>=10.0.0",
# ]
# ///
"""
Batch image generation using Gemini Batch API (50% cheaper, 24hr turnaround).

Usage:
    uv run batch.py submit requests.jsonl          # Submit batch job
    uv run batch.py status                         # Check job status
    uv run batch.py download -o ./images/          # Download completed results
    uv run batch.py clear                          # Clear saved state

Request JSONL format (one per line):
    {"key": "image1", "prompt": "a sunset", "aspect_ratio": "16:9", "resolution": "2K"}
    {"key": "image2", "prompt": "a mountain", "input_images": ["/path/to/ref.png"]}
"""

import argparse
import base64
import json
import os
import sys
from pathlib import Path

SCRIPT_DIR = Path(__file__).parent
STATE_FILE = SCRIPT_DIR / ".batch_state.json"
MODEL = "gemini-3-pro-image-preview"


def get_api_key() -> str | None:
    return os.environ.get("GEMINI_API_KEY")


def load_state() -> dict:
    if STATE_FILE.exists():
        with open(STATE_FILE) as f:
            return json.load(f)
    return {}


def save_state(state: dict):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)


def build_request(item: dict) -> dict:
    """Build a batch API request from a simplified item."""
    parts = []

    # Add input images first
    if "input_images" in item:
        for img_path in item["input_images"]:
            with open(img_path, "rb") as f:
                img_data = base64.b64encode(f.read()).decode()
            parts.append({
                "inline_data": {
                    "mime_type": "image/png",
                    "data": img_data
                }
            })

    # Add prompt
    parts.append({"text": item["prompt"]})

    # Build generation config
    gen_config = {"responseModalities": ["TEXT", "IMAGE"]}

    image_config = {}
    if "aspect_ratio" in item:
        image_config["aspectRatio"] = item["aspect_ratio"]
    if "resolution" in item:
        image_config["imageSize"] = item["resolution"]

    if image_config:
        gen_config["imageConfig"] = image_config

    return {
        "key": item["key"],
        "request": {
            "contents": [{"parts": parts}],
            "generationConfig": gen_config
        }
    }


def cmd_submit(args):
    """Submit a new batch job."""
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: Set GEMINI_API_KEY", file=sys.stderr)
        sys.exit(1)

    from google import genai
    from google.genai import types

    client = genai.Client([REDACTED_SECRET])

    # Read and convert requests
    input_path = Path(args.input)
    if not input_path.exists():
        print(f"Error: {input_path} not found", file=sys.stderr)
        sys.exit(1)

    print(f"Reading requests from {input_path}...")
    requests = []
    keys = []

    with open(input_path) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            item = json.loads(line)
            requests.append(build_request(item))
            keys.append(item["key"])

    print(f"Built {len(requests)} requests")

    # Write batch JSONL
    batch_jsonl = SCRIPT_DIR / ".batch_requests.jsonl"
    with open(batch_jsonl, "w") as f:
        for req in requests:
            f.write(json.dumps(req) + "\n")

    # Upload to Files API
    print("Uploading to Gemini Files API...")
    uploaded_file = client.files.upload(
        file=batch_jsonl,
        config=types.UploadFileConfig(
            display_name=f"batch-{input_path.stem}",
            mime_type="jsonl"
        )
    )
    print(f"Uploaded: {uploaded_file.name}")

    # Create batch job
    print("Creating batch job...")
    batch_job = client.batches.create(
        model=MODEL,
        src=uploaded_file.name,
        config=types.CreateBatchJobConfig(
            display_name=f"gemini-image-batch-{input_path.stem}"
        )
    )

    print(f"✓ Batch job created: {batch_job.name}")
    print(f"  Status: {batch_job.state}")

    # Save state
    state = {
        "job_name": batch_job.name,
        "status": str(batch_job.state),
        "keys": keys,
        "uploaded_file": uploaded_file.name
    }
    save_state(state)
    print(f"✓ State saved")

    # Cleanup
    batch_jsonl.unlink()

    print("\nBatch submitted! Check status with: uv run batch.py status")
    print("Results typically ready within 24 hours.")


def cmd_status(args):
    """Check batch job status."""
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: Set GEMINI_API_KEY", file=sys.stderr)
        sys.exit(1)

    state = load_state()
    if not state or "job_name" not in state:
        print("No active batch job. Use 'submit' first.")
        return

    from google import genai
    client = genai.Client([REDACTED_SECRET])

    batch_job = client.batches.get(name=state["job_name"])
    status = batch_job.state.name if hasattr(batch_job.state, "name") else str(batch_job.state)

    print(f"Job: {batch_job.name}")
    print(f"Status: {status}")
    print(f"Requests: {len(state.get('keys', []))}")

    state["status"] = status
    save_state(state)

    if status == "JOB_STATE_SUCCEEDED":
        print("\n✓ Ready to download: uv run batch.py download -o ./images/")
    elif status == "JOB_STATE_FAILED":
        print(f"\n✗ Job failed: {batch_job.error}")


def cmd_download(args):
    """Download completed batch results."""
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: Set GEMINI_API_KEY", file=sys.stderr)
        sys.exit(1)

    state = load_state()
    if not state or "job_name" not in state:
        print("No active batch job. Use 'submit' first.")
        return

    from google import genai
    client = genai.Client([REDACTED_SECRET])

    batch_job = client.batches.get(name=state["job_name"])
    status = batch_job.state.name if hasattr(batch_job.state, "name") else str(batch_job.state)

    if status != "JOB_STATE_SUCCEEDED":
        print(f"Job not ready. Status: {status}")
        return

    # Create output directory
    output_dir = Path(args.output)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Download results
    result_file = batch_job.dest.file_name
    print(f"Downloading from: {result_file}")

    content_bytes = client.files.download(file=result_file)
    content = content_bytes.decode("utf-8")

    saved = 0
    failed = 0

    for line in content.splitlines():
        if not line:
            continue

        result = json.loads(line)
        key = result.get("key", f"image_{saved}")
        output_path = output_dir / f"{key}.png"

        if "error" in result:
            print(f"  ✗ {key}: {result['error']}")
            failed += 1
            continue

        # Find image in response
        try:
            response = result.get("response", {})
            candidates = response.get("candidates", [])

            for candidate in candidates:
                parts = candidate.get("content", {}).get("parts", [])
                for part in parts:
                    if part.get("inlineData"):
                        img_data = base64.b64decode(part["inlineData"]["data"])
                        with open(output_path, "wb") as f:
                            f.write(img_data)
                        print(f"  ✓ {key}: {output_path}")
                        saved += 1
                        break
        except Exception as e:
            print(f"  ✗ {key}: {e}")
            failed += 1

    print(f"\n✓ Saved {saved} images to {output_dir}")
    if failed:
        print(f"✗ Failed: {failed}")


def cmd_clear(args):
    """Clear batch state."""
    if STATE_FILE.exists():
        STATE_FILE.unlink()
        print("✓ Batch state cleared")
    else:
        print("No state to clear")


def main():
    parser = argparse.ArgumentParser(description="Batch image generation (50% cheaper, 24hr)")
    subparsers = parser.add_subparsers(dest="command", help="Commands")

    # Submit
    sub = subparsers.add_parser("submit", help="Submit batch job")
    sub.add_argument("input", help="JSONL file with requests")
    sub.set_defaults(func=cmd_submit)

    # Status
    sub = subparsers.add_parser("status", help="Check job status")
    sub.set_defaults(func=cmd_status)

    # Download
    sub = subparsers.add_parser("download", help="Download results")
    sub.add_argument("-o", "--output", default="./batch_images", help="Output directory")
    sub.set_defaults(func=cmd_download)

    # Clear
    sub = subparsers.add_parser("clear", help="Clear batch state")
    sub.set_defaults(func=cmd_clear)

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        return

    args.func(args)


if __name__ == "__main__":
    main()


path: scripts/generate.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "google-genai>=1.0.0",
#     "pillow>=10.0.0",
# ]
# ///
"""
Generate or edit images using Gemini API with support for multiple reference images.

Usage:
    uv run generate.py --prompt "description" --output out.png
    uv run generate.py --prompt "combine these" -i ref1.png -i ref2.png --output out.png
    uv run generate.py --prompt "edit this" -i source.png --model gemini-2.5-flash-image --output out.png
"""

import argparse
import os
import sys
from pathlib import Path


MODELS = {
    "flash": "gemini-2.5-flash-image",           # Fast, simpler config
    "flash2": "gemini-3.1-flash-image-preview",  # Nano Banana 2 — best price/perf, thinking, text rendering
    "pro": "gemini-3-pro-image-preview",         # Full image_config support, highest quality
    "exp": "gemini-2.0-flash-exp",               # Experimental, good for edits
}

ASPECT_RATIOS = [
    "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4",
    "9:16", "16:9", "21:9",
    "4:1", "1:4", "8:1", "1:8",  # New in Nano Banana 2
]

# Default model
DEFAULT_MODEL = "pro"


def get_api_key(provided_key: str | None) -> str | None:
    if provided_key:
        return provided_key
    return os.environ.get("GEMINI_API_KEY")


def main():
    parser = argparse.ArgumentParser(description="Generate images with Gemini (multiple references supported)")
    parser.add_argument("--prompt", "-p", required=True, help="Image prompt/instructions")
    parser.add_argument("--output", "-o", required=True, help="Output filename")
    parser.add_argument("--input-image", "-i", action="append", dest="inputs", help="Input image(s) - can repeat")
    parser.add_argument("--model", "-m", choices=list(MODELS.keys()), default=DEFAULT_MODEL,
                        help=f"Model: {', '.join(MODELS.keys())} (default: {DEFAULT_MODEL})")
    parser.add_argument("--resolution", "-r", choices=["512", "1K", "2K", "4K"], default="1K", help="Resolution (default: 1K, 512 for fast iterations)")
    parser.add_argument("--aspect-ratio", "-a", help="Aspect ratio e.g. '16:9', '1:1', '9:16'")
    parser.add_argument("--thinking", "-t", choices=["minimal", "high", "dynamic"], default=None, help="Thinking level (flash2/pro only, default: minimal)")
    parser.add_argument("--api-key", "-k", help="Gemini API key (or set GEMINI_API_KEY)")
    args = parser.parse_args()

    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: No API key. Set GEMINI_API_KEY or use --api-key", file=sys.stderr)
        sys.exit(1)

    # Validate aspect ratio
    if args.aspect_ratio and args.aspect_ratio not in ASPECT_RATIOS:
        print(f"Warning: '{args.aspect_ratio}' not in supported ratios: {', '.join(ASPECT_RATIOS)}", file=sys.stderr)

    # Warn if using features unsupported by model
    if args.model not in ("pro", "flash2") and (args.aspect_ratio or args.resolution != "1K"):
        print(f"Note: --aspect-ratio and --resolution only work with 'pro' and 'flash2' models", file=sys.stderr)
    if args.thinking and args.model not in ("pro", "flash2"):
        print(f"Note: --thinking only works with 'pro' and 'flash2' models", file=sys.stderr)

    from google import genai
    from google.genai import types
    from PIL import Image as PILImage

    client = genai.Client([REDACTED_SECRET])
    model_name = MODELS[args.model]

    # Build contents: images first, then prompt
    contents = []

    if args.inputs:
        if len(args.inputs) > 14:
            print(f"Warning: {len(args.inputs)} images provided, max is 14. Using first 14.", file=sys.stderr)
            args.inputs = args.inputs[:14]

        for img_path in args.inputs:
            try:
                img = PILImage.open(img_path)
                contents.append(img)
                print(f"Loaded: {img_path}")
            except Exception as e:
                print(f"Error loading {img_path}: {e}", file=sys.stderr)
                sys.exit(1)

    contents.append(args.prompt)

    # Build config - pro and flash2 support full image_config
    config_kwargs = {"response_modalities": ["TEXT", "IMAGE"]}

    if args.model in ("pro", "flash2"):
        image_config_kwargs = {}
        if args.resolution:
            image_config_kwargs["image_size"] = args.resolution
        if args.aspect_ratio:
            image_config_kwargs["aspect_ratio"] = args.aspect_ratio
        if image_config_kwargs:
            config_kwargs["image_config"] = types.ImageConfig(**image_config_kwargs)

    # Thinking level (flash2 and pro)
    if args.thinking and args.model in ("pro", "flash2"):
        thinking_map = {"minimal": "MINIMAL", "high": "HIGH", "dynamic": "DYNAMIC"}
        config_kwargs["thinking_config"] = types.ThinkingConfig(
            thinking_budget=thinking_map.get(args.thinking, "MINIMAL")
        )

    config = types.GenerateContentConfig(**config_kwargs)

    print(f"Generating with {model_name}...")

    try:
        response = client.models.generate_content(
            model=model_name,
            contents=contents,
            config=config,
        )

        output_path = Path(args.output)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        image_saved = False
        for part in response.parts:
            if part.text is not None:
                print(f"Model: {part.text}")
            elif part.inline_data is not None:
                from io import BytesIO
                image_data = part.inline_data.data
                if isinstance(image_data, str):
                    import base64
                    image_data = base64.b64decode(image_data)

                image = PILImage.open(BytesIO(image_data))

                # Save as PNG, handle RGBA
                if image.mode == "RGBA":
                    image.save(str(output_path), "PNG")
                elif image.mode == "RGB":
                    image.save(str(output_path), "PNG")
                else:
                    image.convert("RGB").save(str(output_path), "PNG")
                image_saved = True

        if image_saved:
            full_path = output_path.resolve()
            print(f"\nSaved: {full_path}")
            print(f"MEDIA: {full_path}")
        else:
            print("Error: No image in response", file=sys.stderr)
            sys.exit(1)

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: malicious | SkillSpector: suspicious (CRITICAL)