Last October I ran my batch upload twice. Same folder, same command, eleven hours apart. I had forgotten which assets I had already submitted to Adobe Stock, so I re-ran the whole thing. Adobe's review caught the duplicates and my account got a warning. That warning is the reason upload_tracker.py exists. This post is about the four scripts that keep my pipeline from making that kind of mistake again, and what I learned packaging 39 products with them.

The CSV header is a contract, not a suggestion

Every stock platform validates your metadata file before it even looks at your images. Adobe Stock wants a five-column CSV with an exact header order. Vecteezy wants four columns. Dreamstime wants fifteen. Get one column wrong, or put the header in the wrong order, and the upload is rejected before your art is ever reviewed. No partial credit.

My first month, I wrote these CSVs by hand in a spreadsheet. I got the Adobe header right eventually, then pasted it into the Vecteezy file and got rejected, because the column count was wrong. That was the moment I stopped hand-editing and wrote make_metadata.py. You point it at a folder of files, tell it the platform, and it writes the header that platform demands, and nothing else:

python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv --ext .jpg python make_metadata.py ./my-assets --platform dreamstime --out dreamstime.csv --ext .jpg 

By default it title-cases the filename and uses that as the title. That is fine for a first pass, but replace it before you submit. I keep a titles.json with real titles and keywords per asset, and pass it with --titles. The script uses only the Python standard library. No install, no dependencies, nothing to break.

The ledger that stops double-posting

The double-post incident taught me that memory is not a system. Now every asset that goes to a channel gets logged in a plain text ledger by upload_tracker.py:

python upload_tracker.py pending items.txt # what is NOT done yet python upload_tracker.py done "asset-001.jpg" python upload_tracker.py status 

Before any batch run, I check pending first. The re-run problem is gone because the re-run reads the same ledger. This is the least glamorous script in the kit and the one that has saved me the most trouble. It also uses the standard library alone.

Turning a document into a product

Half my products are not art at all, they are documents. Checklists, guides, templates with instructions. The content existed as Markdown long before it looked sellable. The gap between "I have a good doc" and "I have a product" is formatting, and md2pdf.py closes it in one command:

python md2pdf.py content.md product.pdf "My Product Title" 

It handles headings, tables, lists, and links, using reportlab under the hood. That is one of only two third-party packages in the whole kit. The other is Pillow. Everything else is standard library, which matters more than it sounds, because dependency rot kills small pipelines quietly.

Packaging: a zip with a real cover

A bare file does not sell. A zip with a proper cover does. pack_product.py bundles the product folder and renders two images: a 1280x720 cover and a 600x600 thumbnail:

python pack_product.py my-product "My Product" "A short subtitle" 9 

It writes my-product.zip, the cover, and the thumbnail into a _system folder, and you can redirect that with --dir or the PACK_ROOT environment variable. Pillow renders the images. Two sizes, fixed dimensions, done. I stopped agonizing over covers once I accepted that a clean, consistent template beats a unique design every time.

The tiny server rules

None of this ran on a beefy machine. The whole pipeline runs on 1 vCPU, 2 GB RAM, no GPU. That constraint forced habits that are worth more than the hardware they replaced:

  • One process at a time. Never fan out image jobs in parallel on a single core. It does not go faster, it just thrashes.
  • Call gc.collect() after each asset. Python will not free memory promptly on its own, and on a 2 GB box that adds up over a batch of a few hundred files.
  • Measure peak RSS, not average. resource.getrusage(RUSAGE_SELF).ru_maxrss tells you the real ceiling. Average memory numbers lied to me twice before I started watching the peak.
  • One file per subprocess for heavy render steps, so memory is actually returned to the OS between items.
  • Render at the resolution you need. Inspecting a preview at 60 dpi instead of 300 cut one render step's memory use dramatically.

The full catalogue so far is 39 products, about 856 MB of output, built on top of roughly 19,000 lines of pipeline code and over 300 pages of research behind the rules these scripts enforce.

The folder layout that keeps it sane

The last thing that matters is structure. A catalogue becomes a junk drawer without fixed folders, so the kit assumes one:

project/ assets/ raw output, one file per asset output/ ready-to-submit files + metadata.csv packs/ finished zips + covers research/ notes and sources scripts/ these tools 

One warning from a mistake I made exactly once: keep research/ forever. Your notes and logs are the raw material for future products. I deleted a research folder during a cleanup pass and spent a weekend reconstructing it. That will not happen again.

These four scripts are the packaging layer of a larger system. Under them sits a free stack: Hermes Agent as the orchestrator that schedules jobs and retries failures, and 9Router as a local proxy for LLM access, both at zero cost. But the packaging layer is where most solo makers stall, and it is the part I use every single week.

I wrote all of this up, with the scripts themselves, as the ai sticker factory playbook. It is the tool built from this work, not a tutorial that ends at "here's how you'd do it". If any of the failure modes above sound familiar, it will save you the warnings I earned the hard way.

Get ai sticker factory playbook

Building digital products? Free guides & kits for digital product sellers.