bppicker 0.1.0__py3-none-any.whl
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.
- bpp/__init__.py +26 -0
- bpp/ai/__init__.py +1 -0
- bpp/ai/inpainting.py +244 -0
- bpp/cli.py +303 -0
- bpp/commands/__init__.py +42 -0
- bpp/commands/analyze.py +181 -0
- bpp/commands/db_restore.py +216 -0
- bpp/commands/db_restore_impl.py +318 -0
- bpp/commands/demo.py +69 -0
- bpp/commands/model.py +169 -0
- bpp/commands/model_commands.py +442 -0
- bpp/commands/pick.py +167 -0
- bpp/commands/serve.py +329 -0
- bpp/config.py +123 -0
- bpp/config_resolver.py +233 -0
- bpp/config_schema.py +482 -0
- bpp/constants.py +268 -0
- bpp/db/__init__.py +1 -0
- bpp/db/albums.py +447 -0
- bpp/db/backup.py +430 -0
- bpp/db/batch_rename.py +356 -0
- bpp/db/calendar.py +158 -0
- bpp/db/clip.py +299 -0
- bpp/db/connection.py +297 -0
- bpp/db/dedupe.py +241 -0
- bpp/db/dialect.py +162 -0
- bpp/db/edits.py +179 -0
- bpp/db/event_hooks.py +206 -0
- bpp/db/face_cluster_ops.py +181 -0
- bpp/db/face_embedding_safety.py +243 -0
- bpp/db/face_feedback.py +362 -0
- bpp/db/face_identity_remap.py +443 -0
- bpp/db/face_queries.py +257 -0
- bpp/db/groups.py +232 -0
- bpp/db/integrity.py +185 -0
- bpp/db/journal.py +231 -0
- bpp/db/library.py +433 -0
- bpp/db/live_photo.py +476 -0
- bpp/db/memories.py +320 -0
- bpp/db/migrate.py +119 -0
- bpp/db/migrations.py +264 -0
- bpp/db/migrations_latest.py +438 -0
- bpp/db/migrations_recent.py +430 -0
- bpp/db/moments.py +154 -0
- bpp/db/pets.py +371 -0
- bpp/db/photo_hooks.py +120 -0
- bpp/db/photos.py +505 -0
- bpp/db/photos_dates.py +89 -0
- bpp/db/photos_gps.py +139 -0
- bpp/db/photos_lifecycle.py +214 -0
- bpp/db/photos_missing.py +122 -0
- bpp/db/presets.py +58 -0
- bpp/db/registry.py +142 -0
- bpp/db/schema.py +530 -0
- bpp/db/schema_migrate.py +215 -0
- bpp/db/settings.py +66 -0
- bpp/db/smart_album_document.py +87 -0
- bpp/db/smart_album_domains.py +116 -0
- bpp/db/smart_album_ensure.py +87 -0
- bpp/db/smart_album_groups.py +117 -0
- bpp/db/smart_album_people.py +341 -0
- bpp/db/smart_album_pets.py +111 -0
- bpp/db/smart_album_queries.py +299 -0
- bpp/db/smart_album_refreshers.py +444 -0
- bpp/db/smart_album_sensitive.py +64 -0
- bpp/db/smart_album_tags.py +78 -0
- bpp/db/smart_albums.py +441 -0
- bpp/db/stats.py +57 -0
- bpp/db/tags.py +172 -0
- bpp/dedupe/__init__.py +0 -0
- bpp/dedupe/cluster.py +173 -0
- bpp/dedupe/common.py +20 -0
- bpp/dedupe/phash.py +147 -0
- bpp/dedupe/semantic.py +328 -0
- bpp/dedupe/strategy.py +277 -0
- bpp/demo/__init__.py +0 -0
- bpp/demo/generate.py +340 -0
- bpp/errors.py +234 -0
- bpp/exif_utils.py +168 -0
- bpp/io_scan.py +102 -0
- bpp/media_types.py +78 -0
- bpp/output/__init__.py +0 -0
- bpp/output/export.py +376 -0
- bpp/output/export_metadata.py +207 -0
- bpp/output/export_modes.py +296 -0
- bpp/output/gallery.py +148 -0
- bpp/plugin_protocol.py +240 -0
- bpp/plugins/__init__.py +345 -0
- bpp/plugins/example.py +328 -0
- bpp/plugins/registry_protocol.py +92 -0
- bpp/registry/__init__.py +345 -0
- bpp/registry/acceptance.py +360 -0
- bpp/registry/acceptance_log.py +395 -0
- bpp/registry/builtins.py +479 -0
- bpp/registry/byom.py +356 -0
- bpp/registry/derived_data_purge.py +118 -0
- bpp/registry/disclaimers.py +290 -0
- bpp/registry/download_chokepoint.py +430 -0
- bpp/registry/labels.py +146 -0
- bpp/registry/model_registry.py +415 -0
- bpp/registry/overlay.py +242 -0
- bpp/registry/policy.py +320 -0
- bpp/registry/remote_registry.py +286 -0
- bpp/registry/removal.py +142 -0
- bpp/registry/signed_manifest.py +334 -0
- bpp/registry/trusted_keys.py +122 -0
- bpp/registry/use_context.py +188 -0
- bpp/registry/use_context_store.py +236 -0
- bpp/scoring/__init__.py +0 -0
- bpp/scoring/_registry_base.py +50 -0
- bpp/scoring/aggregate.py +431 -0
- bpp/scoring/aggregate_video.py +136 -0
- bpp/scoring/blur.py +89 -0
- bpp/scoring/clip_embed.py +333 -0
- bpp/scoring/clip_tokenizer.py +241 -0
- bpp/scoring/composition.py +67 -0
- bpp/scoring/enhance.py +92 -0
- bpp/scoring/exposure.py +44 -0
- bpp/scoring/face.py +277 -0
- bpp/scoring/face_blazeface_fr.py +181 -0
- bpp/scoring/face_cluster.py +115 -0
- bpp/scoring/face_detector_registry.py +138 -0
- bpp/scoring/face_embed.py +426 -0
- bpp/scoring/face_embed_buffalo.py +132 -0
- bpp/scoring/face_embed_buffalo_s.py +500 -0
- bpp/scoring/face_embed_detect.py +232 -0
- bpp/scoring/face_embed_extractors.py +284 -0
- bpp/scoring/face_embed_landmarks.py +218 -0
- bpp/scoring/face_embed_sface.py +145 -0
- bpp/scoring/face_embed_sface_runtime.py +359 -0
- bpp/scoring/face_embedder_registry.py +126 -0
- bpp/scoring/face_expression.py +186 -0
- bpp/scoring/face_fallback.py +108 -0
- bpp/scoring/face_hand_filter.py +202 -0
- bpp/scoring/face_mediapipe.py +120 -0
- bpp/scoring/face_pipeline.py +418 -0
- bpp/scoring/face_score.py +122 -0
- bpp/scoring/face_scrfd.py +273 -0
- bpp/scoring/face_yunet.py +255 -0
- bpp/scoring/model_base.py +436 -0
- bpp/scoring/model_load_gate.py +57 -0
- bpp/scoring/model_manifest.py +255 -0
- bpp/scoring/models/blaze_face_short_range.tflite +0 -0
- bpp/scoring/nudity.py +316 -0
- bpp/scoring/onnx_providers.py +133 -0
- bpp/scoring/pets.py +460 -0
- bpp/scoring/pose.py +234 -0
- bpp/scoring/registry.py +336 -0
- bpp/scoring/segmentation.py +181 -0
- bpp/scoring/skin.py +18 -0
- bpp/selection/__init__.py +0 -0
- bpp/selection/choose.py +130 -0
- bpp/selection/diversity.py +129 -0
- bpp/utils/__init__.py +0 -0
- bpp/utils/cancel.py +279 -0
- bpp/utils/concurrency.py +58 -0
- bpp/utils/config_snapshot.py +83 -0
- bpp/utils/download.py +200 -0
- bpp/utils/json_utils.py +32 -0
- bpp/utils/logging.py +408 -0
- bpp/utils/path_validation.py +70 -0
- bpp/utils/paths.py +121 -0
- bpp/utils/raw.py +74 -0
- bpp/utils/retry.py +102 -0
- bpp/utils/safe_subprocess.py +1 -0
- bpp/utils/serving_lock.py +302 -0
- bpp/utils/subprocess_runner.py +349 -0
- bpp/utils/timing.py +34 -0
- bpp/utils/video.py +237 -0
- bpp/web/__init__.py +1 -0
- bpp/web/_deprecated_attr.py +92 -0
- bpp/web/analysis_store.py +133 -0
- bpp/web/analyze_archive.py +169 -0
- bpp/web/analyze_face_extract.py +443 -0
- bpp/web/analyze_finalize.py +77 -0
- bpp/web/analyze_model_preflight.py +86 -0
- bpp/web/analyze_phases.py +160 -0
- bpp/web/analyze_scoring.py +202 -0
- bpp/web/analyze_subprocess.py +73 -0
- bpp/web/analyze_worker.py +470 -0
- bpp/web/app.py +384 -0
- bpp/web/base_worker.py +224 -0
- bpp/web/bp_album_overrides.py +120 -0
- bpp/web/bp_albums.py +478 -0
- bpp/web/bp_analysis.py +451 -0
- bpp/web/bp_calendar.py +147 -0
- bpp/web/bp_catalog.py +201 -0
- bpp/web/bp_clip.py +103 -0
- bpp/web/bp_core.py +477 -0
- bpp/web/bp_export.py +361 -0
- bpp/web/bp_faces.py +455 -0
- bpp/web/bp_faces_bbox.py +351 -0
- bpp/web/bp_faces_cluster_ops.py +409 -0
- bpp/web/bp_faces_extract.py +172 -0
- bpp/web/bp_faces_manage.py +264 -0
- bpp/web/bp_faces_photo.py +241 -0
- bpp/web/bp_faces_recluster.py +116 -0
- bpp/web/bp_faces_review.py +475 -0
- bpp/web/bp_groups.py +108 -0
- bpp/web/bp_health.py +312 -0
- bpp/web/bp_inpaint.py +228 -0
- bpp/web/bp_install.py +244 -0
- bpp/web/bp_library.py +215 -0
- bpp/web/bp_logs.py +210 -0
- bpp/web/bp_media.py +344 -0
- bpp/web/bp_memories.py +78 -0
- bpp/web/bp_model_admin.py +202 -0
- bpp/web/bp_model_registry.py +405 -0
- bpp/web/bp_models.py +339 -0
- bpp/web/bp_os_integration.py +186 -0
- bpp/web/bp_pets.py +331 -0
- bpp/web/bp_photos.py +355 -0
- bpp/web/bp_photos_lifecycle.py +318 -0
- bpp/web/bp_photos_manage.py +443 -0
- bpp/web/bp_recompute.py +373 -0
- bpp/web/bp_search.py +339 -0
- bpp/web/bp_settings.py +273 -0
- bpp/web/bp_share.py +307 -0
- bpp/web/bp_tags.py +222 -0
- bpp/web/clip_worker.py +227 -0
- bpp/web/derived_recovery.py +255 -0
- bpp/web/export_worker.py +149 -0
- bpp/web/face_create_helpers.py +260 -0
- bpp/web/face_crop.py +81 -0
- bpp/web/face_extraction_journal.py +418 -0
- bpp/web/face_extraction_phase5.py +346 -0
- bpp/web/face_extraction_phase6.py +140 -0
- bpp/web/face_extraction_phases.py +436 -0
- bpp/web/face_merge_core.py +163 -0
- bpp/web/face_orchestrator.py +141 -0
- bpp/web/face_phase_classes.py +299 -0
- bpp/web/face_phase_pipeline.py +265 -0
- bpp/web/face_phase_types.py +130 -0
- bpp/web/face_recovery.py +253 -0
- bpp/web/face_worker.py +287 -0
- bpp/web/filters.py +57 -0
- bpp/web/health.py +131 -0
- bpp/web/import_worker.py +230 -0
- bpp/web/library_lifecycle.py +82 -0
- bpp/web/model_cache.py +144 -0
- bpp/web/model_filter.py +150 -0
- bpp/web/models_status.py +309 -0
- bpp/web/photo_dict.py +237 -0
- bpp/web/photo_edits.py +380 -0
- bpp/web/recompute.py +322 -0
- bpp/web/request_validation.py +192 -0
- bpp/web/review_meta.py +56 -0
- bpp/web/share.py +493 -0
- bpp/web/share_devices.py +250 -0
- bpp/web/share_proxy.py +199 -0
- bpp/web/share_qr.py +146 -0
- bpp/web/share_runtime.py +221 -0
- bpp/web/sse.py +60 -0
- bpp/web/state.py +488 -0
- bpp/web/state_compat.py +185 -0
- bpp/web/state_helpers.py +112 -0
- bpp/web/state_init.py +318 -0
- bpp/web/state_init_phases.py +250 -0
- bpp/web/state_lifecycle.py +280 -0
- bpp/web/state_ops.py +162 -0
- bpp/web/static/css/app.css +7608 -0
- bpp/web/static/img/apple-touch-icon.png +0 -0
- bpp/web/static/img/icon-192.png +0 -0
- bpp/web/static/img/icon-512.png +0 -0
- bpp/web/static/js/globals.js +446 -0
- bpp/web/static/js/modules/action-registry.mjs +109 -0
- bpp/web/static/js/modules/activity-humanize.mjs +185 -0
- bpp/web/static/js/modules/activity-log.mjs +350 -0
- bpp/web/static/js/modules/albums-menus.mjs +400 -0
- bpp/web/static/js/modules/albums-render-helpers.mjs +61 -0
- bpp/web/static/js/modules/albums-render.mjs +505 -0
- bpp/web/static/js/modules/albums-switch.mjs +292 -0
- bpp/web/static/js/modules/albums.mjs +356 -0
- bpp/web/static/js/modules/analysis-install.mjs +112 -0
- bpp/web/static/js/modules/analysis-preflight.mjs +160 -0
- bpp/web/static/js/modules/analysis-recompute.mjs +257 -0
- bpp/web/static/js/modules/analysis-status.mjs +347 -0
- bpp/web/static/js/modules/analysis.mjs +444 -0
- bpp/web/static/js/modules/api-client.mjs +141 -0
- bpp/web/static/js/modules/app.mjs +412 -0
- bpp/web/static/js/modules/batch-rename.mjs +308 -0
- bpp/web/static/js/modules/calendar-render.mjs +307 -0
- bpp/web/static/js/modules/calendar-selection.mjs +214 -0
- bpp/web/static/js/modules/calendar.mjs +433 -0
- bpp/web/static/js/modules/client-error-beacon.mjs +56 -0
- bpp/web/static/js/modules/clip.mjs +370 -0
- bpp/web/static/js/modules/compare-sibling.mjs +311 -0
- bpp/web/static/js/modules/compare.mjs +450 -0
- bpp/web/static/js/modules/constants.mjs +31 -0
- bpp/web/static/js/modules/core.mjs +409 -0
- bpp/web/static/js/modules/date-format.mjs +96 -0
- bpp/web/static/js/modules/deleted-ctx-menu.mjs +228 -0
- bpp/web/static/js/modules/deleted-enhance.mjs +160 -0
- bpp/web/static/js/modules/deleted.mjs +440 -0
- bpp/web/static/js/modules/dialogs.mjs +157 -0
- bpp/web/static/js/modules/dupe-review.mjs +222 -0
- bpp/web/static/js/modules/editor-constants.mjs +295 -0
- bpp/web/static/js/modules/editor-crop.mjs +382 -0
- bpp/web/static/js/modules/editor-inpaint.mjs +427 -0
- bpp/web/static/js/modules/editor-preview.mjs +199 -0
- bpp/web/static/js/modules/editor-redeye.mjs +118 -0
- bpp/web/static/js/modules/editor-rendering.mjs +411 -0
- bpp/web/static/js/modules/editor-styles.mjs +312 -0
- bpp/web/static/js/modules/editor-tools.mjs +227 -0
- bpp/web/static/js/modules/editor.mjs +456 -0
- bpp/web/static/js/modules/face-embedders-acceptance.mjs +440 -0
- bpp/web/static/js/modules/face-embedders-actions.mjs +270 -0
- bpp/web/static/js/modules/face-embedders-popover.mjs +173 -0
- bpp/web/static/js/modules/face-embedders-rowstate.mjs +499 -0
- bpp/web/static/js/modules/face-embedders-state.mjs +142 -0
- bpp/web/static/js/modules/faces-extraction.mjs +183 -0
- bpp/web/static/js/modules/faces.mjs +350 -0
- bpp/web/static/js/modules/format-helpers.mjs +67 -0
- bpp/web/static/js/modules/groups.mjs +322 -0
- bpp/web/static/js/modules/import-worker.mjs +274 -0
- bpp/web/static/js/modules/inspector.mjs +338 -0
- bpp/web/static/js/modules/library.mjs +305 -0
- bpp/web/static/js/modules/lightbox-actions.mjs +450 -0
- bpp/web/static/js/modules/lightbox-ctxmenu.mjs +77 -0
- bpp/web/static/js/modules/lightbox-face-assign.mjs +382 -0
- bpp/web/static/js/modules/lightbox-face-edit.mjs +479 -0
- bpp/web/static/js/modules/lightbox-face-overlays.mjs +336 -0
- bpp/web/static/js/modules/lightbox-face-picker.mjs +475 -0
- bpp/web/static/js/modules/lightbox-info.mjs +375 -0
- bpp/web/static/js/modules/lightbox-input.mjs +369 -0
- bpp/web/static/js/modules/lightbox-open.mjs +280 -0
- bpp/web/static/js/modules/lightbox.mjs +462 -0
- bpp/web/static/js/modules/map.mjs +273 -0
- bpp/web/static/js/modules/memories.mjs +210 -0
- bpp/web/static/js/modules/modal.mjs +109 -0
- bpp/web/static/js/modules/modals-face-embedders.mjs +496 -0
- bpp/web/static/js/modules/modals-models-list.mjs +226 -0
- bpp/web/static/js/modules/modals-models.mjs +311 -0
- bpp/web/static/js/modules/modals.mjs +487 -0
- bpp/web/static/js/modules/moments-stacks.mjs +414 -0
- bpp/web/static/js/modules/moments-view.mjs +82 -0
- bpp/web/static/js/modules/navigation.mjs +74 -0
- bpp/web/static/js/modules/nudges.mjs +151 -0
- bpp/web/static/js/modules/on-this-day.mjs +183 -0
- bpp/web/static/js/modules/onboarding.mjs +406 -0
- bpp/web/static/js/modules/people-actions.mjs +229 -0
- bpp/web/static/js/modules/people-album-bar.mjs +441 -0
- bpp/web/static/js/modules/people-ctx-menu.mjs +238 -0
- bpp/web/static/js/modules/people-merge.mjs +214 -0
- bpp/web/static/js/modules/people-pair-review.mjs +400 -0
- bpp/web/static/js/modules/people-pickers.mjs +421 -0
- bpp/web/static/js/modules/people-rename.mjs +259 -0
- bpp/web/static/js/modules/people-review.mjs +475 -0
- bpp/web/static/js/modules/people-view.mjs +224 -0
- bpp/web/static/js/modules/people.mjs +383 -0
- bpp/web/static/js/modules/pets-pickers.mjs +375 -0
- bpp/web/static/js/modules/pets.mjs +380 -0
- bpp/web/static/js/modules/phash-status.mjs +74 -0
- bpp/web/static/js/modules/photo-preview.mjs +79 -0
- bpp/web/static/js/modules/photos-batch.mjs +252 -0
- bpp/web/static/js/modules/photos-card.mjs +124 -0
- bpp/web/static/js/modules/photos-helpers.mjs +45 -0
- bpp/web/static/js/modules/photos-hover.mjs +129 -0
- bpp/web/static/js/modules/photos-select.mjs +129 -0
- bpp/web/static/js/modules/photos-vgrid.mjs +229 -0
- bpp/web/static/js/modules/photos.mjs +489 -0
- bpp/web/static/js/modules/presets.mjs +223 -0
- bpp/web/static/js/modules/review-meta.mjs +58 -0
- bpp/web/static/js/modules/score-format.mjs +71 -0
- bpp/web/static/js/modules/search.mjs +499 -0
- bpp/web/static/js/modules/sensitive.mjs +299 -0
- bpp/web/static/js/modules/settings-client.mjs +91 -0
- bpp/web/static/js/modules/share-tab.mjs +389 -0
- bpp/web/static/js/modules/sidebar-safety.mjs +265 -0
- bpp/web/static/js/modules/slideshow.mjs +390 -0
- bpp/web/static/js/modules/state.mjs +206 -0
- bpp/web/static/js/modules/tags-view.mjs +279 -0
- bpp/web/static/js/modules/tags.mjs +409 -0
- bpp/web/static/js/modules/text-format.mjs +78 -0
- bpp/web/static/js/modules/theme.mjs +62 -0
- bpp/web/static/js/modules/timeline.mjs +163 -0
- bpp/web/static/js/modules/toast.mjs +140 -0
- bpp/web/static/js/modules/toolbar.mjs +426 -0
- bpp/web/static/js/modules/tooltip.mjs +112 -0
- bpp/web/static/js/modules/tour.mjs +429 -0
- bpp/web/static/js/modules/ui-helpers.mjs +81 -0
- bpp/web/static/js/modules/update-checker.mjs +157 -0
- bpp/web/static/js/modules/use-context-options.mjs +52 -0
- bpp/web/static/js/modules/utils.mjs +450 -0
- bpp/web/static/js/modules/view-guard.mjs +132 -0
- bpp/web/static/js/modules/wizard.mjs +198 -0
- bpp/web/static/manifest.json +24 -0
- bpp/web/static/vendor/README.md +35 -0
- bpp/web/static/vendor/leaflet/LICENSE +25 -0
- bpp/web/static/vendor/leaflet/dist/images/layers-2x.png +0 -0
- bpp/web/static/vendor/leaflet/dist/images/layers.png +0 -0
- bpp/web/static/vendor/leaflet/dist/images/marker-icon-2x.png +0 -0
- bpp/web/static/vendor/leaflet/dist/images/marker-icon.png +0 -0
- bpp/web/static/vendor/leaflet/dist/images/marker-shadow.png +0 -0
- bpp/web/static/vendor/leaflet/dist/leaflet.css +661 -0
- bpp/web/static/vendor/leaflet/dist/leaflet.js +6 -0
- bpp/web/static/vendor/leaflet.markercluster/MIT-LICENCE.txt +21 -0
- bpp/web/static/vendor/leaflet.markercluster/dist/MarkerCluster.Default.css +60 -0
- bpp/web/static/vendor/leaflet.markercluster/dist/MarkerCluster.css +14 -0
- bpp/web/static/vendor/leaflet.markercluster/dist/leaflet.markercluster.js +2 -0
- bpp/web/templates/index.html +1266 -0
- bpp/web/templates/pair.html +207 -0
- bpp/web/thumbnails.py +239 -0
- bpp/web/update_checker.py +183 -0
- bpp/web/worker_pool.py +128 -0
- bpp/web/worker_registry.py +101 -0
- bppicker-0.1.0.dist-info/METADATA +698 -0
- bppicker-0.1.0.dist-info/RECORD +412 -0
- bppicker-0.1.0.dist-info/WHEEL +4 -0
- bppicker-0.1.0.dist-info/entry_points.txt +2 -0
- bppicker-0.1.0.dist-info/licenses/LICENSE +21 -0
- bppicker-0.1.0.dist-info/licenses/NOTICE.txt +281 -0
bpp/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Best Photo Picker: Local-first photo curation tool."""
|
|
2
|
+
|
|
3
|
+
# Cap OpenCV's decode size before cv2 is imported anywhere. OpenCV reads
|
|
4
|
+
# OPENCV_IO_MAX_IMAGE_PIXELS once at C-extension load time, so it MUST be
|
|
5
|
+
# set before the first `import cv2` in the process — this package __init__
|
|
6
|
+
# runs before any `bpp.*` submodule (and thus before their cv2 imports),
|
|
7
|
+
# in the server, the CLI, and every multiprocessing-spawn child. Without
|
|
8
|
+
# this, the PIL MAX_IMAGE_PIXELS pin in bpp/scoring/aggregate.py does NOT
|
|
9
|
+
# protect the cv2 decode path (cv2.imread is tried first), leaving a
|
|
10
|
+
# decompression-bomb hole: a valid ~50000x50000 image decodes fine in cv2 and
|
|
11
|
+
# OOMs the analyze/phash worker. 200M matches the PIL pin.
|
|
12
|
+
import os as _os
|
|
13
|
+
|
|
14
|
+
_os.environ.setdefault("OPENCV_IO_MAX_IMAGE_PIXELS", str(200_000_000))
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
APP_NAME = "Best Photo Picker"
|
|
18
|
+
|
|
19
|
+
# Eagerly import the model registry so the Batch-3 download chokepoint
|
|
20
|
+
# (item 18 of the legal-posture rollout) installs before any BPP code
|
|
21
|
+
# can import a third-party package with auto-download behavior. The
|
|
22
|
+
# chokepoint patches the upstream downloader if the package is
|
|
23
|
+
# already loaded, and registers a meta-path hook for packages loaded
|
|
24
|
+
# later. Importing it from the top-level bpp package ensures any
|
|
25
|
+
# ``from bpp...`` import path triggers installation.
|
|
26
|
+
import bpp.registry # noqa: E402, F401 — side-effecting import
|
bpp/ai/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI-powered photo editing tools (optional dependencies)."""
|
bpp/ai/inpainting.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Object removal via LaMa inpainting.
|
|
2
|
+
|
|
3
|
+
Requires optional dependency: pip install bppicker[inpaint]
|
|
4
|
+
Model (~200MB, ``big-lama.pt``) is pre-fetched via bpp's verified-
|
|
5
|
+
download helper (SHA-256 pinned, 600 s timeout) and the path is
|
|
6
|
+
handed to ``simple_lama_inpainting`` via the library's documented
|
|
7
|
+
``LAMA_MODEL`` env-var override. The library then loads the
|
|
8
|
+
already-verified file via ``torch.jit.load`` and skips its own
|
|
9
|
+
``torch.hub.download_url_to_file`` (which has no SHA-256
|
|
10
|
+
verification and no enforced timeout — the original integrity gap).
|
|
11
|
+
|
|
12
|
+
History (kept here for the next reviewer): prior to this fix, the
|
|
13
|
+
weights were fetched directly by the library and a compromised
|
|
14
|
+
GitHub Releases host or MITM'd proxy could substitute different
|
|
15
|
+
torch-checkpoint bytes. Torch checkpoints are pickle-based and can
|
|
16
|
+
execute code at unpickling time → RCE. The
|
|
17
|
+
``simple_lama_inpainting`` dep is opt-in
|
|
18
|
+
(``pip install bppicker[inpaint]``), so the previous footgun was
|
|
19
|
+
limited to users who'd installed that extra; still, the asymmetry
|
|
20
|
+
with every other bpp model (all SHA-pinned + verified) was a real
|
|
21
|
+
gap. This fix closes it.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from io import BytesIO
|
|
28
|
+
|
|
29
|
+
from PIL import Image
|
|
30
|
+
|
|
31
|
+
from bpp.scoring.model_base import ModelSingleton
|
|
32
|
+
from bpp.utils.logging import get_logger
|
|
33
|
+
from bpp.utils.paths import models_dir
|
|
34
|
+
|
|
35
|
+
log = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
# Upstream URL hardcoded in simple_lama_inpainting/models/model.py:7-10.
|
|
38
|
+
# We fetch the same file ourselves but verify SHA-256 before letting
|
|
39
|
+
# torch.jit.load see it.
|
|
40
|
+
_LAMA_MODEL_URL = (
|
|
41
|
+
"https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt"
|
|
42
|
+
)
|
|
43
|
+
# SHA-256 of big-lama.pt at the URL above. Computed locally on
|
|
44
|
+
# 2026-05-07 (size: 205,803,670 bytes). Pin both the bytes and the
|
|
45
|
+
# upstream contract — if the release is ever overwritten or the URL
|
|
46
|
+
# starts redirecting elsewhere, our verified-download path refuses to
|
|
47
|
+
# load and the user gets a clear error rather than silent code
|
|
48
|
+
# execution. Re-pin only after auditing the new upstream artifact.
|
|
49
|
+
_LAMA_MODEL_SHA256 = "7ba7aa7ac37a4d41fdbbeba3a2af7ead18058552997e3a3cd1a3b2210c9e6b4c"
|
|
50
|
+
_LAMA_MODEL_PATH = models_dir() / "big-lama.pt"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _import_check() -> None:
|
|
54
|
+
"""Raise ImportError if simple_lama_inpainting is not installed."""
|
|
55
|
+
import simple_lama_inpainting # noqa: F401
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _create_lama(verified_path):
|
|
59
|
+
"""Construct SimpleLama against an already-verified LaMa file.
|
|
60
|
+
|
|
61
|
+
By the time ``ModelSingleton`` calls this, ``ensure_model()`` has
|
|
62
|
+
either downloaded the file with SHA-256 verification or
|
|
63
|
+
re-verified an existing cached file — see
|
|
64
|
+
``bpp.scoring.model_base.ModelSingleton.ensure_model``. We just
|
|
65
|
+
point ``simple_lama_inpainting`` at the verified path via its
|
|
66
|
+
documented ``LAMA_MODEL`` env-var override (the library's
|
|
67
|
+
``models/model.py`` checks this env var BEFORE calling
|
|
68
|
+
``torch.hub.download_url_to_file``, so its unsafe download path
|
|
69
|
+
never runs).
|
|
70
|
+
"""
|
|
71
|
+
from simple_lama_inpainting import SimpleLama
|
|
72
|
+
|
|
73
|
+
os.environ["LAMA_MODEL"] = str(verified_path)
|
|
74
|
+
log.info("Loading LaMa inpainting model from %s", verified_path)
|
|
75
|
+
instance = SimpleLama()
|
|
76
|
+
log.info("LaMa model loaded")
|
|
77
|
+
return instance
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Same lifecycle as every other bpp ML model: ModelSingleton checks
|
|
81
|
+
# `model_path.exists() + verify SHA`, downloads if missing, calls
|
|
82
|
+
# `create_fn(verified_path)`. The Settings → Advanced → ML Models
|
|
83
|
+
# panel reads this registration to surface a Redownload affordance,
|
|
84
|
+
# Uninstall control, and the pinned-download consent prompt before
|
|
85
|
+
# the first inpaint click.
|
|
86
|
+
_LAMA = ModelSingleton(
|
|
87
|
+
name="LaMa inpainting",
|
|
88
|
+
model_path=_LAMA_MODEL_PATH,
|
|
89
|
+
model_url=_LAMA_MODEL_URL,
|
|
90
|
+
model_sha256=_LAMA_MODEL_SHA256,
|
|
91
|
+
create_fn=_create_lama,
|
|
92
|
+
registry_id="lama_inpaint_research",
|
|
93
|
+
import_check=_import_check,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_available() -> bool:
|
|
98
|
+
"""Check if inpainting dependencies are installed."""
|
|
99
|
+
return _LAMA.is_available()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Catalog-loader hooks ────────────────────────────────────────────
|
|
103
|
+
#
|
|
104
|
+
# LaMa is a runtime-fetched catalog entry (weights pulled on demand,
|
|
105
|
+
# not listed in the download manifest). The Settings → Models picker
|
|
106
|
+
# drives its Review → Download → Use → Uninstall lifecycle through
|
|
107
|
+
# this trio, registered in bpp.web.bp_model_registry._catalog_loaders.
|
|
108
|
+
# Mirrors the nudity / buffalo_s catalog hooks.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_on_disk() -> bool:
|
|
112
|
+
"""Return True if the locally-cached LaMa weight file exists.
|
|
113
|
+
|
|
114
|
+
Cheap existence check only (no SHA verify) — the picker reads it to
|
|
115
|
+
decide between "Download" and "Uninstall". A tampered cache is
|
|
116
|
+
caught and re-fetched at load time by :func:`ensure_lama_model`.
|
|
117
|
+
"""
|
|
118
|
+
return _LAMA_MODEL_PATH.exists()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def ensure_lama_model() -> str:
|
|
122
|
+
"""Download + verify the LaMa weights NOW. Returns the local path.
|
|
123
|
+
|
|
124
|
+
Routes through :meth:`ModelSingleton.ensure_model`, which calls the
|
|
125
|
+
canonical :func:`bpp.utils.download.download_file` gate
|
|
126
|
+
(``registry_id="lama_inpaint_research"``) — the policy gate fires
|
|
127
|
+
BEFORE the network call, so the explicit "Download" button cannot
|
|
128
|
+
bypass license acceptance.
|
|
129
|
+
"""
|
|
130
|
+
# Bracket the fetch with start/finish logs so a hang or slow
|
|
131
|
+
# download is visible in server.log (a ~200 MB file fetch was
|
|
132
|
+
# previously silent until the endpoint logged completion).
|
|
133
|
+
# ``ensure_model`` is a no-op verify when the file is already
|
|
134
|
+
# cached, so the wording stays neutral ("Ensuring"/"ready").
|
|
135
|
+
log.info("Ensuring LaMa inpainting weights (source=%s)", _LAMA_MODEL_URL)
|
|
136
|
+
path = _LAMA.ensure_model()
|
|
137
|
+
if path is None:
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
f"LaMa weights could not be downloaded "
|
|
140
|
+
f"(registry_id=lama_inpaint_research, source={_LAMA_MODEL_URL}; "
|
|
141
|
+
f"ensure_model returned None — check network access and the "
|
|
142
|
+
f"pinned upstream URL)."
|
|
143
|
+
)
|
|
144
|
+
log.info("LaMa inpainting weights ready: %s", path)
|
|
145
|
+
return str(path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def remove_local_weights() -> int:
|
|
149
|
+
"""Delete the cached LaMa weights. Returns the bytes freed.
|
|
150
|
+
|
|
151
|
+
Backs the picker's Uninstall action and resets the in-process
|
|
152
|
+
singleton so the next load re-runs ensure → download → verify.
|
|
153
|
+
Symmetric counterpart to :func:`ensure_lama_model`. Idempotent.
|
|
154
|
+
|
|
155
|
+
A failed unlink is logged at WARNING rather than swallowed: the
|
|
156
|
+
Uninstall would otherwise report success while the 200 MB file
|
|
157
|
+
stays on disk, with no trail to diagnose why (project convention:
|
|
158
|
+
nothing should be silent).
|
|
159
|
+
"""
|
|
160
|
+
freed = 0
|
|
161
|
+
if _LAMA_MODEL_PATH.exists():
|
|
162
|
+
freed = _LAMA_MODEL_PATH.stat().st_size
|
|
163
|
+
try:
|
|
164
|
+
_LAMA_MODEL_PATH.unlink()
|
|
165
|
+
except OSError:
|
|
166
|
+
freed = 0
|
|
167
|
+
log.warning(
|
|
168
|
+
"Failed to delete LaMa weights at %s",
|
|
169
|
+
_LAMA_MODEL_PATH,
|
|
170
|
+
exc_info=True,
|
|
171
|
+
)
|
|
172
|
+
tmp = _LAMA_MODEL_PATH.with_suffix(_LAMA_MODEL_PATH.suffix + ".tmp")
|
|
173
|
+
if tmp.exists():
|
|
174
|
+
try:
|
|
175
|
+
tmp.unlink()
|
|
176
|
+
except OSError:
|
|
177
|
+
log.warning("Failed to delete LaMa temp file at %s", tmp, exc_info=True)
|
|
178
|
+
_LAMA.reset()
|
|
179
|
+
return freed
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _get_model():
|
|
183
|
+
"""Return the lazily-initialised LaMa model, or None if unavailable.
|
|
184
|
+
|
|
185
|
+
Enforces the registry policy gate FIRST. LaMa weights are
|
|
186
|
+
research-only / non-commercial; the click-through acceptance
|
|
187
|
+
dialog must have been completed (with separate-rights assertion
|
|
188
|
+
in commercial mode) before the model loads. Raises
|
|
189
|
+
:class:`bpp.registry.ModelLoadBlockedError` otherwise.
|
|
190
|
+
|
|
191
|
+
Kept as a public-ish symbol because existing tests patch it directly.
|
|
192
|
+
Internally delegates to the ModelSingleton — same thread-safe lazy
|
|
193
|
+
init as every other ML model in the codebase.
|
|
194
|
+
"""
|
|
195
|
+
from bpp.registry import enforce_load_policy_for
|
|
196
|
+
|
|
197
|
+
enforce_load_policy_for("lama_inpaint_research")
|
|
198
|
+
return _LAMA.get()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def inpaint(image: Image.Image, mask: Image.Image) -> Image.Image:
|
|
202
|
+
"""Remove masked area from image using LaMa inpainting.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
image: RGB input image.
|
|
206
|
+
mask: Grayscale or binary mask. White (255) = area to remove.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Inpainted RGB image (same size as input).
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
RuntimeError: If simple-lama-inpainting is not installed.
|
|
213
|
+
ValueError: If image/mask sizes don't match.
|
|
214
|
+
"""
|
|
215
|
+
if image.size != mask.size:
|
|
216
|
+
raise ValueError(f"Image size {image.size} doesn't match mask size {mask.size}")
|
|
217
|
+
|
|
218
|
+
# Ensure correct modes
|
|
219
|
+
if image.mode != "RGB":
|
|
220
|
+
image = image.convert("RGB")
|
|
221
|
+
if mask.mode != "L":
|
|
222
|
+
mask = mask.convert("L")
|
|
223
|
+
|
|
224
|
+
model = _get_model()
|
|
225
|
+
if model is None:
|
|
226
|
+
raise RuntimeError("Inpainting not available. Install with: pip install bppicker[inpaint]")
|
|
227
|
+
return model(image, mask)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def inpaint_from_bytes(image_bytes: bytes, mask_bytes: bytes) -> bytes:
|
|
231
|
+
"""Convenience: accept and return PNG bytes."""
|
|
232
|
+
# Context managers release the BytesIO-backed Image handles before
|
|
233
|
+
# inpaint() runs. convert() returns a fresh Image instance so the
|
|
234
|
+
# source handles can close at the end of each `with`.
|
|
235
|
+
with Image.open(BytesIO(image_bytes)) as img_in:
|
|
236
|
+
image = img_in.convert("RGB")
|
|
237
|
+
with Image.open(BytesIO(mask_bytes)) as mask_in:
|
|
238
|
+
mask = mask_in.convert("L")
|
|
239
|
+
|
|
240
|
+
result = inpaint(image, mask)
|
|
241
|
+
|
|
242
|
+
buf = BytesIO()
|
|
243
|
+
result.save(buf, format="PNG")
|
|
244
|
+
return buf.getvalue()
|
bpp/cli.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""CLI entrypoint for bpp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from bpp import APP_NAME, __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="bpp",
|
|
14
|
+
description=f"{APP_NAME} — score, deduplicate, and select the best photos.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
17
|
+
parser.add_argument("--seed", type=int, default=42, help="Random seed for determinism")
|
|
18
|
+
# default pulled from the config registry so a plugin
|
|
19
|
+
# override (e.g. AVIF / RAW format support) propagates without
|
|
20
|
+
# touching this argparse declaration.
|
|
21
|
+
from bpp.config import DEFAULTS
|
|
22
|
+
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--extensions",
|
|
25
|
+
default=DEFAULTS["scan_extensions"],
|
|
26
|
+
help="Comma-separated image extensions to include (default from config: scan_extensions)",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
30
|
+
|
|
31
|
+
# --- analyze ---
|
|
32
|
+
p_analyze = sub.add_parser("analyze", help="Scan images, extract features, compute scores")
|
|
33
|
+
p_analyze.add_argument("--input", required=True, help="Input folder of photos")
|
|
34
|
+
p_analyze.add_argument("--out", required=True, help="Working directory for cache/results")
|
|
35
|
+
p_analyze.add_argument("--config", help="Path to YAML config file")
|
|
36
|
+
p_analyze.add_argument("--max", type=int, default=0, help="Max images to process (0=all)")
|
|
37
|
+
p_analyze.add_argument("--workers", type=int, default=0, help="Parallel workers (0=auto)")
|
|
38
|
+
p_analyze.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
39
|
+
p_analyze.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
40
|
+
|
|
41
|
+
# --- select ---
|
|
42
|
+
p_select = sub.add_parser("select", help="Select best photos from analyzed data")
|
|
43
|
+
p_select.add_argument("--workdir", required=True, help="Working directory from analyze step")
|
|
44
|
+
p_select.add_argument("--k", type=int, default=50, help="Number of photos to select")
|
|
45
|
+
p_select.add_argument("--out", required=True, help="Output directory for selected photos")
|
|
46
|
+
p_select.add_argument("--config", help="Path to YAML config file")
|
|
47
|
+
p_select.add_argument("--gallery", action="store_true", help="Generate HTML gallery")
|
|
48
|
+
p_select.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
49
|
+
mode = p_select.add_mutually_exclusive_group()
|
|
50
|
+
mode.add_argument("--copy", action="store_const", const="copy", dest="export_mode")
|
|
51
|
+
mode.add_argument("--hardlink", action="store_const", const="hardlink", dest="export_mode")
|
|
52
|
+
mode.add_argument("--symlink", action="store_const", const="symlink", dest="export_mode")
|
|
53
|
+
p_select.set_defaults(export_mode="copy")
|
|
54
|
+
|
|
55
|
+
# --- run (one-shot) ---
|
|
56
|
+
p_run = sub.add_parser("run", help="Analyze + select in one shot")
|
|
57
|
+
p_run.add_argument("--input", required=True, help="Input folder of photos")
|
|
58
|
+
p_run.add_argument("--k", type=int, default=50, help="Number of photos to select")
|
|
59
|
+
p_run.add_argument("--out", required=True, help="Output directory for selected photos")
|
|
60
|
+
p_run.add_argument("--config", help="Path to YAML config file")
|
|
61
|
+
p_run.add_argument("--max", type=int, default=0, help="Max images to process (0=all)")
|
|
62
|
+
p_run.add_argument("--workers", type=int, default=0, help="Parallel workers (0=auto)")
|
|
63
|
+
p_run.add_argument("--gallery", action="store_true", help="Generate HTML gallery")
|
|
64
|
+
p_run.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
65
|
+
p_run.add_argument("--dry-run", action="store_true", help="Show what would be done")
|
|
66
|
+
run_mode = p_run.add_mutually_exclusive_group()
|
|
67
|
+
run_mode.add_argument("--copy", action="store_const", const="copy", dest="export_mode")
|
|
68
|
+
run_mode.add_argument("--hardlink", action="store_const", const="hardlink", dest="export_mode")
|
|
69
|
+
run_mode.add_argument("--symlink", action="store_const", const="symlink", dest="export_mode")
|
|
70
|
+
p_run.set_defaults(export_mode="copy")
|
|
71
|
+
|
|
72
|
+
# --- web ---
|
|
73
|
+
p_web = sub.add_parser("web", help="Launch interactive web UI")
|
|
74
|
+
p_web.add_argument("--input", help="Input folder of photos")
|
|
75
|
+
p_web.add_argument("--workdir", help="Working directory with existing analysis")
|
|
76
|
+
p_web.add_argument("--port", type=int, default=5001, help="Port for web server")
|
|
77
|
+
p_web.add_argument(
|
|
78
|
+
"--host",
|
|
79
|
+
default=None,
|
|
80
|
+
help="Bind address (default: 127.0.0.1). Pass 0.0.0.0 for Docker port-forwarding.",
|
|
81
|
+
)
|
|
82
|
+
p_web.add_argument("--no-browser", action="store_true", help="Don't auto-open browser")
|
|
83
|
+
p_web.add_argument("--config", help="Path to YAML config file")
|
|
84
|
+
p_web.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
85
|
+
|
|
86
|
+
# --- serve ---
|
|
87
|
+
p_serve = sub.add_parser("serve", help="Start photo management server")
|
|
88
|
+
p_serve.add_argument(
|
|
89
|
+
"--library",
|
|
90
|
+
help="Library path (default: registry's active library, or ~/Pictures/BestPhotoPicker)",
|
|
91
|
+
)
|
|
92
|
+
p_serve.add_argument(
|
|
93
|
+
"--host",
|
|
94
|
+
default=None,
|
|
95
|
+
help="Bind address. Default: 127.0.0.1 (loopback only) when LAN "
|
|
96
|
+
"sharing is OFF, 0.0.0.0 when it's ON. Pass `0.0.0.0` explicitly "
|
|
97
|
+
"to bind every interface regardless of the share toggle. The "
|
|
98
|
+
"share toggle (Settings → Share) is what gates LAN access in "
|
|
99
|
+
"the auth layer; binding loopback-only is defense-in-depth so "
|
|
100
|
+
"the service isn't even visible on port scan from a coffee-shop "
|
|
101
|
+
"Wi-Fi network. If running behind a reverse proxy (nginx, "
|
|
102
|
+
"Caddy, Docker), set BPP_TRUSTED_PROXIES to the proxy's CIDR "
|
|
103
|
+
"so X-Forwarded-For is honored for the loopback gate — see "
|
|
104
|
+
"docs/security.md.",
|
|
105
|
+
)
|
|
106
|
+
p_serve.add_argument("--port", type=int, default=5001, help="Port for web server")
|
|
107
|
+
p_serve.add_argument("--no-browser", action="store_true", help="Don't auto-open browser")
|
|
108
|
+
p_serve.add_argument("--config", help="Path to YAML config file")
|
|
109
|
+
p_serve.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
110
|
+
|
|
111
|
+
# --- demo ---
|
|
112
|
+
p_demo = sub.add_parser("demo", help="Launch demo with sample photos")
|
|
113
|
+
p_demo.add_argument("--port", type=int, default=5001, help="Port for web server")
|
|
114
|
+
p_demo.add_argument("--no-browser", action="store_true", help="Don't auto-open browser")
|
|
115
|
+
p_demo.add_argument("--keep", action="store_true", help="Keep demo library after exit")
|
|
116
|
+
p_demo.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
117
|
+
|
|
118
|
+
# --- pick (power-user one-liner) ---
|
|
119
|
+
p_pick = sub.add_parser("pick", help="One-liner: score, select, and optionally export")
|
|
120
|
+
p_pick.add_argument("library", help="Path to library directory")
|
|
121
|
+
p_pick.add_argument("--top", "-k", type=int, default=50, help="Number of photos to select")
|
|
122
|
+
p_pick.add_argument(
|
|
123
|
+
"--boost-face",
|
|
124
|
+
action="append",
|
|
125
|
+
default=[],
|
|
126
|
+
dest="boost_face",
|
|
127
|
+
help="Boost a named person (repeatable)",
|
|
128
|
+
)
|
|
129
|
+
p_pick.add_argument("--out", help="Export selected photos to this directory")
|
|
130
|
+
output_fmt = p_pick.add_mutually_exclusive_group()
|
|
131
|
+
output_fmt.add_argument("--json", action="store_true", help="Output as JSON")
|
|
132
|
+
output_fmt.add_argument("--paths-only", action="store_true", help="Output filepaths only")
|
|
133
|
+
p_pick.add_argument(
|
|
134
|
+
"--quality",
|
|
135
|
+
choices=("original", "high", "medium", "low"),
|
|
136
|
+
default="original",
|
|
137
|
+
help="Export JPEG quality preset (default: original)",
|
|
138
|
+
)
|
|
139
|
+
p_pick.add_argument("--dry-run", action="store_true", help="Show selection without exporting")
|
|
140
|
+
|
|
141
|
+
# --- db restore-backup (recovery from a bad migration) ---
|
|
142
|
+
p_model = sub.add_parser(
|
|
143
|
+
"model",
|
|
144
|
+
help=(
|
|
145
|
+
"Model registry + restricted-license acceptance (text-mode parity with the GUI dialog)"
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
model_sub = p_model.add_subparsers(dest="model_command")
|
|
149
|
+
from bpp.commands.model import add_subparsers as _add_model_subparsers
|
|
150
|
+
|
|
151
|
+
_add_model_subparsers(model_sub)
|
|
152
|
+
|
|
153
|
+
p_db = sub.add_parser(
|
|
154
|
+
"db",
|
|
155
|
+
help="Database utilities (restore from backup, etc.)",
|
|
156
|
+
)
|
|
157
|
+
db_sub = p_db.add_subparsers(dest="db_command")
|
|
158
|
+
p_restore = db_sub.add_parser(
|
|
159
|
+
"restore-backup",
|
|
160
|
+
help="Restore the library DB from .backup (or .backup.prev)",
|
|
161
|
+
description=(
|
|
162
|
+
"Recovery path for a failed schema migration or DB "
|
|
163
|
+
"corruption. The current DB is moved aside with a "
|
|
164
|
+
"timestamped suffix; .backup is verified for integrity, "
|
|
165
|
+
"then copied into place. Use --previous to restore from "
|
|
166
|
+
".backup.prev (the older generation) instead."
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
p_restore.add_argument(
|
|
170
|
+
"--library",
|
|
171
|
+
required=True,
|
|
172
|
+
help="Path to the library directory (the one passed to `bpp serve --library`)",
|
|
173
|
+
)
|
|
174
|
+
p_restore.add_argument(
|
|
175
|
+
"--previous",
|
|
176
|
+
action="store_true",
|
|
177
|
+
help="Restore from .backup.prev (older snapshot) instead of .backup",
|
|
178
|
+
)
|
|
179
|
+
p_restore.add_argument(
|
|
180
|
+
"--yes",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="Skip the interactive confirmation",
|
|
183
|
+
)
|
|
184
|
+
p_restore.add_argument(
|
|
185
|
+
"--accept-stale",
|
|
186
|
+
action="store_true",
|
|
187
|
+
help=(
|
|
188
|
+
"Allow restoring from a backup older than 7 days when "
|
|
189
|
+
"combined with --yes. Without this flag, --yes refuses "
|
|
190
|
+
"stale backups so automation can't silently destroy "
|
|
191
|
+
"weeks of work."
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
p_restore.add_argument(
|
|
195
|
+
"--force",
|
|
196
|
+
action="store_true",
|
|
197
|
+
help=(
|
|
198
|
+
"Bypass the running-server lockfile check. Use only "
|
|
199
|
+
"when you're SURE no `bpp serve` / desktop app is "
|
|
200
|
+
"running against this library — overwriting the DB "
|
|
201
|
+
"with a server attached corrupts state silently."
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return parser
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_analyze(args: argparse.Namespace) -> int:
|
|
209
|
+
"""Scan images, extract features, and compute quality scores."""
|
|
210
|
+
from bpp.commands import do_analyze
|
|
211
|
+
|
|
212
|
+
return do_analyze(args)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cmd_select(args: argparse.Namespace) -> int:
|
|
216
|
+
"""Select the best photos from previously analyzed data."""
|
|
217
|
+
from bpp.commands import do_select
|
|
218
|
+
|
|
219
|
+
return do_select(args)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
223
|
+
"""Analyze and select in one shot."""
|
|
224
|
+
from bpp.commands import do_run
|
|
225
|
+
|
|
226
|
+
return do_run(args)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def cmd_web(args: argparse.Namespace) -> int:
|
|
230
|
+
"""Launch the interactive web UI (alias for serve)."""
|
|
231
|
+
from bpp.commands import do_web
|
|
232
|
+
|
|
233
|
+
return do_web(args)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
237
|
+
"""Start the photo management server."""
|
|
238
|
+
from bpp.commands import do_serve
|
|
239
|
+
|
|
240
|
+
return do_serve(args)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cmd_demo(args: argparse.Namespace) -> int:
|
|
244
|
+
"""Generate sample photos and launch the web UI for a quick demo."""
|
|
245
|
+
from bpp.commands import do_demo
|
|
246
|
+
|
|
247
|
+
return do_demo(args)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def cmd_pick(args: argparse.Namespace) -> int:
|
|
251
|
+
"""One-liner: score, select, and optionally export the best photos."""
|
|
252
|
+
from bpp.commands import do_pick
|
|
253
|
+
|
|
254
|
+
return do_pick(args)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def cmd_model(args: argparse.Namespace) -> int:
|
|
258
|
+
"""Dispatch `bpp model ...` to the right sub-handler set up by
|
|
259
|
+
:func:`bpp.commands.model.add_subparsers`."""
|
|
260
|
+
func = getattr(args, "_model_func", None)
|
|
261
|
+
if func is None:
|
|
262
|
+
print(
|
|
263
|
+
"Usage: bpp model <subcommand> (try `bpp model --help`)",
|
|
264
|
+
file=sys.stderr,
|
|
265
|
+
)
|
|
266
|
+
return 1
|
|
267
|
+
return func(args)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_db(args: argparse.Namespace) -> int:
|
|
271
|
+
"""Database utility commands (restore-backup, ...)."""
|
|
272
|
+
if args.db_command == "restore-backup":
|
|
273
|
+
from bpp.commands import do_db_restore_backup
|
|
274
|
+
|
|
275
|
+
return do_db_restore_backup(args)
|
|
276
|
+
print("Usage: bpp db <subcommand> (try `bpp db --help`)", file=sys.stderr)
|
|
277
|
+
return 1
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def main(argv: list[str] | None = None) -> int:
|
|
281
|
+
parser = build_parser()
|
|
282
|
+
args = parser.parse_args(argv)
|
|
283
|
+
|
|
284
|
+
if args.command is None:
|
|
285
|
+
parser.print_help()
|
|
286
|
+
return 1
|
|
287
|
+
|
|
288
|
+
dispatch = {
|
|
289
|
+
"analyze": cmd_analyze,
|
|
290
|
+
"select": cmd_select,
|
|
291
|
+
"run": cmd_run,
|
|
292
|
+
"web": cmd_web,
|
|
293
|
+
"serve": cmd_serve,
|
|
294
|
+
"demo": cmd_demo,
|
|
295
|
+
"pick": cmd_pick,
|
|
296
|
+
"model": cmd_model,
|
|
297
|
+
"db": cmd_db,
|
|
298
|
+
}
|
|
299
|
+
return dispatch[args.command](args)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
sys.exit(main())
|
bpp/commands/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""High-level command implementations for the bpp CLI.
|
|
2
|
+
|
|
3
|
+
This was a single 1159-LOC module until the v0.1 cleanup; now it's
|
|
4
|
+
a package whose submodules each own one CLI command (or a tight
|
|
5
|
+
cluster of related ones):
|
|
6
|
+
|
|
7
|
+
* ``bpp.commands.analyze`` — ``do_analyze``, ``do_select``, ``do_run``
|
|
8
|
+
* ``bpp.commands.serve`` — ``do_web``, ``do_serve``
|
|
9
|
+
* ``bpp.commands.demo`` — ``do_demo``
|
|
10
|
+
* ``bpp.commands.pick`` — ``do_pick``
|
|
11
|
+
* ``bpp.commands.db_restore`` — ``do_db_restore_backup``, ``_do_restore_locked``
|
|
12
|
+
|
|
13
|
+
Everything is re-exported from this package so existing call sites
|
|
14
|
+
keep working unchanged:
|
|
15
|
+
|
|
16
|
+
from bpp.commands import do_serve # still works
|
|
17
|
+
from bpp.commands import _do_restore_locked # still works
|
|
18
|
+
|
|
19
|
+
The ``bpp.cli`` argparse plumbing lazy-imports each ``do_*`` only when
|
|
20
|
+
its subcommand fires, which means subcommand startup cost is bounded
|
|
21
|
+
by the cost of importing one submodule, not all five.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from bpp.commands.analyze import do_analyze, do_run, do_select
|
|
27
|
+
from bpp.commands.db_restore import _do_restore_locked, do_db_restore_backup
|
|
28
|
+
from bpp.commands.demo import do_demo
|
|
29
|
+
from bpp.commands.pick import do_pick
|
|
30
|
+
from bpp.commands.serve import do_serve, do_web
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"_do_restore_locked",
|
|
34
|
+
"do_analyze",
|
|
35
|
+
"do_db_restore_backup",
|
|
36
|
+
"do_demo",
|
|
37
|
+
"do_pick",
|
|
38
|
+
"do_run",
|
|
39
|
+
"do_select",
|
|
40
|
+
"do_serve",
|
|
41
|
+
"do_web",
|
|
42
|
+
]
|