All notable changes to this project are documented here. The format is based on Keep a Changelog and this project follows Semantic Versioning.
- Anonymous crash reports: an unhandled exception sends the exception type and its code location
inside photo-tagger (
module:function:line), never the error message (messages can embed paths). Same opt-outs as the usage telemetry; capped per process. The one-time telemetry notice re-shows once for existing installs so the expanded disclosure is seen. - Telemetry now reports coarse hardware facts (CPU/GPU model, logical cores, RAM in whole GB; probed
once and cached), per-run outcome counters (successes, failures, cache hits, retry recoveries,
workers, token totals, model time, dry-run flag), and failure buckets (fixed labels like
timeoutormetadata-write) so breakage is measurable, not anecdotal. - The telemetry dashboard grew sections for adoption, usage, hardware & performance, and reliability & crashes, plus a global interface filter (all/CLI/GUI).
- The inference cache prunes entries older than 180 days when it opens, so
cache.sqlitestops growing forever (every settings change orphans its old namespace). - A short pause before the retry pass, so an overloaded model server is not immediately re-hit.
--append-to-skip-filerecords full paths instead of bare filenames, so duplicate camera names across folders (A/IMG_0001.CR3,B/IMG_0001.CR3) can no longer make a resumed run silently skip unprocessed photos. Old name-only skip files keep working.- Config-file values now pass through the same conversion and validation as CLI flags, so a mistyped value fails with a clean error at startup instead of a traceback mid-run.
- Photos with identical pixel content in one concurrent run share a single model call instead of each paying their own.
- CSV reports are written as
utf-8-sigso Excel renders accented titles and keywords correctly.
- Passing any CLI flag from an option group no longer silently resets the group's other config-file
values to built-in defaults (e.g.
--temperatureused to revert a configuredmax_tokens). --dry-runno longer appends previewed photos to--append-to-skip-file, which made later real runs skip photos that never got metadata.- The description mirror is written to
XMP-tiff:ImageDescription; the previousXMP-exiftag was silently rejected by ExifTool, so the mirror never reached the file. - Keyword capitalization no longer corrupts acronyms and apostrophes (
NYCstaysNYCinstead of becomingNyc;bird's nestno longer becomesBird'S Nest). - One corrupt or zero-byte photo no longer makes the whole folder read as untagged (and get
re-tagged): the batched ExifTool reads salvage the healthy files, and
--skip-taggedand the GUI's Tagged column now distinguish "could not read" from "no metadata". - Windows: file locking works again (the PID note into the held lock file raised on every acquisition), and the batched metadata reads no longer miss every file over path-separator differences.
- Ctrl-C during concurrent batch start-up is honored instead of grinding through every queued photo; never-submitted photos are reported as pending.
- A single over-long or blank keyword from the model no longer burns the whole validation retry budget (full vision calls); items are cleaned instead.
- GUI: streamed-in thumbnails no longer swallow the next grid click; a non-domain error during generation no longer leaves the window stuck in the running state; removing photos prunes the visible grid and thumbnail cache; bulk actions no longer kick you out of the photo or folder being reviewed.
- The
guicommand only suggests installing the[gui]extra when PySide6/shiboken6 is actually missing; other import errors surface as themselves. - A failed cache initialization no longer leaks its SQLite connection.
0.5.0 - 2026-07-09
- The GUI is translated at runtime (gettext) and ships a complete Brazilian Portuguese (
pt_BR) translation. Pick a language with Settings > Language, thelanguageconfig key, orPHOTO_TAGGER_LANG; the default follows the OS locale. - New
llamacppprovider for llama.cpp'sllama-server. Default endpointhttp://localhost:8080/v1; env varsLLAMA_CPP_BASE_URLandLLAMA_CPP_API_KEY(the key is only needed whenllama-serverwas started with--api-key). - Anonymous, opt-out usage telemetry with a one-time first-run notice. Disable it with
--no-telemetry,PHOTO_TAGGER_NO_TELEMETRY=1(orDO_NOT_TRACK=1),enabled = falseunder[telemetry]in the config file, or the GUI's Settings > Send Anonymous Telemetry toggle. - GUI redesign: split Generate/Save buttons with option menus, thumbnail badges, multi-selection with bulk actions, tree columns for file type, status, and already-tagged, collapsible keyword-change details, reveal-in-file-manager on Windows, Linux, and macOS, and CSV export.
- The GUI now caches results by default (sharing the CLI's cache format), with skip-cache actions for forcing a fresh generation.
- Settings > Save Settings as Defaults merges into the existing config file instead of rewriting it, preserving comments and unknown keys.
- Windowed
photo-tagger-guientry point, so the desktop app launches without a console window. exiftool_pathconfig key andPHOTO_TAGGER_EXIFTOOLenv var for ExifTool installs not onPATH. A GUI launched from Finder also inherits the login shell'sPATHautomatically.packaging/build_macos_app.shbuilds a standalone, double-clickable macOS app with PyInstaller.
- Console logging is capped at
INFOfrom import (so nothing above it leaks before the CLI flags apply), and file logs are serialized as JSON lines. - System prompt now demands English-only, single-script output and at most one hierarchy chain per keyword.
- The GUI's telemetry beacon is flushed before the process exits instead of being lost.
- The GUI result cache is keyed on the image content hash, so Embed in Photo no longer invalidates it.
- Hierarchical keywords written with
>separators are parsed as hierarchies instead of being mangled, and model keywords are deduplicated. - Ctrl-C during a concurrent batch no longer miscounts photos that finished while the pool drained.
0.4.0 - 2026-06-26
- The
--cache-fileinference cache is now keyed on the image data only (ExifTool'sImageDataHash) instead of a hash of the whole file. Writing metadata into a photo no longer changes the key, so a rerun over the same folder hits the cache even after--embed-in-photowrote tags on the first pass. Formats ExifTool cannot hash that way fall back to the whole-file hash. Existing cache files are simply repopulated on the next run.
0.3.0 - 2026-06-22
- Optional desktop GUI (
photo-tagger gui), via theguiextra (pip install 'photo-tagger[gui]'). A PySide6 review-before-write frontend: drag in photos or folders, generate proposals, then review and edit each photo's title, description, and keywords (with a live Lightroom-hierarchy preview) before saving. PySide6 is imported lazily, so the base CLI never depends on Qt. - New
openaiprovider for any hosted OpenAI-compatible endpoint. SetOPENAI_BASE_URL/--urlandOPENAI_API_KEY/--api-key; fails fast when no key is configured. - New
photo-tagger doctorcommand: a pre-flight check that ExifTool is on PATH and the provider is reachable and serves the requested model, exiting non-zero on failure. --csv-file PATHon thetagcommand writes a per-photo CSV report (existing/written metadata, camera, location, GPS, usage, timing), alongside--summary-fileand--json.--write-keywords/--no-write-keywords(default on) refreshes the title and description while leaving existing keywords on disk untouched.- A PEP 561
py.typedmarker so downstream projects can consume the package's type hints.
- Backends now live in a
photo_tagger.providersregistry; adding a backend is a single entry. - Existing keywords use a typed
KeywordSetvalue object instead of a baredict[str, list[str]]. - The package version is read from installed distribution metadata, so
pyproject.tomlis the only code-side source of truth. - CLI option groups moved out of
main.pyintophoto_tagger.cli_options.
- Hierarchical keywords are generated reliably again: the model schema now has a dedicated
hierarchiesfield for taxonomy chains (Golden Eagle<Bird of Prey<Animal) instead of expecting<embedded in the flatkeywordslist, which the model had stopped doing. (The CLI cache is keyed on the user prompt only, so delete a stale--cache-fileto pick this up on already-processed photos; the GUI never caches.)
0.2.2 - 2026-05-30
- MIT License. The
LICENSEfile now ships in the sdist via PEP 639license-files; the deprecatedLicense :: OSI Approved :: MIT Licenseclassifier was dropped.
- System prompt now forbids emitting the camera body, lens model, or capture timestamp as keywords;
these describe equipment, not subject content. Earlier runs sometimes copied literal EXIF strings
(e.g.
Canon Eos R5M2,Rf200-800Mm F6.3-9 Is Usm) into the keyword list.
create_agentno longer has a code path where the provider could be left unbound for a value outside the supported set. Provider construction moved into_build_provider, which returns from each branch and ends inassert_never, keeping the match exhaustive for the type checker.
0.2.1 - 2026-05-27
AttributeError: 'str' object has no attribute 'parent'when writing the summary file after a successful run ifsummary_file(or any otherPathfield) was set via the TOML config rather than the CLI.apply_overridesnow coerces string TOML values toPathfor fields annotated asPathorPath | None.
0.2.0 - 2026-05-26
- TOML config file. Search order:
$PHOTO_TAGGER_CONFIG,./.photo-tagger.toml,~/.config/photo-tagger/config.toml. CLI flags still win. See.photo-tagger.example.tomlfor a template. --workers Nfor thread-pool concurrency (default 1; the model server is usually the bottleneck).--cache-file PATHSQLite cache of model outputs keyed by image content hash plus model, prompt, and sampling settings. Reruns skip the model entirely when nothing relevant changed. WAL mode is enabled.--lock-file PATHexclusive file lock that refuses to start if anotherphoto-taggeralready holds it. Cross-platform (Linux, macOS, Windows).--summary-file PATHwrites a JSON run summary on completion (success counts, failed files, token usage, wall time). Atomic write; parent dir is created.--jsonemits one NDJSON line per processed photo on stdout. Logs and progress stay on stderr so| jqworks.--skip-taggedskips files whose image or sidecar already has keywords, description, or title (catches photos tagged in Lightroom or by hand).--append-to-skip-file PATHrecords each successful filename so a later run with--skip-from PATHresumes where this one stopped.--newer-than/--older-thanISO 8601 mtime filters. Naive timestamps are read as local time.--prompt-file PATHreplaces the default user prompt with file contents; existing photo metadata is still appended.--max-keywords Ncaps the AI keyword count per photo before merging with existing tags.--dry-runruns the model and logs the proposed metadata without writing.--timeout-secondsper-image hard cap; the retry loop handles the abort.--frequency-penalty(default 0.5) suppresses chant-style token loops observed with Qwen3-VL at low temperature.--progress/--no-progressrich progress bar (auto-disabled on non-tty stderr).- Graceful Ctrl-C in batch runs.
- Token usage tracking per call (
InferenceResult) and per batch (BatchTotals).
- System prompt rewritten: anchors on visible image content, treats EXIF/GPS as corroborative evidence only, refuses to copy existing keywords as filler.
- Default console log level is now
INFO(wasDEBUG). - Default
MAX_TOKENSraised to 1200. - Progress bar routed to stderr; stays clean alongside
--json. - Existing keywords are de-duplicated case-insensitively at read time.
WeightedFlatSubjectis now written back when persisting merged keywords.--extmatches case-insensitively; default aligned with the README.
- Lock leak when the PID file write failed after lock acquire.
parse_hierarchical_keywordreturning['']for empty input.- StubAgent exposing
usageas a callable instead of an attribute. - Pydantic-AI deprecation: access
result.usageas a property. - EXIF orientation now honored; PIL file handles closed eagerly.
- Over-long keyword lists are truncated rather than failing validation.
- Cache and lock startup errors degrade to warnings instead of failing the run.
- Cache I/O errors are treated as warnings, not photo failures.
- Skip-list appender is now thread-safe under
--workers > 1. - Numeric env vars (
JPEG_QUALITY,TEMPERATURE, etc.) parse safely with a warning instead of crashing.
--api-keywarns that CLI args are visible in process listings; prefer env vars (OLLAMA_API_KEY,LM_STUDIO_API_KEY,OPENAI_API_KEY).- Lock file permissions tightened to
0o600.
0.1.0 - 2026-02-16
Initial release.
photo-taggerCLI that asks a vision-language model to analyze each photo and writes Lightroom-compatible metadata (title, one-sentence description, and hierarchical keywords).- RAW and standard image support: CR3, CR2, NEF, JPG, PNG, and more.
- XMP sidecars by default;
--embed-in-photowrites metadata directly into the image instead. - Keyword merging with existing metadata by default;
--overwrite-keywordsreplaces.--no-write-title/--no-write-descriptionskip those fields. --no-backup-xmpto skip the ExifTool_originalsnapshot.- Provider support for Ollama and LM Studio via their OpenAI-compatible APIs. Selected with
--provider; endpoint and credentials via--url/--api-keyor env vars (OLLAMA_BASE_URL,OLLAMA_API_KEY,LM_STUDIO_BASE_URL,LM_STUDIO_API_KEY,OPENAI_API_KEY). - Repeatable
-i/--inputaccepting files and directories;--extfilters by extension,-r/--recursivewalks subdirectories. - Inference knobs:
-m/--model,--temperature,--max-tokens,--retries,--jpeg-dimensions,--jpeg-quality. Each also has an env-var override (MODEL_NAME,TEMPERATURE, etc.). - In-memory JPEG conversion to keep token usage low.
- Structured log files for debugging and auditing.