I lost forty minutes on a Tuesday because of a CSV header. Not the art, not the keywords, the header. I had submitted a batch of 12 JPGs to Adobe Stock and every single one came back rejected in under a minute. The error was blunt: wrong file format for the metadata. I had written my own column order. Adobe has a fixed 5-column header and it does not negotiate.
That was the moment I stopped treating "finished" as a feeling and started treating it as a checklist. This post is about what that checklist turned out to be, and how it runs on a server with 1 vCPU and 2 GB of RAM.
Finished means the header matches, exactly
There are three upload CSVs I deal with and each has a fixed shape:
- Adobe Stock: 5 columns. Filename, title, keywords, category, releases. One wrong column name and the whole batch bounces.
- Vecteezy: 4 columns. Same deal, fewer columns, same instant rejection.
- Dreamstime: 15 columns. The widest of the three, and the easiest to fumble by hand.
The lesson is boring and absolute: the header is not something you write, it is something you reproduce. I wrote make_metadata.py so it emits the exact header each platform demands and nothing else. It walks a folder of assets, pulls titles and keywords (from a titles.json if you supply one, from the filename if you do not), and writes the CSV. Filename-as-title is fine for a first pass, but replace it before you submit, reviewers read those.
When "done" is defined by a byte-exact header, a file is either finished or it is not. There is no almost.
Finished means it cannot be posted twice
The second failure mode came from my own batch runner. I re-ran an upload script after a partial failure and nearly double-posted a chunk of my catalogue. Double-posting is the fastest way to get flagged on these platforms, and on a 2 GB box with a cron job you will re-run things, that is guaranteed.
So upload_tracker.py is now part of my definition of finished. A file is finished when the ledger says it is done. Three commands:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Standard library only, no install. A re-run checks the ledger first. This is the least glamorous script I have written and the one I would keep if I could only keep one.
Finished means it renders, on 2 GB, without dying
My server has 1 vCPU, 2 GB RAM, no GPU. Every image job I run competes with the OS for that memory. The habits that keep the pipeline alive:
- One process at a time. No parallel image jobs on one core, ever.
-
gc.collect()after every asset, so Python actually hands memory back. - Measure peak RSS, not average, with
resource.getrusage(RUSAGE_SELF).ru_maxrss. Average lies, the peak is what kills the box. - Heavy render steps run one file per subprocess, so memory returns to the OS when the process exits.
- Inspect previews at 60 dpi, render finals at 300. Rendering at 300 just to check a layout is how you OOM yourself.
A file is finished when it survives the full batch on this machine. If it only works on my laptop, it is not finished.
Finished means it looks like a product
The last gap was packaging. I had folders of good files that sold nothing, because a bare zip with no cover does not read as a product. Two scripts closed that:
-
md2pdf.pyturns a Markdown doc into a styled PDF (headings, tables, lists, links) with reportlab. One command:python md2pdf.py content.md product.pdf "My Product Title". -
pack_product.pyzips the files and renders a 1280×720 cover plus a 600×600 thumbnail with Pillow. It writes into a_systemfolder, and you can redirect it with--dirorPACK_ROOTso nothing gets hardcoded.
Only two third-party packages in the whole kit, Pillow and reportlab. Everything else is standard library, because every dependency is another thing that can break on a small server.
The folder layout that keeps 39 products from becoming a junk drawer
The structure I settled on:
project/
assets/ raw output
output/ ready-to-submit files + metadata.csv
packs/ finished zips + covers
research/ notes, scrape logs, sources
scripts/ the tools
One hard rule: never delete research/. I did that once to "clean up" and lost raw material I later needed. The 300+ pages of research behind the rules in these scripts exist because I stopped throwing notes away.
Where this stands
The full pipeline has produced about 1,700 assets and 58 are live on Adobe Stock. The first sale was $0.99, and I am not going to pretend that number is anything other than a start. The 39 finished products from this system total 856 MB of output, packaged by the same four scripts, running on the same 2 GB machine. Around 19,000 lines of pipeline code sit under all of it.
I bundled the four scripts themselves, with the real headers, the ledger, the PDF renderer, and the packager, into a small kit. It is the packaging layer of a larger system I call the digital asset factory. If you want the exact tools instead of a description of them, that is it. No framework, no cloud account, Python 3.9 and up.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)