Last March I ran a batch of 40 uploads to Adobe Stock overnight. I woke up to a rejection screen and a bad feeling. The art was fine. The CSV was not. One wrong header column and Adobe's uploader rejects the file before it even looks at your images. That morning I learned that the prompt engine was the easy part, and everything after it was where the product actually gets made.
Here is what broke, what fixed it, and the numbers behind it.
The metadata header is a gate, not a suggestion
Adobe's upload CSV has a fixed 5-column header. Vecteezy wants 4 columns. Dreamstime wants 15. If you hand any of them a file with the wrong shape, the review check fails instantly, no partial credit, no "close enough". I lost a full batch to a header that was one column short.
The fix was boring: write the header the platform demands and nothing else. My make_metadata.py script now builds all three formats from one folder of files, with a --platform adobe, --platform vecteezy, or --platform dreamstime flag. It title-cases the filename as a fallback title, but I always pass a titles.json with real titles and keywords before submitting. Filenames like asset-017.jpg do not survive a human reviewer.
If you are building this yourself, do not try to make one universal CSV. Make one per platform, exactly to spec, and validate the column count before upload. Three formats, three headers, zero improvisation.
Double-posting is the fastest way to get flagged
The second failure was mine alone. I re-ran an upload batch after a network hiccup and posted 12 items twice. Duplicate submissions are the kind of thing that gets an account flagged, and I had no record of what had already gone out.
Now every batch runs through a ledger script, upload_tracker.py. It is a plain text ledger with three commands: pending shows what is not done, done marks an item, status gives the overview. It uses the Python standard library alone, no install. A re-run can never double-post because the ledger is checked before anything is submitted.
The lesson generalizes: any automated pipeline that touches an external account needs its own memory. Do not rely on your own recollection at 1 a.m.
A 1 vCPU, 2 GB server teaches you discipline
The whole pipeline runs on a single-core box with 2 GB RAM and no GPU. That constraint shaped everything. The habits that made it work:
- One process at a time. Fanning out image jobs in parallel on one core just thrashes.
-
gc.collect()after each asset, so memory does not creep across a 39-item run. - Measure peak RSS, not average, with
resource.getrusage(RUSAGE_SELF).ru_maxrss. Average told me nothing useful. Peak told me exactly when a render would OOM. - One file per subprocess for heavy render steps, so memory actually returns to the OS.
- Inspect previews at 60 dpi, render finals at 300. I once burned a whole evening debugging a memory spike that turned out to be me rendering inspection copies at full resolution.
None of this is clever. It is just what a small server forces on you, and honestly the constraint made the pipeline simpler, not worse.
Turning content into a product is its own step
I had Markdown documents full of real content and no way to make them look like products. The gap between "a doc" and "a sellable PDF" is styling: headings, tables, lists, links. md2pdf.py closes it with reportlab in one command: python md2pdf.py content.md product.pdf "My Product Title".
Packaging was the same story. A bare zip with no cover does not sell. pack_product.py bundles the files and renders a 1280×720 cover and a 600×600 thumbnail with Pillow, writing the zip, cover, and thumb into a _system folder you can relocate with --dir or PACK_ROOT. Two third-party packages total across the whole kit: Pillow and reportlab. Everything else is standard library.
The folder layout is the real product
The thing I underestimated most: a catalogue becomes a junk drawer without a fixed structure. The layout that finally held is five folders: assets/ for raw output, output/ for ready-to-submit files plus metadata CSVs, packs/ for finished zips and covers, research/ for notes and scrape logs, scripts/ for tools.
One rule I learned the hard way: keep research/ forever. I deleted mine once to "clean up" and lost the raw material for three future products. Scrape logs and notes are inventory, not clutter.
Where this ended up
The pipeline that came out of all this packaged 39 finished products, 856 MB of output, from about 19,000 lines of parent-project code and 300+ pages of research behind the rules the scripts enforce. All validated on that same 1 vCPU, 2 GB RAM box. The four scripts above are the packaging layer that sits under a free orchestration stack, with Hermes Agent scheduling jobs and 9Router handling LLM access locally at $0.
I did not set out to write a toolkit. I set out to stop losing batches. After the third time a friend asked me for "those CSV scripts", I cleaned up the four that actually did the work and put them in one place. No framework, no cloud account, Python 3.9+, and if you only need metadata and tracking you do not even need to pip install anything.
If you want the exact scripts rather than a description of them, they are here:
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)