DEV Community

Cover image for From Filenames to Ranked Keywords: Casing and Title Rules in a Stdlib-Only Metadata Script

From Filenames to Ranked Keywords: Casing and Title Rules in a Stdlib-Only Metadata Script

I once submitted a batch of 40 vectors to Adobe Stock and watched 11 of them bounce for the same dumb reason: my CSV had a header column in the wrong position. Not the art. The metadata. I'd spent a week rendering those files and lost a day to a five-column spreadsheet. That failure is why make_metadata.py exists, and why it is strict about casing and title rules in ways that felt pedantic when I wrote them and feel obvious now.

This post is about the part of that script nobody writes blog posts about: how filenames become titles, how keywords get ranked, and why the whole thing runs on the standard library alone.

The headers are contracts, not suggestions

Each platform demands an exact column layout, and each rejects a wrong one instantly.

Adobe Stock wants 5 columns: Filename, Title, Keywords, Category, Releases. Vecteezy wants 4 columns: Preview File Name, Title, Keywords, Category. Dreamstime is the fat one at 15 columns, including things like description and country codes you will leave blank most of the time.

The script writes the header row the platform demands and nothing else. No extra columns, no reordered fields, no BOM surprises from whatever editor touched the file last. If the header is wrong, the upload fails before any human sees your work. That is the cheapest possible failure to prevent, so the script makes it impossible.

Filenames are a bad title source, so treat them as a first pass

I generate a lot of asset names programmatically, which means filenames like autumn-leaf-pattern-04.jpg. That is not a title anyone searches for.

The default behavior in make_metadata.py is honest about this: it title-cases the filename and uses it as the title. autumn-leaf-pattern-04 becomes Autumn Leaf Pattern 04. That is fine for a first pass and terrible as a final answer, so the script also accepts --titles titles.json, a plain JSON file where you supply real titles and keywords per file. The workflow is: run the batch, get a valid CSV, then replace the lazy titles before you submit.

I tried fancier approaches first, like deriving titles from folder names or templates. They all produced titles that read like filenames with spaces. Title-cased filename plus a manual override file is boring and it works.

Keyword ranking is where the real rejections happen

Adobe allows up to 49 keywords per asset, but the first ten carry the most weight in their relevance ranking. Vecteezy is stricter in practice: their reviewers penalize keyword stuffing, and I have had batches come back with review notes about irrelevant terms.

So the script enforces an order, not just a list. Keywords in titles.json go into the CSV in the order you wrote them, no sorting, no deduplication shuffling things around. The rule I follow: first keyword matches the title, next nine are the strongest literal descriptors, everything after that is style and use case. If a keyword does not appear in the title or describe what is visibly in the image, it does not go in. That single rule cut my review rejections to near zero across the 39 products I packaged with this pipeline.

Casing matters here too. Keywords go in lowercase, titles in title case with no trailing periods. Adobe's reviewer guidelines treat a keyword like Autumn and autumn inconsistently across their search stack, so I normalize everything to lowercase on the keyword column and never think about it again.

Why stdlib only, and why that decision survived

make_metadata.py and upload_tracker.py use nothing but the Python standard library. csv, json, pathlib, argparse. No pandas, no openpyxl, nothing to install.

Two reasons. First, the whole pipeline ran on a 1 vCPU, 2 GB RAM server with no GPU. Pandas alone would have eaten a chunk of that 2 GB for a job that is fundamentally reading and writing text rows. Second, fewer dependencies means the script runs on any machine with Python 3.9 or newer, including the tiny server, with zero setup. Only two third-party packages exist in the entire kit, Pillow for the product covers and reportlab for the PDF output, and neither touches the metadata path.

The server habits that made this work are worth repeating: one process at a time, gc.collect() between assets, measure peak RSS with resource.getrusage(RUSAGE_SELF).ru_maxrss rather than guessing, and inspect previews at 60 dpi instead of rendering at 300. None of that is glamorous. All of it is why a 2 GB box could chew through 856 MB of finished output.

The tracker closes the loop

The other stdlib-only piece, upload_tracker.py, is a flat ledger of what you have already submitted. done "asset-001.jpg" marks it, pending items.txt shows what is left, status gives the count. Re-running a batch after a partial upload is the fastest way to get an account flagged for duplicate submissions, and I have come close to that mistake exactly once. Never again, because the ledger remembers.

Where this lives now

Everything above is baked into the Pipeline Starter Kit: the four scripts (make_metadata.py, upload_tracker.py, md2pdf.py, pack_product.py), the exact Adobe 5-column, Vecteezy 4-column and Dreamstime 15-column headers, and the folder layout that kept 39 products from turning into a junk drawer. It is the tool built from this work, not a description of it, and if you only need the metadata half, those two scripts need nothing but Python.

See the kit


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

Top comments (0)