make_metadata.py: Generating Exact CSV Headers for Each Stock Platform

After an Adobe Stock header error caused 240 files to be rejected, the author developed a script that generates exact CSV headers for each platform.
The article recounts a lesson in which imprecise CSV headers caused 240 files to be instantly rejected by a stock platform, which led to the Python script make_metadata.py. The script hardcodes the headers for Adobe Stock, Vecteezy, and Dreamstime, ensuring the column count and names match each parser's requirements exactly, and supports providing real titles and keywords via a JSON file. The author stresses that keywords must match the actual image content, otherwise they cause more insidious partial rejections. A companion script, upload_tracker.py, uses a standard-library ledger to record upload status and prevent duplicate publications that could get an account flagged. The entire workflow runs on a low-resource server with 1 vCPU and 2 GB RAM, and the script is designed around simplicity and reliability to avoid the formatting damage caused by spreadsheets.
make_metadata.py: Generating Exact CSV Headers for Each Stock Platform
A failed Adobe Stock CSV upload led to a Python script focused on CSV header precision. The author's lesson from having 240 files instantly rejected tells us: in automated upload workflows, header errors are more fatal than content problems.

Why Headers Are Critical
Every stock platform's CSV format is different and mutually incompatible. Adobe Stock requires 5 columns, Vecteezy needs 4, and Dreamstime needs 15. If you submit a 5-column file to Dreamstime, or add columns Adobe doesn't recognize, the upload parser will error out immediately and reject the entire batch.
The author emphasizes that you can't rely solely on documentation when verifying headers, because documentation may lag—but the upload parser won't. make_metadata.py hardcodes the headers for all three platforms in the script. There are no template files to edit and no configuration options to misconfigure. After running python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg, the first row written is exactly the 5-column header Adobe requires, matching character for character.
CSV (Comma-Separated Values) is a format for storing tabular data as plain text, using a delimiter (usually a comma) to separate columns and line breaks to separate rows. Its "standard" is very loose: there are no enforced quoting rules, encoding declarations, or header validation mechanisms, and each platform's parser often parses strictly according to its own implementation. As a result, the same CSV can behave completely differently across platforms—extra columns, misordered fields, a BOM, or CRLF line endings can all trigger rejection. Stock platforms typically perform a structural validation at upload time, and any mismatch in column count or column names fails the batch. This is the technical root of the article's emphasis on "matching headers one by one."
How Row Data Is Filled
Headers are only the first step; each row also needs a title, keywords, and category. The default behavior converts the filename into a title—for example, geometric-fox-pattern-04.jpg becomes "Geometric Fox Pattern 04." This passes the parser but won't win search rankings.
The script therefore supports the --titles titles.json argument, allowing you to provide real titles and keywords for each file. The author's workflow is: first run the script without titles to generate the correct structure, then fill in the JSON before submitting. Replacing default titles takes only a few minutes, whereas replacing a rejected batch costs an entire afternoon.
The author particularly emphasizes a self-imposed rule: keywords must come from the actual image content, not the prompt. Reviewers check whether keywords match the visible content, and mismatches are one of the hidden causes of partial rejections—and partial rejections are more troublesome than full rejections because you may not notice which files failed.
The Duplicate Upload Trap and Its Solution
After fixing the header issue, the author re-ran the batch, and then ran it again a day later for a minor change, resulting in duplicate publications. This is the fastest way to get an account flagged, and it's a mistake every scripted uploader is prone to.
upload_tracker.py is a ledger tool that uses only the standard library. It checks pending items before uploading and records completion status afterward. Because "done" is recorded per file and per ledger rather than relying on memory, re-running won't cause duplicate publications. Like make_metadata.py, it requires no third-party packages. Across the entire workflow, only two scripts need third-party packages: Pillow for cover rendering and reportlab for PDF generation.
How Constraints Shaped the Script Design
The entire workflow runs on a server with 1 vCPU, 2 GB RAM, and no GPU. The author sees this more as discipline than limitation: run only one process at a time, don't parallelize image tasks on a single core; call gc.collect() between assets; measure peak RSS rather than average with resource.getrusage, because peaks are what exhaust 2 GB of memory; render at the resolution needed, and checking previews at 60 dpi instead of 300 dpi is key to fitting within memory.
Because make_metadata.py only does text processing, it easily fits these constraints. Building a CSV for 240 files takes only seconds and uses very little memory. The metadata step is never the bottleneck—image generation is—so the author deliberately keeps the metadata step simple and reliable.
RSS (Resident Set Size) refers to the portion of a process currently resident in physical memory. ru_maxrss, returned by resource.getrusage, is the peak resident memory over the process's lifetime, in KB on Linux. Compared with average memory, the peak better reflects OOM (out-of-memory) risk: a single transient peak can trigger the kernel's OOM Killer. Python's garbage collection consists of reference counting and generational collection; gc.collect() forces a full collection and is suitable for calling proactively after releasing large objects (such as high-resolution image arrays) to avoid overlapping peaks. However, frequent calls add CPU overhead, so it must be used judiciously in low-spec environments.
Why Not Use a Spreadsheet
The author initially tried the manual CSV route, but spreadsheet applications fail in predictable ways: silently reformatting, adding unnecessary quotes, and corrupting encoding. A script that writes files directly produces the same bytes every time. When a platform rejects a batch, you can be certain the problem is with the content, not the tool.
This script is one of four in the workflow toolkit, alongside the tracker, md2pdf.py, and pack_product.py. Together they package 39 products, about 856 MB of output, totaling roughly 19,000 lines of workflow code, backed by 300+ pages of research, all validated on the same 1 vCPU/2 GB RAM machine.
For creators who need to package stock art or templates, if the CSV step is eating your time, make_metadata.py offers a simple, reliable way to automate it and avoid the mistakes the author once made.
One-line Summary
After an Adobe Stock header error caused 240 files to be rejected, the author developed a script that generates exact CSV headers for each platform.
Paragraph Summary
The article recounts a lesson in which imprecise CSV headers caused 240 files to be instantly rejected by a stock platform, which led to the Python script make_metadata.py. The script hardcodes the headers for Adobe Stock, Vecteezy, and Dreamstime, ensuring the column count and names match each parser's requirements exactly, and supports providing real titles and keywords via a JSON file. The author stresses that keywords must match the actual image content, otherwise they cause more insidious partial rejections. A companion script, upload_tracker.py, uses a standard-library ledger to record upload status and prevent duplicate publications that could get an account flagged. The entire workflow runs on a low-resource server with 1 vCPU and 2 GB RAM, and the script is designed around simplicity and reliability to avoid the formatting damage caused by spreadsheets.
Related articles

Random Pruning of the DPO Computation Graph: 1.4x Training Speedup and Lower Peak Memory
DPO training too slow and memory-hungry? This paper proposes a random pruning framework for the DPO computation graph, sampling independently across position, sequence, and branch dimensions; branch pruning achieves a 1.426x step-time speedup and cuts training time by about 30%, with in-depth attribution of memory and time gains.

Why Does JEPA Underperform MLM in Longitudinal EHR Representation Learning?
New study compares JEPA and MLM for longitudinal EHR representation learning on MIMIC-IV: three JEPA variants train stably without collapse but underperform MLM; target size, not heterogeneity, drives prediction error.

Transfer Learning + Visible Light Communication: A New Approach to Energy-Efficient Indoor Positioning
A study proposes a transfer learning-based visible light communication indoor positioning method, achieving 47% accuracy improvement on the BOSCH factory dataset, 67.5% model efficiency under high noise, and introducing the ME composite metric for energy-efficient positioning in energy-constrained industrial IoT scenarios.