One of my side projects is a small self-hosted WordPress site where I review gadgets I actually own. Its covers were generic illustrated title cards with no device in them. They looked fine, but they didn’t show the thing being reviewed, which is a bit odd for a site whose whole promise is “I own this.”
So this morning I had an AI agent rebuild the covers for all nine review posts. I set one rule before it started: no AI-generated gadget images. The agent could use AI for the code, the layout work and background removal, but every device on a cover had to come from a real photo. Here’s how it went, including the part where its dry run caught another agent’s changes.
Why I banned generated product images
Image models are good at “a camera on a desk.” They’re bad at this camera. Buttons move, ports appear, logos melt. On a review site that’s worse than no picture, because a reader who owns the device spots it straight away and stops trusting the review.
The source rules the agent worked to:
- Real photos only. My own photo of the unit first. If I don’t have one yet, the maker’s official product or press image, downloaded from the maker’s own site.
- Masking is allowed, editing isn’t. Removing the background is fine. Extending, relighting, retouching or “enhancing” the photo is not.
- Log every source. A
SOURCES.mdfile records the product page, the direct image URL, the download date and exactly what processing was done, and the untouched original is kept next to the cutout. - Respect the terms. One maker’s press images say they can’t be altered, so that cover shows the photo whole in a card instead of cutting it out.
The template is just HTML
The agent built the cover as an HTML/CSS template with slots for the title, the product image, an optional category label and an “I own this” badge. A small Python script fills the slots and screenshots the page in headless Chromium through Playwright, so one command renders the 1200×630 featured image and a 1080×1080 square for social. The square is a real re-layout, not a crop.
python render.py \
--title "Pocket Camera, *Honestly Reviewed*" \
--image assets/products/pocket-camera.png \
--badge --category CAMERA \
--out output/pocket-camera --jpg
# -> pocket-camera-1200x630.png/.jpg and pocket-camera-1080x1080.png/.jpg
A few rules are baked into the script, so I don’t have to remember them:
- Six words max in the title. The script refuses anything longer unless you force it.
- No star ratings, scores, specs or prices on covers. Those belong in the review, where there’s room to explain them.
- Warnings, not silent shrinking. It warns when a title has to drop below a readable size and when text lands inside the 60px safe margin. That margin matters because the homepage crops featured images to 16:9, which cuts a strip off each side of a 1200×630 image.
Matching the site instead of guessing colours
The first version used colours that were “close enough.” The second pass was better: it opened the live site in Chrome, read the computed styles of the real hero, button and logo elements, and traced each colour back to the CSS rule it came from. Those values became CSS variables at the top of the template, with a note saying where each one came from.
It also checked contrast against the rendered pixels. The site’s main accent colour only reaches about 2.2:1 on the dark background, which is fine as a button fill behind white text but too faint for words. So the covers use it for fills, bars and the edge strip, and use the site’s two lighter accent shades for highlighted title words and labels.
Local AI for the boring part: background removal
Most maker photos sit on a white or grey backdrop. The renderer can cut the product out with rembg, which runs a small segmentation model locally on the CPU. No upload, no API key, and the result is cached next to the source image. That’s the kind of AI I’m happy to have on covers. It decides which pixels belong to the device, but it doesn’t draw any of them.
It doesn’t always get it right, though. On one charger it removed a white charging puck along with the white background. On a silver device shot against white, it struggled to find the edge at all. For those the agent used plain old methods: a flood fill from the image border, or a simple threshold mask, saved as a separate script so it’s obvious no model touched the photo.
Backups before anything touches the site
Before uploading anything, the agent saved a full REST snapshot of every post and its current featured image. Each original image was downloaded with its SHA-256 hash and recorded in a manifest. Then it wrote a revert.py that puts the original featured image IDs back.
The apply script runs as a dry run by default. For each post it checks that the current featured image is still the one recorded in the backup. If it isn’t, it skips the post and says why.
DRY RUN: 9 posts in plan
post A SKIP: featured_media changed since backup (use --force)
post B SKIP: featured_media changed since backup (use --force)
post C would upload + set featured_media
...
That check earned its keep straight away. Two of the nine posts had new featured images that another agent working on the same site had set after my backup. Nothing was broken, but blindly overwriting them would have thrown away someone else’s work with no record. The agent saved copies of those two images as well, and only then did the run go ahead. It’s the same lesson as I Gave My AI Agents One Shared Changelog With a Rollback Line: when several agents share a site, check what changed before you write.
I approved a contact sheet, not nine files
Nothing went live until I’d seen the whole set. The agent generated one contact sheet with every cover side by side, plus thumbnail-size previews and a mock-up of a cover sitting in the real homepage card. Looking at nine covers together is how you spot the one with a cramped title or a product that’s too small. Looking at them one at a time, you miss that.
After I said yes, the apply step did four things for each post. It uploaded the JPG through the REST API, set the alt text and title, set it as the featured image, and read the post back to confirm. A state file remembers what has been uploaded, so a rerun reuses the media instead of uploading duplicates. The site builds its social preview from the featured image, so the share cards updated too. If your SEO plugin stores its own social image override, clear it only when it still points at the old image.
Ship checklist
- Write the image-source rules down first: real photos only, masking allowed, no generated or “enhanced” devices.
- Keep a sources log with the page URL, image URL, date and processing, and keep the untouched originals.
- Build the cover as an HTML template and render it with a headless browser, so changes stay in code.
- Put the guardrails in the script: a word limit, no ratings or prices, a safe-margin warning for homepage crops.
- Take colours from the live site’s computed styles, and check contrast against the rendered pixels.
- Use local background removal, and fall back to a non-AI mask when it eats part of the device.
- Snapshot every post and featured image with hashes, and write the revert script before the apply script.
- Dry run by default, and skip any post that changed since the backup.
- Approve the full set on one contact sheet, then apply, read back and log.
FAQ
Isn’t using a maker’s press photo a licensing problem?
Makers publish product shots so people can identify the product, and editorial use in a review is the normal case. It still isn’t a formal licence, and some press kits carry explicit terms, so read them and follow them. Your own photo avoids the question, which is why it always comes first.
Why not let an image model clean up a dull photo?
Because “clean up” quietly turns into “redraw.” Once a model has repainted part of the device, you can’t honestly say the picture shows the real thing. Removing the background is a selection. Anything beyond that is a new image.
Could this run fully on local hardware?
The rendering and background removal already do. The template, the headless browser and the segmentation model all run on one box with no cloud calls. My local LLM homelab stack post covers what else I keep running at home, and I Built a Local ffmpeg Shorts Pipeline With an AI Agent is the video version of the same idea.
Closest related Builders notes?
7 Rules for Letting an AI Agent Maintain Your Homelab covers the backup-first habit behind the manifest. I Let an AI Agent Patch a Theme With a Tiny Reversible Plugin is the same “small, reversible change” idea applied to code, and I Had an AI Agent Audit My Self-Hosted Blog for Setup Leaks explains why the product names in this post are generic.
