pixcull 2.43.4__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 (164) hide show
  1. pixcull-2.43.4/.gitignore +98 -0
  2. pixcull-2.43.4/LICENSE +21 -0
  3. pixcull-2.43.4/PKG-INFO +147 -0
  4. pixcull-2.43.4/README-PYPI.md +70 -0
  5. pixcull-2.43.4/pixcull/__init__.py +55 -0
  6. pixcull-2.43.4/pixcull/__main__.py +4 -0
  7. pixcull-2.43.4/pixcull/audio_sync.py +269 -0
  8. pixcull-2.43.4/pixcull/cli.py +1377 -0
  9. pixcull-2.43.4/pixcull/config.py +85 -0
  10. pixcull-2.43.4/pixcull/db/__init__.py +0 -0
  11. pixcull-2.43.4/pixcull/db/models.py +33 -0
  12. pixcull-2.43.4/pixcull/detectors/__init__.py +3 -0
  13. pixcull-2.43.4/pixcull/detectors/base.py +30 -0
  14. pixcull-2.43.4/pixcull/detectors/blur.py +60 -0
  15. pixcull-2.43.4/pixcull/detectors/canon.py +407 -0
  16. pixcull-2.43.4/pixcull/detectors/composition.py +224 -0
  17. pixcull-2.43.4/pixcull/detectors/duplicate.py +198 -0
  18. pixcull-2.43.4/pixcull/detectors/exposure.py +32 -0
  19. pixcull-2.43.4/pixcull/detectors/face.py +283 -0
  20. pixcull-2.43.4/pixcull/detectors/scene.py +200 -0
  21. pixcull-2.43.4/pixcull/detectors/subject.py +31 -0
  22. pixcull-2.43.4/pixcull/detectors/wedding_moment.py +71 -0
  23. pixcull-2.43.4/pixcull/error_reporting.py +264 -0
  24. pixcull-2.43.4/pixcull/events.py +160 -0
  25. pixcull-2.43.4/pixcull/i18n.py +175 -0
  26. pixcull-2.43.4/pixcull/io/__init__.py +5 -0
  27. pixcull-2.43.4/pixcull/io/exif.py +161 -0
  28. pixcull-2.43.4/pixcull/io/exif_audit.py +170 -0
  29. pixcull-2.43.4/pixcull/io/formats.py +8 -0
  30. pixcull-2.43.4/pixcull/io/gpmf.py +513 -0
  31. pixcull-2.43.4/pixcull/io/gps_map.py +127 -0
  32. pixcull-2.43.4/pixcull/io/icc.py +205 -0
  33. pixcull-2.43.4/pixcull/io/iptc_embed.py +166 -0
  34. pixcull-2.43.4/pixcull/io/loader.py +272 -0
  35. pixcull-2.43.4/pixcull/io/raw_proxy.py +132 -0
  36. pixcull-2.43.4/pixcull/io/reel_assembly.py +577 -0
  37. pixcull-2.43.4/pixcull/io/video.py +495 -0
  38. pixcull-2.43.4/pixcull/io/xmp.py +457 -0
  39. pixcull-2.43.4/pixcull/license/__init__.py +521 -0
  40. pixcull-2.43.4/pixcull/llm_budget.py +213 -0
  41. pixcull-2.43.4/pixcull/locale/ar_SA.json +194 -0
  42. pixcull-2.43.4/pixcull/locale/de_DE.json +194 -0
  43. pixcull-2.43.4/pixcull/locale/en_US.json +194 -0
  44. pixcull-2.43.4/pixcull/locale/es_ES.json +194 -0
  45. pixcull-2.43.4/pixcull/locale/fr_FR.json +194 -0
  46. pixcull-2.43.4/pixcull/locale/it_IT.json +194 -0
  47. pixcull-2.43.4/pixcull/locale/ja_JP.json +194 -0
  48. pixcull-2.43.4/pixcull/locale/ko_KR.json +194 -0
  49. pixcull-2.43.4/pixcull/locale/nl_NL.json +194 -0
  50. pixcull-2.43.4/pixcull/locale/pt_BR.json +194 -0
  51. pixcull-2.43.4/pixcull/locale/ru_RU.json +194 -0
  52. pixcull-2.43.4/pixcull/locale/tr_TR.json +194 -0
  53. pixcull-2.43.4/pixcull/locale/zh_CN.json +194 -0
  54. pixcull-2.43.4/pixcull/models_manager.py +262 -0
  55. pixcull-2.43.4/pixcull/phrase_generator.py +483 -0
  56. pixcull-2.43.4/pixcull/pipeline/__init__.py +4 -0
  57. pixcull-2.43.4/pixcull/pipeline/burst_peak.py +267 -0
  58. pixcull-2.43.4/pixcull/pipeline/face_audit.py +199 -0
  59. pixcull-2.43.4/pixcull/pipeline/face_clustering.py +528 -0
  60. pixcull-2.43.4/pixcull/pipeline/face_library.py +261 -0
  61. pixcull-2.43.4/pixcull/pipeline/location_clustering.py +189 -0
  62. pixcull-2.43.4/pixcull/pipeline/orchestrator.py +614 -0
  63. pixcull-2.43.4/pixcull/pipeline/parallel.py +261 -0
  64. pixcull-2.43.4/pixcull/pipeline/worker.py +231 -0
  65. pixcull-2.43.4/pixcull/plugins/__init__.py +422 -0
  66. pixcull-2.43.4/pixcull/plugins/builtin/example_wildlife.py +39 -0
  67. pixcull-2.43.4/pixcull/policy_tuner.py +432 -0
  68. pixcull-2.43.4/pixcull/preferred_axes.py +146 -0
  69. pixcull-2.43.4/pixcull/qrcode_svg.py +488 -0
  70. pixcull-2.43.4/pixcull/report/__init__.py +13 -0
  71. pixcull-2.43.4/pixcull/report/contact_sheet.py +278 -0
  72. pixcull-2.43.4/pixcull/report/csv.py +15 -0
  73. pixcull-2.43.4/pixcull/report/executive_pdf.py +946 -0
  74. pixcull-2.43.4/pixcull/report/gallery.py +303 -0
  75. pixcull-2.43.4/pixcull/report/serve_app.py +13014 -0
  76. pixcull-2.43.4/pixcull/report/serve_util.py +139 -0
  77. pixcull-2.43.4/pixcull/report/templates/pages/admin.html +661 -0
  78. pixcull-2.43.4/pixcull/report/templates/pages/admin_perf.html +475 -0
  79. pixcull-2.43.4/pixcull/report/templates/pages/disagreement.html +1 -0
  80. pixcull-2.43.4/pixcull/report/templates/pages/first_run.html +210 -0
  81. pixcull-2.43.4/pixcull/report/templates/pages/history.html +143 -0
  82. pixcull-2.43.4/pixcull/report/templates/pages/library.html +205 -0
  83. pixcull-2.43.4/pixcull/report/templates/pages/privacy.html +109 -0
  84. pixcull-2.43.4/pixcull/report/templates/pages/tether.html +220 -0
  85. pixcull-2.43.4/pixcull/report/templates/pages/upload.html +1715 -0
  86. pixcull-2.43.4/pixcull/report/templates/pages/vertical_bulk.html +331 -0
  87. pixcull-2.43.4/pixcull/report/templates/pages/verticals.html +1848 -0
  88. pixcull-2.43.4/pixcull/report/templates/report.html.j2 +30 -0
  89. pixcull-2.43.4/pixcull/report/templates/results.html +19224 -0
  90. pixcull-2.43.4/pixcull/report/templates/timeline.html +91 -0
  91. pixcull-2.43.4/pixcull/report/templates/video_review.html +517 -0
  92. pixcull-2.43.4/pixcull/scoring/__init__.py +44 -0
  93. pixcull-2.43.4/pixcull/scoring/aesthetic.py +55 -0
  94. pixcull-2.43.4/pixcull/scoring/attribution.py +265 -0
  95. pixcull-2.43.4/pixcull/scoring/audio_events.py +392 -0
  96. pixcull-2.43.4/pixcull/scoring/audio_tagger.py +353 -0
  97. pixcull-2.43.4/pixcull/scoring/axis_rescorer.py +264 -0
  98. pixcull-2.43.4/pixcull/scoring/bias_audit.py +411 -0
  99. pixcull-2.43.4/pixcull/scoring/burst_peak.py +494 -0
  100. pixcull-2.43.4/pixcull/scoring/caption_gen.py +382 -0
  101. pixcull-2.43.4/pixcull/scoring/color_grade.py +243 -0
  102. pixcull-2.43.4/pixcull/scoring/composition_classifier.py +255 -0
  103. pixcull-2.43.4/pixcull/scoring/counterfactual.py +201 -0
  104. pixcull-2.43.4/pixcull/scoring/data/audio_tagger_thresholds.json +4 -0
  105. pixcull-2.43.4/pixcull/scoring/decision.py +175 -0
  106. pixcull-2.43.4/pixcull/scoring/dup_frames.py +113 -0
  107. pixcull-2.43.4/pixcull/scoring/eval_metrics.py +296 -0
  108. pixcull-2.43.4/pixcull/scoring/fusion.py +175 -0
  109. pixcull-2.43.4/pixcull/scoring/genre_strategies.py +279 -0
  110. pixcull-2.43.4/pixcull/scoring/hard_examples.py +187 -0
  111. pixcull-2.43.4/pixcull/scoring/library_index.py +504 -0
  112. pixcull-2.43.4/pixcull/scoring/meta_judge.py +458 -0
  113. pixcull-2.43.4/pixcull/scoring/near_dup.py +175 -0
  114. pixcull-2.43.4/pixcull/scoring/nl_explain.py +175 -0
  115. pixcull-2.43.4/pixcull/scoring/personal_learn.py +199 -0
  116. pixcull-2.43.4/pixcull/scoring/personalized.py +178 -0
  117. pixcull-2.43.4/pixcull/scoring/photo_advice.py +1576 -0
  118. pixcull-2.43.4/pixcull/scoring/photography_canon.py +355 -0
  119. pixcull-2.43.4/pixcull/scoring/reel.py +666 -0
  120. pixcull-2.43.4/pixcull/scoring/reel_caption.py +465 -0
  121. pixcull-2.43.4/pixcull/scoring/rescorer.py +190 -0
  122. pixcull-2.43.4/pixcull/scoring/rubric.py +271 -0
  123. pixcull-2.43.4/pixcull/scoring/rubric_decompose.py +359 -0
  124. pixcull-2.43.4/pixcull/scoring/scenes.py +156 -0
  125. pixcull-2.43.4/pixcull/scoring/self_tune.py +292 -0
  126. pixcull-2.43.4/pixcull/scoring/semantic_search.py +190 -0
  127. pixcull-2.43.4/pixcull/scoring/shot_boundaries.py +123 -0
  128. pixcull-2.43.4/pixcull/scoring/style_guide.py +355 -0
  129. pixcull-2.43.4/pixcull/scoring/style_modes.py +300 -0
  130. pixcull-2.43.4/pixcull/scoring/templates/__init__.py +0 -0
  131. pixcull-2.43.4/pixcull/scoring/templates/scene_templates.yaml +197 -0
  132. pixcull-2.43.4/pixcull/scoring/temporal.py +669 -0
  133. pixcull-2.43.4/pixcull/scoring/transcribe.py +553 -0
  134. pixcull-2.43.4/pixcull/scoring/video_quality.py +320 -0
  135. pixcull-2.43.4/pixcull/scoring/vlm_judge.py +656 -0
  136. pixcull-2.43.4/pixcull/scoring/vlm_vertical_prompts.py +133 -0
  137. pixcull-2.43.4/pixcull/scoring/wedding_moments.py +261 -0
  138. pixcull-2.43.4/pixcull/shortcuts.py +268 -0
  139. pixcull-2.43.4/pixcull/shortlink.py +196 -0
  140. pixcull-2.43.4/pixcull/style/__init__.py +51 -0
  141. pixcull-2.43.4/pixcull/style/clip_clone.py +228 -0
  142. pixcull-2.43.4/pixcull/style/clone.py +237 -0
  143. pixcull-2.43.4/pixcull/sync/__init__.py +84 -0
  144. pixcull-2.43.4/pixcull/sync/discovery.py +277 -0
  145. pixcull-2.43.4/pixcull/sync/event.py +397 -0
  146. pixcull-2.43.4/pixcull/sync/presence.py +219 -0
  147. pixcull-2.43.4/pixcull/sync/push.py +217 -0
  148. pixcull-2.43.4/pixcull/sync/webrtc.py +217 -0
  149. pixcull-2.43.4/pixcull/sync.py +319 -0
  150. pixcull-2.43.4/pixcull/team/__init__.py +38 -0
  151. pixcull-2.43.4/pixcull/team/roles.py +111 -0
  152. pixcull-2.43.4/pixcull/team/taste.py +162 -0
  153. pixcull-2.43.4/pixcull/telemetry.py +175 -0
  154. pixcull-2.43.4/pixcull/tether.py +369 -0
  155. pixcull-2.43.4/pixcull/tether_stream.py +161 -0
  156. pixcull-2.43.4/pixcull/unsplash.py +394 -0
  157. pixcull-2.43.4/pixcull/users.py +354 -0
  158. pixcull-2.43.4/pixcull/utils/__init__.py +0 -0
  159. pixcull-2.43.4/pixcull/utils/image.py +15 -0
  160. pixcull-2.43.4/pixcull/utils/logging.py +26 -0
  161. pixcull-2.43.4/pixcull/utils/timing.py +21 -0
  162. pixcull-2.43.4/pixcull/verticals.py +549 -0
  163. pixcull-2.43.4/pixcull/workflow.py +326 -0
  164. pixcull-2.43.4/pyproject.toml +217 -0
@@ -0,0 +1,98 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ env/
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # IDE
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # PixCull artifacts
30
+ models/*.onnx
31
+ models/*.pt
32
+ models/*.bin
33
+ models/*.safetensors
34
+ output/
35
+ v0_output/
36
+ *.xmp.bak
37
+ *.sqlite
38
+ *.sqlite3
39
+ thumbs/
40
+
41
+ # v2.5 — the committed hermetic visual-smoke fixture run lives under an
42
+ # output/ dir (serve_demo._reload_run_from_disk requires that layout), so
43
+ # un-ignore just it (the broad output/ rule above would otherwise hide it).
44
+ !tests/fixtures/smoke_run/output/
45
+ !tests/fixtures/smoke_run/output/**
46
+
47
+ # Internal / not for public repo
48
+ MARKET_ANALYSIS_*.md
49
+ .claude/
50
+ *.local
51
+ *.notes.md
52
+
53
+ # Per-developer or runtime config — these should stay private even on a
54
+ # fork. The taxonomy and config defaults live in code; anything written
55
+ # at runtime (DeepSeek key, license token, sync paths) belongs in the
56
+ # user's app-data dir, not the repo.
57
+ .envrc
58
+ .env
59
+ .env.local
60
+ *.token
61
+
62
+ # Lr plugin packaged bundles
63
+ lr_plugin/*.zip
64
+ lr_plugin/*.lrtemplate
65
+
66
+ # Large generated artifacts
67
+ *.h5
68
+ *.npz
69
+ face_library*.npz
70
+ *.bak
71
+
72
+ # P-PRO-4.2 — wedding-moment eval output (contains real photographer
73
+ # filenames; goldenset CSV is a local artifact, not a public dataset)
74
+ out_wedding_eval/
75
+ predictions*.csv
76
+
77
+ # Swift Package Manager build cache (generated by `swift build` /
78
+ # Xcode); large + machine-specific. Don't commit.
79
+ mobile/PixCullCompanion/.build/
80
+ out_wedding_eval/realdemo01_smoke/
81
+ goldenset/v0.11/_eval_output/
82
+ goldenset/v0.11/training.csv
83
+
84
+ # modelscope upload cache (transient)
85
+ docs/screenshots/.ms_upload_cache
86
+
87
+ # v2.14 — local labeling sheets (may contain real photo filenames; keep private)
88
+ label_sheet*.csv
89
+
90
+ # v2.19 — local wheel build output
91
+ dist_wheel/
92
+
93
+ # v2.32 — cross-run library index: records REAL absolute photo paths,
94
+ # private to the machine. Never commit; it lives in ~/.pixcull/library/
95
+ # but guard against a stray local copy too.
96
+ library/
97
+ vectors.npy
98
+ manifest.jsonl
pixcull-2.43.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chris Chen (ChrisChen667788)
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,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: pixcull
3
+ Version: 2.43.4
4
+ Summary: Local-first AI photo culling for professional photographers — 6-axis rubric, XMP/IPTC export, Lightroom & Capture One ready.
5
+ Project-URL: Homepage, https://github.com/ChrisChen667788/pixcull
6
+ Project-URL: Repository, https://github.com/ChrisChen667788/pixcull
7
+ Project-URL: Issues, https://github.com/ChrisChen667788/pixcull/issues
8
+ Project-URL: Documentation, https://github.com/ChrisChen667788/pixcull#readme
9
+ Project-URL: ModelScope, https://www.modelscope.cn/profile/haozi667788
10
+ Author-email: Chris Chen <hello@pixcull.dev>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: ai,capture-one,computer-vision,image-classification,image-quality-assessment,iptc,lightroom,local-first,on-device-ai,photo-culling,photography,raw-photography,rubric-scoring,wedding-photography,xmp
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Environment :: Web Environment
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: End Users/Desktop
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: MacOS :: MacOS X
21
+ Classifier: Operating System :: POSIX :: Linux
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Topic :: Multimedia :: Graphics
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
28
+ Requires-Python: <3.13,>=3.11
29
+ Requires-Dist: imagededup>=0.3.2
30
+ Requires-Dist: insightface>=0.7
31
+ Requires-Dist: jinja2>=3.1
32
+ Requires-Dist: numpy<2,>=1.26
33
+ Requires-Dist: onnxruntime>=1.17
34
+ Requires-Dist: opencv-python>=4.9
35
+ Requires-Dist: pandas>=2.2
36
+ Requires-Dist: pillow>=10.2
37
+ Requires-Dist: pydantic>=2.7
38
+ Requires-Dist: pyiqa>=0.1.11
39
+ Requires-Dist: pyyaml>=6.0
40
+ Requires-Dist: rawpy>=0.19
41
+ Requires-Dist: rembg>=2.0.57
42
+ Requires-Dist: rich>=13.7
43
+ Requires-Dist: safetensors>=0.4
44
+ Requires-Dist: sqlmodel>=0.0.16
45
+ Requires-Dist: structlog>=24.1
46
+ Requires-Dist: torch<3,>=2.2
47
+ Requires-Dist: torchvision<1,>=0.17
48
+ Requires-Dist: tqdm>=4.66
49
+ Requires-Dist: transformers<6,>=4.40
50
+ Requires-Dist: typer>=0.12
51
+ Provides-Extra: asr
52
+ Requires-Dist: funasr>=1.1; extra == 'asr'
53
+ Requires-Dist: torchaudio>=2.0; extra == 'asr'
54
+ Provides-Extra: asr-mlx
55
+ Requires-Dist: mlx-whisper>=0.4; extra == 'asr-mlx'
56
+ Provides-Extra: asr-whisper
57
+ Requires-Dist: faster-whisper>=1.0; extra == 'asr-whisper'
58
+ Provides-Extra: bird
59
+ Requires-Dist: segment-anything>=1.0; extra == 'bird'
60
+ Requires-Dist: ultralytics>=8.2; extra == 'bird'
61
+ Provides-Extra: dev
62
+ Requires-Dist: build>=1.0; extra == 'dev'
63
+ Requires-Dist: hatchling>=1.20; extra == 'dev'
64
+ Requires-Dist: mypy>=1.10; extra == 'dev'
65
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
66
+ Requires-Dist: pytest>=8.0; extra == 'dev'
67
+ Requires-Dist: ruff>=0.4; extra == 'dev'
68
+ Requires-Dist: types-pillow; extra == 'dev'
69
+ Requires-Dist: types-pyyaml; extra == 'dev'
70
+ Provides-Extra: face
71
+ Requires-Dist: mediapipe>=0.10.14; extra == 'face'
72
+ Provides-Extra: shots
73
+ Requires-Dist: scenedetect>=0.6; extra == 'shots'
74
+ Provides-Extra: sync
75
+ Requires-Dist: zeroconf>=0.131; extra == 'sync'
76
+ Description-Content-Type: text/markdown
77
+
78
+ # PixCull
79
+
80
+ **Local-first AI photo culling for working photographers.**
81
+ Six calibrated scoring axes · burst folding · style-aware personalization ·
82
+ Lightroom / Capture One round-trip. **No photo ever leaves your disk.**
83
+
84
+ 本地优先的 AI 摄影分拣:6 轴评分 · 连拍折叠 · 个性化学习 · Lr/C1 双向 round-trip,
85
+ 原图永远不离开你的硬盘。
86
+
87
+ ![PixCull results grid](https://raw.githubusercontent.com/ChrisChen667788/pixcull/main/docs/screenshots/01-results-grid.png)
88
+
89
+ ## Install
90
+
91
+ ```bash
92
+ pip install pixcull
93
+ ```
94
+
95
+ Python 3.11–3.12. First run downloads the optional scoring models to
96
+ `~/.pixcull/models/` (everything runs on-device; Apple-silicon accelerated).
97
+
98
+ ## Quickstart
99
+
100
+ ```bash
101
+ # score a folder of photos (JPG / RAW) — keep/maybe/cull + per-axis rubric
102
+ pixcull run /path/to/photos -o ./out
103
+
104
+ # write the decisions back as XMP sidecars for Lightroom / Capture One
105
+ pixcull export ./out --xmp
106
+
107
+ # score a video → temporal windows + reel candidates
108
+ pixcull video clip.mp4 -o ./out
109
+
110
+ # fold near-duplicates, build a contact sheet, learn your taste, …
111
+ pixcull --help
112
+ ```
113
+
114
+ ```bash
115
+ # open the review workspace in your browser (v2.31 — now packaged)
116
+ pixcull serve
117
+ ```
118
+
119
+ The pip package ships the full scoring engine, the CLI **and** the
120
+ interactive review workspace: keyboard-first grid, ⌘K palette, per-axis
121
+ "why" explanations, n-way compare, maybe-resolution queue, XMP export.
122
+ `pixcull serve` stores runs under `~/.pixcull/runs` by default
123
+ (`--root` to change, `--host 0.0.0.0` to share on a trusted LAN).
124
+
125
+ ![PixCull lightbox](https://raw.githubusercontent.com/ChrisChen667788/pixcull/main/docs/screenshots/03-lightbox.png)
126
+
127
+ ## Highlights
128
+
129
+ - **Glass-box scoring** — every keep/cull carries a per-axis breakdown
130
+ (technical / subject / composition / light / moment / aesthetic) and a
131
+ plain-language "why", not just a number.
132
+ - **Learns your taste** — corrections feed a personal profile that tilts
133
+ the axis weights toward what *you* demonstrably value.
134
+ - **Burst & near-dup folding** — stacks collapse to the peak frame with a
135
+ one-key compare.
136
+ - **Video too** — temporal scoring, audio events (laughter / applause /
137
+ music), reel-candidate detection with the same glass-box treatment.
138
+ - **13 UI languages**, dark/light studio-neutral themes, WCAG-conscious.
139
+ - **Local-first, always** — no uploads, no cloud, no telemetry.
140
+
141
+ ## Links
142
+
143
+ - [GitHub + full README](https://github.com/ChrisChen667788/pixcull)
144
+ - [Releases](https://github.com/ChrisChen667788/pixcull/releases)
145
+ - [ModelScope mirror](https://www.modelscope.cn/models/haozi667788/pixcull)
146
+
147
+ MIT © Chris Chen
@@ -0,0 +1,70 @@
1
+ # PixCull
2
+
3
+ **Local-first AI photo culling for working photographers.**
4
+ Six calibrated scoring axes · burst folding · style-aware personalization ·
5
+ Lightroom / Capture One round-trip. **No photo ever leaves your disk.**
6
+
7
+ 本地优先的 AI 摄影分拣:6 轴评分 · 连拍折叠 · 个性化学习 · Lr/C1 双向 round-trip,
8
+ 原图永远不离开你的硬盘。
9
+
10
+ ![PixCull results grid](https://raw.githubusercontent.com/ChrisChen667788/pixcull/main/docs/screenshots/01-results-grid.png)
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install pixcull
16
+ ```
17
+
18
+ Python 3.11–3.12. First run downloads the optional scoring models to
19
+ `~/.pixcull/models/` (everything runs on-device; Apple-silicon accelerated).
20
+
21
+ ## Quickstart
22
+
23
+ ```bash
24
+ # score a folder of photos (JPG / RAW) — keep/maybe/cull + per-axis rubric
25
+ pixcull run /path/to/photos -o ./out
26
+
27
+ # write the decisions back as XMP sidecars for Lightroom / Capture One
28
+ pixcull export ./out --xmp
29
+
30
+ # score a video → temporal windows + reel candidates
31
+ pixcull video clip.mp4 -o ./out
32
+
33
+ # fold near-duplicates, build a contact sheet, learn your taste, …
34
+ pixcull --help
35
+ ```
36
+
37
+ ```bash
38
+ # open the review workspace in your browser (v2.31 — now packaged)
39
+ pixcull serve
40
+ ```
41
+
42
+ The pip package ships the full scoring engine, the CLI **and** the
43
+ interactive review workspace: keyboard-first grid, ⌘K palette, per-axis
44
+ "why" explanations, n-way compare, maybe-resolution queue, XMP export.
45
+ `pixcull serve` stores runs under `~/.pixcull/runs` by default
46
+ (`--root` to change, `--host 0.0.0.0` to share on a trusted LAN).
47
+
48
+ ![PixCull lightbox](https://raw.githubusercontent.com/ChrisChen667788/pixcull/main/docs/screenshots/03-lightbox.png)
49
+
50
+ ## Highlights
51
+
52
+ - **Glass-box scoring** — every keep/cull carries a per-axis breakdown
53
+ (technical / subject / composition / light / moment / aesthetic) and a
54
+ plain-language "why", not just a number.
55
+ - **Learns your taste** — corrections feed a personal profile that tilts
56
+ the axis weights toward what *you* demonstrably value.
57
+ - **Burst & near-dup folding** — stacks collapse to the peak frame with a
58
+ one-key compare.
59
+ - **Video too** — temporal scoring, audio events (laughter / applause /
60
+ music), reel-candidate detection with the same glass-box treatment.
61
+ - **13 UI languages**, dark/light studio-neutral themes, WCAG-conscious.
62
+ - **Local-first, always** — no uploads, no cloud, no telemetry.
63
+
64
+ ## Links
65
+
66
+ - [GitHub + full README](https://github.com/ChrisChen667788/pixcull)
67
+ - [Releases](https://github.com/ChrisChen667788/pixcull/releases)
68
+ - [ModelScope mirror](https://www.modelscope.cn/models/haozi667788/pixcull)
69
+
70
+ MIT © Chris Chen
@@ -0,0 +1,55 @@
1
+ """PixCull — AI photo culling & scoring."""
2
+
3
+ import sys
4
+
5
+ # v2.19 — single-source the version from package metadata (pyproject);
6
+ # the literal is only the fallback for running from a raw source tree.
7
+ try:
8
+ from importlib.metadata import version as _pkg_version
9
+ __version__ = _pkg_version("pixcull")
10
+ except Exception:
11
+ __version__ = "2.43.4"
12
+
13
+
14
+ def _check_numpy_compatibility() -> None:
15
+ """ROADMAP INFRA-5 — runtime guard against the numpy 2.x regression.
16
+
17
+ We've been bitten twice (V18.1 and V22.0.1) when a transitive
18
+ ``pip install`` upgraded numpy to 2.x. The symptoms are subtle:
19
+ mediapipe imports cleanly but its face detector silently returns
20
+ empty results, and pre-V18.3 rescorer joblibs fail to unpickle
21
+ because the ``numpy.random._pcg64.PCG64`` paths differ between
22
+ 1.x and 2.x. Neither failure is loud — face_count just stays 0
23
+ on all images, the rescorer silently falls back to rule-only.
24
+
25
+ Pin in pyproject.toml is ``numpy>=1.26,<2``, but a third-party
26
+ install command (``pip install some-other-package``) can still
27
+ blow past the pin. We don't fail hard here — that would break
28
+ perfectly fine pipelines that don't touch faces — but we DO
29
+ print a loud warning on every import so the regression is
30
+ visible at the top of the log.
31
+ """
32
+ try:
33
+ import numpy
34
+ except ImportError:
35
+ return # numpy missing is a different problem; let downstream report
36
+ ver = getattr(numpy, "__version__", "")
37
+ try:
38
+ major = int(ver.split(".")[0])
39
+ except (ValueError, IndexError):
40
+ return
41
+ if major >= 2:
42
+ print(
43
+ "\n"
44
+ "⚠ PixCull: numpy " + ver + " detected.\n"
45
+ " mediapipe (face detector) and the V18.3 rescorer joblibs\n"
46
+ " need numpy 1.x. Run:\n"
47
+ " pip install 'numpy<2'\n"
48
+ " to fix. Without this, faces won't be detected (face_count\n"
49
+ " will be 0 on every photo) and the rescorer falls back to\n"
50
+ " rule-only mode.\n",
51
+ file=sys.stderr,
52
+ )
53
+
54
+
55
+ _check_numpy_compatibility()
@@ -0,0 +1,4 @@
1
+ from pixcull.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,269 @@
1
+ """v0.10-P1-4 — audio-photo sync for the wedding tether flow.
2
+
3
+ Experimental. Wedding-day shooters often want photos that
4
+ correspond to specific ceremony moments (vows / ring exchange /
5
+ first kiss / cake cutting) auto-flagged with the right
6
+ ``wedding_moment`` so the post-shoot delivery groups them
7
+ cleanly. The hard part is *detecting* the moment in real time —
8
+ we offload that to a client (iOS PixCullCompanion with
9
+ WhisperKit, or an external CLI hooked to the venue's PA system)
10
+ which sends us transcripts.
11
+
12
+ Wire model
13
+ ==========
14
+ Client posts transcript chunks via:
15
+
16
+ POST /api/v1/runs/<run_id>/audio_sync
17
+ body: {
18
+ "transcripts": [
19
+ {"ts_ms": 1700000000000, "text": "I now pronounce you...",
20
+ "confidence": 0.92},
21
+ ...
22
+ ]
23
+ }
24
+
25
+ We:
26
+
27
+ 1. Run keyword matching against a built-in vocabulary
28
+ (CEREMONY_KEYWORDS below). Returns the matched moments
29
+ each transcript hit.
30
+ 2. Correlate transcript ts_ms ± window_s with photo capture
31
+ times (read from scores.csv's mtime column).
32
+ 3. Boost the wedding_moment field on matched rows.
33
+
34
+ The keyword vocabulary is intentionally conservative — we want
35
+ high precision (false moment tags would mislead the delivery)
36
+ at the cost of recall. A photographer who wants more moments
37
+ matched can extend CEREMONY_KEYWORDS in their own deployment.
38
+
39
+ Opt-in only. PIXCULL_AUDIO_SYNC=1 env var required to enable
40
+ the route; otherwise the POST returns 403. Privacy guarantee:
41
+ audio bytes are NEVER sent to the server — only the transcript
42
+ text + timestamps. WhisperKit on iOS does all the STT locally.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import os
48
+ import re
49
+ from typing import Iterable
50
+
51
+
52
+ # Keyword → wedding_moment vocabulary. Lowercased ASCII keys
53
+ # only — clients normalise transcripts to ASCII before sending.
54
+ # Format: phrase → moment_id. ``moment_id`` matches the
55
+ # canonical wedding_moment vocabulary used by
56
+ # pixcull.scoring.wedding_moment.WEDDING_MOMENTS.
57
+ #
58
+ # Conservative — only phrases that are extremely unlikely to
59
+ # appear outside the corresponding moment. "ring" alone is
60
+ # excluded (it shows up in too many other contexts). "the
61
+ # rings" / "exchange rings" are included because those phrasings
62
+ # are strongly ceremony-bound.
63
+ CEREMONY_KEYWORDS: dict[str, str] = {
64
+ # Vows
65
+ "i, take you to be my": "vows",
66
+ "to have and to hold": "vows",
67
+ "from this day forward": "vows",
68
+ "for better or for worse": "vows",
69
+ "till death do us part": "vows",
70
+ "vows": "vows",
71
+ "我愿意": "vows", # Chinese ceremony
72
+
73
+ # Ring exchange
74
+ "exchange rings": "ring_exchange",
75
+ "with this ring": "ring_exchange",
76
+ "place this ring": "ring_exchange",
77
+ "the rings": "ring_exchange",
78
+ "交换戒指": "ring_exchange",
79
+
80
+ # First kiss
81
+ "you may now kiss": "kiss",
82
+ "you may kiss the bride": "kiss",
83
+ "kiss the bride": "kiss",
84
+ "kiss the partner": "kiss",
85
+ "first kiss as": "kiss",
86
+
87
+ # Pronouncement
88
+ "i now pronounce you": "pronouncement",
89
+ "by the power vested in me": "pronouncement",
90
+ "husband and wife": "pronouncement",
91
+ "wife and wife": "pronouncement",
92
+ "husband and husband": "pronouncement",
93
+
94
+ # First dance
95
+ "first dance": "first_dance",
96
+ "their first dance": "first_dance",
97
+ "请新郎新娘跳第一支舞": "first_dance",
98
+
99
+ # Cake cutting
100
+ "cake cutting": "cake_cutting",
101
+ "cut the cake": "cake_cutting",
102
+ "cutting the cake": "cake_cutting",
103
+ "切蛋糕": "cake_cutting",
104
+
105
+ # Toasts
106
+ "to the bride and groom": "toast",
107
+ "raise a glass": "toast",
108
+ "raise our glasses": "toast",
109
+ "举杯": "toast",
110
+
111
+ # Chinese-tradition moments
112
+ "敬茶": "tea_ceremony",
113
+ "tea ceremony": "tea_ceremony",
114
+ "三鞠躬": "bow",
115
+ "跪拜": "bow",
116
+ }
117
+
118
+
119
+ # Default time window: when a transcript matches at ts, photos
120
+ # whose mtime falls within ±window_s get the wedding_moment
121
+ # boost. 90 s is a Goldilocks default — large enough to cover
122
+ # "celebrant says 'I now pronounce you...' then 60s of kiss +
123
+ # applause" but small enough that consecutive ceremony moments
124
+ # (vows then ring then pronouncement) don't all get the same
125
+ # tag. Override via the POST body's `window_s` field.
126
+ DEFAULT_WINDOW_S = 90.0
127
+
128
+
129
+ def is_enabled() -> bool:
130
+ """v0.10-P1-4 is opt-in to keep privacy posture conservative.
131
+
132
+ Photographers who want it set PIXCULL_AUDIO_SYNC=1 in their
133
+ shell env; the server route returns 403 otherwise so a
134
+ runaway client doesn't accidentally boost wedding moments
135
+ on a run that should have stayed scene-only.
136
+ """
137
+ return os.environ.get("PIXCULL_AUDIO_SYNC", "").strip() in ("1", "true", "yes")
138
+
139
+
140
+ def match_transcript(text: str) -> str | None:
141
+ """Return the wedding_moment matched by this transcript, or None.
142
+
143
+ Lowercases the input before matching so the vocab keys can
144
+ stay lowercase. Uses substring match (not regex) for both
145
+ speed and to keep the vocab obvious to a human auditor.
146
+ Matches the FIRST keyword found scanning the vocab in
147
+ insertion order — most-specific phrases first by convention.
148
+ """
149
+ if not text:
150
+ return None
151
+ t = text.lower()
152
+ for kw, moment in CEREMONY_KEYWORDS.items():
153
+ if kw in t:
154
+ return moment
155
+ return None
156
+
157
+
158
+ def match_transcripts(
159
+ transcripts: Iterable[dict],
160
+ *,
161
+ min_confidence: float = 0.6,
162
+ ) -> list[dict]:
163
+ """Apply match_transcript to a batch, with confidence gate.
164
+
165
+ Returns the SUBSET of transcripts that matched, each enriched
166
+ with the inferred ``moment``. Drops anything below
167
+ min_confidence (defensive against noisy STT environments;
168
+ weddings are loud). Order is preserved from the input.
169
+ """
170
+ out = []
171
+ for t in transcripts:
172
+ if not isinstance(t, dict):
173
+ continue
174
+ try:
175
+ ts_ms = int(t.get("ts_ms") or 0)
176
+ except (TypeError, ValueError):
177
+ continue
178
+ try:
179
+ conf = float(t.get("confidence") or 0)
180
+ except (TypeError, ValueError):
181
+ conf = 0.0
182
+ text = str(t.get("text") or "")
183
+ if conf < min_confidence:
184
+ continue
185
+ moment = match_transcript(text)
186
+ if moment is None:
187
+ continue
188
+ out.append({
189
+ "ts_ms": ts_ms,
190
+ "text": text,
191
+ "moment": moment,
192
+ "confidence": conf,
193
+ })
194
+ return out
195
+
196
+
197
+ def correlate_with_rows(
198
+ matches: list[dict],
199
+ rows: list[dict],
200
+ *,
201
+ window_s: float = DEFAULT_WINDOW_S,
202
+ ) -> dict[str, str]:
203
+ """Assign wedding_moment to photo rows whose mtime falls
204
+ within ±window_s of any transcript match.
205
+
206
+ Returns a {filename: moment} dict — caller persists. Rows
207
+ that fall inside multiple moments' windows take the LATEST
208
+ match (the transcript chronologically closest "before" the
209
+ photo wins, because the photographer typically captures
210
+ AFTER the celebrant says the words).
211
+
212
+ Defensive against rows missing mtime: those are skipped
213
+ silently.
214
+ """
215
+ if not matches or not rows:
216
+ return {}
217
+ window_ms = window_s * 1000.0
218
+ sorted_matches = sorted(matches, key=lambda m: m["ts_ms"])
219
+ out: dict[str, str] = {}
220
+ for row in rows:
221
+ if not isinstance(row, dict):
222
+ continue
223
+ fn = row.get("filename")
224
+ if not isinstance(fn, str) or not fn:
225
+ continue
226
+ try:
227
+ # rows store seconds-since-epoch; transcripts ms.
228
+ mtime_ms = float(row.get("mtime") or 0) * 1000.0
229
+ except (TypeError, ValueError):
230
+ continue
231
+ if mtime_ms <= 0:
232
+ continue
233
+ # Walk matches from latest-before to earliest-before so
234
+ # the first valid window wins.
235
+ best: str | None = None
236
+ for m in reversed(sorted_matches):
237
+ if m["ts_ms"] - window_ms <= mtime_ms <= m["ts_ms"] + window_ms:
238
+ best = m["moment"]
239
+ break
240
+ if best is not None:
241
+ out[fn] = best
242
+ return out
243
+
244
+
245
+ def apply_audio_sync(
246
+ transcripts: Iterable[dict],
247
+ rows: list[dict],
248
+ *,
249
+ window_s: float = DEFAULT_WINDOW_S,
250
+ min_confidence: float = 0.6,
251
+ ) -> dict:
252
+ """Top-level — what the HTTP handler calls.
253
+
254
+ Returns the summary the client renders + the suggested
255
+ wedding_moment overrides. The actual write-back to
256
+ scores.csv / annotations.jsonl is the caller's job (the
257
+ server-side handler decides whether to persist or just
258
+ preview).
259
+ """
260
+ matches = match_transcripts(transcripts, min_confidence=min_confidence)
261
+ suggestions = correlate_with_rows(matches, rows, window_s=window_s)
262
+ return {
263
+ "n_transcripts": len(list(transcripts) if not isinstance(transcripts, list) else transcripts),
264
+ "n_matched": len(matches),
265
+ "matches": matches,
266
+ "n_suggestions": len(suggestions),
267
+ "suggestions": suggestions,
268
+ "window_s": window_s,
269
+ }