grape-image-search 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Graham
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,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: grape-image-search
3
+ Version: 0.1.0
4
+ Summary: Find images matching keywords using CLIP. Like grep, but for images.
5
+ Author-email: Mike Graham <mikegraham@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: open-clip-torch>=2.24.0
20
+ Requires-Dist: torch>=2.0.0
21
+ Requires-Dist: dask>=2024.5.0
22
+ Requires-Dist: Pillow>=9.0.0
23
+ Requires-Dist: platformdirs>=3.0
24
+ Requires-Dist: tqdm>=4.60
25
+ Requires-Dist: transformers>=4.49.0
26
+ Requires-Dist: sentencepiece>=0.2.0
27
+ Requires-Dist: Jinja2>=3.1
28
+ Requires-Dist: pywebview>=6.1
29
+ Provides-Extra: view
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # grape
38
+
39
+ Find images matching keywords using [CLIP](https://arxiv.org/abs/2103.00020)
40
+ (Radford et al., 2021). Like grep, but for images.
41
+
42
+ ```
43
+ $ grape -R -s -k sunset ~/Pictures
44
+ 0.327 ~/Pictures/vacation/beach_golden_hour.jpg
45
+ 0.285 ~/Pictures/vacation/pier_evening.jpg
46
+ 0.241 ~/Pictures/hiking/mountain_view.jpg
47
+ ```
48
+
49
+ <img src="docs/screenshot_view.png" alt="grape --view screenshot" width="600">
50
+
51
+ ## Install
52
+
53
+ ```
54
+ pip install grape-image-search
55
+ ```
56
+
57
+ Python 3.10+. Model weights (~1.7 GB for the default) download on first run.
58
+
59
+ For a headless install without the `--view` GUI deps:
60
+
61
+ ```
62
+ pip install grape-image-search --no-deps
63
+ pip install open-clip-torch torch dask Pillow platformdirs tqdm transformers sentencepiece
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ Paths can be files, directories (with `-R`), or `-` to read from stdin.
69
+ At least one of `-k` or `--like` is required.
70
+
71
+ ```bash
72
+ # text search
73
+ grape -R -k sunset ~/Pictures
74
+
75
+ # find images similar to a reference
76
+ grape -R --like ref.jpg ~/Pictures
77
+
78
+ # combine text + reference, and penalize something
79
+ grape -R -k dog -x cat --like my_dog.jpg ~/Pictures
80
+
81
+ # top 5 above a threshold, with scores
82
+ grape -R -s -n 5 -t 0.25 -k sunset ~/Pictures
83
+
84
+ # browse results in a GUI window
85
+ grape -R -k sunset --view ~/Pictures
86
+
87
+ # aesthetic ranking (skip default templating -- prompts are already full sentences)
88
+ grape -R -n 20 --ensemble-prompts '{}' -k 'beautiful photo' -x 'ugly photo' ~/Pictures
89
+
90
+ # read paths from stdin
91
+ find ~/Pictures -mtime -7 -type f | grape -k selfie -
92
+
93
+ # copy the top 10 cat photos to a folder
94
+ grape -R -print0 -n 10 -k cat ~/Pictures | xargs -0 cp -t ~/cats/
95
+
96
+ # open the best match directly
97
+ grape -R -k 'golden gate bridge' -n 1 ~/Pictures | xargs open
98
+
99
+ # interactive selection with fzf
100
+ grape -R -k dog ~/Pictures | fzf --preview 'chafa {}'
101
+ ```
102
+
103
+ Run `grape --help` for the full flag list.
104
+
105
+ ## Worked examples
106
+
107
+ **Per-keyword breakdown** (`-v`) shows how each keyword and `--like`
108
+ reference contributes. The score on the header line is the mean.
109
+
110
+ ```
111
+ $ grape -R -v -n 3 --keywords 'playful puppy,golden retriever' ~/Pictures
112
+ 0.342 /home/me/Pictures/family/biscuit_park.jpg
113
+ playful puppy: 0.301 golden retriever: 0.383
114
+ 0.298 /home/me/Pictures/family/biscuit_couch.jpg
115
+ playful puppy: 0.267 golden retriever: 0.329
116
+ 0.274 /home/me/Pictures/family/old_biscuit.jpg
117
+ playful puppy: 0.198 golden retriever: 0.350
118
+ ```
119
+
120
+ **Calibrate, then threshold.** Run with `-s` first to see what scores real
121
+ matches land at, then set `-t` just below the noise floor.
122
+
123
+ ```
124
+ $ grape -R -s --keywords sunset ~/Pictures | head -5
125
+ 0.312 /home/me/Pictures/travel/santorini_evening.jpg
126
+ 0.287 /home/me/Pictures/deck_sunset.jpg
127
+ 0.244 /home/me/Pictures/travel/ocean_dusk.jpg
128
+ 0.198 /home/me/Pictures/outdoor/golden_leaves.jpg # weak, off-topic
129
+ $ grape -R -s -t 0.24 --keywords sunset ~/Pictures
130
+ 0.312 /home/me/Pictures/travel/santorini_evening.jpg
131
+ 0.287 /home/me/Pictures/deck_sunset.jpg
132
+ 0.244 /home/me/Pictures/travel/ocean_dusk.jpg
133
+ ```
134
+
135
+ **Aesthetic ranking** uses `--exclude` to subtract the mean score of a
136
+ contrast prompt. Scores are smaller here because both sides are competing.
137
+
138
+ ```
139
+ $ grape -R -s -n 5 --ensemble-prompts '{}' \
140
+ --keywords 'beautiful photo' --exclude 'ugly photo' ~/travel/
141
+ 0.163 /home/me/travel/kyoto_arashiyama.jpg
142
+ 0.139 /home/me/travel/iceland_sunset.jpg
143
+ 0.127 /home/me/travel/nepal_trek.jpg
144
+ 0.109 /home/me/travel/paris_evening.jpg
145
+ 0.091 /home/me/travel/tokyo_skyline.jpg
146
+ ```
147
+
148
+ ## Scoring
149
+
150
+ Grape ranks images by cosine similarity between CLIP embeddings. Each
151
+ image's score is:
152
+
153
+ mean(similarity to --keywords and --like) - mean(similarity to --exclude)
154
+
155
+ Cosine similarities are **not probabilities**. Meaningful matches typically
156
+ sit in the 0.15-0.35 range; even strong matches rarely exceed 0.35. Use `-s`
157
+ to calibrate before choosing a `-t` threshold. Scores are **not comparable
158
+ across models** -- switch models and the numbers shift.
159
+
160
+ **`--like` matches semantics, not pixels.** Two different photos of dogs
161
+ playing fetch score high; a photo and its crop do too. For actual duplicate
162
+ detection, use a perceptual-hashing tool like
163
+ [czkawka](https://github.com/qarmin/czkawka).
164
+
165
+ ## Caching
166
+
167
+ On by default at `~/.cache/grape/embeddings.db`
168
+ (`$XDG_CACHE_HOME/grape/embeddings.db`). Cached:
169
+
170
+ - Image embeddings (the expensive part; ~80 ms per image on CPU)
171
+ - Text embeddings, keyed by prompt
172
+ - Model identity, so repeat runs skip the torch/open_clip import entirely
173
+ - Image-vs-non-image detection results, so videos and docs are skipped on rescan
174
+
175
+ A warm query scores thousands of images in under a second via a single
176
+ matrix multiply. Entries are keyed by absolute path, file stat (size, mtime,
177
+ inode), and model ID, so they auto-invalidate when files change.
178
+
179
+ Use `--no-cache` to disable or `--cache PATH` to relocate.
180
+
181
+ ## Models
182
+
183
+ The default is
184
+ [EVA-CLIP's EVA02-L-14](https://arxiv.org/abs/2303.15389) (Sun et al., 2023)
185
+ pretrained as `merged2b_s4b_b131k` -- strong zero-shot accuracy at moderate
186
+ cost. Pick any [OpenCLIP](https://github.com/mlfoundations/open_clip)
187
+ checkpoint with `--model model_name/pretrained_tag`:
188
+
189
+ ```bash
190
+ grape --model ViT-B-32/laion2b_s34b_b79k -R -k sunset ~/Pictures # fast
191
+ grape --model ViT-L-14/laion2b_s32b_b82k -R -k sunset ~/Pictures # strong
192
+ ```
193
+
194
+ Embeddings are scoped per model; switching re-encodes everything.
195
+
196
+ Keywords are matched with
197
+ [prompt ensembling](https://arxiv.org/abs/2103.00020) (Radford et al., 2021,
198
+ Section 3.1.4 and Appendix A): each keyword expands into several templates
199
+ ("a photo of a {}", "a photo of the {}", ...), each is embedded, then the
200
+ embeddings are averaged and renormalized. Improves zero-shot accuracy over
201
+ a single prompt. Override with `--ensemble-prompts`.
@@ -0,0 +1,165 @@
1
+ # grape
2
+
3
+ Find images matching keywords using [CLIP](https://arxiv.org/abs/2103.00020)
4
+ (Radford et al., 2021). Like grep, but for images.
5
+
6
+ ```
7
+ $ grape -R -s -k sunset ~/Pictures
8
+ 0.327 ~/Pictures/vacation/beach_golden_hour.jpg
9
+ 0.285 ~/Pictures/vacation/pier_evening.jpg
10
+ 0.241 ~/Pictures/hiking/mountain_view.jpg
11
+ ```
12
+
13
+ <img src="docs/screenshot_view.png" alt="grape --view screenshot" width="600">
14
+
15
+ ## Install
16
+
17
+ ```
18
+ pip install grape-image-search
19
+ ```
20
+
21
+ Python 3.10+. Model weights (~1.7 GB for the default) download on first run.
22
+
23
+ For a headless install without the `--view` GUI deps:
24
+
25
+ ```
26
+ pip install grape-image-search --no-deps
27
+ pip install open-clip-torch torch dask Pillow platformdirs tqdm transformers sentencepiece
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ Paths can be files, directories (with `-R`), or `-` to read from stdin.
33
+ At least one of `-k` or `--like` is required.
34
+
35
+ ```bash
36
+ # text search
37
+ grape -R -k sunset ~/Pictures
38
+
39
+ # find images similar to a reference
40
+ grape -R --like ref.jpg ~/Pictures
41
+
42
+ # combine text + reference, and penalize something
43
+ grape -R -k dog -x cat --like my_dog.jpg ~/Pictures
44
+
45
+ # top 5 above a threshold, with scores
46
+ grape -R -s -n 5 -t 0.25 -k sunset ~/Pictures
47
+
48
+ # browse results in a GUI window
49
+ grape -R -k sunset --view ~/Pictures
50
+
51
+ # aesthetic ranking (skip default templating -- prompts are already full sentences)
52
+ grape -R -n 20 --ensemble-prompts '{}' -k 'beautiful photo' -x 'ugly photo' ~/Pictures
53
+
54
+ # read paths from stdin
55
+ find ~/Pictures -mtime -7 -type f | grape -k selfie -
56
+
57
+ # copy the top 10 cat photos to a folder
58
+ grape -R -print0 -n 10 -k cat ~/Pictures | xargs -0 cp -t ~/cats/
59
+
60
+ # open the best match directly
61
+ grape -R -k 'golden gate bridge' -n 1 ~/Pictures | xargs open
62
+
63
+ # interactive selection with fzf
64
+ grape -R -k dog ~/Pictures | fzf --preview 'chafa {}'
65
+ ```
66
+
67
+ Run `grape --help` for the full flag list.
68
+
69
+ ## Worked examples
70
+
71
+ **Per-keyword breakdown** (`-v`) shows how each keyword and `--like`
72
+ reference contributes. The score on the header line is the mean.
73
+
74
+ ```
75
+ $ grape -R -v -n 3 --keywords 'playful puppy,golden retriever' ~/Pictures
76
+ 0.342 /home/me/Pictures/family/biscuit_park.jpg
77
+ playful puppy: 0.301 golden retriever: 0.383
78
+ 0.298 /home/me/Pictures/family/biscuit_couch.jpg
79
+ playful puppy: 0.267 golden retriever: 0.329
80
+ 0.274 /home/me/Pictures/family/old_biscuit.jpg
81
+ playful puppy: 0.198 golden retriever: 0.350
82
+ ```
83
+
84
+ **Calibrate, then threshold.** Run with `-s` first to see what scores real
85
+ matches land at, then set `-t` just below the noise floor.
86
+
87
+ ```
88
+ $ grape -R -s --keywords sunset ~/Pictures | head -5
89
+ 0.312 /home/me/Pictures/travel/santorini_evening.jpg
90
+ 0.287 /home/me/Pictures/deck_sunset.jpg
91
+ 0.244 /home/me/Pictures/travel/ocean_dusk.jpg
92
+ 0.198 /home/me/Pictures/outdoor/golden_leaves.jpg # weak, off-topic
93
+ $ grape -R -s -t 0.24 --keywords sunset ~/Pictures
94
+ 0.312 /home/me/Pictures/travel/santorini_evening.jpg
95
+ 0.287 /home/me/Pictures/deck_sunset.jpg
96
+ 0.244 /home/me/Pictures/travel/ocean_dusk.jpg
97
+ ```
98
+
99
+ **Aesthetic ranking** uses `--exclude` to subtract the mean score of a
100
+ contrast prompt. Scores are smaller here because both sides are competing.
101
+
102
+ ```
103
+ $ grape -R -s -n 5 --ensemble-prompts '{}' \
104
+ --keywords 'beautiful photo' --exclude 'ugly photo' ~/travel/
105
+ 0.163 /home/me/travel/kyoto_arashiyama.jpg
106
+ 0.139 /home/me/travel/iceland_sunset.jpg
107
+ 0.127 /home/me/travel/nepal_trek.jpg
108
+ 0.109 /home/me/travel/paris_evening.jpg
109
+ 0.091 /home/me/travel/tokyo_skyline.jpg
110
+ ```
111
+
112
+ ## Scoring
113
+
114
+ Grape ranks images by cosine similarity between CLIP embeddings. Each
115
+ image's score is:
116
+
117
+ mean(similarity to --keywords and --like) - mean(similarity to --exclude)
118
+
119
+ Cosine similarities are **not probabilities**. Meaningful matches typically
120
+ sit in the 0.15-0.35 range; even strong matches rarely exceed 0.35. Use `-s`
121
+ to calibrate before choosing a `-t` threshold. Scores are **not comparable
122
+ across models** -- switch models and the numbers shift.
123
+
124
+ **`--like` matches semantics, not pixels.** Two different photos of dogs
125
+ playing fetch score high; a photo and its crop do too. For actual duplicate
126
+ detection, use a perceptual-hashing tool like
127
+ [czkawka](https://github.com/qarmin/czkawka).
128
+
129
+ ## Caching
130
+
131
+ On by default at `~/.cache/grape/embeddings.db`
132
+ (`$XDG_CACHE_HOME/grape/embeddings.db`). Cached:
133
+
134
+ - Image embeddings (the expensive part; ~80 ms per image on CPU)
135
+ - Text embeddings, keyed by prompt
136
+ - Model identity, so repeat runs skip the torch/open_clip import entirely
137
+ - Image-vs-non-image detection results, so videos and docs are skipped on rescan
138
+
139
+ A warm query scores thousands of images in under a second via a single
140
+ matrix multiply. Entries are keyed by absolute path, file stat (size, mtime,
141
+ inode), and model ID, so they auto-invalidate when files change.
142
+
143
+ Use `--no-cache` to disable or `--cache PATH` to relocate.
144
+
145
+ ## Models
146
+
147
+ The default is
148
+ [EVA-CLIP's EVA02-L-14](https://arxiv.org/abs/2303.15389) (Sun et al., 2023)
149
+ pretrained as `merged2b_s4b_b131k` -- strong zero-shot accuracy at moderate
150
+ cost. Pick any [OpenCLIP](https://github.com/mlfoundations/open_clip)
151
+ checkpoint with `--model model_name/pretrained_tag`:
152
+
153
+ ```bash
154
+ grape --model ViT-B-32/laion2b_s34b_b79k -R -k sunset ~/Pictures # fast
155
+ grape --model ViT-L-14/laion2b_s32b_b82k -R -k sunset ~/Pictures # strong
156
+ ```
157
+
158
+ Embeddings are scoped per model; switching re-encodes everything.
159
+
160
+ Keywords are matched with
161
+ [prompt ensembling](https://arxiv.org/abs/2103.00020) (Radford et al., 2021,
162
+ Section 3.1.4 and Appendix A): each keyword expands into several templates
163
+ ("a photo of a {}", "a photo of the {}", ...), each is embedded, then the
164
+ embeddings are averaged and renormalized. Improves zero-shot accuracy over
165
+ a single prompt. Override with `--ensemble-prompts`.
File without changes
@@ -0,0 +1,3 @@
1
+ from grape.cli import main
2
+
3
+ main()