DEV Community

Cover image for The Two-Layer QC Setup That Kept 39 Generated Products Out Of The Reject Pile

The Two-Layer QC Setup That Kept 39 Generated Products Out Of The Reject Pile

The morning I learned what a reject pile costs, I had opened my Adobe Stock contributor dashboard and found 12 of my last 20 submissions sitting in "Rejected". Not for the art. For metadata. One CSV had a title column that was one column short. Another had keywords that were just the filename split on underscores. Eleven hours of generation and rendering, thrown out over a header row.

That was the day I stopped treating quality control as a final glance and started treating it as two separate layers, each with its own job. Since then, the pipeline has packaged 39 finished products, 856 MB of output, and the reject rate on new submissions is effectively zero. Here is what the two layers actually are, and why they work.

Layer one: the metadata layer, checked before anything ships

Every stock platform validates your CSV before a human ever sees your files. Adobe Stock expects a 5-column upload sheet. Vecteezy expects 4 columns. Dreamstime is the heavy one at 15 columns. If your header row does not match what the uploader demands, the batch fails instantly, and you find out only after waiting for the upload.

So the first QC layer is mechanical: a script, make_metadata.py, that builds each platform's CSV from a folder of files, with the exact header that platform requires and nothing else. No framework, standard library only, so there is nothing to break. I run it three times per asset folder, once per platform:

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
Enter fullscreen mode Exit fullscreen mode

The check is simple: open the CSV, compare the header against the platform's published spec, count the columns. It takes thirty seconds and it is the difference between a submission and a rejection. Without a --titles file, the script title-cases the filename as a placeholder, which is fine for a first pass but I always replace it before submitting. Filenames are not titles, and reviewers can tell.

Layer two: the ledger layer, because double-posting is the real killer

The second layer has nothing to do with metadata and everything to do with account survival. The fastest way to get flagged on any stock platform is submitting the same file twice. When you run automated batches, running a batch twice is not an edge case, it is a certainty. You re-run because a download timed out, because you fixed one file, because you forgot whether you already did it.

upload_tracker.py is a tiny ledger, also standard library only, that remembers what has been posted:

python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Enter fullscreen mode Exit fullscreen mode

The rule is that no file leaves the output folder unless the tracker says it is pending. This one habit has prevented more damage than any image check ever has. A wrong keyword gets one asset rejected. A duplicate submission puts the whole account under review.

Why the two layers have to be separate

I tried doing all QC in one pass at first, checking files, metadata, and posting status in a single script run. It failed for a boring reason: when one check fails, the whole batch stops, and you lose track of which layer caught the problem. Separating them means the metadata check can run on 40 files in seconds, and the ledger check runs at posting time when it is the only thing that matters.

There is a third check I folded into packaging rather than QC: every product gets a real cover and thumbnail, 1280×720 and 600×600, rendered by pack_product.py with Pillow. A zip with a proper cover sells, a bare file does not, and rendering at the resolution you actually need keeps the pipeline honest. Which brings me to the hardware.

All of this ran on 1 vCPU and 2 GB RAM

No GPU, no cloud account. The habits that make it work are not clever, they are disciplined:

  • One process at a time. Never fan out image jobs in parallel on a single core.
  • Call gc.collect() after each asset so memory does not creep across a long batch.
  • Measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average. Peak is what crashes the box.
  • One file per subprocess for heavy render steps, so memory returns to the OS between items.
  • Inspect previews at 60 dpi instead of rendering at 300. You are checking composition, not print quality.

The whole thing sits under a free stack: an orchestrator agent for scheduling and retries, and a local LLM proxy, both at $0 per month. The four scripts are the packaging layer on top.

What the 39 products taught me

The pattern across every rejection I have had, and every one I have heard about from other makers, is that the art is almost never the problem. The failure is in the boring layer: a wrong header, a duplicate post, a missing cover, a filename used as a title. The 300+ pages of research behind these rules mostly document platform requirements that nobody reads until they get burned.

That research and those scripts became the compliance metadata kit. It is the same tooling described here: the CSV builders for Adobe, Vecteezy, and Dreamstime, the upload ledger, the PDF and packaging scripts, plus the notes on running it all on a 2 GB box. If you are building a pipeline and only want to borrow the parts that keep you out of the reject pile, the metadata and tracker scripts alone need nothing but Python installed.

Get compliance metadata kit


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

Top comments (0)