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.
@@ -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,2 @@
1
+ [console_scripts]
2
+ icloud-image-labeler = labeler.cli:main
@@ -0,0 +1,6 @@
1
+ osxphotos
2
+ photoscript
3
+ openai
4
+ Pillow
5
+ pillow-heif
6
+ datasette
@@ -0,0 +1 @@
1
+ """iCloud Image Labeler package."""
@@ -0,0 +1,5 @@
1
+ """Entry point for python -m labeler."""
2
+
3
+ from labeler.cli import main
4
+
5
+ main()
@@ -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)