Embed Image Metadata Using an LLM
A Python tool, run from the command line or a small Flask web UI, that uses Claude to generate accessibility alt text plus a title, description, and keywords in the writing style you choose, writing all of it straight into the image file's XMP and IPTC fields.
Source on GitHub (opens in a new tab)Started March 2026MIT
Most alt-text generators hand you a caption and leave it up to you to store it somewhere. This tool goes further: a vision model writes accessibility alt text into the XMP-iptcCore:AltTextAccessibility field, and can also write a Title, Description, and Keywords list into the IPTC fields Adobe Bridge reads. Everything is embedded in the image itself, so it travels with the file into Lightroom, Bridge, a CMS, or a static site build. No database, no server-side store.
There are two ways to run it: a command-line script driven by flags, or a small web app for bulk uploads through a browser, including from your phone.
Choose exactly the fields you want
Alt text, title, description, and keywords are four independent choices, each with its own overwrite option. A field that already has a value is skipped by default, so you can safely run the tool over a whole folder and fill in only what’s missing. --overwrite-all replaces every selected field when you want a clean regeneration, and fields you don’t select are never touched. Title, description, and keywords are generated together in a single model call, so asking for more of them doesn’t cost extra requests.
Writing styles
Titles and descriptions each come in several styles, because a marketplace listing, a gallery wall label, and a photo caption all want different things:
- Titles: standard, creative, editorial (“Koi Pond, Japanese Friendship Garden”), poetic, and literal.
- Descriptions: standard, creative (a short-story vignette), caption (one factual sentence), and photographic (light, color, and composition in an artist-statement voice).
Editorial titles take their place name from the image or your context and never guess it, and the capped styles are trimmed at a sentence boundary if the model runs long. An optional --context string (“Cherry blossoms at Japanese Friendship Garden”) nudges the model toward more accurate results.
Method 1: command line
update-images.py takes a folder path and a set of flags:
- Scans a folder of images (JPEG, PNG, GIF, WebP, and HEIC/HEIF for iPhone photos), sends each one to Claude through the
llmCLI, and writes the results withexiftool. HEIC/HEIF has no IPTC block, so those fields go into the equivalent XMP fields instead. - Models and prompts live in
models.yaml. The default is Claude Sonnet 5.5, with Sonnet 4.6 also available, and per-model settings such as thinking effort are passed straight through tollm. replacements.yamlis a small dictionary of phrases that are always rewritten in the output (mid-century→midcentury), matched case-insensitively with the original capitalization preserved. Em dashes are always turned into hyphens.
The CLI writes in place with no _original backups, so it’s best run on copies. Images are downscaled to 1024 px on the long side before they’re sent to the Anthropic API.
Method 2: web interface
webapp.py wraps the same image_processor.py pipeline in a Flask app, with no flags to remember:
- Per-field checkboxes with their own overwrite options, an “Overwrite all” switch, and a style menu on the title and description rows.
- Instant thumbnails as soon as you pick files, before anything is uploaded.
- Live, expandable results for each image showing exactly what was written, skipped, or failed for every field.
- A Run again button to reprocess the same images with different options, so you can compare results without re-selecting files.
- A zip download of the tagged images. It works on uploaded copies, so your originals are never touched.
Run it with --host 0.0.0.0 and you can pick photos straight from your phone on your home network. It’s a deliberately single-user tool with no login and in-memory job tracking, so it’s meant for a network you trust. It defaults to port 5001 because macOS AirPlay Receiver already claims 5000.
Unit tests cover caption cleaning, the replacement rules, prompt building, per-field overwrite logic, HEIC handling, CLI flag validation, and the web app’s options.
Browse






