Skip to content
Riley Sklar
Back to blog

May 25, 2026

From DALL-E 3 to gpt-image-1: A Python Batch Script for Blog Headers

How I built a small Python tool to batch-generate topic-tailored blog header images via OpenAI's gpt-image-1, compress them to WebP, and rewrite Astro frontmatter — for ~$1 total.

OpenAI gpt-image-1 Python Astro Image Generation Automation
Soft pastel grid of six abstract illustration tiles

I reskinned this site to a pastel palette last week and noticed the blog headers no longer fit. Six posts, six generic abstract placeholders left over from a previous theme. Manually prompting an image generator six times and dragging the outputs into the right folder felt like exactly the kind of thing a 200-line Python script should do for me.

Here’s what I built, the gotcha that ate twenty minutes of my evening, and the one optimization that mattered more than any of the prompt engineering.

The architecture

The script lives in a small companion repo, image-gen, and does five things in sequence:

  1. Reads every .md file in the blog’s content collection.
  2. Parses the YAML frontmatter to get the slug, title, and description.
  3. Builds a prompt by combining a style preamble (consistent across all images) with a per-slug subject line (topic-specific).
  4. Calls OpenAI’s gpt-image-1 model at 1536x1024, high quality.
  5. Compresses the result to WebP via Pillow, saves it to the portfolio’s public/assets/blog/ directory, and rewrites the entry’s frontmatter img: field to point at the new file.

Total time for six images: about four minutes. Total cost: $1.02.

Prompt architecture: one preamble, six subjects

The single biggest lever on image consistency across a set is not prompt engineering each image individually. It’s writing one strong style preamble and reusing it verbatim.

STYLE_PREAMBLE = (
    "Editorial blog header illustration in a soft pastel palette of sage "
    "green, dusty rose, lavender, and warm clay. Minimal, abstract, "
    "geometric composition with gentle gradients, flat shapes, and subtle "
    "paper grain. Wide cinematic 16:9 composition with generous negative "
    "space. No text, no logos, no human figures, no UI screenshots."
)

SUBJECTS = {
    "framework-integration": (
        "Two large pastel rounded rectangles interlocking like puzzle "
        "pieces, with delicate connection nodes and thin lines suggesting "
        "two technical systems clicking into one."
    ),
    "rip-noaa": (
        "A pastel weather-radar sweep dissolving into negative space, "
        "isobar lines fading at the edges — a quiet sense of disappearing "
        "public infrastructure."
    ),
    # ...
}

def build_prompt(slug, frontmatter):
    return f"{STYLE_PREAMBLE} Subject: {SUBJECTS[slug]}"

The preamble carries the brand. The subject carries the topic. When all six images render, they look like they belong on the same site — same palette, same composition density, same negative space — but each one means something specific.

The gotcha: dall-e-3 doesn’t exist on new accounts

The first time I ran the script, every call failed:

Error code: 400 - {'error': {'message': "The model 'dall-e-3' does not exist.", ...}}

OpenAI deprecated dall-e-3 for newer accounts and replaced it with gpt-image-1. If you wrote anything against the Images API in 2024 or earlier, your code is probably broken now. Three things to know:

1. The model name changed. "dall-e-3""gpt-image-1".

2. The supported sizes changed. DALL-E 3 supported 1792x1024 (a 16:9 landscape). gpt-image-1 supports 1024x1024, 1024x1536, 1536x1024, or "auto". The closest to 16:9 is 1536x1024 (a 3:2 ratio).

3. The response shape changed. DALL-E 3 returned a URL you had to requests.get. gpt-image-1 returns base64-encoded image bytes directly in response.data[0].b64_json. So the download step becomes a decode step:

import base64, io
from PIL import Image

def save_b64_as_webp(b64_str, dest, quality=85):
    raw = base64.b64decode(b64_str)
    with Image.open(io.BytesIO(raw)) as im:
        im.save(dest, "WEBP", quality=quality, method=6)

That Image.open step also gives you a natural seam for compression, which leads to the second-most-important thing I learned.

The optimization that mattered: 15 MB → 660 KB

gpt-image-1 returns PNGs. At 1536x1024 high quality, each PNG was ~2.5 MB. Six images × 2.5 MB = 15 MB of image payload on /blog — enough to obliterate any Lighthouse score and add seconds to LCP on mobile.

Pillow re-encoding to WebP at quality 85 with method=6 (slow encoder, best compression) brought each image down to ~110 KB average. Total payload dropped from 15 MB to 660 KB. That’s a 96% reduction with no visible quality loss at the resolution the images are actually rendered.

Some quick numbers:

ImagePNG (gpt-image-1 output)WebP (after Pillow)
costa-rica-to-code2,994 KB182 KB
framework-integration2,711 KB111 KB
mastering-react2,436 KB83 KB
rip-noaa2,532 KB82 KB
software-developer-journey2,637 KB107 KB
versatile-developer2,524 KB91 KB

If you skip this step and ship the PNGs as-is, you’ve just made every blog page substantially slower for the sake of pixels nobody can see at thumbnail size. Don’t.

Frontmatter rewriting without a YAML round-trip

After saving each WebP, the script also rewrites the corresponding .md file’s img: field. I deliberately avoided round-tripping the whole frontmatter through PyYAML — that would reformat the file, reorder keys, change quoting, and generally produce a noisy diff.

Instead, a single-line regex replace:

def rewrite_img_field(md_path, new_img_path):
    text = md_path.read_text()
    new = re.sub(
        r"^(img:\s*).*$",
        rf"\1{new_img_path}",
        text,
        count=1,
        flags=re.MULTILINE,
    )
    md_path.write_text(new)

Touches one line. Leaves the rest of the file byte-identical. Git diff stays clean.

Dry-run by default

Any script that costs money should default to a no-op. The argparse configuration:

parser.add_argument(
    "--go",
    action="store_true",
    help="Actually call the API and write files. Default is dry-run.",
)
parser.add_argument(
    "--only",
    help="Only process this slug (filename without .md).",
)

Running python3 generate-blog-images.py prints every prompt the script would send but doesn’t actually call the API. You review the prompts, decide they look right, then re-run with --go. The --only flag lets you regenerate one entry — useful when one of the six images comes back weird and you don’t want to re-roll the whole set.

Cost reality

gpt-image-1 at 1536x1024 high quality runs about $0.17 per image (token-priced, so it varies slightly). Six images came out to $1.02 actual on my invoice. Not free, but trivial.

The other cost I almost paid was a billing hard limit I’d set on my OpenAI account months ago and forgotten about. The first real run failed with:

Error code: 400 - {'message': 'Billing hard limit has been reached.', ...}

Worth checking your billing limits page before assuming the script is broken.

When this pattern is the right answer

Reach for a batch script like this when:

  • You have more than three of the same kind of asset to generate.
  • The assets share a consistent style that benefits from a fixed preamble.
  • You need to rewire references (frontmatter, JSON, CMS records) alongside the generation.
  • The cost of one bad output is low — you can --only <slug> and re-roll.

Reach for manual prompting in the OpenAI playground when:

  • You’re generating one or two images.
  • You’re still iterating on style — interactive feedback is faster than editing a Python file.
  • You need precise control over each prompt and don’t want to maintain a preamble.

The full script

It’s about 200 lines of Python with no surprises. The whole thing lives at github.com/rileysklar/image-gen — clone it, drop in your OPENAI_API_KEY, point the PORTFOLIO constant at your content directory, and adapt the SUBJECTS dict to your posts.

A small tool that turned an afternoon of manual prompting into four minutes of automated work, for less than the cost of a coffee. That’s the right scale for “ship it” energy.

Get in touch

Building something AI-first?

I'm open to chat about the intersection of AI and web growth — GEO/AIO strategy, MCP and agentic architecture, marketing-engineering ops. Otherwise, just say hi.