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.
- pixcull-2.43.4/.gitignore +98 -0
- pixcull-2.43.4/LICENSE +21 -0
- pixcull-2.43.4/PKG-INFO +147 -0
- pixcull-2.43.4/README-PYPI.md +70 -0
- pixcull-2.43.4/pixcull/__init__.py +55 -0
- pixcull-2.43.4/pixcull/__main__.py +4 -0
- pixcull-2.43.4/pixcull/audio_sync.py +269 -0
- pixcull-2.43.4/pixcull/cli.py +1377 -0
- pixcull-2.43.4/pixcull/config.py +85 -0
- pixcull-2.43.4/pixcull/db/__init__.py +0 -0
- pixcull-2.43.4/pixcull/db/models.py +33 -0
- pixcull-2.43.4/pixcull/detectors/__init__.py +3 -0
- pixcull-2.43.4/pixcull/detectors/base.py +30 -0
- pixcull-2.43.4/pixcull/detectors/blur.py +60 -0
- pixcull-2.43.4/pixcull/detectors/canon.py +407 -0
- pixcull-2.43.4/pixcull/detectors/composition.py +224 -0
- pixcull-2.43.4/pixcull/detectors/duplicate.py +198 -0
- pixcull-2.43.4/pixcull/detectors/exposure.py +32 -0
- pixcull-2.43.4/pixcull/detectors/face.py +283 -0
- pixcull-2.43.4/pixcull/detectors/scene.py +200 -0
- pixcull-2.43.4/pixcull/detectors/subject.py +31 -0
- pixcull-2.43.4/pixcull/detectors/wedding_moment.py +71 -0
- pixcull-2.43.4/pixcull/error_reporting.py +264 -0
- pixcull-2.43.4/pixcull/events.py +160 -0
- pixcull-2.43.4/pixcull/i18n.py +175 -0
- pixcull-2.43.4/pixcull/io/__init__.py +5 -0
- pixcull-2.43.4/pixcull/io/exif.py +161 -0
- pixcull-2.43.4/pixcull/io/exif_audit.py +170 -0
- pixcull-2.43.4/pixcull/io/formats.py +8 -0
- pixcull-2.43.4/pixcull/io/gpmf.py +513 -0
- pixcull-2.43.4/pixcull/io/gps_map.py +127 -0
- pixcull-2.43.4/pixcull/io/icc.py +205 -0
- pixcull-2.43.4/pixcull/io/iptc_embed.py +166 -0
- pixcull-2.43.4/pixcull/io/loader.py +272 -0
- pixcull-2.43.4/pixcull/io/raw_proxy.py +132 -0
- pixcull-2.43.4/pixcull/io/reel_assembly.py +577 -0
- pixcull-2.43.4/pixcull/io/video.py +495 -0
- pixcull-2.43.4/pixcull/io/xmp.py +457 -0
- pixcull-2.43.4/pixcull/license/__init__.py +521 -0
- pixcull-2.43.4/pixcull/llm_budget.py +213 -0
- pixcull-2.43.4/pixcull/locale/ar_SA.json +194 -0
- pixcull-2.43.4/pixcull/locale/de_DE.json +194 -0
- pixcull-2.43.4/pixcull/locale/en_US.json +194 -0
- pixcull-2.43.4/pixcull/locale/es_ES.json +194 -0
- pixcull-2.43.4/pixcull/locale/fr_FR.json +194 -0
- pixcull-2.43.4/pixcull/locale/it_IT.json +194 -0
- pixcull-2.43.4/pixcull/locale/ja_JP.json +194 -0
- pixcull-2.43.4/pixcull/locale/ko_KR.json +194 -0
- pixcull-2.43.4/pixcull/locale/nl_NL.json +194 -0
- pixcull-2.43.4/pixcull/locale/pt_BR.json +194 -0
- pixcull-2.43.4/pixcull/locale/ru_RU.json +194 -0
- pixcull-2.43.4/pixcull/locale/tr_TR.json +194 -0
- pixcull-2.43.4/pixcull/locale/zh_CN.json +194 -0
- pixcull-2.43.4/pixcull/models_manager.py +262 -0
- pixcull-2.43.4/pixcull/phrase_generator.py +483 -0
- pixcull-2.43.4/pixcull/pipeline/__init__.py +4 -0
- pixcull-2.43.4/pixcull/pipeline/burst_peak.py +267 -0
- pixcull-2.43.4/pixcull/pipeline/face_audit.py +199 -0
- pixcull-2.43.4/pixcull/pipeline/face_clustering.py +528 -0
- pixcull-2.43.4/pixcull/pipeline/face_library.py +261 -0
- pixcull-2.43.4/pixcull/pipeline/location_clustering.py +189 -0
- pixcull-2.43.4/pixcull/pipeline/orchestrator.py +614 -0
- pixcull-2.43.4/pixcull/pipeline/parallel.py +261 -0
- pixcull-2.43.4/pixcull/pipeline/worker.py +231 -0
- pixcull-2.43.4/pixcull/plugins/__init__.py +422 -0
- pixcull-2.43.4/pixcull/plugins/builtin/example_wildlife.py +39 -0
- pixcull-2.43.4/pixcull/policy_tuner.py +432 -0
- pixcull-2.43.4/pixcull/preferred_axes.py +146 -0
- pixcull-2.43.4/pixcull/qrcode_svg.py +488 -0
- pixcull-2.43.4/pixcull/report/__init__.py +13 -0
- pixcull-2.43.4/pixcull/report/contact_sheet.py +278 -0
- pixcull-2.43.4/pixcull/report/csv.py +15 -0
- pixcull-2.43.4/pixcull/report/executive_pdf.py +946 -0
- pixcull-2.43.4/pixcull/report/gallery.py +303 -0
- pixcull-2.43.4/pixcull/report/serve_app.py +13014 -0
- pixcull-2.43.4/pixcull/report/serve_util.py +139 -0
- pixcull-2.43.4/pixcull/report/templates/pages/admin.html +661 -0
- pixcull-2.43.4/pixcull/report/templates/pages/admin_perf.html +475 -0
- pixcull-2.43.4/pixcull/report/templates/pages/disagreement.html +1 -0
- pixcull-2.43.4/pixcull/report/templates/pages/first_run.html +210 -0
- pixcull-2.43.4/pixcull/report/templates/pages/history.html +143 -0
- pixcull-2.43.4/pixcull/report/templates/pages/library.html +205 -0
- pixcull-2.43.4/pixcull/report/templates/pages/privacy.html +109 -0
- pixcull-2.43.4/pixcull/report/templates/pages/tether.html +220 -0
- pixcull-2.43.4/pixcull/report/templates/pages/upload.html +1715 -0
- pixcull-2.43.4/pixcull/report/templates/pages/vertical_bulk.html +331 -0
- pixcull-2.43.4/pixcull/report/templates/pages/verticals.html +1848 -0
- pixcull-2.43.4/pixcull/report/templates/report.html.j2 +30 -0
- pixcull-2.43.4/pixcull/report/templates/results.html +19224 -0
- pixcull-2.43.4/pixcull/report/templates/timeline.html +91 -0
- pixcull-2.43.4/pixcull/report/templates/video_review.html +517 -0
- pixcull-2.43.4/pixcull/scoring/__init__.py +44 -0
- pixcull-2.43.4/pixcull/scoring/aesthetic.py +55 -0
- pixcull-2.43.4/pixcull/scoring/attribution.py +265 -0
- pixcull-2.43.4/pixcull/scoring/audio_events.py +392 -0
- pixcull-2.43.4/pixcull/scoring/audio_tagger.py +353 -0
- pixcull-2.43.4/pixcull/scoring/axis_rescorer.py +264 -0
- pixcull-2.43.4/pixcull/scoring/bias_audit.py +411 -0
- pixcull-2.43.4/pixcull/scoring/burst_peak.py +494 -0
- pixcull-2.43.4/pixcull/scoring/caption_gen.py +382 -0
- pixcull-2.43.4/pixcull/scoring/color_grade.py +243 -0
- pixcull-2.43.4/pixcull/scoring/composition_classifier.py +255 -0
- pixcull-2.43.4/pixcull/scoring/counterfactual.py +201 -0
- pixcull-2.43.4/pixcull/scoring/data/audio_tagger_thresholds.json +4 -0
- pixcull-2.43.4/pixcull/scoring/decision.py +175 -0
- pixcull-2.43.4/pixcull/scoring/dup_frames.py +113 -0
- pixcull-2.43.4/pixcull/scoring/eval_metrics.py +296 -0
- pixcull-2.43.4/pixcull/scoring/fusion.py +175 -0
- pixcull-2.43.4/pixcull/scoring/genre_strategies.py +279 -0
- pixcull-2.43.4/pixcull/scoring/hard_examples.py +187 -0
- pixcull-2.43.4/pixcull/scoring/library_index.py +504 -0
- pixcull-2.43.4/pixcull/scoring/meta_judge.py +458 -0
- pixcull-2.43.4/pixcull/scoring/near_dup.py +175 -0
- pixcull-2.43.4/pixcull/scoring/nl_explain.py +175 -0
- pixcull-2.43.4/pixcull/scoring/personal_learn.py +199 -0
- pixcull-2.43.4/pixcull/scoring/personalized.py +178 -0
- pixcull-2.43.4/pixcull/scoring/photo_advice.py +1576 -0
- pixcull-2.43.4/pixcull/scoring/photography_canon.py +355 -0
- pixcull-2.43.4/pixcull/scoring/reel.py +666 -0
- pixcull-2.43.4/pixcull/scoring/reel_caption.py +465 -0
- pixcull-2.43.4/pixcull/scoring/rescorer.py +190 -0
- pixcull-2.43.4/pixcull/scoring/rubric.py +271 -0
- pixcull-2.43.4/pixcull/scoring/rubric_decompose.py +359 -0
- pixcull-2.43.4/pixcull/scoring/scenes.py +156 -0
- pixcull-2.43.4/pixcull/scoring/self_tune.py +292 -0
- pixcull-2.43.4/pixcull/scoring/semantic_search.py +190 -0
- pixcull-2.43.4/pixcull/scoring/shot_boundaries.py +123 -0
- pixcull-2.43.4/pixcull/scoring/style_guide.py +355 -0
- pixcull-2.43.4/pixcull/scoring/style_modes.py +300 -0
- pixcull-2.43.4/pixcull/scoring/templates/__init__.py +0 -0
- pixcull-2.43.4/pixcull/scoring/templates/scene_templates.yaml +197 -0
- pixcull-2.43.4/pixcull/scoring/temporal.py +669 -0
- pixcull-2.43.4/pixcull/scoring/transcribe.py +553 -0
- pixcull-2.43.4/pixcull/scoring/video_quality.py +320 -0
- pixcull-2.43.4/pixcull/scoring/vlm_judge.py +656 -0
- pixcull-2.43.4/pixcull/scoring/vlm_vertical_prompts.py +133 -0
- pixcull-2.43.4/pixcull/scoring/wedding_moments.py +261 -0
- pixcull-2.43.4/pixcull/shortcuts.py +268 -0
- pixcull-2.43.4/pixcull/shortlink.py +196 -0
- pixcull-2.43.4/pixcull/style/__init__.py +51 -0
- pixcull-2.43.4/pixcull/style/clip_clone.py +228 -0
- pixcull-2.43.4/pixcull/style/clone.py +237 -0
- pixcull-2.43.4/pixcull/sync/__init__.py +84 -0
- pixcull-2.43.4/pixcull/sync/discovery.py +277 -0
- pixcull-2.43.4/pixcull/sync/event.py +397 -0
- pixcull-2.43.4/pixcull/sync/presence.py +219 -0
- pixcull-2.43.4/pixcull/sync/push.py +217 -0
- pixcull-2.43.4/pixcull/sync/webrtc.py +217 -0
- pixcull-2.43.4/pixcull/sync.py +319 -0
- pixcull-2.43.4/pixcull/team/__init__.py +38 -0
- pixcull-2.43.4/pixcull/team/roles.py +111 -0
- pixcull-2.43.4/pixcull/team/taste.py +162 -0
- pixcull-2.43.4/pixcull/telemetry.py +175 -0
- pixcull-2.43.4/pixcull/tether.py +369 -0
- pixcull-2.43.4/pixcull/tether_stream.py +161 -0
- pixcull-2.43.4/pixcull/unsplash.py +394 -0
- pixcull-2.43.4/pixcull/users.py +354 -0
- pixcull-2.43.4/pixcull/utils/__init__.py +0 -0
- pixcull-2.43.4/pixcull/utils/image.py +15 -0
- pixcull-2.43.4/pixcull/utils/logging.py +26 -0
- pixcull-2.43.4/pixcull/utils/timing.py +21 -0
- pixcull-2.43.4/pixcull/verticals.py +549 -0
- pixcull-2.43.4/pixcull/workflow.py +326 -0
- 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.
|
pixcull-2.43.4/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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,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
|
+
}
|