AI & Automation
Seasonal Catalog Media Pipeline
~1,050 images, 166 products, and a plan you review before anything is written
2026Internal tooling
At a glance
Credit
- Built by
- Denandro YusufDesign · Implementation · The naming convention and the dry-run-by-default rule · The season run and its runbooks
Run the demo synthetic data
- Problem
- Each drop meant about 1,050 images bound to 166 products by hand in WordPress.
- What I did
- I built eight tools and made dry-run-by-default a rule for all of them.
- Outcome
- Binding images by hand became a reviewable plan you approve and apply.
Explore
The component graph, from the source repository. Follow the arrows: left to right, and down where two steps share a column. The burgundy bars are where decisions are made; the numbers on the lines are the key flows, written out under the graph.
Key flows
- Design export to media_refresh: re-exports
- Catalog CSV to Filename matcher: product index
- Catalog CSV to ws_link: colour order
- photos_sync to WordPress REST: upload missing
- media_refresh to WordPress REST: delete + re-upload
- ws_link to WordPress REST: PATCH ACF
- Reconciliation tools to WordPress REST: read
- Media library to ws_link: resolve ids by slug
- media_refresh to Audit reports + log: appends log
Go deeper
The full account
- Problem
- A seasonal drop meant getting roughly 1,050 images into WordPress under a strict naming convention, then telling each of 166 products which images are its own, in which order, with which colour label. Source filenames come from a design export and do not match the catalog's colourway names — colours appear in a different order, get abbreviated, and style names contain typos, so a naive string match silently binds the wrong swatch to the wrong colourway.
- What was built
- Eight standalone Python CLI tools, standard library plus Pillow for downscaling, forming a staged pipeline from a design export to live collection pages — every writing tool dry-run by default.
- Role
- Designed and built the pipeline and its safety regime. Set the naming convention, made dry-run-by-default a rule of the toolkit rather than a per-script nicety, built the matching heuristics and the delete-then-upload refresh path, and ran the season with the runbooks that let someone else operate it.
- What changed
- Binding each image to its product by hand in the media library and ACF editor became a reviewable plan you approve and apply.
Context
A first attempt to automate this through a hosted workflow tool failed for an infrastructure reason recorded in the code: the site's shared host blocks the automation vendor's cloud IP ranges. The pipeline had to be rebuilt to run from a local machine.
WordPress also cannot replace a media file's bytes in place. Refreshing a re-exported image is a delete-then-upload — a destructive operation nobody wanted to run blind across a thousand files on a flaky shared host.
Architecture and the system
Eight single-purpose CLIs over one shared matcher and one catalog CSV. Every tool that writes defaults to a dry run and requires an explicit --apply; destructive actions require two flags.
Typo-tolerant matching, human-reviewed
A shared matcher uses token sequence and set matching with yarn-type and word-order tiebreaks to map a design-export filename onto a product and colourway from the catalog CSV.
It is deliberately not trusted blindly: every run prints its plan, and reviewing the matches is a documented operator step. The tool is confident enough to be useful and honest enough to be checked.
Delete-then-upload, one file at a time
The refresh tool compares local file sizes against the media library's recorded sizes and classifies every file as NEW, CHANGED or SAME. On apply, it deletes and re-uploads one file at a time, so a product is never imageless for more than a couple of seconds.
An interrupted refresh can simply be re-invoked; uploads skip what already exists.
Binding swatches in the catalog's own order
The linking tool writes the ACF payload for each product — style, yarn, code, price, collection, section, sort order, plus ten swatch image and name pairs in the catalog's own colour order — and picks the featured image to match swatch one.
Assignment uses a bitmask dynamic-programming pass so the whole set of swatches for a product is solved together rather than greedily one at a time.
Reconciliation as an artifact
Three read-only tools reconcile the live site against the catalog and write duplicate, missing and extra reports to disk. Those files are the record of what still needs a human — the pipeline's output includes its own to-do list.
What was hard
A destructive operation on an unreliable host
Delete-then-upload across a thousand files, on shared hosting, with no transaction. The mitigations are unglamorous and they are the whole project: classify before acting, act one file at a time, make re-running safe, and log every operation to an append-only file.
Names that almost match
Abbreviated colours, reordered words and typos in style names mean exact matching fails and fuzzy matching binds the wrong swatch. Token-based matching with tiebreaks got the hit rate high enough to be useful, and a mandatory dry-run review handles the rest.
Writing tools someone else can operate
Every script opens with a runbook in second person — what must be next to it, the exact commands for dry-run and full runs, what happens to odd cases, and what to run next. They were written to be handed over, not to be run only by their author.
AI and automation
Automation
Replaces renaming 527 swatch files by hand, dragging around 1,050 files into the media library, and opening each of 166 product records to attach up to ten swatch images and ten colour labels in the right order. Deliberately left manual: the export, reviewing every dry-run plan, reviewing typo-tolerant matches, and deciding what to do with extras.
Direction and delivery
The build and its discipline are mine: safety-by-default applied across the whole toolkit rather than per script, with destructive actions gated behind two flags. The record of each run is kept in the artefacts: an append-only timestamped run log, and three audit output files kept in the working directory as the list of what still needs attention.
Size signals
- CLI tools
- 8
- Lines of Python
- 2,145
- Third-party dependencies
- 1 (Pillow)
- Products bound
- 166
- Colourways
- 518
- Images per season
- ~1,050
Counted from the source repository. No impact metric is claimed that the source does not prove.
What I would tell the next person
- Dry-run by default is not a nicety on a pipeline with delete semantics. It is the product.
- Write the docstring as a runbook for the person who is not you.
- When infrastructure blocks the elegant approach — a vendor's IP ranges blocked by shared hosting — rebuild rather than fight it.
Technologies and access
- Python 3.14
- Standard library + Pillow
- WordPress REST API
- Advanced Custom Fields
- Application Passwords
- CSV
- WebP / PNG
Integrations
- WordPress REST (media listing, upload, force-delete, ACF writes)
- Advanced Custom Fields via the product endpoint
Runs against a password-protected wholesale catalog. Product imagery is an unreleased season and is not shown.

