One line in: /santa-skill B0GYY18GYC. Out: Christmas versions of the listing's
secondary images in santa/<ASIN>/christmas/, a before/after contact-sheet.html, and a
short report.md. The main image is never touched (Amazon wants it on pure white). The
product is never touched either: same shape, color, angle, label. The world around it gets a
Christmas you can see at thumbnail size: a lit tree, garlands, gifts; a summer scene turns to
snow and winter coats; a pool or a beach keeps its sun and swimwear and gets a Christmas in the sun.
Many products in: /santa-skill with the product links pasted after it. Out: the same
thing for every product, one folder per ASIN, and one index.html that shows the whole set
dressed. That is catalog mode, the second half of this file.
This is an AUTOPILOT skill. After the environment check passes, the run asks nothing. The only stop is a missing input (Amazon blocked the fetch and no folder was given). Every judgment call is made, logged in plan.json, and shown on the sheet.
Engine: OpenAI gpt-image-2 through images.edit, the original photo as the input
image. Never Nano Banana, never Gemini, never any other image model, not as a fallback.
If gpt-image-2 is unavailable, stop and say so. The two looking passes (classify, verify)
run on gpt-5.4 vision through the same key.
Files
SKILL.md this file
scripts/check-env.sh prints OK / MISSING with exact fix lines
scripts/santa.py one listing: fetch, list, classify, generate, regen, check, verify, sheet
scripts/catalog.py one store: discover, run, status, index
references/christmas-prompt-rules.md the rules every prompt is built from, and the self-check
S=~/.claude/skills/santa-skill/scripts in the commands below. Run everything from the
user's working directory; a listing lands in <cwd>/santa/<ASIN>/, a store in
<cwd>/santa-catalog/<name>/ (my-store for a paste, the file name for a file).
Step 0: environment (first run on a machine, or after an error)
bash $S/check-env.sh
If anything prints MISSING, show the user the fix lines it printed and stop. Needs:
python3, the openai package, Pillow, curl, and OPENAI_API_KEY (plus, optional,
opencv-python-headless for the framing check) (env var, else ./.env,
else ~/Downloads/Claude/.env). gpt-image-2 needs a verified OpenAI org.
Which mode
| The user gives | Mode |
|---|---|
| one ASIN, one product link, one folder of images | single listing, Steps 1 to 5 below |
| two or more product links or ASINs (pasted, or in a .txt), or a Seller Central report | catalog, the section after Step 5 |
| a folder of product folders (one product each, images in gallery order), or several product folders | catalog, from the folders: no Amazon fetch |
| "my store", "my whole catalog", a store link, with no product list | ask for the list, in these words: "Paste the product links or ASINs you want dressed, your gift-worthy best sellers first. 20 is a good start. For the whole catalog, download the Active Listings Report from Seller Central (Reports > Inventory Reports) and give me the file." Then catalog mode |
Why the list and not a store link: "the store" is three different pages on Amazon. The Brand
Store (/stores/...) is JavaScript and often shows only part of the catalog; the seller page
(s?me=...) holds everything the seller sells, other brands included, and Amazon puts it
behind a bot check (measured: after four fetches in ten minutes); a brand search mixes in
other sellers. Product pages were read cleanly all day. A list is unambiguous, works every
time, and the seller decides the scope, so a 500-product catalog is never a surprise.
Single listing
Step 1: fetch
python3 $S/santa.py fetch B0GYY18GYC
python3 $S/santa.py fetch "https://www.amazon.com/dp/B0GYY18GYC"
python3 $S/santa.py fetch /path/to/folder/of/images --out santa/MYPRODUCT --title "Product name"
Plain curl with a desktop User-Agent, one attempt, hi-res URLs parsed from the page's
"hiRes" entries (fallback "large"). Writes originals/01.jpg ... in gallery order
(01 is the main image), listing.json with the title, and page.html for debugging.
Exit code 2 means blocked or fewer than 2 images. The script prints the folder fallback line. Relay it to the user word for word and STOP. Do not retry the Amazon fetch. Do not retry a failed image download. Do not open Amazon in a browser to work around it. When the user comes back with a folder, run the folder form and continue.
Step 2: classify
python3 $S/santa.py list santa/B0GYY18GYC
python3 $S/santa.py classify santa/B0GYY18GYC
list prints every original and writes the plan.json template. classify looks at every
picture with gpt-5.4 vision and fills each entry: seen (what is in the picture), role
(main | lifestyle | packshot | infographic | logo | collage | variant; only lifestyle transforms), action
(transform | skip), a scene line for every transform, a reason for every skip, and
text_on_image (the headline and labels it read, so the verify pass can read them back).
About 5 seconds and half a cent per gallery.
Then OPEN plan.json and the originals and check the calls. You are the second opinion:
a lifestyle frame marked infographic, or a collage marked lifestyle, gets edited by hand
before generation. --force reclassifies everything. The decision rules:
01(first in gallery) is the main image. Skip, always.- Lifestyle scene, with or without a headline band: transform. A headline plus a sub-line is the normal Amazon lifestyle frame; gpt-image-2 keeps short text intact.
- Infographic (five or more text elements, feature bullets, spec tables, dimension arrows, diagrams, or text across the product's body): skip. Small text gets mangled.
- The product on a plain white or studio background, in any form: skip, "product on white". Santa dresses lifestyle images only: never the main image, never a packshot, never an infographic. The code enforces it too: a plan that says transform on a packshot is skipped.
- A color variant (the product in another color on white) is also named as such, so the
report says why. The classifier sees the whole gallery as a numbered strip
(
gallery-strip.jpg) beside each image, so it can tell. - Brand logo card, brand graphic (logo + claim on a flat color), retail box, multi-panel collage, several colors side by side: skip.
A few borderline frames (a lifestyle photo with a lot of text) can land either way between runs. That costs a skipped frame or a few cents, never a broken image: every transform still goes through the verify gate.
The Christmas is RICH, on purpose: it has to read as Christmas at Amazon thumbnail size.
Only lifestyle images are transformed. Each gets a mode that picks its prompt (full detail
in the rules file):
scene: one large anchor (a lit, decorated tree in any home interior; a big wreath or lit garland elsewhere) plus 3 or 4 supporting elements, string lights generously. Clothing unchanged.winter: a summer scene becomes full winter: snow outdoors, the same people in winter clothing in the same poses, then the Christmas additions.sun: people in the water, at a pool or on a beach, or in swimwear. Never snow or coats: the same swimwear and sunshine, plus string lights, tinsel, a small decorated palm or tree, gifts, red and gold. It wins overwinter; the code also turns a winter pick whoseseenline names a pool, a beach, the sea or swimwear intosun. At most one Santa hat, on an adult. The scene line says what goes WHERE and what must stay exactly ("Keep the dog and the gray dog bed exactly as they are, nothing on the bed.").
Step 3: generate
python3 $S/santa.py generate santa/B0GYY18GYC
Parallel (4 workers), quality medium by default, ONE attempt per image, skips outputs that
already exist. Prompt = the fixed template from the rules file with the scene line dropped
in. Output size follows the original's aspect: square 1024x1024, landscape 1536x1024,
portrait 1024x1536. When the original's aspect differs from that by more than 3 percent (a
1500x659 skillet banner, say), the original is placed on a canvas of the output's aspect,
filled with its own border colour, and the output is cropped back to the original's frame.
Without that the model reframes and the product shrinks (measured: four Lodge frames came
back at 62 to 80 percent of their size). Each call appends its token usage and a cost estimate to cost.jsonl.
Measured: 14 to 17 seconds per image, four images in 17 seconds wall clock.
A moderation block or an API error is logged and skipped. No retry loop, ever: the SDK's own retries are off for edits, and every call has a timeout (edit 180 s, vision 60 s, one retry for vision), because four hung calls once held a run for five minutes. Disney-licensed products (Mickey, princess plates) are refused by OpenAI moderation; the report names them.
Step 4: verify
python3 $S/santa.py check santa/B0GYY18GYC
python3 $S/santa.py verify santa/B0GYY18GYC
check prints mean brightness before vs after and flags a drop over 8 percent; dark props
on a bright edge trip it, so it is a pointer, not a verdict. verify puts each pair in front
of gpt-5.4 with the nine questions from the rules file (same product, nothing added on it, not
darker, no tint, nothing floating, people unchanged, text intact, nothing banned, obviously
Christmas at thumbnail size), then MEASURES a tenth: same_framing. It finds the original's product inside the output at 55 to
120 percent scale (OpenCV template match) and fails the frame below 0.88 or when the product
moved more than 12 percent of the width. The model cannot judge size: it passed two of three
frames whose product had shrunk by a quarter; the measurement got twelve of twelve. Without
OpenCV the ninth check is skipped (check-env says so). It writes stage_ready, verdict,
checks, framing and, on a fail, a regen_note into plan.json; a framing fail puts
"keep the product at exactly the same size and position" into that note, and in the test
both shrunk frames came back at full size on their one retry.
Then OPEN every christmas/*.png beside its original yourself. Read the small text on the
product's own label at native size: a digit that changed is a note in the verdict, and the
vision pass can miss it. Your eye overrides the gate in both directions; edit plan.json.
One failing image gets ONE regeneration, the failure written into the scene (--note
overrides the stored regen_note):
python3 $S/santa.py regen santa/B0GYY18GYC 06.jpg --note "The tent must stay orange. No ribbon on the product."
python3 $S/santa.py verify santa/B0GYY18GYC --only 06.jpg
regen refuses a second retry by design. After it, the verdict says not stage ready and
why, and the run moves on.
Step 5: contact sheet + report
python3 $S/santa.py sheet santa/B0GYY18GYC
Writes contact-sheet.html (self-contained, thumbnails embedded, product title on top,
one before/after row per image, skipped images shown with their reason, the verdict under
each row) and report.md (transformed, skipped and why, cost per image and per listing).
Open the sheet for the user: open santa/B0GYY18GYC/contact-sheet.html.
What you tell the user at the end (single listing)
Four lines, then the paths. Which images were transformed and which skipped (with the
one-word reason), the per-image verdicts with anything that drifted, the estimated cost
of the run, and the two paths: santa/<ASIN>/christmas/ and
santa/<ASIN>/contact-sheet.html. The images are ready to upload as they are.
Catalog mode: a whole store
Same engine, same rules, every product in the store, one folder per ASIN, and one page that shows the store dressed. Two commands.
C1: discover
Pass what the user pasted, as it came. Links from any Amazon marketplace, tracking junk and
all, app share links (amzn.to, a.co), commas or new lines between them:
python3 $S/catalog.py discover "<everything the user pasted>"
python3 $S/catalog.py discover my-products.txt # the same, in a file
python3 $S/catalog.py discover "Active+Listings+Report.txt" # Seller Central, asin1 column
python3 $S/catalog.py discover "Active+Listings+Report.txt" --host www.amazon.co.uk
Every item becomes one ASIN on its own marketplace (a .co.uk link is fetched from
amazon.co.uk). A share link costs one HEAD request; its first redirect already names the
ASIN. Duplicates collapse. Anything that is not a product (a non-Amazon link, a store page,
a search page) is printed as skipped ...: why and the rest go ahead; relay the skipped
lines to the user.
Also accepted, second choice: the seller page https://www.amazon.com/s?me=SELLERID (walked
page by page, one attempt each, stops on the first bot check and keeps what it read), or the
.html of that page saved from the browser. A Brand Store or /shop/ link is refused with
the words to ask for a list instead; relay them.
No Amazon at all: discover path/to/store/ (its subfolders are one product each) or
discover folderA folderB. Each folder holds its images in gallery order by name (01 is the main
image); an optional product.json ({"asin": "B0...", "title": "..."}) names the product and its
output folder, else the folder name does. run, status and index work the same.
Writes santa-catalog/<name>/catalog.json and prints how many products were found, how many
were taken (default cap 50, in the order given; --max 300 takes 300, --max 0 all), and a
rough cost and time estimate, and the run line with a budget that covers it. Show those
lines to the user, then start C2 straight away with that run line. Do not stop to ask: this
is an autopilot skill and the budget is the answer. Only a user who asked for a dry run, or
named their own budget, changes that.
C2: run
python3 $S/catalog.py run santa-catalog/my-store --budget 10
python3 $S/catalog.py run santa-catalog/my-store --budget 10 --dry-run # fetch + classify, no spend
Phases, in order, each one resumable (run the same command again and it continues):
- Fetch. Every listing, one attempt, polite pause. Three captchas in a row PAUSE the run with a message; the same command an hour later retries the blocked ones once.
- Dedupe. An image already fetched for another ASIN (bundles, variations share galleries) is generated once and copied into every folder that uses it. No double spend.
- Classify. The vision pass fills every plan.json. Duplicates never reach it.
- Cost gate. Prints the count, the estimate (about $0.087 an image at medium), what was spent so far, the budget and an ETA.
--dry-runstops here. Otherwise the run generates as many images as the budget covers, in catalog order, and names how many it held back. Nothing asks; the budget is the answer. - Generate. One pool, 4 workers across the whole store.
- Verify. Every pair through the eight questions plus the measured framing check. Every failing image gets its one
regeneration with the
regen_notein the scene, then is verified again. Regenerations count against the budget too. - Finish. Duplicate outputs copied across, a
contact-sheet.htmlandreport.mdper ASIN, thenindex.htmlandreport.mdat the top: the whole store, every Christmas image, hover for the original, a redcheckbadge on anything not stage ready.
python3 $S/catalog.py status santa-catalog/my-store # one line per product
python3 $S/catalog.py index santa-catalog/my-store # rebuild the top page from disk
Measured 26.9.2026 on a three-product store (13 gallery images, 9 unique): discover 3 s, run about 2 minutes end to end, images about $0.30 at low.
What a 300-product catalog does
Only arrives as a Seller Central report or a long paste, so the seller chose it. Discover
takes the first 50 in the order given and says how to take the rest (put the gift-worthy
sellers first). At four transforms a product, 300 products is 1,200 images, about $105 at
medium and about 3.5 hours. The run spends up to --budget and stops, so a $105 catalog
with a $35 budget does the first third and says so; the same command with --budget 105
finishes it. Listing pages are fetched one at a time with a pause, so 300 take
about 20 minutes, and three bot checks in a row pause the run instead of burning the address.
What you tell the user at the end (catalog)
Four lines, then two paths. Products done of products found; Christmas images made and
how many passed every check; estimated cost and wall time; anything held back (budget,
blocks, listings with fewer than two images). Then open the page for them
(open santa-catalog/<name>/index.html) and give the path of the per-ASIN folders. The images
are ready to upload. Remind them that originals/ is their way back in January.
Spot-check before you hand it over: open index.html, then open two or three contact sheets and look at the product in each pair yourself. The gate is good; it is not you.
Cost and quality
One run at medium; what comes out is ready to upload. Measured 26.9.2026 on the same four
frames at each quality (text, faces, props):
| quality | per image | per image, wall | what it looks like |
|---|---|---|---|
| low | $0.025 | 15 s | text identical; drawn elements (a Santa hat, skin) a little waxy |
| medium (default) | $0.087 | 41 s | natural fur and skin, the same text |
| high | $0.298 | 118 s | no visible gain over medium |
Text is identical at every quality because this is an edit: most pixels come from the
original. Do not run low as a preview and high as the final: a second run at another quality
is a NEW image, not a render of the one that was approved. QUALITY=low stays available for a
cheap first try on a product or two. Classify is about $0.002 per image and verify about $0.004
per pair on gpt-5.4. A listing with six secondary images is about $0.55 at medium. The estimate
uses published per-token rates; the OpenAI usage page has the exact figure.
Amazon policy notes
- Main image stays white. This skill never writes to
01. - Amazon wants at least 1000 px on the longest side: 1024 passes. For a sharper zoom, upscale to 1500 or 2000 with any upscaler; a high-quality re-run does not add pixels.
- Seasonal images are secondary images. Sellers swap them in for Q4 and swap them out
in January; keep
originals/so they have the way back.