pyimgtag 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. pyimgtag-0.1.0/LICENSE +21 -0
  2. pyimgtag-0.1.0/PKG-INFO +377 -0
  3. pyimgtag-0.1.0/README.md +327 -0
  4. pyimgtag-0.1.0/pyproject.toml +68 -0
  5. pyimgtag-0.1.0/setup.cfg +4 -0
  6. pyimgtag-0.1.0/src/pyimgtag/__init__.py +5 -0
  7. pyimgtag-0.1.0/src/pyimgtag/__main__.py +8 -0
  8. pyimgtag-0.1.0/src/pyimgtag/applescript_writer.py +187 -0
  9. pyimgtag-0.1.0/src/pyimgtag/cache.py +40 -0
  10. pyimgtag-0.1.0/src/pyimgtag/commands/__init__.py +1 -0
  11. pyimgtag-0.1.0/src/pyimgtag/commands/db.py +74 -0
  12. pyimgtag-0.1.0/src/pyimgtag/commands/faces.py +209 -0
  13. pyimgtag-0.1.0/src/pyimgtag/commands/preflight_cmd.py +31 -0
  14. pyimgtag-0.1.0/src/pyimgtag/commands/query.py +70 -0
  15. pyimgtag-0.1.0/src/pyimgtag/commands/review_cmd.py +27 -0
  16. pyimgtag-0.1.0/src/pyimgtag/commands/run.py +383 -0
  17. pyimgtag-0.1.0/src/pyimgtag/commands/tags.py +100 -0
  18. pyimgtag-0.1.0/src/pyimgtag/dedup.py +97 -0
  19. pyimgtag-0.1.0/src/pyimgtag/exif_reader.py +223 -0
  20. pyimgtag-0.1.0/src/pyimgtag/exif_writer.py +342 -0
  21. pyimgtag-0.1.0/src/pyimgtag/face_clustering.py +73 -0
  22. pyimgtag-0.1.0/src/pyimgtag/face_detection.py +99 -0
  23. pyimgtag-0.1.0/src/pyimgtag/face_embedding.py +100 -0
  24. pyimgtag-0.1.0/src/pyimgtag/filters.py +54 -0
  25. pyimgtag-0.1.0/src/pyimgtag/geocoder.py +85 -0
  26. pyimgtag-0.1.0/src/pyimgtag/heic_converter.py +80 -0
  27. pyimgtag-0.1.0/src/pyimgtag/main.py +315 -0
  28. pyimgtag-0.1.0/src/pyimgtag/models.py +163 -0
  29. pyimgtag-0.1.0/src/pyimgtag/ollama_client.py +295 -0
  30. pyimgtag-0.1.0/src/pyimgtag/output_writer.py +62 -0
  31. pyimgtag-0.1.0/src/pyimgtag/preflight.py +166 -0
  32. pyimgtag-0.1.0/src/pyimgtag/progress_db.py +695 -0
  33. pyimgtag-0.1.0/src/pyimgtag/raw_converter.py +175 -0
  34. pyimgtag-0.1.0/src/pyimgtag/review_server.py +456 -0
  35. pyimgtag-0.1.0/src/pyimgtag/scanner.py +52 -0
  36. pyimgtag-0.1.0/src/pyimgtag.egg-info/PKG-INFO +377 -0
  37. pyimgtag-0.1.0/src/pyimgtag.egg-info/SOURCES.txt +58 -0
  38. pyimgtag-0.1.0/src/pyimgtag.egg-info/dependency_links.txt +1 -0
  39. pyimgtag-0.1.0/src/pyimgtag.egg-info/entry_points.txt +2 -0
  40. pyimgtag-0.1.0/src/pyimgtag.egg-info/requires.txt +40 -0
  41. pyimgtag-0.1.0/src/pyimgtag.egg-info/top_level.txt +1 -0
  42. pyimgtag-0.1.0/tests/test_applescript_writer.py +478 -0
  43. pyimgtag-0.1.0/tests/test_cache.py +30 -0
  44. pyimgtag-0.1.0/tests/test_dedup.py +102 -0
  45. pyimgtag-0.1.0/tests/test_exif_reader.py +115 -0
  46. pyimgtag-0.1.0/tests/test_exif_writer.py +492 -0
  47. pyimgtag-0.1.0/tests/test_face_clustering.py +174 -0
  48. pyimgtag-0.1.0/tests/test_face_detection.py +179 -0
  49. pyimgtag-0.1.0/tests/test_face_embedding.py +207 -0
  50. pyimgtag-0.1.0/tests/test_filters.py +90 -0
  51. pyimgtag-0.1.0/tests/test_heic_converter.py +153 -0
  52. pyimgtag-0.1.0/tests/test_main.py +892 -0
  53. pyimgtag-0.1.0/tests/test_models.py +131 -0
  54. pyimgtag-0.1.0/tests/test_ollama_client.py +246 -0
  55. pyimgtag-0.1.0/tests/test_output_writer.py +57 -0
  56. pyimgtag-0.1.0/tests/test_preflight.py +185 -0
  57. pyimgtag-0.1.0/tests/test_progress_db.py +1244 -0
  58. pyimgtag-0.1.0/tests/test_raw_converter.py +270 -0
  59. pyimgtag-0.1.0/tests/test_review_server.py +173 -0
  60. pyimgtag-0.1.0/tests/test_scanner.py +81 -0
pyimgtag-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pyimgtag contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,377 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyimgtag
3
+ Version: 0.1.0
4
+ Summary: Tag macOS Photos library images using local Gemma model for searchable tags
5
+ Author: pyimgtag contributors
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Topic :: Multimedia :: Graphics
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.28
19
+ Requires-Dist: Pillow>=10.0
20
+ Requires-Dist: imagehash>=4.3.1
21
+ Requires-Dist: exifread>=3.0
22
+ Provides-Extra: heic
23
+ Requires-Dist: pillow-heif>=0.10.0; extra == "heic"
24
+ Provides-Extra: photos
25
+ Requires-Dist: photoscript>=0.5.3; extra == "photos"
26
+ Provides-Extra: face
27
+ Requires-Dist: face-recognition>=1.3; extra == "face"
28
+ Requires-Dist: scikit-learn>=1.3; extra == "face"
29
+ Provides-Extra: review
30
+ Requires-Dist: fastapi>=0.100; extra == "review"
31
+ Requires-Dist: uvicorn>=0.20; extra == "review"
32
+ Requires-Dist: pydantic>=2.0; extra == "review"
33
+ Provides-Extra: raw
34
+ Requires-Dist: rawpy>=0.18; extra == "raw"
35
+ Provides-Extra: all
36
+ Requires-Dist: pillow-heif>=0.10.0; extra == "all"
37
+ Requires-Dist: photoscript>=0.5.3; extra == "all"
38
+ Requires-Dist: rawpy>=0.18; extra == "all"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=9.0; extra == "dev"
41
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
42
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
43
+ Provides-Extra: lint
44
+ Requires-Dist: mypy>=1.0; extra == "lint"
45
+ Requires-Dist: ruff>=0.1.0; extra == "lint"
46
+ Provides-Extra: security
47
+ Requires-Dist: bandit>=1.7; extra == "security"
48
+ Requires-Dist: pip-audit>=2.6; extra == "security"
49
+ Dynamic: license-file
50
+
51
+ # pyimgtag
52
+
53
+ [![CI](https://github.com/kurok/pyimgtag/actions/workflows/python-package.yml/badge.svg)](https://github.com/kurok/pyimgtag/actions/workflows/python-package.yml)
54
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
56
+
57
+ Tag images using a local Gemma model for searchable tags, with optional Apple Photos integration on macOS.
58
+
59
+ ## Overview
60
+
61
+ pyimgtag uses a locally-running Gemma model (via [Ollama](https://ollama.ai)) to
62
+ analyse images and generate 1-5 descriptive tags per photo. It reads EXIF GPS
63
+ coordinates and resolves them to the nearest city/place using OpenStreetMap
64
+ Nominatim. Everything runs on-device -- no cloud, no data leaves your computer.
65
+
66
+ Works on **macOS, Linux, and Windows**. Apple Photos integration (write-back) is macOS-only.
67
+
68
+ **Key features:**
69
+
70
+ - One local model call per image, compact prompt, low token usage
71
+ - Rich AI metadata: scene category, emotional tone, cleanup classification, text detection, event hints
72
+ - EXIF GPS as source of truth for location (never guessed from image content)
73
+ - Open reverse geocoding via Nominatim with local disk cache
74
+ - Supports exported folders and Apple Photos library originals (macOS only)
75
+ - Apple Photos write-back: push AI tags and descriptions back as keywords/captions (macOS only)
76
+ - Subcommands: `run`, `status`, `reprocess`, `cleanup`, `preflight`
77
+ - Dry-run mode, date/limit filters, JSON/CSV export
78
+ - SQLite progress DB with schema versioning for incremental re-runs
79
+
80
+ ## Requirements
81
+
82
+ - Python 3.11+
83
+ - [Ollama](https://ollama.ai) installed and running
84
+ - Gemma 4 model pulled: `ollama pull gemma4:e4b`
85
+
86
+ **macOS-specific:**
87
+ - Apple Silicon or Intel Mac
88
+ - Optional: `exiftool` for reliable HEIC EXIF (falls back to Pillow)
89
+ - Optional: `pillow-heif` for HEIC image loading
90
+
91
+ **All platforms:**
92
+ - Works on macOS, Linux, and Windows
93
+ - EXIF writing via `exiftool` (if installed) works across platforms
94
+ - Apple Photos write-back requires macOS
95
+
96
+ ## Quick Start
97
+
98
+ ```bash
99
+ pip install -e ".[dev]"
100
+
101
+ # Pull the model
102
+ ollama pull gemma4:e4b
103
+
104
+ # Dry-run on an exported folder, first 20 images
105
+ pyimgtag run --input-dir ~/Pictures/exported --limit 20 --dry-run
106
+
107
+ # Single date
108
+ pyimgtag run --input-dir ~/Pictures/exported --date 2026-04-01 --dry-run
109
+
110
+ # Date range with JSON output
111
+ pyimgtag run --input-dir ~/Pictures/exported \
112
+ --date-from 2026-03-01 --date-to 2026-03-31 \
113
+ --output-json results.json
114
+
115
+ # Photos library
116
+ pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary \
117
+ --limit 50 --dry-run
118
+
119
+ # Check processing progress
120
+ pyimgtag status
121
+
122
+ # Re-tag all photos (e.g. after prompt improvements)
123
+ pyimgtag reprocess
124
+
125
+ # List photos flagged for deletion
126
+ pyimgtag cleanup
127
+ ```
128
+
129
+ ## Installation
130
+
131
+ ```bash
132
+ # From source
133
+ git clone https://github.com/kurok/pyimgtag.git
134
+ cd pyimgtag
135
+ pip install -e ".[dev]"
136
+
137
+ # Optional HEIC support
138
+ pip install pillow-heif
139
+
140
+ # Optional exiftool (better EXIF for HEIC)
141
+ brew install exiftool
142
+ ```
143
+
144
+ ## Platform Support
145
+
146
+ | Feature | macOS | Linux | Windows |
147
+ |---------|-------|-------|---------|
148
+ | Image tagging via Ollama | ✅ | ✅ | ✅ |
149
+ | EXIF reading (GPS, dates) | ✅ | ✅ | ✅ |
150
+ | Reverse geocoding (Nominatim) | ✅ | ✅ | ✅ |
151
+ | EXIF writing via `exiftool` | ✅ | ✅ | ✅ |
152
+ | Apple Photos library scanning | ✅ | ❌ | ❌ |
153
+ | Apple Photos write-back | ✅ | ❌ | ❌ |
154
+
155
+ **Note:** Most features work cross-platform. Apple Photos integration is macOS-only since it requires macOS-specific AppleScript functionality.
156
+
157
+ ### Cross-Platform Examples
158
+
159
+ **Linux/Windows (export folders only):**
160
+ ```bash
161
+ # Tag exported images with EXIF writing
162
+ pyimgtag run --input-dir /mnt/photos \
163
+ --output-json results.json \
164
+ --write-exif # If exiftool is installed
165
+
166
+ # Tags and descriptions stored in results.json and EXIF
167
+ ```
168
+
169
+ **macOS (both export folders and Photos library):**
170
+ ```bash
171
+ # Tag Photos library with direct write-back to Photos app
172
+ pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary \
173
+ --write-back # Push tags/descriptions to Apple Photos
174
+
175
+ # Or export folder with both EXIF and JSON output
176
+ pyimgtag run --input-dir ~/Downloads/exported \
177
+ --write-exif --output-json results.json
178
+ ```
179
+
180
+ The tool gracefully handles missing features—if you use `--write-back` on Linux/Windows, it will warn you and proceed without it.
181
+
182
+ ## Usage
183
+
184
+ ### Subcommands
185
+
186
+ pyimgtag uses subcommands. Run `pyimgtag --help` for the full list.
187
+
188
+ #### `pyimgtag run` — tag images
189
+
190
+ ```bash
191
+ # Exported image folder
192
+ pyimgtag run --input-dir /path/to/photos
193
+
194
+ # Apple Photos library
195
+ pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary
196
+
197
+ # With filters
198
+ pyimgtag run --input-dir /path/to/photos \
199
+ --limit 100 --date-from 2026-03-01 --date-to 2026-03-31
200
+
201
+ # Write tags back to Apple Photos as keywords
202
+ pyimgtag run --photos-library ~/Pictures/Photos\ Library.photoslibrary \
203
+ --write-back --limit 10
204
+
205
+ # Deduplicate by perceptual hash
206
+ pyimgtag run --input-dir /path/to/photos --dedup
207
+
208
+ # Export to JSON
209
+ pyimgtag run --input-dir /path/to/photos --output-json results.json
210
+ ```
211
+
212
+ **Run flags:**
213
+
214
+ | Flag | Description |
215
+ |---|---|
216
+ | `--input-dir PATH` | Exported image folder |
217
+ | `--photos-library PATH` | Apple Photos library package *(macOS only)* |
218
+ | `--limit N` | Max images to process |
219
+ | `--date YYYY-MM-DD` | Single date filter |
220
+ | `--date-from` / `--date-to` | Date range filter |
221
+ | `--extensions jpg,png` | File types (default: jpg,jpeg,heic,png) |
222
+ | `--skip-no-gps` | Skip images without GPS data |
223
+ | `--dry-run` | Verbose output, no DB writes |
224
+ | `--verbose` / `-v` | Detailed per-file output |
225
+ | `--output-json FILE` | Write results to JSON |
226
+ | `--output-csv FILE` | Write results to CSV |
227
+ | `--jsonl-stdout` | JSONL output to stdout |
228
+ | `--write-back` | Write tags/description back to Apple Photos *(macOS only)* |
229
+ | `--write-exif` | Write description and keywords to image EXIF |
230
+ | `--dedup` | Skip duplicates via perceptual hash |
231
+ | `--dedup-threshold N` | Hamming distance threshold (default: 5) |
232
+ | `--model NAME` | Ollama model (default: gemma4:e4b) |
233
+ | `--ollama-url URL` | Ollama API URL |
234
+ | `--max-dim N` | Max image dimension (default: 1280) |
235
+ | `--timeout N` | Model request timeout in seconds |
236
+ | `--db PATH` | Progress database path |
237
+ | `--no-cache` | Skip progress DB, reprocess all |
238
+
239
+ #### `pyimgtag status` — check progress
240
+
241
+ ```bash
242
+ # Show processing stats
243
+ pyimgtag status
244
+
245
+ # Output:
246
+ # Progress: 142 / 200 (71%)
247
+ # ok: 140
248
+ # error: 2
249
+ # pending: 58
250
+ ```
251
+
252
+ #### `pyimgtag reprocess` — reset for re-tagging
253
+
254
+ ```bash
255
+ # Reset everything (e.g. after prompt improvements)
256
+ pyimgtag reprocess
257
+
258
+ # Reset only failed entries
259
+ pyimgtag reprocess --status error
260
+ ```
261
+
262
+ #### `pyimgtag cleanup` — find photos to delete
263
+
264
+ ```bash
265
+ # List photos the AI flagged as "delete"
266
+ pyimgtag cleanup
267
+
268
+ # Also include "review" (uncertain) candidates
269
+ pyimgtag cleanup --include-review
270
+
271
+ # Output:
272
+ # Cleanup candidates (delete): 12
273
+ #
274
+ # [delete] /path/to/blurry_photo.jpg | 2026-03-15 | tags: blurry, dark
275
+ # [delete] /path/to/screenshot.png | 2026-04-01 | tags: screenshot, text
276
+ ```
277
+
278
+ #### `pyimgtag preflight` — check prerequisites
279
+
280
+ ```bash
281
+ # Verify Ollama, model, and source path
282
+ pyimgtag preflight --input-dir ~/Pictures/exported
283
+ ```
284
+
285
+ ### Sample verbose output
286
+
287
+ ```
288
+ [1/50] sunset_beach.jpg
289
+ Path: /Users/me/Pictures/exported/sunset_beach.jpg
290
+ Date: 2026-04-01 14:30:00
291
+ Tags: sunset, beach, ocean, waves, sand
292
+ Summary: golden hour sunset over the Pacific
293
+ Scene: outdoor_leisure
294
+ Tone: positive
295
+ Cleanup: keep
296
+ Event: outing
297
+ Signif.: high
298
+ GPS: 37.7749, -122.4194
299
+ Location: San Francisco, California, United States
300
+ Status: ok
301
+
302
+ --- Summary ---
303
+ Scanned: 200
304
+ Processed: 50
305
+ Skipped (date): 0
306
+ Skipped (no GPS): 0
307
+ Skipped (no file):0
308
+ Model failures: 2
309
+ Geocode failures: 0
310
+ ```
311
+
312
+ ### Output schema
313
+
314
+ Each result (JSON/CSV) includes:
315
+
316
+ | Field | Description |
317
+ |---|---|
318
+ | `file_path` | Full path to image |
319
+ | `file_name` | Filename |
320
+ | `source_type` | `directory` or `photos_library` |
321
+ | `image_date` | EXIF or file date |
322
+ | `tags` | 1-5 vision model tags |
323
+ | `scene_summary` | Short scene description |
324
+ | `scene_category` | `indoor_home`, `indoor_work`, `outdoor_leisure`, `outdoor_travel`, `transport`, `other` |
325
+ | `emotional_tone` | `positive`, `neutral`, `negative`, `mixed` |
326
+ | `cleanup_class` | `keep`, `review`, `delete` |
327
+ | `has_text` | Whether image contains readable text |
328
+ | `text_summary` | Extracted text summary (if `has_text`) |
329
+ | `event_hint` | `outing`, `gathering`, `work`, `travel`, `daily`, `other` |
330
+ | `significance` | `high`, `medium`, `low` |
331
+ | `gps_lat` / `gps_lon` | EXIF GPS coordinates |
332
+ | `nearest_place` | Village/town/suburb |
333
+ | `nearest_city` | City |
334
+ | `nearest_region` | State/region |
335
+ | `nearest_country` | Country |
336
+ | `processing_status` | `ok` or `error` |
337
+ | `error_message` | Error details if any |
338
+ | `phash` | Perceptual hash (when `--dedup` used) |
339
+
340
+ ## Architecture
341
+
342
+ ```
343
+ src/pyimgtag/
344
+ main.py CLI entry point, subcommand dispatch
345
+ models.py Data classes (ExifData, TagResult, GeoResult, ImageResult)
346
+ scanner.py Directory and Photos library scanning
347
+ exif_reader.py EXIF GPS + date extraction (exiftool + Pillow)
348
+ ollama_client.py Ollama vision API client (rich structured response)
349
+ geocoder.py Nominatim reverse geocoder with disk cache
350
+ filters.py Date/GPS filter logic
351
+ output_writer.py JSON/CSV/JSONL output
352
+ progress_db.py SQLite progress DB with versioned migrations
353
+ applescript_writer.py Apple Photos keyword/description write-back
354
+ dedup.py Perceptual hash duplicate detection
355
+ heic_converter.py HEIC to JPEG conversion (macOS sips)
356
+ cache.py Simple JSON disk cache
357
+ ```
358
+
359
+ ## Development
360
+
361
+ ```bash
362
+ pip install -e ".[dev,lint,security]"
363
+
364
+ pytest tests/ -v
365
+ ruff format src/ tests/ && ruff check src/ tests/ --fix
366
+ python -m mypy src/pyimgtag/ --ignore-missing-imports --disable-error-code import-untyped
367
+ python -m bandit -r src/pyimgtag/ -c pyproject.toml
368
+ pre-commit install && pre-commit run --all-files
369
+ ```
370
+
371
+ ## Contributing
372
+
373
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
374
+
375
+ ## License
376
+
377
+ MIT -- see [LICENSE](LICENSE).