I still have the rejection email open in a tab. Sixty-four assets, a full weekend of rendering, and Adobe Stock bounced the whole batch because my CSV had six columns in the header row instead of five. No partial accept, no warnings. The upload just failed and I had to rebuild the metadata from scratch while the images sat there finished and useless.
That was the moment I stopped trusting "I'll just do it manually" and wrote four small Python scripts instead. They ran on a 1 vCPU, 2 GB RAM server with no GPU, no cloud account, and they packaged 39 finished products, about 856 MB of output. Here is what each one does and why the problem it solves is real.
Metadata is a format problem, not a writing problem
All three of the big channels validate the CSV header before they even look at your files. Adobe wants 5 columns. Vecteezy wants 4. Dreamstime wants 15. Get one column wrong and the upload rejects instantly, the same way mine did.
So make_metadata.py writes the exact header each platform demands and nothing else. You point it at a folder:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
It walks the folder, title-cases the filename into a title, and emits the CSV. If you have real titles and keywords, pass --titles titles.json and it uses those instead. The filename fallback is fine for a first pass, but replace it before you submit. A title like "Asset 001 Final V2" reads as automation to a reviewer, and reviewers reject things that read as automation.
This script and the tracker use only the Python standard library. No pip install, no dependencies to break.
Double-posting is the fastest way to get flagged
I ran a batch twice early on because I lost track of what had already gone up. That is the classic mistake. Re-submitting the same files across accounts or in duplicate looks like spam to platform review systems and it puts your account at risk.
upload_tracker.py is a plain ledger. Before you upload, you check:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
A re-run now shows you exactly what is not done yet. It is not clever. It is a text file with a few commands around it, and that is the point. Clever systems fail quietly on a small server. A ledger does not.
Turning a document into something that looks like a product
You can have genuinely good content in a Markdown file and still have nothing sellable, because a raw .md file is not a product. md2pdf.py takes one Markdown file and renders a styled PDF with headings, tables, lists, and working links:
python md2pdf.py content.md product.pdf "My Product Title"
It uses reportlab, which is one of only two third-party packages in this whole pipeline. The other is Pillow. Everything else is standard library. I kept the dependency count at two on purpose, because every extra package is another thing that can break on a 2 GB box with no one watching it.
A zip with a cover sells, a bare file does not
The last step is packaging, and it matters more than I expected. A zip with a real 1280×720 cover and a 600×600 thumbnail looks like something a store should list. A naked zip looks like a download accident.
pack_product.py does both in one command:
python pack_product.py my-product "My Product" "A short subtitle" 9
It writes the zip, the cover, and the thumbnail into a _system folder. You can redirect output with --dir or a PACK_ROOT environment variable, so you never have to edit the script. Pillow renders the images, which is cheap enough to run one at a time on a single core without swapping.
The small-server habits that made this work
None of these scripts are heavy, but the rendering steps around them were, and the 2 GB ceiling forced habits worth copying:
- One process at a time. Never fan out image jobs in parallel on a 1-core box.
- Call
gc.collect()after each asset so memory actually frees between items. - Measure peak RSS, not average.
resource.getrusage(RUSAGE_SELF).ru_maxrssgives you the real ceiling your process hits. - Run heavy render steps as one file per subprocess so memory returns to the OS.
- Inspect previews at 60 dpi instead of rendering at 300. You only need full resolution for the final file.
The whole thing ran against roughly 19,000 lines of pipeline code in the parent project and about 300 pages of research behind the rules these scripts enforce. The scripts themselves are small. The rules inside them are not.
Where to get them
I bundled the four scripts, the folder layout I use (assets, output, packs, research, scripts), and the install and quick-start commands into a package called the Pipeline Starter Kit. It sits under a free stack: a local orchestrator for scheduling and retries, and a local LLM proxy, both at $0 per month. The kit is the packaging layer, the part that turns finished files into listed products.
It is the same tooling that packaged those 39 products, not a cleaned-up rewrite. If you are at the stage where your assets are done and the metadata CSV is the thing standing between you and an upload, that is exactly the problem it was built for.
If you want the art side too, the vector prompt pack is the prompt engine that fed this pipeline in the first place.
Building digital products? Free guides & kits for digital product sellers.
Top comments (0)