DEV Community

Cover image for Writing Ranked Keywords And Title Casing Rules Into A Stock Metadata Generator

Writing Ranked Keywords And Title Casing Rules Into A Stock Metadata Generator

Last March I lost a whole weekend to a CSV header. I had 60 assets ready for Adobe Stock, generated the metadata file with a script I'd written in an afternoon, uploaded, and every single row got rejected on ingest. The reason was boring: my header had 4 columns where Adobe demands 5. Not "wrong values". Wrong column count. The platform checks the header before it reads a single row of your data, and if the count is off, nothing downstream matters.

That failure is why make_metadata.py exists, and why it now hard-codes three different header layouts instead of trusting me to remember them.

The header is the contract

Each platform has its own fixed column count, and they do not match:

  • Adobe: 5 columns (Filename, Title, Keywords, Category, Releases)
  • Vecteezy: 4 columns
  • Dreamstime: 15 columns

My first version had one "universal" CSV I would reshape per platform. That was the bug factory. Now the script writes the exact header each platform demands, and nothing else. No extra columns "for my own reference", no reordered fields I thought were cleaner. The header is not mine to redesign. If Adobe wants 5 columns in that order, the script emits 5 columns in that order, byte for byte.

The practical test: open the generated file in a text editor, not a spreadsheet. Spreadsheets hide trailing delimiters and quietly re-quote things. The rejection I hit was invisible in LibreOffice and obvious in less.

Title casing is a rule, not a preference

The second rejection I got, on Vecteezy this time, was for a title in ALL CAPS. I had generated filenames like boho-rainbow-clipart.png and my script title-cased them with Python's str.title(), which produced Boho-Rainbow-Clipart.Png. The extension got capitalized. The hyphens stayed. Both are problems a human reviewer notices.

The rule the script enforces now: strip the extension, split on separators, capitalize each word, join with spaces, leave the extension alone. boho-rainbow-clipart becomes Boho Rainbow Clipart. That's it. It's about 6 lines of code, but the version before it shipped a .Png into a title field and I did not catch it until after upload.

One more thing I learned the hard way: str.title() also mangles apostrophes. wedding-invitation-template is fine, but anything with a quote character turns Don'T into your title. If your asset names ever contain apostrophes, use string.capwords() or capitalize manually per word.

Keywords: ranked, not dumped

Adobe takes up to 49 keywords and the first 10 carry the most weight in search. That means keyword order is a decision, not an afterthought. My early CSVs just dumped keywords in whatever order the generator produced them, which meant "background" sometimes outranked the actual subject.

What works and what I built into the script:

  1. Subject first. What the thing actually is, in plain nouns.
  2. Style and technique second (flat, watercolor, line art).
  3. Use case third (invitation, sticker, sublimation).
  4. Everything else after, and I rarely fill all 49 slots. 20 to 30 relevant keywords beat 49 padded ones.

The --titles titles.json option exists because of this. Filename-derived titles are fine for a first pass, but the whole point of the ranked keyword work is that a human (me) decides the order once, per theme, and the script applies it across the batch. I keep a JSON file per collection and reuse it. Writing 30 keywords by hand for one theme, then having the script apply that ordering to 40 files, is the ratio that makes this tolerable.

The re-run problem

The third failure was mine alone: I ran my upload batch twice on the same folder because I lost track of which files I had already submitted. Duplicate submissions are one of the fastest ways to get an account flagged, and on a batch of 60 files I genuinely could not remember where I stopped.

upload_tracker.py is a ledger, standard library only, three subcommands:

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 I follow now: done gets called the moment a file is submitted, not at the end of the session. A ledger that updates at the end is a ledger that lies when your laptop dies at file 40.

Why the whole thing runs on the standard library

make_metadata.py and upload_tracker.py import nothing outside Python's standard library. No pandas, no CSV library beyond csv, no install step. That sounds like a style preference. It is actually a constraint from the hardware: the whole pipeline runs on 1 vCPU with 2 GB RAM and no GPU. Pandas alone wants more headroom than that box likes to give when an image render is also running. The csv module handles quoting and delimiters correctly for these files, and it starts in milliseconds.

The habit that keeps the small box alive: one process at a time, gc.collect() after each asset, and measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss, not average usage. Average numbers told me I had plenty of room. Peak numbers told me the truth.

Where this lives now

Everything above is baked into the Pipeline Starter Kit: the three CSV writers with the exact Adobe, Vecteezy, and Dreamstime headers, the title casing that does not produce .Png, the ranked-keyword JSON workflow, and the upload ledger. It is the same set of scripts that packaged 39 products (856 MB of output) out of roughly 19,000 lines of pipeline code, validated on that same 1 vCPU, 2 GB machine. Four standalone Python scripts, two third-party packages total (Pillow and reportlab, and only if you use the packaging and PDF tools). If you are building your own metadata step, the kit is the reference I wish I had before the rejected weekend.

See the kit


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

Top comments (0)