Last spring I lost a whole evening to a CSV header. I had 40 assets ready for Adobe Stock, generated the metadata file myself in a spreadsheet, hit upload, and the whole batch bounced back instantly. The problem was the header row. Adobe's uploader expects a specific 5-column format, and I had a 4-column one because I'd been looking at Vecteezy's format, which uses 4 columns. Same mistake in reverse a week later on Dreamstime, which wants 15 columns. Three platforms, three different header widths, and every rejection costs you a review cycle before you even get to the actual review queue.
That night I stopped hand-building CSVs and wrote a script instead. This post is about why the cheapest possible stack, two Python packages and a handful of standard library scripts, turned out to be the right answer for a solo asset pipeline.
The metadata problem is a format problem, not a content problem
The stock sites don't reject your keywords or your titles first. They reject the file structure. Adobe's bulk upload CSV has a fixed 5-column header, Vecteezy uses 4, Dreamstime uses 15. Get one column wrong and the upload fails before a human ever sees your work.
So make_metadata.py does one thing: it walks a folder of files and writes the exact header each platform demands, then fills the rows from filenames and an optional titles JSON. No spreadsheet, no copy-paste between tabs. The script itself uses only the Python standard library, csv, pathlib, argparse, nothing to install. That matters more than it sounds, because a metadata tool that needs a virtual environment is a tool you'll skip when you're tired.
The honest caveat: without a titles file, the script title-cases your filename and uses that as the title. That's fine for a first pass and bad for a final one. Replace it before you submit. Filename titles read like filename titles.
Double-posting is the fastest way to get flagged
This one cost me a warning, not just time. I re-ran a batch script after a partial failure and posted a set of assets twice. Duplicates in a review queue look exactly like what they are, a pipeline with no state.
The fix is embarrassingly simple. upload_tracker.py is a small ledger with three commands: pending shows what hasn't been posted, done marks an item, status gives you the overview. Any batch job checks the ledger first. A re-run can no longer double-post because the script refuses to process what the ledger says is finished.
It's a text file and some file locking logic, all standard library. I considered a database and decided a flat file I can read with cat was worth more than query power I don't need at this scale.
Two packages, chosen on purpose
Everything visual in the pipeline comes down to two dependencies: Pillow for rendering covers and thumbnails, and reportlab for turning Markdown-structured content into styled PDFs. That's the whole requirements.txt.
md2pdf.py takes a Markdown file and produces a clean PDF with real headings, tables, lists, and links in one command. pack_product.py bundles a product folder into a zip and renders a 1280×720 cover plus a 600×600 thumbnail. A zip with a real cover sells. A bare file looks like homework.
Everything else is stdlib. Fewer dependencies means fewer breaking changes, smaller install, and nothing to debug at 11pm when pip resolves a conflict you didn't ask for.
The 2 GB server habits
The whole pipeline runs on 1 vCPU, 2 GB RAM, no GPU. It works because of discipline, not horsepower:
- One process at a time. Never fan out image jobs in parallel on a single core.
- Call
gc.collect()after each asset so memory doesn't accumulate across a batch. - Measure peak RSS with
resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage. Peak is what kills the box. - Run heavy render steps one file per subprocess so memory is actually returned to the OS.
- Inspect previews at 60 dpi instead of rendering at 300. You only need full resolution for the final output.
The AI image generation itself is the only real cost in the system, and it's small when you generate deliberately instead of spraying prompts and hoping. Everything around it, metadata, tracking, PDFs, packaging, costs nothing because it's scripts you own.
Where the numbers landed
This setup packaged 39 finished products, about 856 MB of output, from roughly 19,000 lines of pipeline code, backed by over 300 pages of research on what the platforms actually accept. All of it validated on that same 1 vCPU / 2 GB box.
I'm not claiming the scripts are clever. They're boring on purpose. Boring is what survives a re-run at midnight.
I put the four scripts, the folder layout, and the install notes into one download because other makers kept asking for the exact files rather than a description of them. It's the packaging layer of the pipeline, not a course, and it assumes you can run Python 3.9. If that's useful to you, it's here:
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)