Danny Englander

← All code projects

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.

  • Python
  • llm CLI
  • exiftool
  • Flask
  • Claude
  • Pillow

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 llm CLI, and writes the results with exiftool. 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 to llm.
  • replacements.yaml is 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

Art Collections