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/commands/analyze.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""`bpp analyze`, `bpp select`, `bpp run` — the scoring pipeline trio.
|
|
2
|
+
|
|
3
|
+
Extracted from bpp.commands during the v0.1 cleanup. Re-exported from
|
|
4
|
+
`bpp.commands` for backwards compatibility with the CLI (`bpp.cli`)
|
|
5
|
+
and tests that still import `from bpp.commands import do_analyze`.
|
|
6
|
+
`do_run` is just `do_analyze` + `do_select` glue, so they live
|
|
7
|
+
together to keep cross-calls local.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from bpp.config import load_config
|
|
16
|
+
from bpp.utils.logging import get_logger, setup_logging
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def do_analyze(args: argparse.Namespace) -> int:
|
|
20
|
+
setup_logging(debug=getattr(args, "debug", False))
|
|
21
|
+
log = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
# load plugin entry-points before consuming the scoring
|
|
24
|
+
# registry. Idempotent under repeated calls within a process.
|
|
25
|
+
from bpp.plugins import load_plugin_entry_points
|
|
26
|
+
|
|
27
|
+
load_plugin_entry_points()
|
|
28
|
+
|
|
29
|
+
cfg = load_config(getattr(args, "config", None))
|
|
30
|
+
extensions = [e.strip().lower() for e in args.extensions.split(",")]
|
|
31
|
+
|
|
32
|
+
from bpp.io_scan import scan_images
|
|
33
|
+
from bpp.scoring.aggregate import analyze_all
|
|
34
|
+
from bpp.utils.timing import Timer
|
|
35
|
+
|
|
36
|
+
timer = Timer()
|
|
37
|
+
|
|
38
|
+
input_path = os.path.abspath(args.input)
|
|
39
|
+
workdir = os.path.abspath(args.out)
|
|
40
|
+
|
|
41
|
+
if not os.path.isdir(input_path):
|
|
42
|
+
log.error("Input path does not exist or is not a directory: %s", input_path)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
os.makedirs(workdir, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
with timer.section("scan"):
|
|
48
|
+
images = scan_images(
|
|
49
|
+
input_path,
|
|
50
|
+
extensions=extensions,
|
|
51
|
+
follow_symlinks=cfg.get("follow_symlinks", False),
|
|
52
|
+
max_images=args.max,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not images:
|
|
56
|
+
log.warning("No images found in %s", input_path)
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
log.info("Found %d images to analyze", len(images))
|
|
60
|
+
|
|
61
|
+
if args.dry_run:
|
|
62
|
+
log.info("[dry-run] Would analyze %d images", len(images))
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
with timer.section("analyze"):
|
|
66
|
+
results = analyze_all(
|
|
67
|
+
images,
|
|
68
|
+
workdir=workdir,
|
|
69
|
+
config=cfg,
|
|
70
|
+
workers=args.workers,
|
|
71
|
+
seed=args.seed,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
log.info(
|
|
75
|
+
"Analysis complete: %d processed, %d skipped",
|
|
76
|
+
results["processed"],
|
|
77
|
+
results["skipped"],
|
|
78
|
+
)
|
|
79
|
+
log.info("Workdir: %s", workdir)
|
|
80
|
+
timer.summary()
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def do_select(args: argparse.Namespace) -> int:
|
|
85
|
+
setup_logging(debug=False)
|
|
86
|
+
log = get_logger(__name__)
|
|
87
|
+
|
|
88
|
+
cfg = load_config(getattr(args, "config", None))
|
|
89
|
+
|
|
90
|
+
from bpp.dedupe.cluster import deduplicate
|
|
91
|
+
from bpp.output.export import export_selected
|
|
92
|
+
from bpp.scoring.aggregate import load_analysis
|
|
93
|
+
from bpp.selection.choose import choose
|
|
94
|
+
from bpp.utils.timing import Timer
|
|
95
|
+
|
|
96
|
+
timer = Timer()
|
|
97
|
+
|
|
98
|
+
workdir = os.path.abspath(args.workdir)
|
|
99
|
+
outdir = os.path.abspath(args.out)
|
|
100
|
+
|
|
101
|
+
if not os.path.isdir(workdir):
|
|
102
|
+
log.error("Workdir does not exist: %s", workdir)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
with timer.section("load"):
|
|
106
|
+
analysis = load_analysis(workdir)
|
|
107
|
+
|
|
108
|
+
if not analysis:
|
|
109
|
+
log.error("No analysis data found in %s. Run 'analyze' first.", workdir)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
log.info("Loaded analysis for %d images", len(analysis))
|
|
113
|
+
|
|
114
|
+
with timer.section("dedupe"):
|
|
115
|
+
candidates = deduplicate(analysis, config=cfg)
|
|
116
|
+
|
|
117
|
+
log.info("After dedup: %d candidates from %d clusters", len(candidates), len(analysis))
|
|
118
|
+
|
|
119
|
+
with timer.section("select"):
|
|
120
|
+
selected = choose(candidates, k=args.k, config=cfg, seed=args.seed)
|
|
121
|
+
|
|
122
|
+
log.info("Selected %d photos", len(selected))
|
|
123
|
+
|
|
124
|
+
if args.dry_run:
|
|
125
|
+
log.info("[dry-run] Would export %d photos to %s", len(selected), outdir)
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
with timer.section("export"):
|
|
129
|
+
export_selected(
|
|
130
|
+
selected,
|
|
131
|
+
analysis=analysis,
|
|
132
|
+
outdir=outdir,
|
|
133
|
+
mode=args.export_mode,
|
|
134
|
+
gallery=args.gallery,
|
|
135
|
+
config=cfg,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
log.info("Output written to %s", outdir)
|
|
139
|
+
timer.summary()
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def do_run(args: argparse.Namespace) -> int:
|
|
144
|
+
"""One-shot: analyze + select."""
|
|
145
|
+
setup_logging(debug=getattr(args, "debug", False))
|
|
146
|
+
|
|
147
|
+
# Create a temporary workdir
|
|
148
|
+
outdir = os.path.abspath(args.out)
|
|
149
|
+
workdir = os.path.join(outdir, ".workdir")
|
|
150
|
+
|
|
151
|
+
# Build analyze args
|
|
152
|
+
analyze_ns = argparse.Namespace(
|
|
153
|
+
input=args.input,
|
|
154
|
+
out=workdir,
|
|
155
|
+
config=getattr(args, "config", None),
|
|
156
|
+
max=args.max,
|
|
157
|
+
workers=args.workers,
|
|
158
|
+
debug=getattr(args, "debug", False),
|
|
159
|
+
dry_run=args.dry_run,
|
|
160
|
+
seed=args.seed,
|
|
161
|
+
extensions=args.extensions,
|
|
162
|
+
)
|
|
163
|
+
rc = do_analyze(analyze_ns)
|
|
164
|
+
if rc != 0:
|
|
165
|
+
return rc
|
|
166
|
+
|
|
167
|
+
if args.dry_run:
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
select_ns = argparse.Namespace(
|
|
171
|
+
workdir=workdir,
|
|
172
|
+
k=args.k,
|
|
173
|
+
out=outdir,
|
|
174
|
+
config=getattr(args, "config", None),
|
|
175
|
+
export_mode=args.export_mode,
|
|
176
|
+
gallery=getattr(args, "gallery", False),
|
|
177
|
+
dry_run=args.dry_run,
|
|
178
|
+
seed=args.seed,
|
|
179
|
+
extensions=args.extensions,
|
|
180
|
+
)
|
|
181
|
+
return do_select(select_ns)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""`bpp db restore-backup` — recovery path from .backup / .backup.prev.
|
|
2
|
+
|
|
3
|
+
Extracted from bpp.commands during the v0.1 cleanup. The outer
|
|
4
|
+
`do_db_restore_backup` handles the maintenance lock acquire/release;
|
|
5
|
+
the body runs in `_do_restore_locked` so the outer function can
|
|
6
|
+
release the lock in a single try/finally on every exit path.
|
|
7
|
+
|
|
8
|
+
Re-exported from `bpp.commands` for backwards compatibility with
|
|
9
|
+
the CLI and tests (`test_db_restore_command`,
|
|
10
|
+
`test_migration_multi_step`, `test_state_change_logging`).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from bpp.utils.logging import get_logger, setup_logging # noqa: F401
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def do_db_restore_backup(args: argparse.Namespace) -> int:
|
|
23
|
+
"""Restore the library DB from .backup (or .backup.prev).
|
|
24
|
+
|
|
25
|
+
Recovery path for a failed schema migration or DB corruption.
|
|
26
|
+
Migrations are forward-only — once a bad migration commits to
|
|
27
|
+
the live DB, the user can't undo it from the running app. This
|
|
28
|
+
command lets them roll back to the pre-migration `.backup`
|
|
29
|
+
snapshot from the CLI.
|
|
30
|
+
|
|
31
|
+
Behavior:
|
|
32
|
+
1. Resolve the library DB path (`<library>/data/photopicker.db`).
|
|
33
|
+
2. Pick the source: `.backup` by default, `.backup.prev` if
|
|
34
|
+
`--previous` was passed.
|
|
35
|
+
3. Verify the source's integrity. Refuse to restore from a
|
|
36
|
+
corrupt backup — the user is better off keeping the current
|
|
37
|
+
DB than copying garbage on top of it.
|
|
38
|
+
4. Move the current DB aside with a timestamped suffix
|
|
39
|
+
(`.before-restore-<utc-timestamp>`). Keep WAL/SHM
|
|
40
|
+
alongside so the moved-aside DB stays readable.
|
|
41
|
+
5. Copy the source `.backup` (and its sibling WAL/SHM if
|
|
42
|
+
present) into place.
|
|
43
|
+
6. Print clear next steps.
|
|
44
|
+
|
|
45
|
+
Refuses to run if a server is currently bound to the library
|
|
46
|
+
(best-effort: `data/.serving.lock` would be the cleanest signal
|
|
47
|
+
but doesn't exist yet — for now the user is reminded to stop
|
|
48
|
+
the server first).
|
|
49
|
+
"""
|
|
50
|
+
import contextlib
|
|
51
|
+
|
|
52
|
+
from bpp.db.connection import check_integrity
|
|
53
|
+
|
|
54
|
+
library = os.path.expanduser(args.library)
|
|
55
|
+
db_path = os.path.join(library, "data", "photopicker.db")
|
|
56
|
+
workdir = os.path.dirname(db_path)
|
|
57
|
+
backup_path = db_path + (".backup.prev" if args.previous else ".backup")
|
|
58
|
+
|
|
59
|
+
if not os.path.isfile(db_path):
|
|
60
|
+
print(f"error: library DB not found: {db_path}", file=sys.stderr)
|
|
61
|
+
return 2
|
|
62
|
+
if not os.path.isfile(backup_path):
|
|
63
|
+
which = ".backup.prev" if args.previous else ".backup"
|
|
64
|
+
print(f"error: {which} not found at {backup_path}", file=sys.stderr)
|
|
65
|
+
if args.previous:
|
|
66
|
+
print(
|
|
67
|
+
"(.backup.prev only exists once a second backup has rotated.)",
|
|
68
|
+
file=sys.stderr,
|
|
69
|
+
)
|
|
70
|
+
return 2
|
|
71
|
+
|
|
72
|
+
# Running-server guard: refuse to restore while a server
|
|
73
|
+
# has the DB open. Otherwise the running server keeps writing to
|
|
74
|
+
# the now-orphaned old file and the user's "restored" library is
|
|
75
|
+
# missing every change made between the restore and the next
|
|
76
|
+
# restart. --force bypasses (last-resort escape hatch for stale
|
|
77
|
+
# lockfiles that the kill-probe couldn't recover from, e.g.
|
|
78
|
+
# PID reuse during a crash window).
|
|
79
|
+
from bpp.utils.serving_lock import read_lock_pid_if_alive
|
|
80
|
+
|
|
81
|
+
if not getattr(args, "force", False):
|
|
82
|
+
live_pid = read_lock_pid_if_alive(workdir)
|
|
83
|
+
if live_pid is not None:
|
|
84
|
+
print(
|
|
85
|
+
f"error: a server is running against this library "
|
|
86
|
+
f"(PID {live_pid}). Stop it first:\n"
|
|
87
|
+
f" kill {live_pid}\n"
|
|
88
|
+
f"or pass --force to bypass this check (only safe if "
|
|
89
|
+
"you're sure no server / desktop app is attached).",
|
|
90
|
+
file=sys.stderr,
|
|
91
|
+
)
|
|
92
|
+
return 6
|
|
93
|
+
|
|
94
|
+
# per-library maintenance lock. The serving-lock guard
|
|
95
|
+
# above prevents server vs restore races, but does NOT prevent
|
|
96
|
+
# two restore-backup invocations from racing each other (e.g.,
|
|
97
|
+
# one shell reads the .backup, mid-copy a second shell starts,
|
|
98
|
+
# they interleave moves of live DB / WAL / SHM and the result
|
|
99
|
+
# is a half-restored DB the user can't recover from).
|
|
100
|
+
#
|
|
101
|
+
# The maintenance lock is owned by THIS process for the entire
|
|
102
|
+
# restore. --force does NOT bypass it — that would silently
|
|
103
|
+
# re-introduce the race. If a prior restore crashed leaving a
|
|
104
|
+
# stale lock, the user can manually remove it after confirming
|
|
105
|
+
# no other restore is running.
|
|
106
|
+
# write PID + create lockfile atomically via the
|
|
107
|
+
# tmp+link pattern. The previous shape did O_EXCL → (later)
|
|
108
|
+
# fdopen+write, which left a window where the lockfile existed
|
|
109
|
+
# with empty content. A racing contender during that window
|
|
110
|
+
# would read empty content → ValueError → treat as stale →
|
|
111
|
+
# remove → silently win the lock too. Both processes then
|
|
112
|
+
# entered the restore body together, racing the DB/WAL/SHM
|
|
113
|
+
# moves the lock was supposed to serialize.
|
|
114
|
+
#
|
|
115
|
+
# Fix: write the PID to a process-unique tmp file FIRST, then
|
|
116
|
+
# atomically link it into place at .restore.lock. os.link
|
|
117
|
+
# fails-if-exists (atomic on POSIX); the lockfile, when it
|
|
118
|
+
# appears, ALWAYS already contains the PID. The empty-content
|
|
119
|
+
# window is gone.
|
|
120
|
+
maintenance_lock = os.path.join(workdir, ".restore.lock")
|
|
121
|
+
pid_str = str(os.getpid())
|
|
122
|
+
pid_bytes = pid_str.encode("utf-8")
|
|
123
|
+
tmp_lock = maintenance_lock + f".tmp.{pid_str}"
|
|
124
|
+
|
|
125
|
+
def _try_link(_retry_after_stale: bool = False) -> int | None:
|
|
126
|
+
"""Acquire the maintenance lock atomically. Returns rc on
|
|
127
|
+
failure, None on success."""
|
|
128
|
+
# Write PID-bearing tmp file
|
|
129
|
+
try:
|
|
130
|
+
fd = os.open(tmp_lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
131
|
+
except OSError as e:
|
|
132
|
+
print(
|
|
133
|
+
f"error: could not create maintenance-lock tmp file {tmp_lock}: {e}",
|
|
134
|
+
file=sys.stderr,
|
|
135
|
+
)
|
|
136
|
+
return 7
|
|
137
|
+
try:
|
|
138
|
+
with os.fdopen(fd, "wb") as f:
|
|
139
|
+
f.write(pid_bytes)
|
|
140
|
+
except OSError as e:
|
|
141
|
+
with contextlib.suppress(OSError):
|
|
142
|
+
os.remove(tmp_lock)
|
|
143
|
+
print(
|
|
144
|
+
f"error: could not write PID to maintenance-lock tmp: {e}",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
return 7
|
|
148
|
+
# Atomic publish: link tmp → lockfile. Fails if lockfile
|
|
149
|
+
# exists. The published file ALWAYS contains the PID.
|
|
150
|
+
try:
|
|
151
|
+
os.link(tmp_lock, maintenance_lock)
|
|
152
|
+
except FileExistsError:
|
|
153
|
+
# Another restore got there first. Inspect their PID.
|
|
154
|
+
with contextlib.suppress(OSError):
|
|
155
|
+
os.remove(tmp_lock)
|
|
156
|
+
try:
|
|
157
|
+
with open(maintenance_lock, encoding="utf-8") as f:
|
|
158
|
+
holder_raw = f.read().strip()
|
|
159
|
+
holder_pid = int(holder_raw)
|
|
160
|
+
os.kill(holder_pid, 0)
|
|
161
|
+
except (OSError, ValueError):
|
|
162
|
+
# Stale (PID gone, file unreadable, or empty content
|
|
163
|
+
# from an old non-atomic version). Remove + retry once.
|
|
164
|
+
if _retry_after_stale:
|
|
165
|
+
print(
|
|
166
|
+
"error: maintenance lock keeps being recreated; manual cleanup required.",
|
|
167
|
+
file=sys.stderr,
|
|
168
|
+
)
|
|
169
|
+
return 7
|
|
170
|
+
with contextlib.suppress(OSError):
|
|
171
|
+
os.remove(maintenance_lock)
|
|
172
|
+
return _try_link(_retry_after_stale=True)
|
|
173
|
+
else:
|
|
174
|
+
print(
|
|
175
|
+
f"error: another `bpp db restore-backup` is already "
|
|
176
|
+
f"running (PID {holder_pid}). Wait for it to finish, or "
|
|
177
|
+
f"remove {maintenance_lock} if you're sure no restore is "
|
|
178
|
+
"in progress.",
|
|
179
|
+
file=sys.stderr,
|
|
180
|
+
)
|
|
181
|
+
return 7
|
|
182
|
+
except OSError as e:
|
|
183
|
+
with contextlib.suppress(OSError):
|
|
184
|
+
os.remove(tmp_lock)
|
|
185
|
+
print(
|
|
186
|
+
f"error: could not link maintenance lock at {maintenance_lock}: {e}",
|
|
187
|
+
file=sys.stderr,
|
|
188
|
+
)
|
|
189
|
+
return 7
|
|
190
|
+
# Linked successfully; tmp is no longer needed (lockfile is
|
|
191
|
+
# the same inode now via hardlink, but keep the lockfile
|
|
192
|
+
# canonical and remove the tmp).
|
|
193
|
+
with contextlib.suppress(OSError):
|
|
194
|
+
os.remove(tmp_lock)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
rc = _try_link()
|
|
198
|
+
if rc is not None:
|
|
199
|
+
return rc
|
|
200
|
+
|
|
201
|
+
# from here through the end of the function, the
|
|
202
|
+
# maintenance lock is held. Wrap the body in try/finally so
|
|
203
|
+
# every exit path (success, validation refusal, mid-restore
|
|
204
|
+
# error) releases the lock — otherwise a subsequent restore
|
|
205
|
+
# would refuse to start with a stale lockfile.
|
|
206
|
+
try:
|
|
207
|
+
return _do_restore_locked(args, db_path, backup_path, check_integrity)
|
|
208
|
+
finally:
|
|
209
|
+
with contextlib.suppress(OSError):
|
|
210
|
+
os.remove(maintenance_lock)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Restore body lives in db_restore_impl.py since the v0.1 cleanup so this
|
|
214
|
+
# module stays under the 500-LOC soft cap. The outer wrapper above just
|
|
215
|
+
# owns the maintenance-lock; the meat is in the impl module.
|
|
216
|
+
from bpp.commands.db_restore_impl import _do_restore_locked # noqa: E402
|