Conversation
Adds --xmp, which writes an XMP sidecar next to the image instead of a .txt caption, holding the caption as dc:description and model generated tags as dc:subject. The sidecar keeps the full image name (image.jpg.xmp), which is what exiftool, digiKam and Lightroom expect. Keywords come from a second pass of the same vision model over the same image, so no extra model has to be configured; --keyword-model overrides it, --keyword-prompt and --keyword-system tune it, --max-keywords caps the list and --no-keywords skips it entirely. The keyword pass ignores --force-one-sentence, whose stop token would truncate a keyword list. Models answer keywords in whatever shape they please, so ParseKeywords accepts comma lists, bullet lists, numbered lists and JSON arrays, strips markdown decoration and lead-ins, and dedupes case insensitively. Both fields are XML escaped, and dc:subject is left out when no keywords survive parsing. The skip-existing check now looks at whichever file would be written, so .txt and .xmp runs of the same directory stay independent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keyword pass re-encodes and re-prefills the image, which roughly doubles the time per image. --single-pass instead asks for both fields in one request, using two labelled lines: DESCRIPTION: A fluffy orange cat sitting on a sunny wooden deck. KEYWORDS: cat, outdoor, sunny, deck Measured on qwen2.5vl:3b with a warm model, this cut 22.2s per image down to 9.9s. The labels are searched for anywhere in the reply, so a model that answers on one line is handled as well as one that uses two, and CAPTION:, TAGS:, markdown decoration and bullet lists are accepted since small models drift. When no keyword label turns up at all, the whole reply is kept as the description and a warning is logged, so a malformed answer never costs the caption as well. It stays opt-in because smaller vision models write a weaker caption when the same turn also has to produce tags. --force-one-sentence is refused in this mode, as its stop token would cut the answer before the keywords, and so is --keyword-model, which has nothing to act on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--language takes a name ("Spanish") or a BCP 47 code ("es", "es-ES") and
appends the instruction to whichever prompt is in use, so it applies to
.txt captions, both XMP modes and a custom --prompt alike. English stays
the default and adds no instruction at all.
In a sidecar the caption is written twice, as x-default for readers that
ignore languages and again tagged with the language itself, which is what
the XMP spec asks for and what exiftool reports as XMP-dc:Description-es.
dc:subject stays untagged, as XMP defines it as an unordered bag of plain
text.
--single-pass asks the model to keep its two labels in English, but models
translate them regardless, so the parser now also accepts the Spanish,
French, German, Italian, Portuguese and Catalan forms.
A language outside the table is still passed to the model by name, so an
unlisted one keeps working for the caption; only the xml:lang tag is
dropped, and a warning says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A JPEG XL file named .jpg makes Ollama answer "Failed to load image or audio file", and log.Fatalf then aborted the whole run over one bad file. Phones and photo managers leave exactly such files behind, so a large library is likely to hold some. Every file is now identified by its header before a request is spent on it. JPEG and PNG are sent; JPEG XL, HEIC, AVIF, WebP, GIF, BMP, TIFF and SVG are recognised by name so the message says what the file really is, and anything unrecognised is reported as not a JPEG or PNG. Either way the file is skipped and the walk continues. --no-format-check turns this off for a backend that accepts more than the two. The check sits after the skip-existing test, so an image that already has a caption still costs nothing. The OpenAI path now derives its media type from the content as well, which fixes a PNG named .jpg being announced as image/jpeg. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fifteen table driven tests over the parts that have to cope with whatever
a model or a photo library hands them:
xmp_test.go keyword shapes, the single pass labels, XML escaping,
the language entries of dc:description
language_test.go names, codes, regions and unknown languages, and that
English adds no instruction at all
format_test.go the magic bytes of eight formats, the near misses
(RIFF that is not WebP, ftyp that is not HEIC) and
short, empty and missing files
main_test.go sidecar naming, and two regressions worth pinning:
struct tag escapes reaching the model literally, and
--force-one-sentence leaking into a keyword list
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release workflow went straight from the tag to the cross compiled binaries, so nothing checked the code first. A test job now runs go vet and go test once, and the build matrix waits on it, which stops a broken parser from reaching a release artifact. It runs as its own job rather than as a step in the matrix, so the suite runs once instead of four times, and it runs natively because the matrix sets GOOS and GOARCH only on its build step. The workflow still only triggers on v* tags and workflow_dispatch, so this gates releases rather than every push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release workflow only gates a tagged release, so a broken parser could sit on a branch until tag time. This runs the same two checks on every branch push and pull request. The trigger lists branches only, which excludes tag pushes, so a v* tag runs the release workflow rather than both. In-flight runs of a branch are cancelled when it is pushed again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi @oderwat ,
I found your tool very useful, but I needed some extra features. Instead of reinventing the wheel, I took the liberty to fork your project and add those features on top: XMP output instead of .txt, language support and file format validation with magic-byte.
Take a look and if you are fine with it, just accept the PR.
Thank you for your work,
Regards,
José