Check an AI thumbnail stays readable at 320x180 with a Pillow sheet

Generate a 1280x720 thumbnail with GPT Image 2.5, then shrink it to 640x360, 320x180 and 160x90 side by side to catch tiny text and weak contrast.

5 min readSume
All posts

The quickest way to test whether an AI thumbnail will read in a feed is to shrink it to the sizes people actually see and look at all of them at once. Generate 1280x720 with GPT Image 2.5, then build a contact sheet at 640x360, 320x180 and 160x90 with Pillow. If the subject or the lettering disappears at 320x180, redo the shot before you upload.

1280x720 is a legal GPT Image 2.5 custom size, unlike 1920x1080. Both edges are multiples of 16 (80 x 16 and 45 x 16), the shape is 16:9, and the area is 921,600 pixels.

Sizes to test

Pick the sizes your platform shows in its lists, not only the upload size. The three below are a generic ladder: each step halves the width. Replace them with your real card sizes.

Shrink ladder for a 1280x720 thumbnail (read 2026-10-05)
StepSizeScale from masterPixels
Master1280x7201.0921,600
Large card640x3600.5230,400
Small card320x1800.2557,600
Tiny160x900.12514,400

Script

It requests the image, then pastes each size onto one canvas with a gap. Open thumb-check.png and judge by eye. Nothing here measures quality for you; it just puts the small sizes in front of you.

import os, requests
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def generate(body):
    r = requests.post("https://api.sume.com/v1/images", headers=H, json=body, timeout=60)
    if r.status_code != 200:  # 202 = still running, read data.status_url
        raise SystemExit(f"{r.status_code}: {r.text[:300]}")
    return r.json()["data"][0]["url"]
from io import BytesIO
from PIL import Image

url = generate({
    "model": "openai/gpt-image-2.5",
    "prompt": "Two hands holding a glowing green plant seedling, dark teal background, bold shapes, no text",
    "image_size": {"width": 1280, "height": 720},
    "quality": "medium",
})
master = Image.open(BytesIO(requests.get(url, timeout=60).content)).convert("RGB")
sizes = [(640, 360), (320, 180), (160, 90)]
sheet = Image.new("RGB", (640 + 320 + 160 + 80, 380), (30, 30, 30))
x = 20
for s in sizes:
    sheet.paste(master.resize(s, Image.LANCZOS), (x, 10))
    x += s[0] + 20
sheet.save("thumb-check.png")

Reading the sheet

Four quick questions give a pass or a redo.

  • Cover the 640 version with your hand. Can you still name the subject at 160x90?
  • Is there text? Anything under about 10 percent of the frame height will blur at the small steps.
  • Do the subject and the background differ in brightness, not just in hue? Contrast survives shrinking and color alone often does not.
  • Is the face or product in the middle two thirds? List cards and overlays often sit on the corners.

Cost of redoing a thumbnail

A 1280x720 render costs less than a 1024 square at the same quality, according to the token formula behind the fal Flare page. Sume bills list times 1.25, before a small prompt-token charge.

Output-only price per 1280x720 image (read 2026-10-05)
QualityProvider listSume at list x 1.25
low$0.0032$0.004
medium$0.0074$0.0092
high$0.0284$0.0355

Draft cheap, finish once

A display set usually needs several concepts before one is approved. Request the first round at quality: "low", pick the layout, then repeat only the winner at high. Quality is a per-request field on both ChatGPT Image 2.5 variants (openai/gpt-image-2.5 and openai/gpt-image-2.5-sunburst), and the accepted values are auto, low, medium, high, xhigh and max. Avoid auto: Sume reserves the max price for it, so set the tier yourself.

Flare is the faster variant and Sunburst is tuned for edit precision, according to the fal pages. Both share the same token rates, so for an ad pass you can pick on behavior, not on price. Auto routing on Sume (sume/auto) uses Flare.

If the call returns 202 instead of 200

POST /v1/images waits up to 30 seconds and answers 200 with the images when the job finishes in time. When it does not, the route answers 202 with the standard job envelope, and the images come from GET /v1/jobs/{id}/result. Slow settings such as 4K, high quality and large n are the likeliest to fall back. A 1-2 megapixel ad master at high quality usually stays in the wait budget, but the code should branch on the status code, not the body shape.

For a batch of ad sizes, send each request with mode: "async" and read the results afterwards, or add a webhook_url with mode: "webhook". A failed synchronous job returns 502 with an error code and a next_action, and Sume does not bill failed generations.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume