One Folder, Three Platform CSVs: Automating Adobe, Vecteezy, and Dreamstime Metadata

Treat asset metadata as a build artifact: scripts for headers, a ledger against duplicates, subprocesses for memory.
A stock asset seller turns multi-platform submission from hand-built spreadsheets into a scripted pipeline. make_metadata.py hardcodes the headers for Adobe Stock (5 columns), Vecteezy (4), and Dreamstime (15), generating three CSVs from one folder with headers that never drift. A plain-text ledger prevents duplicate uploads, a fixed directory layout keeps things findable, and on a 1 vCPU / 2 GB server the pipeline relies on serial execution, per-file gc.collect(), peak RSS monitoring, and one file per subprocess.
The most tedious part of selling stock assets isn't creating them — it's submitting them. The same batch of files has to become three CSVs with completely different headers, and the first thing every platform checks is the header: get one column wrong and the entire batch gets rejected. The author shares a complete solution that evolved from hand-building spreadsheets to a scripted pipeline, built on one core principle: treat metadata as a build artifact of code, not a document you edit by hand.

Headers Are the Gate — Manage Them Like Code
Adobe Stock needs 5 columns, Vecteezy needs 4, and Dreamstime needs 15. All three reject bad headers outright, before any human or model review — there's no such thing as "partial credit." The author learned this the hard way last March: a 120-row asset sheet had an extra sixth column, and pasting it into Adobe produced an instant parse error. Not a single row made it into review.
The root cause is assembling things by hand in a spreadsheet. A spreadsheet isn't a build artifact — it's a breeding ground for silent errors. The fix was to write make_metadata.py, hardcoding each platform's correct header into the script and only ever outputting that one format:
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
One folder, three CSVs, and headers that never drift — because they aren't typed out, they're fixed in code. When there's no title file the script falls back to a title-cased filename, which works for a first pass but should be replaced before submission — a filename is not a set of keywords.
CSV header validation: When asset platforms accept bulk uploads (usually CSVs or platform-specific spreadsheet templates), they first do a structural parse, reading the file in as a data table. This step only checks whether the column names, count, and order match the template exactly — it doesn't understand the semantics of the content. Because it's the gate that lets an entire batch into the review queue, any column deviation makes the parser fail on the whole table. Platforms typically don't tolerate errors row by row, and they won't give you a friendly "column N is wrong" message — just a parse error. This is why assembling sheets by hand in Excel is especially dangerous: merged cells, hidden columns, trailing commas, and BOM encoding can all silently add a column or change column order in the export. Hardcoding the header in a script essentially makes the table structure a version-controlled single source of truth, rather than relying on a manually copied template.
Duplicate Listings Are the Fastest Way to Get Flagged
The author made this mistake early on: reran a batch of jobs overnight, forgot that half of them had already been uploaded, and piled duplicates into the review queue. Duplicate submissions make reviewers lose trust in your entire catalog — and for a small seller, trust is your account's only real asset.
The solution is dumb but effective: a ledger. upload_tracker.py uses a plain-text file to remember what's already been published, relying only on the standard library — no dependencies, no database:
python upload_tracker.py pending items.txt
python upload_tracker.py done "asset-001.jpg"
python upload_tracker.py status
Check pending before each upload, check status before marking anything done, and completed items are skipped automatically on a rerun. The author's verdict: a boring solution is the one you'll actually keep using.
One Directory Structure — Don't Build a Junk Drawer
Once you're past forty or fifty products, what slows you down isn't generation speed — it's not being able to find anything. The whole pipeline assumes this layout:
project/
assets/ raw output
output/ files to submit + metadata.csv
packs/ finished archives + covers
research/ notes and sources
scripts/ tooling scripts
There's one rule the author learned only by deleting files: research/ is kept forever. He once deleted his notes to "clean up," then spent a week rebuilding decisions he'd already made. Scrape logs and notes are the raw material for next quarter's products.
Survival Habits on a Small Server
The entire pipeline runs on a machine with 1 vCPU, 2 GB of RAM, and no GPU — and that constraint shaped the design more than any platform rule. A few habits that actually work:
- Run one process at a time. On a single core, parallel image jobs bring no gain — only OOM.
- Call
gc.collect()after each asset finishes. This is the difference between a stable loop and a crash at item 90. - Watch peak RSS, not averages, measured with
resource.getrusage(RUSAGE_SELF).ru_maxrss. Averages hide the ceiling. - For heavy rendering steps, have each subprocess handle exactly one file — that's the only way memory actually goes back to the OS.
- Render at the resolution you need. Previews at 60 dpi instead of 300 dpi save a huge amount of that 2 GB.
As for dependencies, keep only Pillow (for covers) and reportlab (for PDFs) — everything else is standard library. On a 2 GB machine, every extra dependency is a future 3 a.m. debugging session.
Peak RSS and ru_maxrss: RSS (Resident Set Size) is the size of the pages a process is actually using and keeping resident in physical memory, excluding anything swapped out to disk. On Linux, resource.getrusage(RUSAGE_SELF).ru_maxrss returns the peak RSS since the process started, in KB (on macOS it's in bytes, so watch the conversion when going cross-platform). The reason to look at peak rather than average is that the OOM killer triggers on the instantaneous high-water mark: a job that averages only 300 MB but spikes to 1.9 GB during one render step shows nothing in the average — yet the process gets killed at that moment. Recording peak RSS helps you pinpoint which stage is eating memory, so you can decide whether to split steps, lower render resolution, or switch to one subprocess per file.
Why subprocesses can actually return memory: Python's reference counting and generational garbage collection only free the heap memory used by Python objects themselves, but image processing, PDF generation, and similar operations often allocate large buffers at the C extension layer (the libraries behind Pillow, reportlab, etc.). That memory isn't necessarily returned to the OS the moment the object is freed — it may sit in the process's allocator for reuse. That's why long-running loop processes show RSS climbing in steps and never coming down. Put each heavy task in its own subprocess and exit after one file, and the OS reclaims its entire address space in one go when the process ends — the only way to guarantee memory truly goes back to the system. The main process just handles scheduling and aggregation, staying lightweight itself.
Packaging Is Half the Sale
Archives with a 1280x720 cover and a 600x600 thumbnail sell; bare files don't — the author says his own catalog's sales data proves it. pack_product.py handles the packaging and both image renders in one command:
python pack_product.py my-product "My Product" "A short subtitle" 9
The zip, cover, and thumbnail all go into a _system folder, redirectable via --dir or the PACK_ROOT environment variable — no script edits, no per-product config.
What This Actually Produced
This isn't theory. These scripts produced 39 finished products and 856 MB of output, from a roughly 19,000-line parent pipeline, backed by 300+ pages of research on platform rules — the scripts execute those rules, all validated on the same 1 vCPU / 2 GB machine. The author packaged the full method (including the prompt engine and the sticker niche section) into an AI sticker factory playbook.
For multi-platform sellers, there's really only one transferable lesson: reliability at submission time comes from engineering, not willpower. Hand headers to code, duplicates to a ledger, memory to subprocesses — and save the remaining energy for creating.
One-line Summary
Treat asset metadata as a build artifact: scripts generate headers, a ledger prevents duplicates, subprocesses control memory.
Paragraph Summary
The article describes how a stock asset seller turned multi-platform submission from hand-built spreadsheets into a scripted pipeline. The core idea is treating metadata as a build artifact of code: make_metadata.py hardcodes the headers for Adobe Stock (5 columns), Vecteezy (4 columns), and Dreamstime (15 columns), generating three CSVs from one asset folder with headers that never drift. A plain-text ledger, upload_tracker.py, records what's already been uploaded to avoid duplicate submissions that erode account trust. It fixes the directory structure as assets/output/packs/research/scripts and stresses that research notes must be kept forever. On a 1 vCPU / 2 GB machine with no GPU, it sticks to single-process serial execution, per-file gc.collect(), watching peak RSS, one file per subprocess for heavy tasks, rendering at reduced resolution as needed, and keeping dependencies to just Pillow and reportlab. pack_product.py produces the zip, a 1280x720 cover, and a 600x600 thumbnail in one command — and the author says packaged products clearly sell better.
Related articles

Chain of Thought Isn't Just for AI: How Humans Can Use CoT to Improve Thinking Quality
Chain of Thought isn't just a prompt trick for large models. Starting from a Hacker News discussion, this article analyzes why CoT works for human thinking too, breaks down actionable ways to transfer it, and highlights the key differences between human CoT and machine CoT.
Cloudflare Teams Up with OpenAI: Closi…
Cloudflare Teams Up with OpenAI: Closing the Loop from Vulnerability Discovery to Defense with AI
Cloudflare uses OpenAI Daybreak to investigate security vulnerabilities and generate reviewable remediation plans, while developers use Codex to boost coding efficiency and quality.

Orca Multi-Agent Orchestration in Practice: A Guide to Parallel Collaboration with Claude and Codex
Orca is Stable AI's free open-source agent workbench supporting 34 agents like Claude Code and Codex in parallel. Learn its Git worktree isolation, multi-model orchestration, and mobile management.