Last March I lost four hours of batch work in about ninety seconds. I had 60 vector JPGs ready for Adobe Stock, generated a CSV by hand in a spreadsheet, hit upload, and got rejected on the first row. The header was wrong. Not the content, the header. Adobe's uploader checks the column names before it looks at a single file, and I had the columns in the wrong order. I fixed that, then Vecteezy rejected the same batch for a different reason: I had exported five columns when their importer expects four. That was the moment I stopped hand-building CSVs and wrote one script to do it forever.
Here is what I learned getting make_metadata.py right, because the details are the whole problem.
Each platform demands a different header, and none of them warn you
The three stock channels I sell through each expect a fixed column layout, and the uploader validates the header row before anything else happens.
Adobe Stock wants five columns: Filename, Title, Keywords, Category, Releases. That is the exact order. If you export extra columns, the uploader ignores them at best and complains at worst. Keywords are a single semicolon-separated string inside one cell, which is the part everyone gets wrong in a spreadsheet, because Excel and Google Sheets both want to reinterpret semicolons depending on your locale settings.
Vecteezy wants four columns, and the column count difference is exactly why you cannot reuse the Adobe file. Their importer reads a different set of fields, and a five-column Adobe CSV will fail or silently drop data.
Dreamstime is the one that pushed me to automate. Their upload CSV is fifteen columns. Nobody builds that by hand twice. Fifteen fields per row, per asset, across a folder of hundreds of files, is a full afternoon of copy-paste errors waiting to happen.
The script reads a folder, walks the files matching your extension, and writes the exact header each platform expects, nothing else. Filename becomes the title (title-cased), keywords come from a titles.json if you supply one, or fall back to the filename. The fallback is fine for a first pass, but replace it before you submit. Reviewers can tell when a title is just asset_0042.jpg cleaned up.
The re-run problem is worse than the CSV problem
About a month into running batches I did something dumber than a bad header. I re-ran an upload batch because I could not remember which files had gone up. Some assets got posted twice. Duplicate submissions are one of the fastest ways to get an account flagged on these platforms, and cleaning it up meant manually finding and deleting the dupes.
That is why upload_tracker.py exists and why it is the second script in the kit, not an afterthought. It is a plain ledger: mark a file done, ask for what is pending, check status. Before any batch run I check pending first. It uses only the standard library, so there is nothing to install and nothing to break. The rule I follow now: never trust memory about what was uploaded, trust the ledger.
One script, three outputs, zero dependencies
The part I am most satisfied with: make_metadata.py needs no pip install at all. It is pure standard library, csv and pathlib and argparse. Two third-party packages exist in the whole kit (Pillow for covers, reportlab for PDFs) and neither one touches the metadata path.
Usage is one line per platform:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv
python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv
Same folder, three correct files. When a platform changes its required columns, I fix one dictionary in one script instead of re-learning three spreadsheet templates.
Why this matters more on a small machine
The whole pipeline this came from runs on a 1 vCPU, 2 GB RAM server with no GPU. The metadata step is trivially cheap compared to image generation, which is exactly why it should be automated and boring. My habit is to generate assets in one careful pass (one process at a time, gc.collect() between items, measure peak RSS with resource.getrusage), then build all three CSVs in one shot at the end. The metadata script runs in well under a second on a folder of hundreds of files, so it costs nothing to re-run after a fix.
One practical note if you are generating AI images for stock: budget for generation failures and re-rolls. Every rejected or re-rolled image costs you generation credits or API spend, so the fewer review rejections you get from bad metadata, the less you pay per accepted asset. A correct CSV the first time is not cosmetic, it is part of your per-asset cost.
The folder structure that keeps this sane
None of this works without a fixed layout, because the script assumes it:
project/
assets/ raw output
output/ ready-to-submit files + the three CSVs
packs/ finished product zips
research/ notes, scrape logs
scripts/ the tools
Keep research/ forever. I deleted mine once during a cleanup and regretted it within weeks. Your notes and logs are the raw material for the next product.
Where to get the scripts
Everything above is in the Pipeline Starter Kit, the same four scripts that packaged 39 products (856 MB of output) from a pipeline of about 19,000 lines: make_metadata.py for the Adobe 5-column, Vecteezy 4-column and Dreamstime 15-column CSVs, upload_tracker.py for the double-post ledger, plus md2pdf.py and pack_product.py for turning docs and asset folders into sellable products. Python 3.9+, two optional packages, runs fine on a 2 GB box. It is the tool I built because I was tired of losing batches to a header row.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)