selects 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.
Files changed (216) hide show
  1. selects-0.1.0/.github/workflows/ci.yml +41 -0
  2. selects-0.1.0/.github/workflows/release.yml +89 -0
  3. selects-0.1.0/.gitignore +63 -0
  4. selects-0.1.0/LICENSE +21 -0
  5. selects-0.1.0/PKG-INFO +255 -0
  6. selects-0.1.0/README.md +217 -0
  7. selects-0.1.0/design/app.js +118 -0
  8. selects-0.1.0/design/clusters.html +277 -0
  9. selects-0.1.0/design/index.html +249 -0
  10. selects-0.1.0/design/stories.html +192 -0
  11. selects-0.1.0/design/styles.css +981 -0
  12. selects-0.1.0/docs/superpowers/plans/2026-05-23-travelcull-m1-indexer-and-ui-shell.md +3542 -0
  13. selects-0.1.0/docs/superpowers/specs/2026-05-23-travelcull-design.md +164 -0
  14. selects-0.1.0/docs/superpowers/specs/2026-05-24-aesthetic-curation-design.md +158 -0
  15. selects-0.1.0/examples/ladakh/README.md +34 -0
  16. selects-0.1.0/examples/ladakh/keywords.json +8 -0
  17. selects-0.1.0/examples/ladakh/landmarks.json +302 -0
  18. selects-0.1.0/examples/ladakh/tag_prompts.json +23 -0
  19. selects-0.1.0/frontend/index.html +14 -0
  20. selects-0.1.0/frontend/package-lock.json +1797 -0
  21. selects-0.1.0/frontend/package.json +25 -0
  22. selects-0.1.0/frontend/src/App.tsx +97 -0
  23. selects-0.1.0/frontend/src/api/client.ts +172 -0
  24. selects-0.1.0/frontend/src/api/dedup.ts +65 -0
  25. selects-0.1.0/frontend/src/api/export.ts +102 -0
  26. selects-0.1.0/frontend/src/api/faceQuality.ts +36 -0
  27. selects-0.1.0/frontend/src/api/recap.ts +23 -0
  28. selects-0.1.0/frontend/src/api/search2.ts +88 -0
  29. selects-0.1.0/frontend/src/api/taste.ts +39 -0
  30. selects-0.1.0/frontend/src/api/types.ts +159 -0
  31. selects-0.1.0/frontend/src/api/videos.ts +102 -0
  32. selects-0.1.0/frontend/src/api/watch.ts +25 -0
  33. selects-0.1.0/frontend/src/components/BurstThumb.tsx +32 -0
  34. selects-0.1.0/frontend/src/components/CompareView.css +182 -0
  35. selects-0.1.0/frontend/src/components/CompareView.tsx +272 -0
  36. selects-0.1.0/frontend/src/components/Dedup.css +245 -0
  37. selects-0.1.0/frontend/src/components/ExportPanel.css +201 -0
  38. selects-0.1.0/frontend/src/components/ExportPanel.tsx +282 -0
  39. selects-0.1.0/frontend/src/components/EyesBadge.css +50 -0
  40. selects-0.1.0/frontend/src/components/EyesBadge.tsx +99 -0
  41. selects-0.1.0/frontend/src/components/FolderPicker.tsx +148 -0
  42. selects-0.1.0/frontend/src/components/KbdFooter.tsx +48 -0
  43. selects-0.1.0/frontend/src/components/ModeViewBar.tsx +96 -0
  44. selects-0.1.0/frontend/src/components/ModelsCard.tsx +142 -0
  45. selects-0.1.0/frontend/src/components/Rail.tsx +209 -0
  46. selects-0.1.0/frontend/src/components/RecapButton.css +14 -0
  47. selects-0.1.0/frontend/src/components/RecapButton.tsx +41 -0
  48. selects-0.1.0/frontend/src/components/ScoresCard.tsx +162 -0
  49. selects-0.1.0/frontend/src/components/StackPhoto.tsx +277 -0
  50. selects-0.1.0/frontend/src/components/StatusRow.tsx +38 -0
  51. selects-0.1.0/frontend/src/components/TagBrowser.css +132 -0
  52. selects-0.1.0/frontend/src/components/TagBrowser.tsx +88 -0
  53. selects-0.1.0/frontend/src/components/TasteCard.css +61 -0
  54. selects-0.1.0/frontend/src/components/TasteCard.tsx +103 -0
  55. selects-0.1.0/frontend/src/components/Topbar.tsx +29 -0
  56. selects-0.1.0/frontend/src/components/Videos.css +314 -0
  57. selects-0.1.0/frontend/src/components/WatchCard.css +122 -0
  58. selects-0.1.0/frontend/src/components/WatchCard.tsx +153 -0
  59. selects-0.1.0/frontend/src/hooks/useCullKeys.ts +112 -0
  60. selects-0.1.0/frontend/src/hooks/useLikes.ts +60 -0
  61. selects-0.1.0/frontend/src/main.tsx +13 -0
  62. selects-0.1.0/frontend/src/styles.css +2307 -0
  63. selects-0.1.0/frontend/src/views/BestOf.tsx +334 -0
  64. selects-0.1.0/frontend/src/views/BurstCull.tsx +974 -0
  65. selects-0.1.0/frontend/src/views/Calibrate.tsx +791 -0
  66. selects-0.1.0/frontend/src/views/CalibrateDashboard.tsx +414 -0
  67. selects-0.1.0/frontend/src/views/ClusterDetail.tsx +338 -0
  68. selects-0.1.0/frontend/src/views/Clusters.tsx +361 -0
  69. selects-0.1.0/frontend/src/views/Curated.tsx +391 -0
  70. selects-0.1.0/frontend/src/views/Dedup.tsx +203 -0
  71. selects-0.1.0/frontend/src/views/Doctor.tsx +407 -0
  72. selects-0.1.0/frontend/src/views/Libraries.tsx +244 -0
  73. selects-0.1.0/frontend/src/views/Map.tsx +144 -0
  74. selects-0.1.0/frontend/src/views/Onboarding.tsx +361 -0
  75. selects-0.1.0/frontend/src/views/PersonDetail.tsx +208 -0
  76. selects-0.1.0/frontend/src/views/Persons.tsx +165 -0
  77. selects-0.1.0/frontend/src/views/Search.tsx +366 -0
  78. selects-0.1.0/frontend/src/views/Stories.tsx +1220 -0
  79. selects-0.1.0/frontend/src/views/Videos.tsx +278 -0
  80. selects-0.1.0/frontend/tsconfig.json +20 -0
  81. selects-0.1.0/frontend/vite.config.ts +13 -0
  82. selects-0.1.0/packaging/build.py +120 -0
  83. selects-0.1.0/packaging/entry.py +9 -0
  84. selects-0.1.0/packaging/selects.spec +144 -0
  85. selects-0.1.0/pyproject.toml +69 -0
  86. selects-0.1.0/scripts/analyze_quality.py +85 -0
  87. selects-0.1.0/scripts/bench_vlms.py +245 -0
  88. selects-0.1.0/scripts/check_clusters_api.py +15 -0
  89. selects-0.1.0/scripts/check_db_tables.py +16 -0
  90. selects-0.1.0/scripts/check_tags.py +44 -0
  91. selects-0.1.0/scripts/compare_scores.py +188 -0
  92. selects-0.1.0/scripts/compute_ap25.py +95 -0
  93. selects-0.1.0/scripts/compute_categories.py +126 -0
  94. selects-0.1.0/scripts/compute_nima.py +87 -0
  95. selects-0.1.0/scripts/inspect_ladakh.py +39 -0
  96. selects-0.1.0/scripts/iqa_calibration.py +68 -0
  97. selects-0.1.0/scripts/personal_performance.py +232 -0
  98. selects-0.1.0/selects/__init__.py +3 -0
  99. selects-0.1.0/selects/__main__.py +4 -0
  100. selects-0.1.0/selects/classical/__init__.py +0 -0
  101. selects-0.1.0/selects/classical/aesthetic_grade.py +101 -0
  102. selects-0.1.0/selects/classical/auto_reject.py +29 -0
  103. selects-0.1.0/selects/classical/blur.py +27 -0
  104. selects-0.1.0/selects/classical/exposure.py +29 -0
  105. selects-0.1.0/selects/classical/faces.py +65 -0
  106. selects-0.1.0/selects/classical/straighten.py +111 -0
  107. selects-0.1.0/selects/cli.py +176 -0
  108. selects-0.1.0/selects/config.py +74 -0
  109. selects-0.1.0/selects/db/__init__.py +127 -0
  110. selects-0.1.0/selects/db/migrations/env.py +67 -0
  111. selects-0.1.0/selects/db/migrations/script.py.mako +26 -0
  112. selects-0.1.0/selects/db/migrations/versions/8ff743c44fc7_baseline_schema.py +297 -0
  113. selects-0.1.0/selects/db/migrations/versions/a1b2c3d4e5f6_add_source_to_photo_tags_pk.py +105 -0
  114. selects-0.1.0/selects/db/migrations/versions/c3d4e5f6a7b8_add_face_attribute_columns.py +63 -0
  115. selects-0.1.0/selects/db/migrations/versions/d4e5f6a7b8c9_add_video_analysis_columns.py +75 -0
  116. selects-0.1.0/selects/db/models.py +393 -0
  117. selects-0.1.0/selects/decode/__init__.py +20 -0
  118. selects-0.1.0/selects/decode/heic.py +16 -0
  119. selects-0.1.0/selects/decode/jpeg.py +41 -0
  120. selects-0.1.0/selects/decode/raw.py +27 -0
  121. selects-0.1.0/selects/decode/video.py +61 -0
  122. selects-0.1.0/selects/dedup.py +273 -0
  123. selects-0.1.0/selects/export.py +316 -0
  124. selects-0.1.0/selects/gpu.py +78 -0
  125. selects-0.1.0/selects/indexer/__init__.py +0 -0
  126. selects-0.1.0/selects/indexer/exif.py +147 -0
  127. selects-0.1.0/selects/indexer/orchestrator.py +113 -0
  128. selects-0.1.0/selects/indexer/preview.py +39 -0
  129. selects-0.1.0/selects/indexer/walker.py +84 -0
  130. selects-0.1.0/selects/ml/__init__.py +0 -0
  131. selects-0.1.0/selects/ml/caption.py +168 -0
  132. selects-0.1.0/selects/ml/curation.py +321 -0
  133. selects-0.1.0/selects/ml/deblur_nafnet.py +275 -0
  134. selects-0.1.0/selects/ml/embed.py +171 -0
  135. selects-0.1.0/selects/ml/face_attributes.py +396 -0
  136. selects-0.1.0/selects/ml/faces.py +126 -0
  137. selects-0.1.0/selects/ml/locations.py +460 -0
  138. selects-0.1.0/selects/ml/lowlight.py +173 -0
  139. selects-0.1.0/selects/ml/model_assets.py +326 -0
  140. selects-0.1.0/selects/ml/moments.py +214 -0
  141. selects-0.1.0/selects/ml/nl_story.py +261 -0
  142. selects-0.1.0/selects/ml/persons.py +149 -0
  143. selects-0.1.0/selects/ml/ram_tags.py +260 -0
  144. selects-0.1.0/selects/ml/retouch_csrnet.py +173 -0
  145. selects-0.1.0/selects/ml/search.py +68 -0
  146. selects-0.1.0/selects/ml/smart_clusters.py +539 -0
  147. selects-0.1.0/selects/ml/stories.py +594 -0
  148. selects-0.1.0/selects/ml/tags.py +150 -0
  149. selects-0.1.0/selects/ml/taste.py +390 -0
  150. selects-0.1.0/selects/ml/thematic_clusters.py +270 -0
  151. selects-0.1.0/selects/ml/trip_data.py +195 -0
  152. selects-0.1.0/selects/ml/trip_type.py +84 -0
  153. selects-0.1.0/selects/pipeline.py +128 -0
  154. selects-0.1.0/selects/recap.py +588 -0
  155. selects-0.1.0/selects/server/__init__.py +0 -0
  156. selects-0.1.0/selects/server/app.py +155 -0
  157. selects-0.1.0/selects/server/dedup_routes.py +91 -0
  158. selects-0.1.0/selects/server/export_routes.py +252 -0
  159. selects-0.1.0/selects/server/faces2_routes.py +128 -0
  160. selects-0.1.0/selects/server/fs_routes.py +49 -0
  161. selects-0.1.0/selects/server/libraries.py +87 -0
  162. selects-0.1.0/selects/server/library_manager.py +295 -0
  163. selects-0.1.0/selects/server/models_routes.py +62 -0
  164. selects-0.1.0/selects/server/pipeline_runner.py +41 -0
  165. selects-0.1.0/selects/server/recap_routes.py +52 -0
  166. selects-0.1.0/selects/server/routes.py +2392 -0
  167. selects-0.1.0/selects/server/search2_routes.py +212 -0
  168. selects-0.1.0/selects/server/static/assets/index-8kseOkrj.js +89 -0
  169. selects-0.1.0/selects/server/static/assets/index-DBCjYqrq.css +1 -0
  170. selects-0.1.0/selects/server/static/index.html +15 -0
  171. selects-0.1.0/selects/server/taste_routes.py +44 -0
  172. selects-0.1.0/selects/server/video_routes.py +199 -0
  173. selects-0.1.0/selects/server/watch_routes.py +60 -0
  174. selects-0.1.0/selects/server/ws.py +48 -0
  175. selects-0.1.0/selects/video.py +392 -0
  176. selects-0.1.0/selects/watcher.py +314 -0
  177. selects-0.1.0/tests/__init__.py +0 -0
  178. selects-0.1.0/tests/conftest.py +52 -0
  179. selects-0.1.0/tests/fixtures/small.heic +0 -0
  180. selects-0.1.0/tests/fixtures/small.jpg +0 -0
  181. selects-0.1.0/tests/fixtures/small.mp4 +0 -0
  182. selects-0.1.0/tests/test_auto_reject.py +22 -0
  183. selects-0.1.0/tests/test_classical_blur.py +16 -0
  184. selects-0.1.0/tests/test_classical_exposure.py +24 -0
  185. selects-0.1.0/tests/test_classical_faces.py +15 -0
  186. selects-0.1.0/tests/test_cli.py +33 -0
  187. selects-0.1.0/tests/test_config.py +57 -0
  188. selects-0.1.0/tests/test_db.py +100 -0
  189. selects-0.1.0/tests/test_decode_heic.py +11 -0
  190. selects-0.1.0/tests/test_decode_jpeg.py +16 -0
  191. selects-0.1.0/tests/test_decode_raw.py +17 -0
  192. selects-0.1.0/tests/test_decode_video.py +18 -0
  193. selects-0.1.0/tests/test_dedup.py +176 -0
  194. selects-0.1.0/tests/test_exif.py +43 -0
  195. selects-0.1.0/tests/test_export.py +259 -0
  196. selects-0.1.0/tests/test_face_attributes.py +403 -0
  197. selects-0.1.0/tests/test_gpu.py +47 -0
  198. selects-0.1.0/tests/test_indexer_orchestrator.py +59 -0
  199. selects-0.1.0/tests/test_migrations.py +200 -0
  200. selects-0.1.0/tests/test_ml_embed.py +163 -0
  201. selects-0.1.0/tests/test_ml_moments.py +264 -0
  202. selects-0.1.0/tests/test_ml_stories.py +313 -0
  203. selects-0.1.0/tests/test_ml_tags.py +126 -0
  204. selects-0.1.0/tests/test_model_assets.py +273 -0
  205. selects-0.1.0/tests/test_pipeline_stage1.py +42 -0
  206. selects-0.1.0/tests/test_preview.py +37 -0
  207. selects-0.1.0/tests/test_recap.py +126 -0
  208. selects-0.1.0/tests/test_search2_routes.py +145 -0
  209. selects-0.1.0/tests/test_server_libraries.py +122 -0
  210. selects-0.1.0/tests/test_server_routes.py +64 -0
  211. selects-0.1.0/tests/test_server_ws.py +20 -0
  212. selects-0.1.0/tests/test_taste.py +202 -0
  213. selects-0.1.0/tests/test_trip_data.py +142 -0
  214. selects-0.1.0/tests/test_video.py +286 -0
  215. selects-0.1.0/tests/test_walker.py +127 -0
  216. selects-0.1.0/tests/test_watcher.py +117 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, windows-latest, macos-latest]
14
+ runs-on: ${{ matrix.os }}
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.11"
20
+ cache: pip
21
+ - name: Install package + test deps (CPU torch)
22
+ run: |
23
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
24
+ pip install -e ".[dev]" transformers huggingface_hub
25
+ - name: Run tests
26
+ run: pytest -q
27
+
28
+ frontend:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version: 20
35
+ cache: npm
36
+ cache-dependency-path: frontend/package-lock.json
37
+ - name: Build frontend (strict TS)
38
+ working-directory: frontend
39
+ run: |
40
+ npm ci
41
+ npm run build
@@ -0,0 +1,89 @@
1
+ name: Release builds
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ include:
13
+ - os: windows-latest
14
+ artifact: selects-windows-x64
15
+ - os: macos-latest
16
+ artifact: selects-macos-arm64
17
+ - os: ubuntu-latest
18
+ artifact: selects-linux-x64
19
+ runs-on: ${{ matrix.os }}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.11"
25
+ cache: pip
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ - name: Install package (CPU torch) + PyInstaller
30
+ run: |
31
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
32
+ pip install -e . pyinstaller
33
+ - name: Build bundle
34
+ run: python packaging/build.py
35
+ - name: Zip bundle
36
+ shell: bash
37
+ run: |
38
+ cd dist
39
+ if [ "$RUNNER_OS" = "Windows" ]; then
40
+ 7z a ../${{ matrix.artifact }}.zip selects
41
+ else
42
+ zip -qr ../${{ matrix.artifact }}.zip selects
43
+ fi
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: ${{ matrix.artifact }}
47
+ path: ${{ matrix.artifact }}.zip
48
+
49
+ pypi-publish:
50
+ runs-on: ubuntu-latest
51
+ environment: pypi
52
+ permissions:
53
+ id-token: write # OIDC token for PyPI Trusted Publishing — no API token needed
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ - uses: actions/setup-python@v5
57
+ with:
58
+ python-version: "3.11"
59
+ - uses: actions/setup-node@v4
60
+ with:
61
+ node-version: 20
62
+ - name: Build frontend into packaged static dir
63
+ run: |
64
+ npm --prefix frontend ci
65
+ npm --prefix frontend run build
66
+ rm -rf selects/server/static
67
+ cp -r frontend/dist selects/server/static
68
+ - name: Build sdist + wheel
69
+ run: |
70
+ pip install build
71
+ python -m build
72
+ - name: Publish to PyPI
73
+ uses: pypa/gh-action-pypi-publish@release/v1
74
+
75
+ release:
76
+ needs: build
77
+ runs-on: ubuntu-latest
78
+ permissions:
79
+ contents: write
80
+ steps:
81
+ - uses: actions/download-artifact@v4
82
+ with:
83
+ merge-multiple: true
84
+ - name: Create GitHub release with artifacts
85
+ uses: softprops/action-gh-release@v2
86
+ with:
87
+ files: "*.zip"
88
+ generate_release_notes: true
89
+ draft: true
@@ -0,0 +1,63 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.whl
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Testing
19
+ .pytest_cache/
20
+ .coverage
21
+ htmlcov/
22
+ .tox/
23
+
24
+ # IDE
25
+ .vscode/
26
+ .idea/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+
31
+ # Frontend
32
+ frontend/node_modules/
33
+ frontend/dist/
34
+
35
+ # Packaging
36
+ selects/server/static/
37
+ packaging/dist/
38
+ packaging/build/
39
+
40
+ # Tooling state
41
+ .claude/
42
+ .playwright-mcp/
43
+ screenshots/
44
+ *.png
45
+
46
+ # Project state
47
+ .selects/
48
+
49
+ # OS
50
+ .DS_Store
51
+ Thumbs.db
52
+ desktop.ini
53
+
54
+ # Misc
55
+ *.log
56
+ *.tmp
57
+
58
+ # Ad-hoc report artifacts at repo root (not frontend/docs html)
59
+ /iqa_calibration.html
60
+ /personal_performance.html
61
+ /score_comparison.html
62
+ /test_enhanced.jpg
63
+ /*.jpg
selects-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 selects contributors
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.
selects-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.4
2
+ Name: selects
3
+ Version: 0.1.0
4
+ Summary: Local AI-assisted travel photo and video culling
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: alembic>=1.13
9
+ Requires-Dist: click>=8.1
10
+ Requires-Dist: fastapi>=0.110
11
+ Requires-Dist: numpy>=1.26
12
+ Requires-Dist: opencv-python>=4.9
13
+ Requires-Dist: pillow-heif>=0.16
14
+ Requires-Dist: pillow>=10.2
15
+ Requires-Dist: pydantic-settings>=2.2
16
+ Requires-Dist: pydantic>=2.6
17
+ Requires-Dist: pyexiv2>=2.11
18
+ Requires-Dist: python-multipart>=0.0.9
19
+ Requires-Dist: rawpy>=0.21
20
+ Requires-Dist: sqlalchemy>=2.0
21
+ Requires-Dist: uvicorn[standard]>=0.27
22
+ Requires-Dist: websockets>=12.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: httpx>=0.26; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.3; extra == 'dev'
28
+ Provides-Extra: ml
29
+ Requires-Dist: hdbscan>=0.8.33; extra == 'ml'
30
+ Requires-Dist: insightface>=0.7; extra == 'ml'
31
+ Requires-Dist: onnxruntime>=1.17; extra == 'ml'
32
+ Requires-Dist: requests>=2.31; extra == 'ml'
33
+ Requires-Dist: scikit-learn>=1.4; extra == 'ml'
34
+ Requires-Dist: torch>=2.2; extra == 'ml'
35
+ Requires-Dist: transformers>=4.40; extra == 'ml'
36
+ Requires-Dist: umap-learn>=0.5.5; extra == 'ml'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # Selects
40
+
41
+ Selects is a local-first, AI-assisted photo culling tool for travel photography. Point it at a
42
+ folder of photos (and videos), and it indexes, scores, clusters, and groups them into stories so
43
+ you can quickly find your keepers — without uploading anything anywhere. All ML inference (image
44
+ embeddings, tagging, face clustering, aesthetic scoring, optional enhancement) runs on your own
45
+ machine; the only network calls are optional reverse-geocoding lookups for place names.
46
+
47
+ ## Features
48
+
49
+ - Fast local indexing of JPEG/HEIC/RAW photos and video files, with thumbnail/preview generation
50
+ - Classical signal scoring: blur, exposure, clipped highlights, face detection, auto-reject
51
+ - SigLIP image embeddings for semantic search and similarity
52
+ - Zero-shot and RAM++-based photo tagging
53
+ - ArcFace face embeddings + clustering into "Person" identities, with name labeling
54
+ - GPS/time-based location clustering and "story" building (day-by-day, place-by-place)
55
+ - "Moment" grouping to collapse near-duplicate bursts into a single best pick
56
+ - AP25 + NIMA-based aesthetic curation (per-scope and library-wide percentile gating)
57
+ - Optional local enhancement models: NAFNet (deblur), Zero-DCE++ (low-light), CSRNet (retouch)
58
+ - React/Vite web UI for reviewing clusters, people, stories, and doing burst culling
59
+ - CLI for headless indexing and running individual pipeline stages
60
+
61
+ ## Install
62
+
63
+ **Desktop app (recommended).** Download the bundle for your OS from the
64
+ [latest release](https://github.com/bihanikeshav/selects/releases) and run it — no Python
65
+ required. The app downloads its AI models on first launch, with progress.
66
+
67
+ **Via pip** (needs Python 3.11+):
68
+
69
+ ```bash
70
+ pip install selects # app + web GUI + CLI
71
+ pip install "selects[ml]" # add the on-device AI (torch, insightface, …)
72
+ selects serve # open the web UI
73
+ selects index /path/to/trip # or run headless from the CLI
74
+ ```
75
+
76
+ RAM++ tagging installs separately (no PyPI release):
77
+ `pip install git+https://github.com/xinyu1205/recognize-anything.git`
78
+
79
+ > **GPU acceleration is coming soon.** These builds run on **CPU** — they work on any machine,
80
+ > just slower. GPU-accelerated builds (Apple Silicon and NVIDIA first, AMD to follow) are next up.
81
+ > The pipeline already detects your hardware and will use it once the accelerated packages ship.
82
+
83
+ ## Quickstart (from source)
84
+
85
+ Requires Python 3.11+ and Node 18+.
86
+
87
+ ```bash
88
+ # Backend
89
+ pip install -e ".[ml]" # add the ML stack (torch, transformers, insightface, etc.)
90
+ selects serve /path/to/photos
91
+
92
+ # Frontend (separate terminal)
93
+ cd frontend
94
+ npm install
95
+ npm run dev
96
+ ```
97
+
98
+ `selects serve` starts the FastAPI backend, opens the web UI in your browser (pass
99
+ `--no-browser` to skip), and (unless `--no-background` is passed) kicks off indexing in the
100
+ background. The frontend dev server (`npm run dev`) proxies to the backend for a hot-reloading UI
101
+ during development.
102
+
103
+ For a single-process run, build the frontend once (`cd frontend && npm run build`) and the backend
104
+ will serve the compiled UI itself at the same origin — no separate `npm run dev` needed. You can
105
+ also run `selects serve` with **no folder argument**: it opens the active library from your
106
+ registry, or, if none exists, starts on the onboarding page so you can add your first library from
107
+ the browser.
108
+
109
+ If you only need the classical (non-ML) stages — indexing, previews, blur/exposure/face
110
+ auto-reject — you can skip the `ml` extra: `pip install -e .`.
111
+
112
+ You can also drive the pipeline directly from the CLI:
113
+
114
+ ```bash
115
+ selects index /path/to/photos # run all default stages
116
+ selects index /path/to/photos --pass embed # run a single named stage
117
+ selects doctor # report CUDA/GPU capabilities
118
+ ```
119
+
120
+ ## How the pipeline works
121
+
122
+ Each stage reads/writes to a per-folder SQLite database at `<folder>/.selects/index.db` and can
123
+ be re-run independently via `selects index <folder> --pass <stage>`:
124
+
125
+ 1. **index** — walk the folder, hash files, decode previews/thumbnails, read EXIF/GPS
126
+ 2. **classical** — blur, exposure, clipped-highlight, and face-count scoring; auto-reject gate
127
+ 3. **embed** — SigLIP-SO400M image embeddings + CLIP-IQA aesthetic score
128
+ 4. **tag** — zero-shot tagging via SigLIP text-prompt similarity
129
+ 5. **ram_tag** — RAM++ open-vocabulary tagging (more descriptive labels than zero-shot)
130
+ 6. **smart_tag** — HDBSCAN clustering over embeddings + VLM-generated cluster names
131
+ 7. **thematic / date** — rule-driven location and day clustering from GPS/time
132
+ 8. **face_embed** — ArcFace face embeddings for detected faces
133
+ 9. **moment** — collapse near-duplicate/burst photos into a single representative pick
134
+ 10. **story** — build day/place "stories" combining moments, tags, and locations
135
+
136
+ Aesthetic curation (surfacing the "best of" a trip) combines AP25 and NIMA scores with configurable
137
+ per-scope and library-wide percentile thresholds — see `ap_weight`, `nima_weight`,
138
+ `aesthetic_per_scope_pct`, and `aesthetic_library_pct` below.
139
+
140
+ ## Configuration
141
+
142
+ Configuration is per-folder, via `pydantic-settings`. Every field can be overridden with an
143
+ `SELECTS_`-prefixed environment variable (or a `.env` file in the working directory), e.g.
144
+ `SELECTS_WEB_PORT=9000`. Fields (see `selects/config.py`):
145
+
146
+ | Field | Default | Notes |
147
+ |---|---|---|
148
+ | `web_port` | `8765` | Port for the web UI/API |
149
+ | `web_host` | `127.0.0.1` | Bind host |
150
+ | `burst_window_seconds` | `3` | Time window used to group burst shots |
151
+ | `burst_similarity_threshold` | `0.92` | Similarity cutoff for burst grouping |
152
+ | `ap_weight` | `0.6` | Weight of AP25 in the combined aesthetic score |
153
+ | `nima_weight` | `0.4` | Weight of NIMA in the combined aesthetic score |
154
+ | `aesthetic_per_scope_pct` | `75.0` | Photo must be in the top `(100 - pct)`% within its scope |
155
+ | `aesthetic_library_pct` | `50.0` | Photo must also be in the top `(100 - pct)`% library-wide |
156
+ | `speed_mode` | `full` | `fast` skips some ML stages for a quick preview pass |
157
+
158
+ Derived, non-configurable paths under `<folder>/.selects/`: `index.db`, `thumbs/`, `previews/`.
159
+
160
+ ### Per-trip customization
161
+
162
+ The location and tagging stages ship with travel-generic defaults, but you can tune them per
163
+ library by dropping optional JSON files into `<folder>/.selects/`:
164
+
165
+ | File | Purpose |
166
+ |---|---|
167
+ | `landmarks.json` | Named GPS landmarks (`{"name", "lat", "lon", "radius_m"}`) used as a fast-path override for reverse geocoding. Without it, geocoding relies entirely on Nominatim. |
168
+ | `keywords.json` | Theme buckets (`{label: [keyword, ...]}`) for pattern/thematic stories. |
169
+ | `tag_prompts.json` | Zero-shot SigLIP tag taxonomy (`{tag: [prompt, ...]}`). |
170
+
171
+ Each file is optional; a missing or malformed file falls back to the built-in defaults. See
172
+ [`examples/ladakh/`](examples/ladakh/) for a complete worked example (the original Ladakh dataset)
173
+ and copy any file into your own `.selects/` to customize.
174
+
175
+ ## Hardware notes
176
+
177
+ The ML stages (embedding, tagging, face recognition, clustering, enhancement) are much faster with
178
+ a CUDA GPU — `selects doctor` reports what's available (CUDA, NVDEC via torchcodec,
179
+ nvImageCodec, `cv2.cuda`). Everything falls back to CPU, but expect the ML stages to be
180
+ significantly slower, especially the enhancement models (NAFNet/Zero-DCE++/CSRNet) and the VLM
181
+ cluster-naming pass. Classical scoring (blur/exposure/face-detect) and indexing run fine on CPU.
182
+
183
+ ## Project layout
184
+
185
+ - `selects/` — Python package: CLI, config, pipeline orchestration, DB models
186
+ - `classical/` — non-ML signal scoring (blur, exposure, face detect, auto-reject, straighten)
187
+ - `decode/` — image/video/RAW decoding
188
+ - `indexer/` — folder walking, EXIF reading, preview generation, orchestration
189
+ - `ml/` — embedding, tagging, faces, clustering, stories, moments, enhancement models
190
+ - `server/` — FastAPI app, routes, websocket progress bus
191
+ - `frontend/` — React + Vite + TypeScript web UI
192
+ - `tests/` — pytest suite
193
+ - `scripts/` — standalone analysis/benchmarking scripts (not part of the package)
194
+ - `docs/` — design notes and specs
195
+ - `design/` — visual design references
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ pip install -e ".[dev]"
201
+ pytest
202
+ ruff check .
203
+ ```
204
+
205
+ Database schema is managed with Alembic. Migrations ship inside the package at
206
+ `selects/db/migrations/` (there is no `alembic.ini`), and `init_db()` brings
207
+ each library's SQLite DB up to head automatically on open — fresh DBs are created
208
+ and stamped, pre-existing DBs are stamped at the baseline and upgraded. After
209
+ changing `selects/db/models.py`, create a new revision with autogenerate by
210
+ building an in-code `Config` pointing `script_location` at
211
+ `selects/db/migrations` and `sqlalchemy.url` at a throwaway SQLite file, then
212
+ calling `alembic.command.revision(cfg, autogenerate=True, message="...")`. Always
213
+ review the generated script — SQLite ALTERs must go through `render_as_batch`
214
+ (already enabled in `env.py`).
215
+
216
+ ## Packaging a desktop build
217
+
218
+ Produce a self-contained bundle (no Python install required on the target machine) with
219
+ PyInstaller:
220
+
221
+ ```bash
222
+ pip install "pyinstaller>=6.6"
223
+ python packaging/build.py
224
+ ```
225
+
226
+ The script builds the frontend (`npm run build`), copies `frontend/dist` into
227
+ `selects/server/static/` so the packaged app serves the UI same-origin, then runs PyInstaller in
228
+ **onedir** mode using `packaging/selects.spec`. The result lands in `dist/selects/` — launch
229
+ `dist/selects/selects.exe` (or `./selects` on macOS/Linux).
230
+
231
+ By default the ML stack (torch, transformers, insightface, …) is **excluded** to keep the bundle
232
+ small and the build fast — the base app still indexes, previews, and runs the classical auto-reject
233
+ stages. To bundle the ML deps, use `python packaging/build.py --ml` (or set
234
+ `SELECTS_BUNDLE_ML=1`); any ML package that isn't installed is skipped, so the build always
235
+ degrades gracefully.
236
+
237
+ CPU-torch tip: the default CUDA torch wheels are multiple GB. For a much smaller ML bundle, install
238
+ the CPU build first:
239
+
240
+ ```bash
241
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
242
+ python packaging/build.py --ml
243
+ ```
244
+
245
+ ## Known limitations
246
+
247
+ - Aesthetic and burst-detection thresholds were tuned against a single trip's photo set and may
248
+ need adjustment for very different shooting styles or camera gear
249
+ - List endpoints use simple offset/limit pagination, not cursor-based
250
+ - RAM++ tagging depends on a model with no PyPI release (installed via git URL); expect a slower,
251
+ less reproducible install than the rest of the `ml` extra
252
+
253
+ ## License
254
+
255
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,217 @@
1
+ # Selects
2
+
3
+ Selects is a local-first, AI-assisted photo culling tool for travel photography. Point it at a
4
+ folder of photos (and videos), and it indexes, scores, clusters, and groups them into stories so
5
+ you can quickly find your keepers — without uploading anything anywhere. All ML inference (image
6
+ embeddings, tagging, face clustering, aesthetic scoring, optional enhancement) runs on your own
7
+ machine; the only network calls are optional reverse-geocoding lookups for place names.
8
+
9
+ ## Features
10
+
11
+ - Fast local indexing of JPEG/HEIC/RAW photos and video files, with thumbnail/preview generation
12
+ - Classical signal scoring: blur, exposure, clipped highlights, face detection, auto-reject
13
+ - SigLIP image embeddings for semantic search and similarity
14
+ - Zero-shot and RAM++-based photo tagging
15
+ - ArcFace face embeddings + clustering into "Person" identities, with name labeling
16
+ - GPS/time-based location clustering and "story" building (day-by-day, place-by-place)
17
+ - "Moment" grouping to collapse near-duplicate bursts into a single best pick
18
+ - AP25 + NIMA-based aesthetic curation (per-scope and library-wide percentile gating)
19
+ - Optional local enhancement models: NAFNet (deblur), Zero-DCE++ (low-light), CSRNet (retouch)
20
+ - React/Vite web UI for reviewing clusters, people, stories, and doing burst culling
21
+ - CLI for headless indexing and running individual pipeline stages
22
+
23
+ ## Install
24
+
25
+ **Desktop app (recommended).** Download the bundle for your OS from the
26
+ [latest release](https://github.com/bihanikeshav/selects/releases) and run it — no Python
27
+ required. The app downloads its AI models on first launch, with progress.
28
+
29
+ **Via pip** (needs Python 3.11+):
30
+
31
+ ```bash
32
+ pip install selects # app + web GUI + CLI
33
+ pip install "selects[ml]" # add the on-device AI (torch, insightface, …)
34
+ selects serve # open the web UI
35
+ selects index /path/to/trip # or run headless from the CLI
36
+ ```
37
+
38
+ RAM++ tagging installs separately (no PyPI release):
39
+ `pip install git+https://github.com/xinyu1205/recognize-anything.git`
40
+
41
+ > **GPU acceleration is coming soon.** These builds run on **CPU** — they work on any machine,
42
+ > just slower. GPU-accelerated builds (Apple Silicon and NVIDIA first, AMD to follow) are next up.
43
+ > The pipeline already detects your hardware and will use it once the accelerated packages ship.
44
+
45
+ ## Quickstart (from source)
46
+
47
+ Requires Python 3.11+ and Node 18+.
48
+
49
+ ```bash
50
+ # Backend
51
+ pip install -e ".[ml]" # add the ML stack (torch, transformers, insightface, etc.)
52
+ selects serve /path/to/photos
53
+
54
+ # Frontend (separate terminal)
55
+ cd frontend
56
+ npm install
57
+ npm run dev
58
+ ```
59
+
60
+ `selects serve` starts the FastAPI backend, opens the web UI in your browser (pass
61
+ `--no-browser` to skip), and (unless `--no-background` is passed) kicks off indexing in the
62
+ background. The frontend dev server (`npm run dev`) proxies to the backend for a hot-reloading UI
63
+ during development.
64
+
65
+ For a single-process run, build the frontend once (`cd frontend && npm run build`) and the backend
66
+ will serve the compiled UI itself at the same origin — no separate `npm run dev` needed. You can
67
+ also run `selects serve` with **no folder argument**: it opens the active library from your
68
+ registry, or, if none exists, starts on the onboarding page so you can add your first library from
69
+ the browser.
70
+
71
+ If you only need the classical (non-ML) stages — indexing, previews, blur/exposure/face
72
+ auto-reject — you can skip the `ml` extra: `pip install -e .`.
73
+
74
+ You can also drive the pipeline directly from the CLI:
75
+
76
+ ```bash
77
+ selects index /path/to/photos # run all default stages
78
+ selects index /path/to/photos --pass embed # run a single named stage
79
+ selects doctor # report CUDA/GPU capabilities
80
+ ```
81
+
82
+ ## How the pipeline works
83
+
84
+ Each stage reads/writes to a per-folder SQLite database at `<folder>/.selects/index.db` and can
85
+ be re-run independently via `selects index <folder> --pass <stage>`:
86
+
87
+ 1. **index** — walk the folder, hash files, decode previews/thumbnails, read EXIF/GPS
88
+ 2. **classical** — blur, exposure, clipped-highlight, and face-count scoring; auto-reject gate
89
+ 3. **embed** — SigLIP-SO400M image embeddings + CLIP-IQA aesthetic score
90
+ 4. **tag** — zero-shot tagging via SigLIP text-prompt similarity
91
+ 5. **ram_tag** — RAM++ open-vocabulary tagging (more descriptive labels than zero-shot)
92
+ 6. **smart_tag** — HDBSCAN clustering over embeddings + VLM-generated cluster names
93
+ 7. **thematic / date** — rule-driven location and day clustering from GPS/time
94
+ 8. **face_embed** — ArcFace face embeddings for detected faces
95
+ 9. **moment** — collapse near-duplicate/burst photos into a single representative pick
96
+ 10. **story** — build day/place "stories" combining moments, tags, and locations
97
+
98
+ Aesthetic curation (surfacing the "best of" a trip) combines AP25 and NIMA scores with configurable
99
+ per-scope and library-wide percentile thresholds — see `ap_weight`, `nima_weight`,
100
+ `aesthetic_per_scope_pct`, and `aesthetic_library_pct` below.
101
+
102
+ ## Configuration
103
+
104
+ Configuration is per-folder, via `pydantic-settings`. Every field can be overridden with an
105
+ `SELECTS_`-prefixed environment variable (or a `.env` file in the working directory), e.g.
106
+ `SELECTS_WEB_PORT=9000`. Fields (see `selects/config.py`):
107
+
108
+ | Field | Default | Notes |
109
+ |---|---|---|
110
+ | `web_port` | `8765` | Port for the web UI/API |
111
+ | `web_host` | `127.0.0.1` | Bind host |
112
+ | `burst_window_seconds` | `3` | Time window used to group burst shots |
113
+ | `burst_similarity_threshold` | `0.92` | Similarity cutoff for burst grouping |
114
+ | `ap_weight` | `0.6` | Weight of AP25 in the combined aesthetic score |
115
+ | `nima_weight` | `0.4` | Weight of NIMA in the combined aesthetic score |
116
+ | `aesthetic_per_scope_pct` | `75.0` | Photo must be in the top `(100 - pct)`% within its scope |
117
+ | `aesthetic_library_pct` | `50.0` | Photo must also be in the top `(100 - pct)`% library-wide |
118
+ | `speed_mode` | `full` | `fast` skips some ML stages for a quick preview pass |
119
+
120
+ Derived, non-configurable paths under `<folder>/.selects/`: `index.db`, `thumbs/`, `previews/`.
121
+
122
+ ### Per-trip customization
123
+
124
+ The location and tagging stages ship with travel-generic defaults, but you can tune them per
125
+ library by dropping optional JSON files into `<folder>/.selects/`:
126
+
127
+ | File | Purpose |
128
+ |---|---|
129
+ | `landmarks.json` | Named GPS landmarks (`{"name", "lat", "lon", "radius_m"}`) used as a fast-path override for reverse geocoding. Without it, geocoding relies entirely on Nominatim. |
130
+ | `keywords.json` | Theme buckets (`{label: [keyword, ...]}`) for pattern/thematic stories. |
131
+ | `tag_prompts.json` | Zero-shot SigLIP tag taxonomy (`{tag: [prompt, ...]}`). |
132
+
133
+ Each file is optional; a missing or malformed file falls back to the built-in defaults. See
134
+ [`examples/ladakh/`](examples/ladakh/) for a complete worked example (the original Ladakh dataset)
135
+ and copy any file into your own `.selects/` to customize.
136
+
137
+ ## Hardware notes
138
+
139
+ The ML stages (embedding, tagging, face recognition, clustering, enhancement) are much faster with
140
+ a CUDA GPU — `selects doctor` reports what's available (CUDA, NVDEC via torchcodec,
141
+ nvImageCodec, `cv2.cuda`). Everything falls back to CPU, but expect the ML stages to be
142
+ significantly slower, especially the enhancement models (NAFNet/Zero-DCE++/CSRNet) and the VLM
143
+ cluster-naming pass. Classical scoring (blur/exposure/face-detect) and indexing run fine on CPU.
144
+
145
+ ## Project layout
146
+
147
+ - `selects/` — Python package: CLI, config, pipeline orchestration, DB models
148
+ - `classical/` — non-ML signal scoring (blur, exposure, face detect, auto-reject, straighten)
149
+ - `decode/` — image/video/RAW decoding
150
+ - `indexer/` — folder walking, EXIF reading, preview generation, orchestration
151
+ - `ml/` — embedding, tagging, faces, clustering, stories, moments, enhancement models
152
+ - `server/` — FastAPI app, routes, websocket progress bus
153
+ - `frontend/` — React + Vite + TypeScript web UI
154
+ - `tests/` — pytest suite
155
+ - `scripts/` — standalone analysis/benchmarking scripts (not part of the package)
156
+ - `docs/` — design notes and specs
157
+ - `design/` — visual design references
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ pip install -e ".[dev]"
163
+ pytest
164
+ ruff check .
165
+ ```
166
+
167
+ Database schema is managed with Alembic. Migrations ship inside the package at
168
+ `selects/db/migrations/` (there is no `alembic.ini`), and `init_db()` brings
169
+ each library's SQLite DB up to head automatically on open — fresh DBs are created
170
+ and stamped, pre-existing DBs are stamped at the baseline and upgraded. After
171
+ changing `selects/db/models.py`, create a new revision with autogenerate by
172
+ building an in-code `Config` pointing `script_location` at
173
+ `selects/db/migrations` and `sqlalchemy.url` at a throwaway SQLite file, then
174
+ calling `alembic.command.revision(cfg, autogenerate=True, message="...")`. Always
175
+ review the generated script — SQLite ALTERs must go through `render_as_batch`
176
+ (already enabled in `env.py`).
177
+
178
+ ## Packaging a desktop build
179
+
180
+ Produce a self-contained bundle (no Python install required on the target machine) with
181
+ PyInstaller:
182
+
183
+ ```bash
184
+ pip install "pyinstaller>=6.6"
185
+ python packaging/build.py
186
+ ```
187
+
188
+ The script builds the frontend (`npm run build`), copies `frontend/dist` into
189
+ `selects/server/static/` so the packaged app serves the UI same-origin, then runs PyInstaller in
190
+ **onedir** mode using `packaging/selects.spec`. The result lands in `dist/selects/` — launch
191
+ `dist/selects/selects.exe` (or `./selects` on macOS/Linux).
192
+
193
+ By default the ML stack (torch, transformers, insightface, …) is **excluded** to keep the bundle
194
+ small and the build fast — the base app still indexes, previews, and runs the classical auto-reject
195
+ stages. To bundle the ML deps, use `python packaging/build.py --ml` (or set
196
+ `SELECTS_BUNDLE_ML=1`); any ML package that isn't installed is skipped, so the build always
197
+ degrades gracefully.
198
+
199
+ CPU-torch tip: the default CUDA torch wheels are multiple GB. For a much smaller ML bundle, install
200
+ the CPU build first:
201
+
202
+ ```bash
203
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
204
+ python packaging/build.py --ml
205
+ ```
206
+
207
+ ## Known limitations
208
+
209
+ - Aesthetic and burst-detection thresholds were tuned against a single trip's photo set and may
210
+ need adjustment for very different shooting styles or camera gear
211
+ - List endpoints use simple offset/limit pagination, not cursor-based
212
+ - RAM++ tagging depends on a model with no PyPI release (installed via git URL); expect a slower,
213
+ less reproducible install than the rest of the `ml` extra
214
+
215
+ ## License
216
+
217
+ MIT — see [LICENSE](LICENSE).