photo-tagger 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.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: photo-tagger
3
+ Version: 0.1.0
4
+ Summary: CLI tool to describe photos and add keywords using AI
5
+ Keywords: photo,tagging,cli,ai,exif,metadata,image recognition
6
+ Author: Julio Batista Silva
7
+ Author-email: Julio Batista Silva <python@juliobs.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Multimedia
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
19
+ Requires-Dist: cyclopts>=4.5.2
20
+ Requires-Dist: httpx>=0.28.1
21
+ Requires-Dist: rawpy>=0.26.1
22
+ Requires-Dist: pydantic>=2.12.5
23
+ Requires-Dist: pydantic-ai>=1.59.0
24
+ Requires-Dist: loguru>=0.7.3
25
+ Requires-Dist: pyexiftool>=0.5.6
26
+ Requires-Dist: pillow>=12.1.1
27
+ Requires-Python: >=3.14, <4
28
+ Project-URL: Homepage, https://juliobs.com/projects/photo-tagger/
29
+ Project-URL: Documentation, https://github.com/jbsilva/photo-tagger
30
+ Project-URL: Repository, https://github.com/jbsilva/photo-tagger
31
+ Project-URL: Issues, https://github.com/jbsilva/photo-tagger/issues
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Photo Tagger
35
+
36
+ Photo Tagger is a command-line helper that asks a vision-language model to analyze your photos and
37
+ writes Lightroom-compatible metadata.
38
+
39
+ By default it keeps your originals untouched by creating XMP sidecars, but you can embed the updates
40
+ directly into each photo with `--embed-in-photo`.
41
+
42
+ ## Highlights
43
+
44
+ - Works with RAW and standard image formats (CR3, CR2, NEF, JPG, PNG, and more)
45
+ - Generates a title, a concise description, and hierarchical keywords
46
+ - Merges with existing metadata unless you opt-in to overwrite
47
+ - Supports Ollama and LM Studio compatible OpenAI APIs
48
+ - Converts images to compact JPEG bytes to minimize token usage
49
+ - Generates detailed log files for easy debugging and auditing
50
+ - Highly configurable via CLI flags and environment variables
51
+
52
+ ## Requirements
53
+
54
+ - Python 3.14+
55
+ - [ExifTool](https://exiftool.org/) available on `PATH`
56
+ - A running Ollama or LM Studio server exposing a vision-language model (for example Qwen-VL)
57
+ - `libraw` support for `rawpy` (install via Homebrew on macOS: `brew install libraw`)
58
+
59
+ ## Installation
60
+
61
+ For end-users, the recommended installation method is via
62
+ [uv](https://docs.astral.sh/uv/guides/tools/):
63
+
64
+ ```bash
65
+ uv tool install photo-tagger
66
+ ```
67
+
68
+ For development (tests, linting):
69
+
70
+ ```bash
71
+ uv sync --group dev --group test
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ Environment variables provide defaults so you can keep the CLI concise:
77
+
78
+ - `OLLAMA_BASE_URL` – override the Ollama HTTP endpoint (default `http://localhost:11434/v1`)
79
+ - `OLLAMA_API_KEY` – optional API key passed to Ollama requests
80
+ - `LM_STUDIO_BASE_URL` – override the LM Studio endpoint (default `http://localhost:1234/v1`)
81
+ - `LM_STUDIO_API_KEY` / `OPENAI_API_KEY` – API key for LM Studio’s OpenAI-compatible server
82
+ - `MODEL_NAME` – default model name (default `qwen3-vl:32b`)
83
+ - `JPEG_DIMENSIONS`, `JPEG_QUALITY`, `TEMPERATURE`, `MAX_TOKENS`, `RETRIES` – fine-tune runtime
84
+
85
+ Any CLI flag takes precedence over the environment.
86
+
87
+ ## Usage
88
+
89
+ The CLI is exposed as `photo-tagger` once installed, or you can invoke it directly:
90
+
91
+ ```bash
92
+ photo-tagger -i ./photos --ext cr3,jpg -r
93
+ ```
94
+
95
+ Key options:
96
+
97
+ - `-i/--input PATH` – repeatable; mix files and directories
98
+ - `--ext` – comma-separated extension list used when scanning directories (default `cr3,jpg`)
99
+ - `-r/--recursive` – recurse into subdirectories while scanning inputs
100
+ - `-m/--model` – model identifier understood by your provider
101
+ - `--provider` – `ollama` or `lmstudio` (defaults to `lmstudio`)
102
+ - `--url` / `--api-key` – override provider endpoint and credentials
103
+ - `--overwrite-keywords` – replace instead of merge existing keyword metadata
104
+ - `--no-write-title` / `--no-write-description` – skip writing those fields
105
+ - `--no-backup-xmp` – avoid creating `*_original` snapshot before writing
106
+ - `--embed-in-photo` – write metadata directly into the image instead of creating an XMP sidecar
107
+ - `--jpeg-dimensions`, `--jpeg-quality`, `--temperature`, `--max-tokens`, `--retries` – control
108
+ inference behavior
109
+
110
+ A successful run creates or updates an `.xmp` sidecar for every processed image (unless you embed
111
+ the metadata). Existing metadata is merged so Lightroom keeps hierarchical keywords such as
112
+ `Animal|Bird|Osprey` intact.
113
+
114
+ ### Examples
115
+
116
+ Process a folder of RAW and JPEG files recursively:
117
+
118
+ ```bash
119
+ photo-tagger -i ~/Pictures/Portfolio --ext cr3,jpg -r
120
+ ```
121
+
122
+ Tag a few explicit files and overwrite existing keywords:
123
+
124
+ ```bash
125
+ photo-tagger \
126
+ -i IMG_0001.CR3 \
127
+ -i IMG_0002.CR3 \
128
+ --overwrite-keywords
129
+ ```
130
+
131
+ Embed metadata directly into a set of JPEGs:
132
+
133
+ ```bash
134
+ photo-tagger -i ./exports --ext jpg --embed-in-photo
135
+ ```
136
+
137
+ Send requests to a remote Ollama host with a custom model:
138
+
139
+ ```bash
140
+ photo-tagger -i ./shoot --provider ollama --model llava:34b --url http://ollama-box:11434/v1
141
+ ```
142
+
143
+ ## Logging
144
+
145
+ Logs are written to stderr and to a timestamped file (for example `20260101...-photo_tagger.log`).
146
+ Adjust levels with `--console-log-level` and `--file-log-level`, or disable either by setting the
147
+ value to `OFF`.
148
+
149
+ ## Testing
150
+
151
+ Run the unit tests with:
152
+
153
+ ```bash
154
+ pytest
155
+ ```
156
+
157
+ If you plan to contribute, also run `ruff check` for linting before opening a PR.
@@ -0,0 +1,124 @@
1
+ # Photo Tagger
2
+
3
+ Photo Tagger is a command-line helper that asks a vision-language model to analyze your photos and
4
+ writes Lightroom-compatible metadata.
5
+
6
+ By default it keeps your originals untouched by creating XMP sidecars, but you can embed the updates
7
+ directly into each photo with `--embed-in-photo`.
8
+
9
+ ## Highlights
10
+
11
+ - Works with RAW and standard image formats (CR3, CR2, NEF, JPG, PNG, and more)
12
+ - Generates a title, a concise description, and hierarchical keywords
13
+ - Merges with existing metadata unless you opt-in to overwrite
14
+ - Supports Ollama and LM Studio compatible OpenAI APIs
15
+ - Converts images to compact JPEG bytes to minimize token usage
16
+ - Generates detailed log files for easy debugging and auditing
17
+ - Highly configurable via CLI flags and environment variables
18
+
19
+ ## Requirements
20
+
21
+ - Python 3.14+
22
+ - [ExifTool](https://exiftool.org/) available on `PATH`
23
+ - A running Ollama or LM Studio server exposing a vision-language model (for example Qwen-VL)
24
+ - `libraw` support for `rawpy` (install via Homebrew on macOS: `brew install libraw`)
25
+
26
+ ## Installation
27
+
28
+ For end-users, the recommended installation method is via
29
+ [uv](https://docs.astral.sh/uv/guides/tools/):
30
+
31
+ ```bash
32
+ uv tool install photo-tagger
33
+ ```
34
+
35
+ For development (tests, linting):
36
+
37
+ ```bash
38
+ uv sync --group dev --group test
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Environment variables provide defaults so you can keep the CLI concise:
44
+
45
+ - `OLLAMA_BASE_URL` – override the Ollama HTTP endpoint (default `http://localhost:11434/v1`)
46
+ - `OLLAMA_API_KEY` – optional API key passed to Ollama requests
47
+ - `LM_STUDIO_BASE_URL` – override the LM Studio endpoint (default `http://localhost:1234/v1`)
48
+ - `LM_STUDIO_API_KEY` / `OPENAI_API_KEY` – API key for LM Studio’s OpenAI-compatible server
49
+ - `MODEL_NAME` – default model name (default `qwen3-vl:32b`)
50
+ - `JPEG_DIMENSIONS`, `JPEG_QUALITY`, `TEMPERATURE`, `MAX_TOKENS`, `RETRIES` – fine-tune runtime
51
+
52
+ Any CLI flag takes precedence over the environment.
53
+
54
+ ## Usage
55
+
56
+ The CLI is exposed as `photo-tagger` once installed, or you can invoke it directly:
57
+
58
+ ```bash
59
+ photo-tagger -i ./photos --ext cr3,jpg -r
60
+ ```
61
+
62
+ Key options:
63
+
64
+ - `-i/--input PATH` – repeatable; mix files and directories
65
+ - `--ext` – comma-separated extension list used when scanning directories (default `cr3,jpg`)
66
+ - `-r/--recursive` – recurse into subdirectories while scanning inputs
67
+ - `-m/--model` – model identifier understood by your provider
68
+ - `--provider` – `ollama` or `lmstudio` (defaults to `lmstudio`)
69
+ - `--url` / `--api-key` – override provider endpoint and credentials
70
+ - `--overwrite-keywords` – replace instead of merge existing keyword metadata
71
+ - `--no-write-title` / `--no-write-description` – skip writing those fields
72
+ - `--no-backup-xmp` – avoid creating `*_original` snapshot before writing
73
+ - `--embed-in-photo` – write metadata directly into the image instead of creating an XMP sidecar
74
+ - `--jpeg-dimensions`, `--jpeg-quality`, `--temperature`, `--max-tokens`, `--retries` – control
75
+ inference behavior
76
+
77
+ A successful run creates or updates an `.xmp` sidecar for every processed image (unless you embed
78
+ the metadata). Existing metadata is merged so Lightroom keeps hierarchical keywords such as
79
+ `Animal|Bird|Osprey` intact.
80
+
81
+ ### Examples
82
+
83
+ Process a folder of RAW and JPEG files recursively:
84
+
85
+ ```bash
86
+ photo-tagger -i ~/Pictures/Portfolio --ext cr3,jpg -r
87
+ ```
88
+
89
+ Tag a few explicit files and overwrite existing keywords:
90
+
91
+ ```bash
92
+ photo-tagger \
93
+ -i IMG_0001.CR3 \
94
+ -i IMG_0002.CR3 \
95
+ --overwrite-keywords
96
+ ```
97
+
98
+ Embed metadata directly into a set of JPEGs:
99
+
100
+ ```bash
101
+ photo-tagger -i ./exports --ext jpg --embed-in-photo
102
+ ```
103
+
104
+ Send requests to a remote Ollama host with a custom model:
105
+
106
+ ```bash
107
+ photo-tagger -i ./shoot --provider ollama --model llava:34b --url http://ollama-box:11434/v1
108
+ ```
109
+
110
+ ## Logging
111
+
112
+ Logs are written to stderr and to a timestamped file (for example `20260101...-photo_tagger.log`).
113
+ Adjust levels with `--console-log-level` and `--file-log-level`, or disable either by setting the
114
+ value to `OFF`.
115
+
116
+ ## Testing
117
+
118
+ Run the unit tests with:
119
+
120
+ ```bash
121
+ pytest
122
+ ```
123
+
124
+ If you plan to contribute, also run `ruff check` for linting before opening a PR.
@@ -0,0 +1,133 @@
1
+ [project]
2
+ name = "photo-tagger"
3
+ version = "0.1.0"
4
+ description = "CLI tool to describe photos and add keywords using AI"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14,<4"
7
+ authors = [{ name = "Julio Batista Silva", email = "python@juliobs.com" }]
8
+ license = "MIT"
9
+ keywords = [
10
+ "photo",
11
+ "tagging",
12
+ "cli",
13
+ "ai",
14
+ "exif",
15
+ "metadata",
16
+ "image recognition",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Environment :: Console",
21
+ "Intended Audience :: End Users/Desktop",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Natural Language :: English",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Multimedia",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ "Topic :: Scientific/Engineering :: Image Recognition",
29
+ ]
30
+ dependencies = [
31
+ "cyclopts>=4.5.2",
32
+ "httpx>=0.28.1",
33
+ "rawpy>=0.26.1",
34
+ "pydantic>=2.12.5",
35
+ "pydantic-ai>=1.59.0",
36
+ "loguru>=0.7.3",
37
+ "pyexiftool>=0.5.6",
38
+ "pillow>=12.1.1",
39
+ ]
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ "bandit>=1.9.3",
44
+ "litellm>=1.81.12",
45
+ "ruff>=0.15.1",
46
+ "zuban>=0.5.1",
47
+ ]
48
+ test = [
49
+ "inline-snapshot>=0.31.1",
50
+ "pytest-cov>=7.0.0",
51
+ "pytest-random-order>=1.2.0",
52
+ "pytest-xdist[psutil]>=3.8.0",
53
+ "pytest>=9.0.2",
54
+ ]
55
+
56
+ [project.urls]
57
+ Homepage = "https://juliobs.com/projects/photo-tagger/"
58
+ Documentation = "https://github.com/jbsilva/photo-tagger"
59
+ Repository = "https://github.com/jbsilva/photo-tagger"
60
+ Issues = "https://github.com/jbsilva/photo-tagger/issues"
61
+
62
+ [project.scripts]
63
+ photo-tagger = "photo_tagger.main:app"
64
+
65
+ [build-system]
66
+ requires = ["uv_build >= 0.10.3"]
67
+ build-backend = "uv_build"
68
+
69
+ [tool.ruff]
70
+ line-length = 100
71
+ indent-width = 4
72
+ target-version = "py314"
73
+ fix = true
74
+ unsafe-fixes = false
75
+
76
+ [tool.ruff.lint]
77
+ select = ["ALL"]
78
+ ignore = [
79
+ "D203", # one-blank-line-before-class
80
+ "D212", # multi-line-summary-first-line
81
+ ]
82
+
83
+ [tool.ruff.lint.per-file-ignores]
84
+ "**/tests/{test_*.py,conftest.py}" = [
85
+ "S101", # Allow assert in tests
86
+ "S105", # Allow hardcoded password in strings in tests
87
+ "S106", # Allow hardcoded password in function calls in tests
88
+ "ARG001", # Allow unused arguments in tests (for pytest fixtures)
89
+ ]
90
+ "**/__init__.py" = [
91
+ "D104", # Allow empty __init__.py files
92
+ ]
93
+
94
+ [tool.ruff.format]
95
+ quote-style = "double"
96
+ indent-style = "space"
97
+ line-ending = "lf"
98
+
99
+ [tool.ruff.lint.isort]
100
+ combine-as-imports = true
101
+ force-wrap-aliases = true
102
+ lines-after-imports = 2
103
+
104
+ [tool.zuban]
105
+ strict = true
106
+ disallow_untyped_defs = true
107
+ warn_unreachable = true
108
+ python_executable = ".venv/bin/python"
109
+
110
+ [tool.bandit]
111
+ exclude_dirs = [".git", ".venv", "__pycache__", "tests"]
112
+
113
+ [tool.bandit.assert_used]
114
+ skips = ['tests/test_*.py']
115
+
116
+ [tool.pytest.ini_options]
117
+ addopts = [
118
+ "-ra",
119
+ "--durations=10",
120
+ "--import-mode=importlib",
121
+ "--random-order",
122
+ "--numprocesses=logical",
123
+ "--dist=load",
124
+ "--tb=short",
125
+ "--verbose",
126
+ "--cov=src/photo_tagger",
127
+ "--cov-report=term-missing",
128
+ "-m",
129
+ "not integration",
130
+ ]
131
+ markers = [
132
+ "integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
133
+ ]
File without changes