All work

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.

Seasonal Catalog Media Pipeline: architectureData flows through 13 components: Design export (Source: 527 swatches + 524 photos); rename_swatches (Decision logic: Export naming → site convention); Catalog CSV (Source: 166 products · 518 colourways); Filename matcher (Decision logic: Token matching, typo-tolerant); ws_link (Service: Bitmask-DP swatch assignment); media_refresh (Service: Size diff → delete + re-upload); photos_sync (Service: Uploads only what is missing); Reconciliation tools (Decision logic: Read-only; opt-in dupe deletion); Audit reports + log (Output); WordPress REST (External: App password · throttle + backoff); Media library (Store); Product ACF fields (Store: swatch_1..10 image/name pairs); Collection pages (Interface: Renders swatches by field order). Connections: Design export to rename_swatches; Design export to photos_sync; Design export to media_refresh (re-exports); Catalog CSV to Filename matcher (product index); Filename matcher to photos_sync; Filename matcher to media_refresh; rename_swatches to media_refresh; 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); WordPress REST to Media library; WordPress REST to Product ACF fields; Media library to ws_link (resolve ids by slug); Product ACF fields to Collection pages; Media library to Collection pages; Reconciliation tools to Audit reports + log; media_refresh to Audit reports + log (appends log).Design export — 527 swatches + 524 photosDesign export527 swatches + 524photosrename_swatches — Export naming → site conventionrename_swatchesExport naming → siteconventionCatalog CSV — 166 products · 518 colourwaysCatalog CSV166 products · 518colourwaysFilename matcher — Token matching, typo-tolerantFilename matcherToken matching,typo-tolerantws_link — Bitmask-DP swatch assignmentws_linkBitmask-DP swatchassignmentmedia_refresh — Size diff → delete + re-uploadmedia_refreshSize diff → delete +re-uploadphotos_sync — Uploads only what is missingphotos_syncUploads only what ismissingReconciliation tools — Read-only; opt-in dupe deletionReconciliationtoolsRead-only; opt-in dupedeletionAudit reports + logAudit reports + logWordPress REST — App password · throttle + backoffWordPress RESTApp password ·throttle + backoffMedia libraryMedia libraryProduct ACF fields — swatch_1..10 image/name pairsProduct ACF fieldsswatch_1..10image/name pairsCollection pages — Renders swatches by field orderCollection pagesRenders swatches byfield order123456789

Key flows

  1. Design export to media_refresh: re-exports
  2. Catalog CSV to Filename matcher: product index
  3. Catalog CSV to ws_link: colour order
  4. photos_sync to WordPress REST: upload missing
  5. media_refresh to WordPress REST: delete + re-upload
  6. ws_link to WordPress REST: PATCH ACF
  7. Reconciliation tools to WordPress REST: read
  8. Media library to ws_link: resolve ids by slug
  9. 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.