Skip to content

Caching and locking

Two optional artifacts make repeated runs faster and safer: a SQLite cache that skips redundant model calls, and a cross-platform lock that stops two runs from racing on the same folder. Both are opt-in and controlled by a single flag each.

Caching

cache.py provides InferenceCache, a SQLite database opened in WAL (write-ahead logging) mode. Enable it with --cache-file PATH. The file is created if it does not exist, and a cache hit skips the model call entirely, so reruns on an unchanged folder are fast and free.

Cache key

Each entry is keyed by two parts:

Part What it is
Image-data hash ExifTool's ImageDataHash over the image stream only, so identical pixels match
Namespace digest A digest of the model name, user prompt, and sampling settings

The image-data hash deliberately ignores metadata. ExifTool hashes only the decoded image stream, not the EXIF/XMP/IPTC blocks around it, so writing keywords, a title, or a description into a photo does not change the key. That is what makes the cache survive --embed-in-photo: the first run tags the file and stores the result, and a later run over the same folder still hits the cache even though the file's bytes changed. It is read in the same ExifTool call that already collects existing metadata, so a hit costs no extra subprocess. Formats ExifTool cannot hash that way fall back to a BLAKE2b digest of the whole file; for those, re-embedding metadata does produce a fresh key.

build_cache_namespace() produces the namespace digest from the model name, the user prompt, and the sampling settings: temperature, max_tokens, frequency_penalty, jpeg_dimensions, and jpeg_quality. The system prompt is intentionally excluded from the digest because it ships with the code and changes only when you upgrade photo-tagger.

The split key means a cached result is reused only when both the image and every input that could change the model's answer are the same. Change the model or the prompt and you naturally get fresh entries, with the old ones simply going unused.

Lookup flow

flowchart TD
    A[Read metadata + ImageDataHash via ExifTool] --> B{Hash available?}
    B -->|Yes| C[Use ImageDataHash]
    B -->|No, unsupported format| D[BLAKE2b of whole file]
    C --> E[Combine with namespace digest]
    D --> E
    E --> F{Key in cache?}
    F -->|Hit| G[Reuse stored metadata, skip model call]
    F -->|Miss| H[Run AI call, then store result]

Managing the cache

The cache file is safe to delete at any time. The next run simply repopulates it: deleting it costs you only the model calls you would have skipped, never any metadata already written to your photos. Because changing the model or prompt produces a different namespace digest, you do not need to clear the cache when you switch models; stale entries are just never looked up again.

Combine --cache-file across reruns

Point every run at the same --cache-file to get the most out of it. A first pass tags the folder and fills the cache; a later rerun (for example after adding a few new photos, or with --workers bumped up) reuses the stored results for every unchanged image and only calls the model for the new or changed ones.

Locking

locking.py provides FileLock, a thin wrapper over the filelock library that works on Linux, macOS, and Windows. Enable it with --lock-file PATH.

The lock is exclusive and non-blocking: photo-tagger calls acquire(timeout=0) and, if another photo-tagger process already holds the lock, raises LockHeldError and refuses to start instead of waiting. This guarantees that two runs cannot race on the same folder and write conflicting metadata at the same time.

# Point both runs at the same lock file; the second one refuses to start.
photo-tagger -i ~/Photos --lock-file ~/Photos/.photo-tagger.lock

Use one lock file per folder

The lock guards whatever you choose to protect, not the input path automatically. Always use the same --lock-file for runs that touch the same folder, otherwise two runs with different lock files will not see each other and can still overlap.

  • Processing pipeline shows where the cache lookup and lock acquisition sit in run_batch().
  • CLI reference lists --cache-file and --lock-file with their defaults.