Automated Blog Image Generation with Gemini API (Free Tier)
- Ctrl Man
- Software Development , AI Tools , Productivity
- 12 Mar, 2026
The Problem: 138 Images to Create
I needed featured images for every blog article. Manually creating each one would take hours. My options:
- Canva/Figma — Manual, ~15 minutes per image = 32+ hours
- Midjourney — $10-30/month, still need manual prompting
- Google Gemini Free Tier — Free, API-accessible, batchable
I chose option 3. Here’s how to do it yourself.
What You’ll Build
A Python script that:
- Reads image prompts from a markdown file
- Calls Google’s Gemini API (
gemini-3.1-flash-image-preview) - Respects free tier rate limits (2 requests/minute)
- Saves images automatically with correct filenames
- Logs progress for resuming interrupted batches
Time saved: ~30 hours of manual work Cost: $0 (free tier)
Prerequisites
1. Google AI Studio Account
Go to Google AI Studio and sign in with your Google account.
2. Get Your API Key
- Click “Get API Key” in the top right
- Create a new project or use existing
- Copy your API key (starts with
AIzaSy...)
3. Install Dependencies
pip install google-generativeai pathlib
Or create a virtual environment:
python -m venv .venv-images
source .venv-images/bin/activate
pip install google-generativeai
The Complete Script
Save this as scripts/generate-images-automated.py:
#!/usr/bin/env python3
"""
ctrlman.dev — Automated Image Generator via Gemini API
Uses Gemini API with Imagen to generate blog images.
Respects free tier limit: 2 requests/minute.
Usage:
python generate-images-automated.py
python generate-images-automated.py --resume-from 10
python generate-images-automated.py --dry-run
"""
import subprocess
import time
import json
from pathlib import Path
from datetime import datetime
import google.generativeai as genai
# Configuration
API_KEY = "YOUR_API_KEY_HERE" # Replace with your key
genai.configure(api_key=API_KEY)
OUTPUT_DIR = Path("/path/to/your/blog/public/images")
LOG_FILE = Path("/path/to/your/workspace/image-generation-log.json")
# Rate limiting (free tier: 2 requests/minute)
REQUESTS_PER_MINUTE = 2
DELAY_BETWEEN_REQUESTS = 60 / REQUESTS_PER_MINUTE # 30 seconds
# Your prompts list
PROMPTS = [
{
"filename": "javascript-fundamentals.png",
"prompt": "Flat blog illustration for JavaScript programming fundamentals..."
},
# Add more prompts here
]
def generate_image(prompt_text, filename):
"""Generate a single image and save it."""
try:
model = genai.GenerativeModel('gemini-3.1-flash-image-preview')
response = model.generate_content(
prompt_text,
generation_config={"response_modalities": ["IMAGE"]}
)
for part in response.parts:
if part.inline_data:
img_data = part.inline_data.data
output_path = OUTPUT_DIR / filename
with open(output_path, "wb") as f:
f.write(img_data)
print(f"✓ Saved: {filename}")
return True
print(f"✗ No image data in response: {filename}")
return False
except Exception as e:
print(f"✗ Error generating {filename}: {str(e)}")
return False
def main():
"""Main batch generation loop."""
print(f"Starting image generation at {datetime.now()}")
print(f"Output directory: {OUTPUT_DIR}")
print(f"Total images: {len(PROMPTS)}")
print(f"Rate limit: {REQUESTS_PER_MINUTE} requests/minute")
print("-" * 60)
# Track progress
generated = 0
failed = 0
start_time = time.time()
for i, image_config in enumerate(PROMPTS, 1):
filename = image_config["filename"]
prompt = image_config["prompt"]
# Skip if already exists (for resuming)
if (OUTPUT_DIR / filename).exists():
print(f"⊘ Skipping (exists): {filename}")
continue
print(f"[{i}/{len(PROMPTS)}] Generating: {filename}")
success = generate_image(prompt, filename)
if success:
generated += 1
else:
failed += 1
# Rate limiting
if i < len(PROMPTS):
print(f" Waiting {DELAY_BETWEEN_REQUESTS}s (rate limit)...")
time.sleep(DELAY_BETWEEN_REQUESTS)
# Final report
elapsed = time.time() - start_time
print("-" * 60)
print(f"Generation complete!")
print(f" Generated: {generated} images")
print(f" Failed: {failed} images")
print(f" Skipped: {len(PROMPTS) - generated - failed} images (already existed)")
print(f" Total time: {elapsed/60:.1f} minutes")
print(f" Average: {elapsed/len(PROMPTS):.1f}s per image")
if __name__ == "__main__":
main()
Understanding the Free Tier Limits
Google Gemini Free Tier (as of March 2026)
| Limit | Value |
|---|---|
| Requests per minute | 2 |
| Requests per day | 1,500 |
| Images per request | 4 variations |
| Resolution | 1024x1024 |
Why Rate Limiting Matters
Without rate limiting, you’ll hit errors:
429 Too Many Requests
Resource has been exhausted (e.g. check quota)
My script enforces a 30-second delay between requests:
DELAY_BETWEEN_REQUESTS = 60 / REQUESTS_PER_MINUTE # 30 seconds
time.sleep(DELAY_BETWEEN_REQUESTS)
Math:
- 2 requests/minute × 60 minutes = 120 requests/hour
- 120 requests/hour × 24 hours = 2,880 requests/day
- But daily limit is 1,500, so you’re capped there
For 138 images:
- 138 images ÷ 2 per minute = 69 minutes
- Plus error buffer: ~90 minutes total
The Prompts: My Complete List
I created 138 prompts following this formula:
"Flat blog illustration for {TOPIC}, showing {VISUAL_ELEMENTS},
modern minimal vector style, limited color palette:
#1e2b3a #ff4757 #2ed573 #f1f5f9 #0f172a #ffffff,
no gradients, professional tech blog header, 1024x1024"
Example Prompts
PROMPTS = [
{
"filename": "javascript-fundamentals.png",
"prompt": "Flat blog illustration for JavaScript programming fundamentals tutorial, showing JS shield logo with code blocks and programming icons, modern minimal vector art style, limited color palette: #1e2b3a #ff4757 #2ed573 #f1f5f9 #0f172a #ffffff, clean geometric shapes, no gradients, professional tech blog header, 1024x1024"
},
{
"filename": "intro-to-react.png",
"prompt": "Flat blog illustration for React.js beginner tutorial, showing component tree structure with nested boxes, modern minimal vector style, limited color palette, clean geometric shapes, no gradients, tech blog header, 1024x1024"
},
{
"filename": "mysql-setup-guide.png",
"prompt": "Flat blog illustration for MySQL database setup tutorial, showing database server icon with configuration gear, modern minimal vector style, ctrlman.dev brand colors, no gradients, professional tech blog header, 1024x1024"
},
# ... 135 more
]
Brand Colors Explained
My six-color palette (from GENERATE-IMAGES-WITH-GOOGLE.md):
| Color Name | Hex | Usage |
|---|---|---|
| Deep Gunmetal | #1e2b3a | Primary text, outlines |
| Pomodoro Red | #ff4757 | Alerts, important elements |
| Glacial Blue | #2ed573 | Success states, highlights |
| Slate 100 | #f1f5f9 | Background |
| Slate 900 | #0f172a | Dark text |
| Pure White | #ffffff | Contrast, space |
Why this matters: Consistent colors = cohesive blog design.
Running the Script
Step 1: Test with One Image
# Modify script to only have 1 prompt
python scripts/generate-images-automated.py
Expected output:
Starting image generation at 2026-03-12 14:00:00
Output directory: /path/to/images
Total images: 1
Rate limit: 2 requests/minute
------------------------------------------------------------
[1/1] Generating: test-image.png
✓ Saved: test-image.png
------------------------------------------------------------
Generation complete!
Generated: 1 images
Failed: 0 images
Total time: 0.5 minutes
Step 2: Run Full Batch
# Restore all 138 prompts
python scripts/generate-images-automated.py
Watch it run:
[1/138] Generating: javascript-fundamentals.png
✓ Saved: javascript-fundamentals.png
Waiting 30.0s (rate limit)...
[2/138] Generating: javascript-fundamentals-2.png
✓ Saved: javascript-fundamentals-2.png
Waiting 30.0s (rate limit)...
...
Step 3: Resume After Interruption
Script crashed? No problem. It skips existing files:
# Just run again - it auto-resumes
python scripts/generate-images-automated.py
Output:
⊘ Skipping (exists): javascript-fundamentals.png
⊘ Skipping (exists): javascript-fundamentals-2.png
[3/138] Generating: javascript-debugging.png
...
Troubleshooting
Error: 429 Too Many Requests
Cause: You hit the rate limit.
Fix: Increase delay:
DELAY_BETWEEN_REQUESTS = 60 # 60 seconds instead of 30
Error: API Key Not Valid
Cause: Wrong API key or not configured.
Fix:
# Option 1: Hardcode (for testing)
API_KEY = "AIzaSyB_nr8Uozr98sgdjL7xWARKmJFXAsUFcUE"
# Option 2: Environment variable (production)
import os
API_KEY = os.environ.get("GOOGLE_GENAI_API_KEY")
Error: No Image Data in Response
Cause: Prompt was rejected or model returned error.
Fix: Check prompt for:
- Banned content (violence, copyrighted material)
- Too vague descriptions
- Missing style keywords
Good prompt:
"Flat blog illustration for JavaScript tutorial, showing JS logo with code icons, modern minimal vector style, #1e2b3a #ff4757 #2ed573 #f1f5f9 #0f172a #ffffff, no gradients, 1024x1024"
Bad prompt:
"Make a cool JavaScript picture"
Post-Processing: Optimization
Generated images are ~4.2MB PNGs. Too big for web!
Convert to WebP + Optimize
python scripts/batch-optimize-images.py \
-i public/images-new \
-o public/images \
--colors 6 --format webp \
--quality 85
Results:
| Stage | Size | Count | Total |
|---|---|---|---|
| Raw PNG | 4.2 MB | 138 | 579 MB |
| Optimized WebP | 80 KB | 138 | 11 MB |
| Savings | 98% | — | 568 MB |
Cost Breakdown
Free Tier (What I Used)
- Monthly cost: $0
- Daily limit: 1,500 requests
- Speed: 2 requests/minute
- Time for 138 images: ~90 minutes
Paid Tier (If You Need More)
- Google AI Pro: ~$20/month
- Higher limits: 10-20 requests/minute
- Time for 138 images: ~10 minutes
My recommendation: Start with free tier. Upgrade only if you need speed.
Alternative: Manual Generation via Web UI
Don’t want to code? Use the web interface:
- Go to Gemini
- Paste prompt
- Click “Generate”
- Download best variation
- Repeat 137 times 😅
Time estimate: 15 minutes × 138 = 34.5 hours
Script time: 90 minutes (unattended)
Time saved: 33+ hours
What I Learned
✅ What Worked
- Free tier is sufficient — 2 requests/minute is slow but workable
- Consistent prompts = consistent results — Use a template
- Brand colors matter — Specify hex codes in every prompt
- Resume capability is essential — Network happens
❌ What Didn’t Work
- MCP Server setup — Too complex, switched to direct SDK
- No rate limiting — Got 429 errors immediately
- Vague prompts — Got photorealistic images instead of vector art
🎯 Key Insight
The difference between “good enough” and “perfect” is 30 hours of manual work. For a solo developer, automation isn’t lazy—it’s survival.
Your Turn
Quick Start Checklist
# 1. Get API key
https://aistudio.google.com/app/apikey
# 2. Install dependencies
pip install google-generativeai
# 3. Create script
# Copy the script above into generate-images-automated.py
# 4. Add your prompts
# Use the template formula
# 5. Run it
python generate-images-automated.py
# 6. Optimize results
python batch-optimize-images.py -i input -o output --format webp
Prompt Template (Copy-Paste Ready)
Flat blog illustration for {TOPIC}, showing {VISUAL_ELEMENTS},
modern minimal vector style, limited color palette:
#1e2b3a #ff4757 #2ed573 #f1f5f9 #0f172a #ffffff,
no gradients, professional tech blog header, 1024x1024
Resources
- Google AI Studio: https://aistudio.google.com/
- Gemini API Docs: https://ai.google.dev/docs
- Imagen Model: https://deepmind.google/technologies/imagen/
- My Image Generation Guide:
/workspace/GENERATE-IMAGES-WITH-GOOGLE.md - WebP Optimization Script:
/workspace/scripts/batch-optimize-images.py
This article is part of the “My AI Journey” series—real lessons from building real projects with AI assistance. No theory, just what actually worked (and what spectacularly failed).