Generating Stock Metadata with Python Scripts: Title Casing and Keyword Ordering in Practice

When batch-generating stock metadata, four pitfalls matter most: headers, title casing, keyword ordering, and the upload ledger.
This article shares hard-won lessons from a Python script that generates CSV metadata for Adobe Stock, Vecteezy, and Dreamstime. The four main takeaways: each platform's CSV header has a different column count and must be hardcoded and verified in a text editor; title casing needs manual rule-based handling to avoid str.title() producing .Png and Don'T; Adobe's first 10 keywords weigh most, so order by subject, style, and use case, with 20-30 precise keywords beating 49 padded ones; and the upload ledger must update the moment a file is submitted. The whole pipeline uses only the standard library on a 1-core, 2 GB, no-GPU machine, where peak memory matters more than average.
Losing an entire weekend over a CSV header is an experience many people who list digital products can relate to. The author turned that failure into make_metadata.py, a Python script built specifically to generate metadata files for Adobe Stock, Vecteezy, and Dreamstime. Its core logic isn't complicated, but the pitfalls it hit are worth a look for anyone batch-uploading stock assets.

The Header Is a Contract, Not Your Design Space
Every stock platform's CSV header has a completely different column count: Adobe wants 5 columns (Filename, Title, Keywords, Category, Releases), Vecteezy wants 4, and Dreamstime wants 15. The author originally wrote a "universal" CSV and then adapted it per platform, which turned into a bug factory. Platforms validate the header before reading the first row of data — if the column count is wrong, nothing after it gets processed.
The current approach is that the script hardcodes the corresponding header for each platform, not one column more, order unchanged. The verification method is equally plain: open the generated file in a text editor, not a spreadsheet app. LibreOffice hides line-ending delimiter and quote issues, while less exposes them at a glance — that rejected CSV looked completely normal in the spreadsheet.
CSV header validation essentially depends on each platform's upload API reading the first row and mapping column names to front-end form fields before parsing data rows. If the column count doesn't match or column names differ, the API usually rejects the entire file outright rather than giving per-row error messages — which is why a file that "looks like it opens fine" still gets bounced.
A file that opens fine in LibreOffice or similar spreadsheet software but fails on actual upload often does so because the spreadsheet app auto-tolerates: it guesses the encoding, ignores extra line-ending delimiters, completes unclosed quotes, and even repositions commas. The platform backend, by contrast, uses a strict CSV parser with fixed expectations about whether line endings are \n or \r\n, whether fields are quote-wrapped, and how commas inside quotes are handled. Inspecting the raw bytes with less or cat -A reveals the differences the spreadsheet app hid.
Title Casing Is a Rules Problem, Not an Aesthetics Problem
The author's second rejection on Vecteezy was because the title was all uppercase. He used Python's str.title() to convert boho-rainbow-clipart.png into Boho-Rainbow-Clipart.Png — the extension got capitalized and the hyphens weren't even handled.
The rule the script now enforces is: strip the extension, split on separators, capitalize each word's first letter, join with spaces, keep the extension as-is. boho-rainbow-clipart becomes Boho Rainbow Clipart, and nothing more. It's roughly 6 lines of code, but the earlier version really did send .Png into the title field.
There's another detail: str.title() turns Don't into Don'T. If asset names might contain apostrophes, string.capwords() processed word by word is safer.
Python's str.title() splits on word boundaries, capitalizing each word's first letter and lowercasing the rest. Its criterion is "is the previous character a letter" rather than "is it a word separator," so the hyphen in boho-rainbow-clipart is treated as a boundary, yielding Boho-Rainbow-Clipart; the p in the extension .png also gets capitalized, becoming .Png.
string.capwords(s, sep) behaves differently: it first splits the string into words using the specified separator, calls str.capitalize() on each word, then rejoins with the same separator. It doesn't misidentify word boundaries across separators, and it doesn't touch apostrophes — don't yields Don't, not Don'T. For asset filenames where hyphens and underscores are the primary separators, capwords combined with manual separator replacement is more controllable and easier to write as deterministic logic under a few lines.
Keywords Need Ordering, Not Piling Up
Adobe accepts at most 49 keywords, and the first 10 carry the highest weight in search. Keyword order is a decision that needs to be made actively. The author's earlier approach wrote generator-produced keywords straight into the CSV, and sometimes "background" ranked higher than the actual subject.
The priority he now follows is: subject nouns first, then style and technique (flat, watercolor, line art), then use case (invitation, sticker, sublimation), and whatever's left fills in as needed. 20 to 30 highly relevant keywords are more effective than padding out to 49.
The script provides a --titles titles.json argument. Titles derived from filenames suit a first pass, but the core logic of keyword ordering is: a person decides the order once by theme, and the script applies that order across the whole batch. Maintaining one JSON file per theme and reusing it — hand-writing 30 keywords for 40 files and then having the script apply them — is barely an acceptable efficiency ratio.
Adobe Stock's keyword weighting isn't simply "the first 10 matter most." Official search ranking combines keyword position in the title and keyword list with click and download feedback, but the general experience is that the earliest keywords contribute more directly to search matching, and later keywords have weaker influence. The keyword cap is 49; exceeding it gets truncated or causes upload failure.
Ordering keywords by subject, style, and use case essentially freezes human thematic judgment into a reusable JSON template. Because a batch of files under the same theme often shares a large set of keywords, maintaining one ordered list is enough for the script to apply to dozens of files. This guarantees consistency in ordering and avoids re-making the ordering decision for every file.
Duplicate Uploads and the Real Constraints of Limited Resources
The third problem was duplicate submissions. Unable to remember which files had already been uploaded, the author ran the same batch of 60 files twice. Duplicate submissions are one of the common reasons accounts get flagged. upload_tracker.py implements a three-command ledger with the standard library: pending, done, status. The rule is to call done the moment a file is submitted, not to fill in the record after the whole session ends — a ledger updated only at session end will outright lie when the laptop runs out of battery.
The entire pipeline depends only on the standard library — no pandas, no extra CSV library. This isn't a style preference; it's a hardware constraint: the whole pipeline runs on a 1-core CPU, 2 GB RAM, no GPU machine, while also running image rendering. pandas' startup overhead is unacceptable in this environment, while the csv module handles quotes and delimiters correctly enough and starts in milliseconds.
The author's survival habits: run only one process at a time, call gc.collect() after each asset is processed, and measure peak memory with resource.getrusage(RUSAGE_SELF).ru_maxrss rather than average. The average told him "plenty of room"; the peak told him the truth.
These scripts were consolidated into the Pipeline Starter Kit, containing CSV writers for the three platforms, title casing that never produces .Png, a ranked-keyword JSON workflow, and an upload ledger. The same set of scripts on a 1 vCPU, 2 GB RAM machine packaged 39 products (856 MB output) from roughly 19,000 lines of pipeline code — 4 standalone Python scripts in total, plus 2 third-party libraries (Pillow and reportlab, needed only when using packaging and PDF tools).
resource.getrusage(RUSAGE_SELF).ru_maxrss returns the peak resident memory of the process since startup, in KB on Linux and bytes on macOS. It reflects the high-water mark, while averages dilute short-lived spikes — on a 2 GB machine also running image rendering, the peak is the key metric determining whether the process gets killed by the OOM Killer.
gc.collect() is used to actively trigger garbage collection after each asset is processed, promptly releasing objects that are no longer referenced. Python's reference counting itself reclaims most objects, but circular references require the garbage collector, and its timing is uncontrollable. When memory is tight and you can't use libraries with heavy startup overhead like pandas, manually calling gc.collect() is a way to trade a little CPU time for predictable memory usage — combined with running only one process at a time, it keeps the pipeline's memory curve within a controllable range.
One-line Summary
When batch-generating metadata for stock platforms, these four pitfalls — headers, title casing, keyword ordering, and the upload ledger — are the ones most worth heading off early.
Paragraph Summary
This article documents the hard-won lessons from a Python script that generates CSV metadata files for Adobe Stock, Vecteezy, and Dreamstime. There are four core takeaways: each platform's CSV header has a different column count, so it must be hardcoded in the script and verified with a text editor rather than spreadsheet software; title casing must be handled manually by rules, avoiding str.title() producing .Png and Don'T; Adobe's first 10 keywords carry the highest weight and should be ordered by subject, style, and use case, with 20 to 30 precise keywords beating padding out to 49; and the upload ledger must be updated the moment a file is submitted, or you'll resubmit duplicates. The entire pipeline uses only the standard library and runs on a 1-core, 2 GB, no-GPU machine, where peak memory matters more than average.
Related articles

RAG Getting Started in Practice: From Principles to Building an Enterprise Knowledge Base
Bilibili RAG course highlights: from retrieval-augmented generation principles, document chunking and vectorization, to five accuracy factors and enterprise knowledge base boundaries, with fine-tuning vs RAG selection advice.

AI Model Selection Guide: DeepSeek vs. Qwen3 in Practice
How should AI developers choose models? Based on a Bilibili tutorial, this article breaks down the Function Calling differences between DeepSeek V3 and R1, Qwen3's programmatic thinking mode switching and agent capabilities, and outlines a multi-model learning path and practical selection logic.

Anchoring, Loss Aversion, and Nudges: 5 Psychological Tactics Retailers Use to Make You Spend
Behavioral economics reveals how retailers use anchoring, framing, loss aversion, and nudge theory to manipulate your spending decisions. This article breaks down 5 common psychological tactics and offers counterstrategies like slowing down decisions and setting your own anchors to see through the pricing design behind discounts.