icloud-image-labeler 0.1.2__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.
- icloud_image_labeler-0.1.2/LICENSE +21 -0
- icloud_image_labeler-0.1.2/PKG-INFO +194 -0
- icloud_image_labeler-0.1.2/README.md +173 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/PKG-INFO +194 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/SOURCES.txt +23 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/dependency_links.txt +1 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/entry_points.txt +2 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/requires.txt +6 -0
- icloud_image_labeler-0.1.2/icloud_image_labeler.egg-info/top_level.txt +1 -0
- icloud_image_labeler-0.1.2/labeler/__init__.py +1 -0
- icloud_image_labeler-0.1.2/labeler/__main__.py +5 -0
- icloud_image_labeler-0.1.2/labeler/checks.py +37 -0
- icloud_image_labeler-0.1.2/labeler/cli.py +266 -0
- icloud_image_labeler-0.1.2/labeler/config.py +71 -0
- icloud_image_labeler-0.1.2/labeler/daemon.py +98 -0
- icloud_image_labeler-0.1.2/labeler/discovery.py +69 -0
- icloud_image_labeler-0.1.2/labeler/exporter.py +249 -0
- icloud_image_labeler-0.1.2/labeler/init.py +87 -0
- icloud_image_labeler-0.1.2/labeler/llm.py +152 -0
- icloud_image_labeler-0.1.2/labeler/metrics.py +206 -0
- icloud_image_labeler-0.1.2/labeler/processor.py +629 -0
- icloud_image_labeler-0.1.2/labeler/shutdown.py +38 -0
- icloud_image_labeler-0.1.2/labeler/writer.py +50 -0
- icloud_image_labeler-0.1.2/pyproject.toml +46 -0
- icloud_image_labeler-0.1.2/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ziad Alzarka
|
|
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,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: icloud-image-labeler
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: macOS CLI tool that auto-labels iCloud Photos using any OpenAI-compatible LLM
|
|
5
|
+
Author: Ziad Alzarka
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: icloud,photos,labeling,llm,macos
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: osxphotos
|
|
15
|
+
Requires-Dist: photoscript
|
|
16
|
+
Requires-Dist: openai
|
|
17
|
+
Requires-Dist: Pillow
|
|
18
|
+
Requires-Dist: pillow-heif
|
|
19
|
+
Requires-Dist: datasette
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# iCloud Image Labeler
|
|
23
|
+
|
|
24
|
+
Auto-label your Apple Photos library using any OpenAI-compatible LLM. Generates keywords, titles, descriptions, and OCR text for photos and videos, then writes the metadata back into Photos.app.
|
|
25
|
+
|
|
26
|
+
Works with any OpenAI-compatible API — designed for [LM Studio](https://lmstudio.ai/) but also works with Ollama, vLLM, or cloud providers.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Photos.app ──> Discovery ──> Export ──> LLM ──> Writer ──> Photos.app
|
|
32
|
+
(osxphotos) (Pillow) (API) (PhotoScript)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
1. **Discovery** — Queries the Photos library for media with no keywords set, filtered by date range and media type.
|
|
36
|
+
2. **Export** — Exports photos as JPEG (with HEIC/iCloud fallbacks), extracts frames from videos via ffmpeg.
|
|
37
|
+
3. **LLM** — Sends images to an OpenAI-compatible API. Returns structured JSON with keywords, title, description, and OCR text.
|
|
38
|
+
4. **Writer** — Writes metadata back into Photos.app via PhotoScript (AppleScript automation).
|
|
39
|
+
|
|
40
|
+
Photos are processed in parallel (configurable thread count), videos sequentially.
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- macOS (requires Photos.app and AppleScript)
|
|
45
|
+
- Python 3.10+
|
|
46
|
+
- [ffmpeg](https://ffmpeg.org/) (for video frame extraction)
|
|
47
|
+
- An OpenAI-compatible LLM API ([LM Studio](https://lmstudio.ai/), Ollama, etc.)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
brew tap ziadalzarka/tap
|
|
53
|
+
brew install icloud-image-labeler
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### iCloud Settings
|
|
57
|
+
|
|
58
|
+
For best performance, go to **System Settings → Apple Account → iCloud → Photos** and enable **Download Originals to this Mac**. This downloads all photos in advance so the labeler doesn't have to wait for individual iCloud downloads during processing.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# Interactive setup — connects to your LLM and picks a model
|
|
64
|
+
icloud-image-labeler init
|
|
65
|
+
|
|
66
|
+
# Process all unprocessed media
|
|
67
|
+
icloud-image-labeler run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
# Process 5 items from the last 30 days
|
|
74
|
+
icloud-image-labeler run --limit 5 --days 30
|
|
75
|
+
|
|
76
|
+
# Preview without writing (dry run)
|
|
77
|
+
icloud-image-labeler run --dry-run
|
|
78
|
+
|
|
79
|
+
# Photos only, skip videos
|
|
80
|
+
icloud-image-labeler run --no-video
|
|
81
|
+
|
|
82
|
+
# Process media from 60 to 30 days ago
|
|
83
|
+
icloud-image-labeler run --days 60 --to-days 30
|
|
84
|
+
|
|
85
|
+
# Process specific photos by UUID
|
|
86
|
+
icloud-image-labeler run --uuid ABC123 DEF456
|
|
87
|
+
|
|
88
|
+
# Run continuously (daemon-style loop)
|
|
89
|
+
icloud-image-labeler run --loop
|
|
90
|
+
|
|
91
|
+
# Use a specific model or API endpoint
|
|
92
|
+
icloud-image-labeler run --base-url http://localhost:1234/v1 --model my-model
|
|
93
|
+
|
|
94
|
+
# Debug logging
|
|
95
|
+
icloud-image-labeler run -v
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### All `run` Options
|
|
99
|
+
|
|
100
|
+
| Flag | Description |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `--limit N` | Max items to process |
|
|
103
|
+
| `--days N` | Look back N days (0 = all time) |
|
|
104
|
+
| `--to-days N` | Skip most recent N days |
|
|
105
|
+
| `--photo` / `--no-photo` | Include/exclude photos |
|
|
106
|
+
| `--video` / `--no-video` | Include/exclude videos |
|
|
107
|
+
| `--write` | Force write metadata |
|
|
108
|
+
| `--dry-run` | Preview without writing |
|
|
109
|
+
| `--base-url URL` | LLM API endpoint |
|
|
110
|
+
| `--api-key KEY` | API key for LLM |
|
|
111
|
+
| `--model NAME` | Model identifier |
|
|
112
|
+
| `--threads N` | Parallel photo threads |
|
|
113
|
+
| `--video-frames N` | Frames to extract per video |
|
|
114
|
+
| `--uuid UUID [...]` | Process specific photo UUIDs |
|
|
115
|
+
| `--loop` | Run continuously with poll interval |
|
|
116
|
+
| `-v, --verbose` | Debug logging |
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Config is stored at `~/.image-labeler/config.json`. Run `icloud-image-labeler init` for interactive setup, or manage directly:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
icloud-image-labeler config show # View current config
|
|
124
|
+
icloud-image-labeler config set days 14 # Update a value
|
|
125
|
+
icloud-image-labeler config reset # Reset to defaults
|
|
126
|
+
icloud-image-labeler config path # Print config file path
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If no config exists when you run `icloud-image-labeler run`, the setup wizard launches automatically.
|
|
130
|
+
|
|
131
|
+
| Key | Default | Description |
|
|
132
|
+
|-----|---------|-------------|
|
|
133
|
+
| `base_url` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
|
|
134
|
+
| `api_key` | `""` | API key (optional, empty for local LM Studio) |
|
|
135
|
+
| `model` | `qwen/qwen3.5-9b` | Model identifier |
|
|
136
|
+
| `days` | `0` | Look back N days (0 = all time) |
|
|
137
|
+
| `to_days` | `0` | Skip most recent N days |
|
|
138
|
+
| `limit_per_cycle` | `0` | Items per cycle (0 = all) |
|
|
139
|
+
| `threads` | `4` | Max parallel photo threads |
|
|
140
|
+
| `video_frames` | `5` | Frames to extract from videos |
|
|
141
|
+
| `max_dimension` | `1024` | Max image dimension sent to LLM (px) |
|
|
142
|
+
| `photo` | `true` | Include photos in processing |
|
|
143
|
+
| `video` | `true` | Include videos in processing |
|
|
144
|
+
| `write` | `true` | Write metadata to Photos.app |
|
|
145
|
+
| `poll_interval` | `300` | Seconds between cycles in loop/daemon mode |
|
|
146
|
+
|
|
147
|
+
## Daemon
|
|
148
|
+
|
|
149
|
+
Run as a background service that auto-starts on login via macOS Launch Agent:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
icloud-image-labeler daemon start # Install and start Launch Agent
|
|
153
|
+
icloud-image-labeler daemon status # Check if running
|
|
154
|
+
icloud-image-labeler daemon restart # Apply config changes
|
|
155
|
+
icloud-image-labeler daemon stop # Uninstall Launch Agent
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The daemon runs as `com.image-labeler` under `~/Library/LaunchAgents/`. It auto-restarts on crash and starts on login. Logs go to `~/.image-labeler/daemon.log`.
|
|
159
|
+
|
|
160
|
+
## Metrics
|
|
161
|
+
|
|
162
|
+
Processing metrics are stored in SQLite at `~/.image-labeler/metrics.db`.
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
icloud-image-labeler metrics serve # Open Datasette UI at http://localhost:8001
|
|
166
|
+
icloud-image-labeler metrics serve --port 9000 # Custom port
|
|
167
|
+
icloud-image-labeler metrics path # Print database path
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Per-item metrics:** export/LLM/write durations, image dimensions, LLM retry count, keyword count, OCR presence, error details, and model used.
|
|
171
|
+
|
|
172
|
+
**Per-run metrics:** items found/processed/failed, wall-clock duration, model, thread count, and average LLM latency.
|
|
173
|
+
|
|
174
|
+
## Architecture
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
labeler/
|
|
178
|
+
cli.py — argparse CLI (init, run, daemon, config, metrics subcommands)
|
|
179
|
+
init.py — interactive first-run setup wizard
|
|
180
|
+
config.py — ~/.image-labeler/config.json management
|
|
181
|
+
discovery.py — osxphotos query for unprocessed media
|
|
182
|
+
exporter.py — photo export (HEIC/iCloud fallbacks), video frame extraction
|
|
183
|
+
llm.py — OpenAI-compatible client, JSON parsing, retry logic
|
|
184
|
+
processor.py — batch orchestration, parallel photos + sequential videos
|
|
185
|
+
writer.py — PhotoScript metadata writes (thread-safe)
|
|
186
|
+
metrics.py — SQLite metrics recording
|
|
187
|
+
daemon.py — macOS Launch Agent lifecycle
|
|
188
|
+
shutdown.py — graceful shutdown (SIGINT/SIGTERM handling)
|
|
189
|
+
checks.py — pre-flight dependency checks (ffmpeg, Photos.app)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# iCloud Image Labeler
|
|
2
|
+
|
|
3
|
+
Auto-label your Apple Photos library using any OpenAI-compatible LLM. Generates keywords, titles, descriptions, and OCR text for photos and videos, then writes the metadata back into Photos.app.
|
|
4
|
+
|
|
5
|
+
Works with any OpenAI-compatible API — designed for [LM Studio](https://lmstudio.ai/) but also works with Ollama, vLLM, or cloud providers.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Photos.app ──> Discovery ──> Export ──> LLM ──> Writer ──> Photos.app
|
|
11
|
+
(osxphotos) (Pillow) (API) (PhotoScript)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
1. **Discovery** — Queries the Photos library for media with no keywords set, filtered by date range and media type.
|
|
15
|
+
2. **Export** — Exports photos as JPEG (with HEIC/iCloud fallbacks), extracts frames from videos via ffmpeg.
|
|
16
|
+
3. **LLM** — Sends images to an OpenAI-compatible API. Returns structured JSON with keywords, title, description, and OCR text.
|
|
17
|
+
4. **Writer** — Writes metadata back into Photos.app via PhotoScript (AppleScript automation).
|
|
18
|
+
|
|
19
|
+
Photos are processed in parallel (configurable thread count), videos sequentially.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- macOS (requires Photos.app and AppleScript)
|
|
24
|
+
- Python 3.10+
|
|
25
|
+
- [ffmpeg](https://ffmpeg.org/) (for video frame extraction)
|
|
26
|
+
- An OpenAI-compatible LLM API ([LM Studio](https://lmstudio.ai/), Ollama, etc.)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
brew tap ziadalzarka/tap
|
|
32
|
+
brew install icloud-image-labeler
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### iCloud Settings
|
|
36
|
+
|
|
37
|
+
For best performance, go to **System Settings → Apple Account → iCloud → Photos** and enable **Download Originals to this Mac**. This downloads all photos in advance so the labeler doesn't have to wait for individual iCloud downloads during processing.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
# Interactive setup — connects to your LLM and picks a model
|
|
43
|
+
icloud-image-labeler init
|
|
44
|
+
|
|
45
|
+
# Process all unprocessed media
|
|
46
|
+
icloud-image-labeler run
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
# Process 5 items from the last 30 days
|
|
53
|
+
icloud-image-labeler run --limit 5 --days 30
|
|
54
|
+
|
|
55
|
+
# Preview without writing (dry run)
|
|
56
|
+
icloud-image-labeler run --dry-run
|
|
57
|
+
|
|
58
|
+
# Photos only, skip videos
|
|
59
|
+
icloud-image-labeler run --no-video
|
|
60
|
+
|
|
61
|
+
# Process media from 60 to 30 days ago
|
|
62
|
+
icloud-image-labeler run --days 60 --to-days 30
|
|
63
|
+
|
|
64
|
+
# Process specific photos by UUID
|
|
65
|
+
icloud-image-labeler run --uuid ABC123 DEF456
|
|
66
|
+
|
|
67
|
+
# Run continuously (daemon-style loop)
|
|
68
|
+
icloud-image-labeler run --loop
|
|
69
|
+
|
|
70
|
+
# Use a specific model or API endpoint
|
|
71
|
+
icloud-image-labeler run --base-url http://localhost:1234/v1 --model my-model
|
|
72
|
+
|
|
73
|
+
# Debug logging
|
|
74
|
+
icloud-image-labeler run -v
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### All `run` Options
|
|
78
|
+
|
|
79
|
+
| Flag | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `--limit N` | Max items to process |
|
|
82
|
+
| `--days N` | Look back N days (0 = all time) |
|
|
83
|
+
| `--to-days N` | Skip most recent N days |
|
|
84
|
+
| `--photo` / `--no-photo` | Include/exclude photos |
|
|
85
|
+
| `--video` / `--no-video` | Include/exclude videos |
|
|
86
|
+
| `--write` | Force write metadata |
|
|
87
|
+
| `--dry-run` | Preview without writing |
|
|
88
|
+
| `--base-url URL` | LLM API endpoint |
|
|
89
|
+
| `--api-key KEY` | API key for LLM |
|
|
90
|
+
| `--model NAME` | Model identifier |
|
|
91
|
+
| `--threads N` | Parallel photo threads |
|
|
92
|
+
| `--video-frames N` | Frames to extract per video |
|
|
93
|
+
| `--uuid UUID [...]` | Process specific photo UUIDs |
|
|
94
|
+
| `--loop` | Run continuously with poll interval |
|
|
95
|
+
| `-v, --verbose` | Debug logging |
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
Config is stored at `~/.image-labeler/config.json`. Run `icloud-image-labeler init` for interactive setup, or manage directly:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
icloud-image-labeler config show # View current config
|
|
103
|
+
icloud-image-labeler config set days 14 # Update a value
|
|
104
|
+
icloud-image-labeler config reset # Reset to defaults
|
|
105
|
+
icloud-image-labeler config path # Print config file path
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If no config exists when you run `icloud-image-labeler run`, the setup wizard launches automatically.
|
|
109
|
+
|
|
110
|
+
| Key | Default | Description |
|
|
111
|
+
|-----|---------|-------------|
|
|
112
|
+
| `base_url` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
|
|
113
|
+
| `api_key` | `""` | API key (optional, empty for local LM Studio) |
|
|
114
|
+
| `model` | `qwen/qwen3.5-9b` | Model identifier |
|
|
115
|
+
| `days` | `0` | Look back N days (0 = all time) |
|
|
116
|
+
| `to_days` | `0` | Skip most recent N days |
|
|
117
|
+
| `limit_per_cycle` | `0` | Items per cycle (0 = all) |
|
|
118
|
+
| `threads` | `4` | Max parallel photo threads |
|
|
119
|
+
| `video_frames` | `5` | Frames to extract from videos |
|
|
120
|
+
| `max_dimension` | `1024` | Max image dimension sent to LLM (px) |
|
|
121
|
+
| `photo` | `true` | Include photos in processing |
|
|
122
|
+
| `video` | `true` | Include videos in processing |
|
|
123
|
+
| `write` | `true` | Write metadata to Photos.app |
|
|
124
|
+
| `poll_interval` | `300` | Seconds between cycles in loop/daemon mode |
|
|
125
|
+
|
|
126
|
+
## Daemon
|
|
127
|
+
|
|
128
|
+
Run as a background service that auto-starts on login via macOS Launch Agent:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
icloud-image-labeler daemon start # Install and start Launch Agent
|
|
132
|
+
icloud-image-labeler daemon status # Check if running
|
|
133
|
+
icloud-image-labeler daemon restart # Apply config changes
|
|
134
|
+
icloud-image-labeler daemon stop # Uninstall Launch Agent
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The daemon runs as `com.image-labeler` under `~/Library/LaunchAgents/`. It auto-restarts on crash and starts on login. Logs go to `~/.image-labeler/daemon.log`.
|
|
138
|
+
|
|
139
|
+
## Metrics
|
|
140
|
+
|
|
141
|
+
Processing metrics are stored in SQLite at `~/.image-labeler/metrics.db`.
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
icloud-image-labeler metrics serve # Open Datasette UI at http://localhost:8001
|
|
145
|
+
icloud-image-labeler metrics serve --port 9000 # Custom port
|
|
146
|
+
icloud-image-labeler metrics path # Print database path
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Per-item metrics:** export/LLM/write durations, image dimensions, LLM retry count, keyword count, OCR presence, error details, and model used.
|
|
150
|
+
|
|
151
|
+
**Per-run metrics:** items found/processed/failed, wall-clock duration, model, thread count, and average LLM latency.
|
|
152
|
+
|
|
153
|
+
## Architecture
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
labeler/
|
|
157
|
+
cli.py — argparse CLI (init, run, daemon, config, metrics subcommands)
|
|
158
|
+
init.py — interactive first-run setup wizard
|
|
159
|
+
config.py — ~/.image-labeler/config.json management
|
|
160
|
+
discovery.py — osxphotos query for unprocessed media
|
|
161
|
+
exporter.py — photo export (HEIC/iCloud fallbacks), video frame extraction
|
|
162
|
+
llm.py — OpenAI-compatible client, JSON parsing, retry logic
|
|
163
|
+
processor.py — batch orchestration, parallel photos + sequential videos
|
|
164
|
+
writer.py — PhotoScript metadata writes (thread-safe)
|
|
165
|
+
metrics.py — SQLite metrics recording
|
|
166
|
+
daemon.py — macOS Launch Agent lifecycle
|
|
167
|
+
shutdown.py — graceful shutdown (SIGINT/SIGTERM handling)
|
|
168
|
+
checks.py — pre-flight dependency checks (ffmpeg, Photos.app)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: icloud-image-labeler
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: macOS CLI tool that auto-labels iCloud Photos using any OpenAI-compatible LLM
|
|
5
|
+
Author: Ziad Alzarka
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: icloud,photos,labeling,llm,macos
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: osxphotos
|
|
15
|
+
Requires-Dist: photoscript
|
|
16
|
+
Requires-Dist: openai
|
|
17
|
+
Requires-Dist: Pillow
|
|
18
|
+
Requires-Dist: pillow-heif
|
|
19
|
+
Requires-Dist: datasette
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# iCloud Image Labeler
|
|
23
|
+
|
|
24
|
+
Auto-label your Apple Photos library using any OpenAI-compatible LLM. Generates keywords, titles, descriptions, and OCR text for photos and videos, then writes the metadata back into Photos.app.
|
|
25
|
+
|
|
26
|
+
Works with any OpenAI-compatible API — designed for [LM Studio](https://lmstudio.ai/) but also works with Ollama, vLLM, or cloud providers.
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Photos.app ──> Discovery ──> Export ──> LLM ──> Writer ──> Photos.app
|
|
32
|
+
(osxphotos) (Pillow) (API) (PhotoScript)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
1. **Discovery** — Queries the Photos library for media with no keywords set, filtered by date range and media type.
|
|
36
|
+
2. **Export** — Exports photos as JPEG (with HEIC/iCloud fallbacks), extracts frames from videos via ffmpeg.
|
|
37
|
+
3. **LLM** — Sends images to an OpenAI-compatible API. Returns structured JSON with keywords, title, description, and OCR text.
|
|
38
|
+
4. **Writer** — Writes metadata back into Photos.app via PhotoScript (AppleScript automation).
|
|
39
|
+
|
|
40
|
+
Photos are processed in parallel (configurable thread count), videos sequentially.
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- macOS (requires Photos.app and AppleScript)
|
|
45
|
+
- Python 3.10+
|
|
46
|
+
- [ffmpeg](https://ffmpeg.org/) (for video frame extraction)
|
|
47
|
+
- An OpenAI-compatible LLM API ([LM Studio](https://lmstudio.ai/), Ollama, etc.)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
brew tap ziadalzarka/tap
|
|
53
|
+
brew install icloud-image-labeler
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### iCloud Settings
|
|
57
|
+
|
|
58
|
+
For best performance, go to **System Settings → Apple Account → iCloud → Photos** and enable **Download Originals to this Mac**. This downloads all photos in advance so the labeler doesn't have to wait for individual iCloud downloads during processing.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# Interactive setup — connects to your LLM and picks a model
|
|
64
|
+
icloud-image-labeler init
|
|
65
|
+
|
|
66
|
+
# Process all unprocessed media
|
|
67
|
+
icloud-image-labeler run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
# Process 5 items from the last 30 days
|
|
74
|
+
icloud-image-labeler run --limit 5 --days 30
|
|
75
|
+
|
|
76
|
+
# Preview without writing (dry run)
|
|
77
|
+
icloud-image-labeler run --dry-run
|
|
78
|
+
|
|
79
|
+
# Photos only, skip videos
|
|
80
|
+
icloud-image-labeler run --no-video
|
|
81
|
+
|
|
82
|
+
# Process media from 60 to 30 days ago
|
|
83
|
+
icloud-image-labeler run --days 60 --to-days 30
|
|
84
|
+
|
|
85
|
+
# Process specific photos by UUID
|
|
86
|
+
icloud-image-labeler run --uuid ABC123 DEF456
|
|
87
|
+
|
|
88
|
+
# Run continuously (daemon-style loop)
|
|
89
|
+
icloud-image-labeler run --loop
|
|
90
|
+
|
|
91
|
+
# Use a specific model or API endpoint
|
|
92
|
+
icloud-image-labeler run --base-url http://localhost:1234/v1 --model my-model
|
|
93
|
+
|
|
94
|
+
# Debug logging
|
|
95
|
+
icloud-image-labeler run -v
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### All `run` Options
|
|
99
|
+
|
|
100
|
+
| Flag | Description |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `--limit N` | Max items to process |
|
|
103
|
+
| `--days N` | Look back N days (0 = all time) |
|
|
104
|
+
| `--to-days N` | Skip most recent N days |
|
|
105
|
+
| `--photo` / `--no-photo` | Include/exclude photos |
|
|
106
|
+
| `--video` / `--no-video` | Include/exclude videos |
|
|
107
|
+
| `--write` | Force write metadata |
|
|
108
|
+
| `--dry-run` | Preview without writing |
|
|
109
|
+
| `--base-url URL` | LLM API endpoint |
|
|
110
|
+
| `--api-key KEY` | API key for LLM |
|
|
111
|
+
| `--model NAME` | Model identifier |
|
|
112
|
+
| `--threads N` | Parallel photo threads |
|
|
113
|
+
| `--video-frames N` | Frames to extract per video |
|
|
114
|
+
| `--uuid UUID [...]` | Process specific photo UUIDs |
|
|
115
|
+
| `--loop` | Run continuously with poll interval |
|
|
116
|
+
| `-v, --verbose` | Debug logging |
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Config is stored at `~/.image-labeler/config.json`. Run `icloud-image-labeler init` for interactive setup, or manage directly:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
icloud-image-labeler config show # View current config
|
|
124
|
+
icloud-image-labeler config set days 14 # Update a value
|
|
125
|
+
icloud-image-labeler config reset # Reset to defaults
|
|
126
|
+
icloud-image-labeler config path # Print config file path
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If no config exists when you run `icloud-image-labeler run`, the setup wizard launches automatically.
|
|
130
|
+
|
|
131
|
+
| Key | Default | Description |
|
|
132
|
+
|-----|---------|-------------|
|
|
133
|
+
| `base_url` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
|
|
134
|
+
| `api_key` | `""` | API key (optional, empty for local LM Studio) |
|
|
135
|
+
| `model` | `qwen/qwen3.5-9b` | Model identifier |
|
|
136
|
+
| `days` | `0` | Look back N days (0 = all time) |
|
|
137
|
+
| `to_days` | `0` | Skip most recent N days |
|
|
138
|
+
| `limit_per_cycle` | `0` | Items per cycle (0 = all) |
|
|
139
|
+
| `threads` | `4` | Max parallel photo threads |
|
|
140
|
+
| `video_frames` | `5` | Frames to extract from videos |
|
|
141
|
+
| `max_dimension` | `1024` | Max image dimension sent to LLM (px) |
|
|
142
|
+
| `photo` | `true` | Include photos in processing |
|
|
143
|
+
| `video` | `true` | Include videos in processing |
|
|
144
|
+
| `write` | `true` | Write metadata to Photos.app |
|
|
145
|
+
| `poll_interval` | `300` | Seconds between cycles in loop/daemon mode |
|
|
146
|
+
|
|
147
|
+
## Daemon
|
|
148
|
+
|
|
149
|
+
Run as a background service that auto-starts on login via macOS Launch Agent:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
icloud-image-labeler daemon start # Install and start Launch Agent
|
|
153
|
+
icloud-image-labeler daemon status # Check if running
|
|
154
|
+
icloud-image-labeler daemon restart # Apply config changes
|
|
155
|
+
icloud-image-labeler daemon stop # Uninstall Launch Agent
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The daemon runs as `com.image-labeler` under `~/Library/LaunchAgents/`. It auto-restarts on crash and starts on login. Logs go to `~/.image-labeler/daemon.log`.
|
|
159
|
+
|
|
160
|
+
## Metrics
|
|
161
|
+
|
|
162
|
+
Processing metrics are stored in SQLite at `~/.image-labeler/metrics.db`.
|
|
163
|
+
|
|
164
|
+
```sh
|
|
165
|
+
icloud-image-labeler metrics serve # Open Datasette UI at http://localhost:8001
|
|
166
|
+
icloud-image-labeler metrics serve --port 9000 # Custom port
|
|
167
|
+
icloud-image-labeler metrics path # Print database path
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Per-item metrics:** export/LLM/write durations, image dimensions, LLM retry count, keyword count, OCR presence, error details, and model used.
|
|
171
|
+
|
|
172
|
+
**Per-run metrics:** items found/processed/failed, wall-clock duration, model, thread count, and average LLM latency.
|
|
173
|
+
|
|
174
|
+
## Architecture
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
labeler/
|
|
178
|
+
cli.py — argparse CLI (init, run, daemon, config, metrics subcommands)
|
|
179
|
+
init.py — interactive first-run setup wizard
|
|
180
|
+
config.py — ~/.image-labeler/config.json management
|
|
181
|
+
discovery.py — osxphotos query for unprocessed media
|
|
182
|
+
exporter.py — photo export (HEIC/iCloud fallbacks), video frame extraction
|
|
183
|
+
llm.py — OpenAI-compatible client, JSON parsing, retry logic
|
|
184
|
+
processor.py — batch orchestration, parallel photos + sequential videos
|
|
185
|
+
writer.py — PhotoScript metadata writes (thread-safe)
|
|
186
|
+
metrics.py — SQLite metrics recording
|
|
187
|
+
daemon.py — macOS Launch Agent lifecycle
|
|
188
|
+
shutdown.py — graceful shutdown (SIGINT/SIGTERM handling)
|
|
189
|
+
checks.py — pre-flight dependency checks (ffmpeg, Photos.app)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
icloud_image_labeler.egg-info/PKG-INFO
|
|
5
|
+
icloud_image_labeler.egg-info/SOURCES.txt
|
|
6
|
+
icloud_image_labeler.egg-info/dependency_links.txt
|
|
7
|
+
icloud_image_labeler.egg-info/entry_points.txt
|
|
8
|
+
icloud_image_labeler.egg-info/requires.txt
|
|
9
|
+
icloud_image_labeler.egg-info/top_level.txt
|
|
10
|
+
labeler/__init__.py
|
|
11
|
+
labeler/__main__.py
|
|
12
|
+
labeler/checks.py
|
|
13
|
+
labeler/cli.py
|
|
14
|
+
labeler/config.py
|
|
15
|
+
labeler/daemon.py
|
|
16
|
+
labeler/discovery.py
|
|
17
|
+
labeler/exporter.py
|
|
18
|
+
labeler/init.py
|
|
19
|
+
labeler/llm.py
|
|
20
|
+
labeler/metrics.py
|
|
21
|
+
labeler/processor.py
|
|
22
|
+
labeler/shutdown.py
|
|
23
|
+
labeler/writer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
labeler
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""iCloud Image Labeler package."""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Verify external tool dependencies at startup."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_dependencies():
|
|
9
|
+
"""Check that required external tools are available. Exits if any are missing."""
|
|
10
|
+
missing = []
|
|
11
|
+
|
|
12
|
+
for tool in ("ffmpeg", "ffprobe"):
|
|
13
|
+
if not shutil.which(tool):
|
|
14
|
+
missing.append(tool)
|
|
15
|
+
|
|
16
|
+
if not shutil.which("pgrep"):
|
|
17
|
+
missing.append("pgrep")
|
|
18
|
+
|
|
19
|
+
# Check Photos.app exists
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["mdfind", "kMDItemCFBundleIdentifier == 'com.apple.Photos'"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
)
|
|
25
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
26
|
+
missing.append("Photos.app")
|
|
27
|
+
|
|
28
|
+
if missing:
|
|
29
|
+
print(
|
|
30
|
+
f"Error: missing required dependencies: {', '.join(missing)}",
|
|
31
|
+
file=sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
if {"ffmpeg", "ffprobe"} & set(missing):
|
|
34
|
+
print(" Install ffmpeg: brew install ffmpeg", file=sys.stderr)
|
|
35
|
+
if "Photos.app" in missing:
|
|
36
|
+
print(" Photos.app is required on macOS", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|