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
|