Skip to main content

Tag macOS Photos library images using local Gemma model for searchable tags

Project description

pyimgtag

CI CodeQL PyPI version Python versions License: MIT codecov

Tag images using a local Gemma model for searchable tags, with optional Apple Photos integration on macOS.

Overview

pyimgtag uses a locally-running Gemma model (via Ollama) to analyse images and generate 1-5 descriptive tags per photo. It reads EXIF GPS coordinates and resolves them to the nearest city/place using OpenStreetMap Nominatim. Everything runs on-device -- no cloud, no data leaves your computer.

Works on macOS, Linux, and Windows. Apple Photos integration (write-back) is macOS-only.

Key features:

  • One local model call per image, compact prompt, low token usage
  • Rich AI metadata: scene category, emotional tone, cleanup classification, text detection, event hints
  • EXIF GPS as source of truth for location (never guessed from image content)
  • Open reverse geocoding via Nominatim with local disk cache
  • Supports exported folders and Apple Photos library originals (macOS only)
  • Apple Photos write-back: push AI tags and descriptions back as keywords/captions (macOS only)
  • Subcommands: run, judge, status, reprocess, cleanup, preflight, query, tags
  • Photo quality scoring with professional 13-criterion rubric (new: judge subcommand)
  • Dry-run mode, date/limit filters, JSON/CSV export
  • SQLite progress DB with schema versioning for incremental re-runs

Requirements

  • Python 3.11+
  • Ollama installed and running
  • Gemma 4 model pulled: ollama pull gemma4:e4b

macOS-specific:

  • Apple Silicon or Intel Mac
  • Optional: exiftool for reliable HEIC EXIF (falls back to Pillow)
  • Optional: pillow-heif for HEIC image loading

All platforms:

  • Works on macOS, Linux, and Windows
  • EXIF writing via exiftool (if installed) works across platforms
  • Apple Photos write-back requires macOS

Quick Start

pip install -e ".[dev]"

# Pull the model
ollama pull gemma4:e4b

# Dry-run on an exported folder, first 20 images
pyimgtag run --input-dir ~/Pictures/exported --limit 20 --dry-run

# Single date
pyimgtag run --input-dir ~/Pictures/exported --date 2026-04-01 --dry-run

# Date range with JSON output
pyimgtag run --input-dir ~/Pictures/exported \
  --date-from 2026-03-01 --date-to 2026-03-31 \
  --output-json results.json

# Photos library
pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary \
  --limit 50 --dry-run

# Check processing progress
pyimgtag status

# Re-tag all photos (e.g. after prompt improvements)
pyimgtag reprocess

# List photos flagged for deletion
pyimgtag cleanup

# Score photos by quality (judge)
pyimgtag judge --input-dir ~/Pictures/exported --limit 20 --verbose

# Filter to only strong photos, save ranking to JSON
pyimgtag judge --input-dir ~/Pictures/exported \
  --min-score 3.5 --output-json ranking.json

Installation

# From source
git clone https://github.com/kurok/pyimgtag.git
cd pyimgtag
pip install -e ".[dev]"

# Optional HEIC support
pip install pillow-heif

# Optional exiftool (better EXIF for HEIC)
brew install exiftool

Platform Support

Feature macOS Linux Windows
Image tagging via Ollama
EXIF reading (GPS, dates)
Reverse geocoding (Nominatim)
EXIF writing via exiftool
HEIC conversion (sips / pillow-heif) ✅ sips + pillow-heif ✅ pillow-heif ✅ pillow-heif
RAW image support (rawpy)
Apple Photos library scanning
Apple Photos write-back
Face management (Apple Photos)

Note: Most features work cross-platform. Apple Photos integration and face management are macOS-only — they require AppleScript via osascript.

macOS Setup

# Prerequisites
brew install ollama exiftool
ollama pull gemma4:e4b

# Install
pip install "pyimgtag[all]"   # includes pillow-heif, photoscript, rawpy
# or from source
git clone https://github.com/kurok/pyimgtag.git
cd pyimgtag
pip install -e ".[all,dev]"

Features available: everything including Apple Photos integration, HEIC via sips, face management.

Typical macOS workflow:

# Tag your Photos library directly
pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary --write-back --limit 50

# Score photo quality
pyimgtag judge --photos-library ~/Pictures/Photos\ Library.photoslibrary --min-score 4.0

# Import named faces from Apple Photos
pyimgtag faces import --photos-library ~/Pictures/Photos\ Library.photoslibrary

Note: Apple Photos library access requires Full Disk Access permission for your terminal app — grant it in System Settings > Privacy & Security > Full Disk Access.

Linux Setup

# Ubuntu/Debian
sudo apt-get install exiftool python3.11 python3-pip
# or install exiftool from https://exiftool.org

# Fedora/RHEL
sudo dnf install perl-Image-ExifTool python3.11

# Arch
sudo pacman -S perl-image-exiftool python

# Install Ollama
curl -fsSL https://ollama.com/install.sh | sh
ollama pull gemma4:e4b

# Install pyimgtag
pip install "pyimgtag[heic]"   # includes pillow-heif for HEIC
# or from source
git clone https://github.com/kurok/pyimgtag.git
cd pyimgtag
pip install -e ".[heic,dev]"

Features available: image tagging, EXIF reading/writing, geocoding, judge, dedup, JSON/CSV export. No Apple Photos integration.

Typical Linux workflow:

# Tag an exported photo directory
pyimgtag run --input-dir ~/Pictures/exported --output-json results.json

# With EXIF write-back (requires exiftool)
pyimgtag run --input-dir ~/Pictures/exported --write-exif

# Score photo quality
pyimgtag judge --input-dir ~/Pictures/exported --min-score 3.5 --output-json ranking.json

Note: --write-back (Apple Photos) is silently skipped on Linux with a warning. Use --write-exif instead.

Windows Setup

# Install Python 3.11+ from https://python.org
# Install Ollama from https://ollama.com

# Install exiftool — download from https://exiftool.org/
# Or via Chocolatey:
choco install exiftool

# Or via winget:
winget install OliverBetz.ExifTool

ollama pull gemma4:e4b

# Install pyimgtag
pip install "pyimgtag[heic]"
# or from source
git clone https://github.com/kurok/pyimgtag.git
cd pyimgtag
pip install -e ".[heic,dev]"

Features available: same as Linux — tagging, EXIF, geocoding, judge, dedup, export. No Apple Photos integration.

Typical Windows workflow (PowerShell):

# Tag photos in a folder
pyimgtag run --input-dir C:\Users\Me\Pictures\exported --output-json results.json

# Score photo quality
pyimgtag judge --input-dir C:\Users\Me\Pictures\exported --min-score 3.5

# Check what was processed
pyimgtag status

Note: On Windows, use \ path separators or quote paths with spaces: "C:\My Photos".

Platform Troubleshooting

macOS:

  • "Operation not permitted" on Photos library → grant Full Disk Access to Terminal in System Settings > Privacy & Security > Full Disk Access
  • exiftool not found → brew install exiftool
  • HEIC files not loading → pip install pillow-heif
  • Ollama not running → brew services start ollama or run ollama serve

Linux:

  • exiftool not found → install via package manager (see setup above)
  • HEIC files not loading → pip install pillow-heif
  • Ollama not running → ollama serve in a separate terminal
  • Permission denied on image folder → check directory permissions with ls -la

Windows:

  • exiftool not found → add exiftool directory to PATH, or install via Chocolatey/winget
  • Python not found → ensure Python 3.11+ is installed and added to PATH during install
  • HEIC files not loading → pip install pillow-heif
  • Ollama not running → start Ollama from system tray or run ollama serve
  • Long paths issue → enable long path support: Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1

Usage

Subcommands

pyimgtag uses subcommands. Run pyimgtag --help for the full list.

pyimgtag run — tag images

# Exported image folder
pyimgtag run --input-dir /path/to/photos

# Apple Photos library
pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary

# With filters
pyimgtag run --input-dir /path/to/photos \
  --limit 100 --date-from 2026-03-01 --date-to 2026-03-31

# Write tags back to Apple Photos as keywords
pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary \
  --write-back --limit 10

# Deduplicate by perceptual hash
pyimgtag run --input-dir /path/to/photos --dedup

# Export to JSON
pyimgtag run --input-dir /path/to/photos --output-json results.json

Run flags:

Flag Description
--input-dir PATH Exported image folder
--photos-library PATH Apple Photos library package (macOS only)
--limit N Max images to process
--date YYYY-MM-DD Single date filter
--date-from / --date-to Date range filter
--extensions jpg,png File types (default: jpg,jpeg,heic,png)
--skip-no-gps Skip images without GPS data
--dry-run Verbose output, no DB writes
--verbose / -v Detailed per-file output
--output-json FILE Write results to JSON
--output-csv FILE Write results to CSV
--jsonl-stdout JSONL output to stdout
--write-back Write tags/description back to Apple Photos (macOS only)
--write-exif Write description and keywords to image EXIF
--dedup Skip duplicates via perceptual hash
--dedup-threshold N Hamming distance threshold (default: 5)
--model NAME Ollama model (default: gemma4:e4b)
--ollama-url URL Ollama API URL
--max-dim N Max image dimension (default: 1280)
--timeout N Model request timeout in seconds
--db PATH Progress database path
--no-cache Skip progress DB, reprocess all

pyimgtag status — check progress

# Show processing stats
pyimgtag status

# Output:
# Progress: 142 / 200 (71%)
#   ok:      140
#   error:   2
#   pending: 58

pyimgtag reprocess — reset for re-tagging

# Reset everything (e.g. after prompt improvements)
pyimgtag reprocess

# Reset only failed entries
pyimgtag reprocess --status error

pyimgtag cleanup — find photos to delete

# List photos the AI flagged as "delete"
pyimgtag cleanup

# Also include "review" (uncertain) candidates
pyimgtag cleanup --include-review

# Output:
# Cleanup candidates (delete): 12
#
#   [delete]  /path/to/blurry_photo.jpg  | 2026-03-15  | tags: blurry, dark
#   [delete]  /path/to/screenshot.png    | 2026-04-01  | tags: screenshot, text

pyimgtag query — search tagged images

# Search by tag
pyimgtag query --tag sunset

# Search by location
pyimgtag query --location "San Francisco"

# Output as JSON
pyimgtag query --tag beach --output-json matches.json

pyimgtag tags — manage tags

# List all tags with image counts
pyimgtag tags list

# Rename a tag across all images
pyimgtag tags rename old-name new-name

# Delete a tag from all images
pyimgtag tags delete unwanted-tag --dry-run

# Merge one tag into another
pyimgtag tags merge source-tag target-tag

pyimgtag preflight — check prerequisites

# Verify Ollama, model, and source path
pyimgtag preflight --input-dir ~/Pictures/exported

pyimgtag judge — score photo quality

Score each image against a 13-criterion professional rubric. Outputs a ranked list with weighted scores on a 1–5 scale. Requires Ollama.

# Score all images in a folder
pyimgtag judge --input-dir ~/Pictures/exported

# Only show photos scoring 3.5 or above
pyimgtag judge --input-dir ~/Pictures/exported --min-score 3.5

# Verbose breakdown (per-criterion scores)
pyimgtag judge --input-dir ~/Pictures/exported --limit 20 --verbose

# Sort by filename instead of score
pyimgtag judge --input-dir ~/Pictures/exported --sort-by name

# Score Photos library
pyimgtag judge --photos-library ~/Pictures/Photos\ Library.photoslibrary \
  --limit 50 --min-score 4.0

# Save full ranking to JSON
pyimgtag judge --input-dir ~/Pictures/exported \
  --output-json ranking.json

Sample output (brief mode):

[1/5] golden_hour.jpg → 4.32/5 strong | + impact, composition_center | - edit_integrity, noise_cleanliness
  Golden light over the cityscape; strong composition but slight haloing on edges.
[2/5] portrait.jpg → 3.87/5 solid | + focus_sharpness, lighting | - creativity_style, color_mood
  Well-lit portrait; technically solid but conventional treatment.

Sample output (--verbose):

[1/5] golden_hour.jpg
  Score:   4.32/5  (core: 4.55, visible: 3.90)
  Best:    impact=5, composition_center=5, lighting=4
  Weakest: edit_integrity=3, noise_cleanliness=3, subject_separation=3
  Verdict: Golden light over the cityscape; strong composition but slight haloing on edges.

Judge flags:

Flag Default Description
--input-dir PATH Exported image folder
--photos-library PATH Apple Photos library (macOS only)
--limit N unlimited Max images to score
--extensions EXT,... jpg,jpeg,heic,png,tiff,webp File types
--min-score SCORE Only show images scoring ≥ SCORE
--sort-by score|name score Final sort order
--output-json FILE Write ranked results to JSON
--verbose false Per-criterion breakdown
--no-recursive false Do not scan subdirectories
--model NAME gemma4:e4b Ollama model
--ollama-url URL http://localhost:11434 Ollama API URL
--max-dim N 1280 Max image dimension before resize
--timeout N 120 Request timeout (seconds)

Sample verbose output

[1/50] sunset_beach.jpg
  Path:     /Users/me/Pictures/exported/sunset_beach.jpg
  Date:     2026-04-01 14:30:00
  Tags:     sunset, beach, ocean, waves, sand
  Summary:  golden hour sunset over the Pacific
  Scene:    outdoor_leisure
  Tone:     positive
  Cleanup:  keep
  Event:    outing
  Signif.:  high
  GPS:      37.7749, -122.4194
  Location: San Francisco, California, United States
  Status:   ok

--- Summary ---
  Scanned:          200
  Processed:        50
  Skipped (date):   0
  Skipped (no GPS): 0
  Skipped (no file):0
  Model failures:   2
  Geocode failures: 0

Output schema

Each result (JSON/CSV) includes:

Field Description
file_path Full path to image
file_name Filename
source_type directory or photos_library
image_date EXIF or file date
tags 1-5 vision model tags
scene_summary Short scene description
scene_category indoor_home, indoor_work, outdoor_leisure, outdoor_travel, transport, other
emotional_tone positive, neutral, negative, mixed
cleanup_class keep, review, delete
has_text Whether image contains readable text
text_summary Extracted text summary (if has_text)
event_hint outing, gathering, work, travel, daily, other
significance high, medium, low
gps_lat / gps_lon EXIF GPS coordinates
nearest_place Village/town/suburb
nearest_city City
nearest_region State/region
nearest_country Country
processing_status ok or error
error_message Error details if any
phash Perceptual hash (when --dedup used)

Judge output schema

Results from pyimgtag judge --output-json use a different structure:

Field Description
file_path Full path to image
file_name Filename
weighted_score Overall weighted score (1.0–5.0)
core_score Artistic criteria average (impact, composition, lighting, etc.)
visible_score Technical criteria average (focus, exposure, noise, etc.)
verdict One-sentence summary of key strength and weakness
scores.impact Emotional pull and memorability (1–5)
scores.story_subject Clear subject and meaning (1–5)
scores.composition_center Visual flow, balance, center of interest (1–5)
scores.lighting Quality, control, mood support (1–5)
scores.creativity_style Originality of treatment (1–5)
scores.color_mood Color balance and mood fit (1–5)
scores.presentation_crop Crop, framing, aspect ratio (1–5)
scores.technical_excellence Exposure, retouching, overall finish (1–5)
scores.focus_sharpness Critical detail is sharp (1–5)
scores.exposure_tonal Highlights and shadows under control (1–5)
scores.noise_cleanliness Clean detail, no distracting grain (1–5)
scores.subject_separation Subject stands out from background (1–5)
scores.edit_integrity No halos, overprocessing, or clone artefacts (1–5)

Architecture

src/pyimgtag/
  main.py              CLI entry point and subcommand dispatch (thin)
  models.py            Data classes (ExifData, TagResult, GeoResult, ImageResult)
  scanner.py           Directory and Photos library scanning
  exif_reader.py       EXIF GPS + date extraction (exiftool + Pillow)
  ollama_client.py     Ollama vision API client (rich structured response)
  geocoder.py          Nominatim reverse geocoder with disk cache
  filters.py           Date/GPS filter logic
  output_writer.py     JSON/CSV/JSONL output
  progress_db.py       SQLite progress DB with versioned migrations
  applescript_writer.py  Apple Photos keyword/description write-back
  dedup.py             Perceptual hash duplicate detection
  heic_converter.py    HEIC to JPEG conversion (macOS sips)
  cache.py             Simple JSON disk cache
  judge_scorer.py        Weighted rubric score computation (13-criterion)
  commands/
    run.py             `pyimgtag run` handler
    judge.py           `pyimgtag judge` handler
    db.py              `pyimgtag status/reprocess/cleanup` handlers
    query.py           `pyimgtag query` handler
    tags.py            `pyimgtag tags` handler
    faces.py           `pyimgtag faces` handler
    preflight_cmd.py   `pyimgtag preflight` handler
    review_cmd.py      `pyimgtag review` handler

Development

pip install -e ".[dev,lint,security]"

pytest tests/ -v
ruff format src/ tests/ && ruff check src/ tests/ --fix
python -m mypy src/pyimgtag/ --ignore-missing-imports --disable-error-code import-untyped
python -m bandit -r src/pyimgtag/ -c pyproject.toml
pre-commit install && pre-commit run --all-files

Contributing

See CONTRIBUTING.md.

License

MIT -- see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyimgtag-0.4.1.tar.gz (118.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyimgtag-0.4.1-py3-none-any.whl (75.3 kB view details)

Uploaded Python 3

File details

Details for the file pyimgtag-0.4.1.tar.gz.

File metadata

  • Download URL: pyimgtag-0.4.1.tar.gz
  • Upload date:
  • Size: 118.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyimgtag-0.4.1.tar.gz
Algorithm Hash digest
SHA256 107e02e0f22bde4e87e7f3dc7beb560c06d17a0b4fbf05c40034dbbfb2f00e14
MD5 15325da39f7e806f43ae368257554489
BLAKE2b-256 7209025d2d2b229a6f9418d68648b0af54b57d7b372b7a5b0e3432a9bd89daee

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyimgtag-0.4.1.tar.gz:

Publisher: publish.yml on kurok/pyimgtag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyimgtag-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: pyimgtag-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 75.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyimgtag-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 271abe95ea9d21142e8ed8fb35b61fab8a4184b7750870f1c3139b0c3c93ef11
MD5 f2e2e2a4349ee4d0a4fe27bd9cab4cf6
BLAKE2b-256 824bcc21c8794f8be8f535b188fd3beedc08cd84051dff807f6cfcd76c421e87

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyimgtag-0.4.1-py3-none-any.whl:

Publisher: publish.yml on kurok/pyimgtag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page