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.
Files changed (412) hide show
  1. bpp/__init__.py +26 -0
  2. bpp/ai/__init__.py +1 -0
  3. bpp/ai/inpainting.py +244 -0
  4. bpp/cli.py +303 -0
  5. bpp/commands/__init__.py +42 -0
  6. bpp/commands/analyze.py +181 -0
  7. bpp/commands/db_restore.py +216 -0
  8. bpp/commands/db_restore_impl.py +318 -0
  9. bpp/commands/demo.py +69 -0
  10. bpp/commands/model.py +169 -0
  11. bpp/commands/model_commands.py +442 -0
  12. bpp/commands/pick.py +167 -0
  13. bpp/commands/serve.py +329 -0
  14. bpp/config.py +123 -0
  15. bpp/config_resolver.py +233 -0
  16. bpp/config_schema.py +482 -0
  17. bpp/constants.py +268 -0
  18. bpp/db/__init__.py +1 -0
  19. bpp/db/albums.py +447 -0
  20. bpp/db/backup.py +430 -0
  21. bpp/db/batch_rename.py +356 -0
  22. bpp/db/calendar.py +158 -0
  23. bpp/db/clip.py +299 -0
  24. bpp/db/connection.py +297 -0
  25. bpp/db/dedupe.py +241 -0
  26. bpp/db/dialect.py +162 -0
  27. bpp/db/edits.py +179 -0
  28. bpp/db/event_hooks.py +206 -0
  29. bpp/db/face_cluster_ops.py +181 -0
  30. bpp/db/face_embedding_safety.py +243 -0
  31. bpp/db/face_feedback.py +362 -0
  32. bpp/db/face_identity_remap.py +443 -0
  33. bpp/db/face_queries.py +257 -0
  34. bpp/db/groups.py +232 -0
  35. bpp/db/integrity.py +185 -0
  36. bpp/db/journal.py +231 -0
  37. bpp/db/library.py +433 -0
  38. bpp/db/live_photo.py +476 -0
  39. bpp/db/memories.py +320 -0
  40. bpp/db/migrate.py +119 -0
  41. bpp/db/migrations.py +264 -0
  42. bpp/db/migrations_latest.py +438 -0
  43. bpp/db/migrations_recent.py +430 -0
  44. bpp/db/moments.py +154 -0
  45. bpp/db/pets.py +371 -0
  46. bpp/db/photo_hooks.py +120 -0
  47. bpp/db/photos.py +505 -0
  48. bpp/db/photos_dates.py +89 -0
  49. bpp/db/photos_gps.py +139 -0
  50. bpp/db/photos_lifecycle.py +214 -0
  51. bpp/db/photos_missing.py +122 -0
  52. bpp/db/presets.py +58 -0
  53. bpp/db/registry.py +142 -0
  54. bpp/db/schema.py +530 -0
  55. bpp/db/schema_migrate.py +215 -0
  56. bpp/db/settings.py +66 -0
  57. bpp/db/smart_album_document.py +87 -0
  58. bpp/db/smart_album_domains.py +116 -0
  59. bpp/db/smart_album_ensure.py +87 -0
  60. bpp/db/smart_album_groups.py +117 -0
  61. bpp/db/smart_album_people.py +341 -0
  62. bpp/db/smart_album_pets.py +111 -0
  63. bpp/db/smart_album_queries.py +299 -0
  64. bpp/db/smart_album_refreshers.py +444 -0
  65. bpp/db/smart_album_sensitive.py +64 -0
  66. bpp/db/smart_album_tags.py +78 -0
  67. bpp/db/smart_albums.py +441 -0
  68. bpp/db/stats.py +57 -0
  69. bpp/db/tags.py +172 -0
  70. bpp/dedupe/__init__.py +0 -0
  71. bpp/dedupe/cluster.py +173 -0
  72. bpp/dedupe/common.py +20 -0
  73. bpp/dedupe/phash.py +147 -0
  74. bpp/dedupe/semantic.py +328 -0
  75. bpp/dedupe/strategy.py +277 -0
  76. bpp/demo/__init__.py +0 -0
  77. bpp/demo/generate.py +340 -0
  78. bpp/errors.py +234 -0
  79. bpp/exif_utils.py +168 -0
  80. bpp/io_scan.py +102 -0
  81. bpp/media_types.py +78 -0
  82. bpp/output/__init__.py +0 -0
  83. bpp/output/export.py +376 -0
  84. bpp/output/export_metadata.py +207 -0
  85. bpp/output/export_modes.py +296 -0
  86. bpp/output/gallery.py +148 -0
  87. bpp/plugin_protocol.py +240 -0
  88. bpp/plugins/__init__.py +345 -0
  89. bpp/plugins/example.py +328 -0
  90. bpp/plugins/registry_protocol.py +92 -0
  91. bpp/registry/__init__.py +345 -0
  92. bpp/registry/acceptance.py +360 -0
  93. bpp/registry/acceptance_log.py +395 -0
  94. bpp/registry/builtins.py +479 -0
  95. bpp/registry/byom.py +356 -0
  96. bpp/registry/derived_data_purge.py +118 -0
  97. bpp/registry/disclaimers.py +290 -0
  98. bpp/registry/download_chokepoint.py +430 -0
  99. bpp/registry/labels.py +146 -0
  100. bpp/registry/model_registry.py +415 -0
  101. bpp/registry/overlay.py +242 -0
  102. bpp/registry/policy.py +320 -0
  103. bpp/registry/remote_registry.py +286 -0
  104. bpp/registry/removal.py +142 -0
  105. bpp/registry/signed_manifest.py +334 -0
  106. bpp/registry/trusted_keys.py +122 -0
  107. bpp/registry/use_context.py +188 -0
  108. bpp/registry/use_context_store.py +236 -0
  109. bpp/scoring/__init__.py +0 -0
  110. bpp/scoring/_registry_base.py +50 -0
  111. bpp/scoring/aggregate.py +431 -0
  112. bpp/scoring/aggregate_video.py +136 -0
  113. bpp/scoring/blur.py +89 -0
  114. bpp/scoring/clip_embed.py +333 -0
  115. bpp/scoring/clip_tokenizer.py +241 -0
  116. bpp/scoring/composition.py +67 -0
  117. bpp/scoring/enhance.py +92 -0
  118. bpp/scoring/exposure.py +44 -0
  119. bpp/scoring/face.py +277 -0
  120. bpp/scoring/face_blazeface_fr.py +181 -0
  121. bpp/scoring/face_cluster.py +115 -0
  122. bpp/scoring/face_detector_registry.py +138 -0
  123. bpp/scoring/face_embed.py +426 -0
  124. bpp/scoring/face_embed_buffalo.py +132 -0
  125. bpp/scoring/face_embed_buffalo_s.py +500 -0
  126. bpp/scoring/face_embed_detect.py +232 -0
  127. bpp/scoring/face_embed_extractors.py +284 -0
  128. bpp/scoring/face_embed_landmarks.py +218 -0
  129. bpp/scoring/face_embed_sface.py +145 -0
  130. bpp/scoring/face_embed_sface_runtime.py +359 -0
  131. bpp/scoring/face_embedder_registry.py +126 -0
  132. bpp/scoring/face_expression.py +186 -0
  133. bpp/scoring/face_fallback.py +108 -0
  134. bpp/scoring/face_hand_filter.py +202 -0
  135. bpp/scoring/face_mediapipe.py +120 -0
  136. bpp/scoring/face_pipeline.py +418 -0
  137. bpp/scoring/face_score.py +122 -0
  138. bpp/scoring/face_scrfd.py +273 -0
  139. bpp/scoring/face_yunet.py +255 -0
  140. bpp/scoring/model_base.py +436 -0
  141. bpp/scoring/model_load_gate.py +57 -0
  142. bpp/scoring/model_manifest.py +255 -0
  143. bpp/scoring/models/blaze_face_short_range.tflite +0 -0
  144. bpp/scoring/nudity.py +316 -0
  145. bpp/scoring/onnx_providers.py +133 -0
  146. bpp/scoring/pets.py +460 -0
  147. bpp/scoring/pose.py +234 -0
  148. bpp/scoring/registry.py +336 -0
  149. bpp/scoring/segmentation.py +181 -0
  150. bpp/scoring/skin.py +18 -0
  151. bpp/selection/__init__.py +0 -0
  152. bpp/selection/choose.py +130 -0
  153. bpp/selection/diversity.py +129 -0
  154. bpp/utils/__init__.py +0 -0
  155. bpp/utils/cancel.py +279 -0
  156. bpp/utils/concurrency.py +58 -0
  157. bpp/utils/config_snapshot.py +83 -0
  158. bpp/utils/download.py +200 -0
  159. bpp/utils/json_utils.py +32 -0
  160. bpp/utils/logging.py +408 -0
  161. bpp/utils/path_validation.py +70 -0
  162. bpp/utils/paths.py +121 -0
  163. bpp/utils/raw.py +74 -0
  164. bpp/utils/retry.py +102 -0
  165. bpp/utils/safe_subprocess.py +1 -0
  166. bpp/utils/serving_lock.py +302 -0
  167. bpp/utils/subprocess_runner.py +349 -0
  168. bpp/utils/timing.py +34 -0
  169. bpp/utils/video.py +237 -0
  170. bpp/web/__init__.py +1 -0
  171. bpp/web/_deprecated_attr.py +92 -0
  172. bpp/web/analysis_store.py +133 -0
  173. bpp/web/analyze_archive.py +169 -0
  174. bpp/web/analyze_face_extract.py +443 -0
  175. bpp/web/analyze_finalize.py +77 -0
  176. bpp/web/analyze_model_preflight.py +86 -0
  177. bpp/web/analyze_phases.py +160 -0
  178. bpp/web/analyze_scoring.py +202 -0
  179. bpp/web/analyze_subprocess.py +73 -0
  180. bpp/web/analyze_worker.py +470 -0
  181. bpp/web/app.py +384 -0
  182. bpp/web/base_worker.py +224 -0
  183. bpp/web/bp_album_overrides.py +120 -0
  184. bpp/web/bp_albums.py +478 -0
  185. bpp/web/bp_analysis.py +451 -0
  186. bpp/web/bp_calendar.py +147 -0
  187. bpp/web/bp_catalog.py +201 -0
  188. bpp/web/bp_clip.py +103 -0
  189. bpp/web/bp_core.py +477 -0
  190. bpp/web/bp_export.py +361 -0
  191. bpp/web/bp_faces.py +455 -0
  192. bpp/web/bp_faces_bbox.py +351 -0
  193. bpp/web/bp_faces_cluster_ops.py +409 -0
  194. bpp/web/bp_faces_extract.py +172 -0
  195. bpp/web/bp_faces_manage.py +264 -0
  196. bpp/web/bp_faces_photo.py +241 -0
  197. bpp/web/bp_faces_recluster.py +116 -0
  198. bpp/web/bp_faces_review.py +475 -0
  199. bpp/web/bp_groups.py +108 -0
  200. bpp/web/bp_health.py +312 -0
  201. bpp/web/bp_inpaint.py +228 -0
  202. bpp/web/bp_install.py +244 -0
  203. bpp/web/bp_library.py +215 -0
  204. bpp/web/bp_logs.py +210 -0
  205. bpp/web/bp_media.py +344 -0
  206. bpp/web/bp_memories.py +78 -0
  207. bpp/web/bp_model_admin.py +202 -0
  208. bpp/web/bp_model_registry.py +405 -0
  209. bpp/web/bp_models.py +339 -0
  210. bpp/web/bp_os_integration.py +186 -0
  211. bpp/web/bp_pets.py +331 -0
  212. bpp/web/bp_photos.py +355 -0
  213. bpp/web/bp_photos_lifecycle.py +318 -0
  214. bpp/web/bp_photos_manage.py +443 -0
  215. bpp/web/bp_recompute.py +373 -0
  216. bpp/web/bp_search.py +339 -0
  217. bpp/web/bp_settings.py +273 -0
  218. bpp/web/bp_share.py +307 -0
  219. bpp/web/bp_tags.py +222 -0
  220. bpp/web/clip_worker.py +227 -0
  221. bpp/web/derived_recovery.py +255 -0
  222. bpp/web/export_worker.py +149 -0
  223. bpp/web/face_create_helpers.py +260 -0
  224. bpp/web/face_crop.py +81 -0
  225. bpp/web/face_extraction_journal.py +418 -0
  226. bpp/web/face_extraction_phase5.py +346 -0
  227. bpp/web/face_extraction_phase6.py +140 -0
  228. bpp/web/face_extraction_phases.py +436 -0
  229. bpp/web/face_merge_core.py +163 -0
  230. bpp/web/face_orchestrator.py +141 -0
  231. bpp/web/face_phase_classes.py +299 -0
  232. bpp/web/face_phase_pipeline.py +265 -0
  233. bpp/web/face_phase_types.py +130 -0
  234. bpp/web/face_recovery.py +253 -0
  235. bpp/web/face_worker.py +287 -0
  236. bpp/web/filters.py +57 -0
  237. bpp/web/health.py +131 -0
  238. bpp/web/import_worker.py +230 -0
  239. bpp/web/library_lifecycle.py +82 -0
  240. bpp/web/model_cache.py +144 -0
  241. bpp/web/model_filter.py +150 -0
  242. bpp/web/models_status.py +309 -0
  243. bpp/web/photo_dict.py +237 -0
  244. bpp/web/photo_edits.py +380 -0
  245. bpp/web/recompute.py +322 -0
  246. bpp/web/request_validation.py +192 -0
  247. bpp/web/review_meta.py +56 -0
  248. bpp/web/share.py +493 -0
  249. bpp/web/share_devices.py +250 -0
  250. bpp/web/share_proxy.py +199 -0
  251. bpp/web/share_qr.py +146 -0
  252. bpp/web/share_runtime.py +221 -0
  253. bpp/web/sse.py +60 -0
  254. bpp/web/state.py +488 -0
  255. bpp/web/state_compat.py +185 -0
  256. bpp/web/state_helpers.py +112 -0
  257. bpp/web/state_init.py +318 -0
  258. bpp/web/state_init_phases.py +250 -0
  259. bpp/web/state_lifecycle.py +280 -0
  260. bpp/web/state_ops.py +162 -0
  261. bpp/web/static/css/app.css +7608 -0
  262. bpp/web/static/img/apple-touch-icon.png +0 -0
  263. bpp/web/static/img/icon-192.png +0 -0
  264. bpp/web/static/img/icon-512.png +0 -0
  265. bpp/web/static/js/globals.js +446 -0
  266. bpp/web/static/js/modules/action-registry.mjs +109 -0
  267. bpp/web/static/js/modules/activity-humanize.mjs +185 -0
  268. bpp/web/static/js/modules/activity-log.mjs +350 -0
  269. bpp/web/static/js/modules/albums-menus.mjs +400 -0
  270. bpp/web/static/js/modules/albums-render-helpers.mjs +61 -0
  271. bpp/web/static/js/modules/albums-render.mjs +505 -0
  272. bpp/web/static/js/modules/albums-switch.mjs +292 -0
  273. bpp/web/static/js/modules/albums.mjs +356 -0
  274. bpp/web/static/js/modules/analysis-install.mjs +112 -0
  275. bpp/web/static/js/modules/analysis-preflight.mjs +160 -0
  276. bpp/web/static/js/modules/analysis-recompute.mjs +257 -0
  277. bpp/web/static/js/modules/analysis-status.mjs +347 -0
  278. bpp/web/static/js/modules/analysis.mjs +444 -0
  279. bpp/web/static/js/modules/api-client.mjs +141 -0
  280. bpp/web/static/js/modules/app.mjs +412 -0
  281. bpp/web/static/js/modules/batch-rename.mjs +308 -0
  282. bpp/web/static/js/modules/calendar-render.mjs +307 -0
  283. bpp/web/static/js/modules/calendar-selection.mjs +214 -0
  284. bpp/web/static/js/modules/calendar.mjs +433 -0
  285. bpp/web/static/js/modules/client-error-beacon.mjs +56 -0
  286. bpp/web/static/js/modules/clip.mjs +370 -0
  287. bpp/web/static/js/modules/compare-sibling.mjs +311 -0
  288. bpp/web/static/js/modules/compare.mjs +450 -0
  289. bpp/web/static/js/modules/constants.mjs +31 -0
  290. bpp/web/static/js/modules/core.mjs +409 -0
  291. bpp/web/static/js/modules/date-format.mjs +96 -0
  292. bpp/web/static/js/modules/deleted-ctx-menu.mjs +228 -0
  293. bpp/web/static/js/modules/deleted-enhance.mjs +160 -0
  294. bpp/web/static/js/modules/deleted.mjs +440 -0
  295. bpp/web/static/js/modules/dialogs.mjs +157 -0
  296. bpp/web/static/js/modules/dupe-review.mjs +222 -0
  297. bpp/web/static/js/modules/editor-constants.mjs +295 -0
  298. bpp/web/static/js/modules/editor-crop.mjs +382 -0
  299. bpp/web/static/js/modules/editor-inpaint.mjs +427 -0
  300. bpp/web/static/js/modules/editor-preview.mjs +199 -0
  301. bpp/web/static/js/modules/editor-redeye.mjs +118 -0
  302. bpp/web/static/js/modules/editor-rendering.mjs +411 -0
  303. bpp/web/static/js/modules/editor-styles.mjs +312 -0
  304. bpp/web/static/js/modules/editor-tools.mjs +227 -0
  305. bpp/web/static/js/modules/editor.mjs +456 -0
  306. bpp/web/static/js/modules/face-embedders-acceptance.mjs +440 -0
  307. bpp/web/static/js/modules/face-embedders-actions.mjs +270 -0
  308. bpp/web/static/js/modules/face-embedders-popover.mjs +173 -0
  309. bpp/web/static/js/modules/face-embedders-rowstate.mjs +499 -0
  310. bpp/web/static/js/modules/face-embedders-state.mjs +142 -0
  311. bpp/web/static/js/modules/faces-extraction.mjs +183 -0
  312. bpp/web/static/js/modules/faces.mjs +350 -0
  313. bpp/web/static/js/modules/format-helpers.mjs +67 -0
  314. bpp/web/static/js/modules/groups.mjs +322 -0
  315. bpp/web/static/js/modules/import-worker.mjs +274 -0
  316. bpp/web/static/js/modules/inspector.mjs +338 -0
  317. bpp/web/static/js/modules/library.mjs +305 -0
  318. bpp/web/static/js/modules/lightbox-actions.mjs +450 -0
  319. bpp/web/static/js/modules/lightbox-ctxmenu.mjs +77 -0
  320. bpp/web/static/js/modules/lightbox-face-assign.mjs +382 -0
  321. bpp/web/static/js/modules/lightbox-face-edit.mjs +479 -0
  322. bpp/web/static/js/modules/lightbox-face-overlays.mjs +336 -0
  323. bpp/web/static/js/modules/lightbox-face-picker.mjs +475 -0
  324. bpp/web/static/js/modules/lightbox-info.mjs +375 -0
  325. bpp/web/static/js/modules/lightbox-input.mjs +369 -0
  326. bpp/web/static/js/modules/lightbox-open.mjs +280 -0
  327. bpp/web/static/js/modules/lightbox.mjs +462 -0
  328. bpp/web/static/js/modules/map.mjs +273 -0
  329. bpp/web/static/js/modules/memories.mjs +210 -0
  330. bpp/web/static/js/modules/modal.mjs +109 -0
  331. bpp/web/static/js/modules/modals-face-embedders.mjs +496 -0
  332. bpp/web/static/js/modules/modals-models-list.mjs +226 -0
  333. bpp/web/static/js/modules/modals-models.mjs +311 -0
  334. bpp/web/static/js/modules/modals.mjs +487 -0
  335. bpp/web/static/js/modules/moments-stacks.mjs +414 -0
  336. bpp/web/static/js/modules/moments-view.mjs +82 -0
  337. bpp/web/static/js/modules/navigation.mjs +74 -0
  338. bpp/web/static/js/modules/nudges.mjs +151 -0
  339. bpp/web/static/js/modules/on-this-day.mjs +183 -0
  340. bpp/web/static/js/modules/onboarding.mjs +406 -0
  341. bpp/web/static/js/modules/people-actions.mjs +229 -0
  342. bpp/web/static/js/modules/people-album-bar.mjs +441 -0
  343. bpp/web/static/js/modules/people-ctx-menu.mjs +238 -0
  344. bpp/web/static/js/modules/people-merge.mjs +214 -0
  345. bpp/web/static/js/modules/people-pair-review.mjs +400 -0
  346. bpp/web/static/js/modules/people-pickers.mjs +421 -0
  347. bpp/web/static/js/modules/people-rename.mjs +259 -0
  348. bpp/web/static/js/modules/people-review.mjs +475 -0
  349. bpp/web/static/js/modules/people-view.mjs +224 -0
  350. bpp/web/static/js/modules/people.mjs +383 -0
  351. bpp/web/static/js/modules/pets-pickers.mjs +375 -0
  352. bpp/web/static/js/modules/pets.mjs +380 -0
  353. bpp/web/static/js/modules/phash-status.mjs +74 -0
  354. bpp/web/static/js/modules/photo-preview.mjs +79 -0
  355. bpp/web/static/js/modules/photos-batch.mjs +252 -0
  356. bpp/web/static/js/modules/photos-card.mjs +124 -0
  357. bpp/web/static/js/modules/photos-helpers.mjs +45 -0
  358. bpp/web/static/js/modules/photos-hover.mjs +129 -0
  359. bpp/web/static/js/modules/photos-select.mjs +129 -0
  360. bpp/web/static/js/modules/photos-vgrid.mjs +229 -0
  361. bpp/web/static/js/modules/photos.mjs +489 -0
  362. bpp/web/static/js/modules/presets.mjs +223 -0
  363. bpp/web/static/js/modules/review-meta.mjs +58 -0
  364. bpp/web/static/js/modules/score-format.mjs +71 -0
  365. bpp/web/static/js/modules/search.mjs +499 -0
  366. bpp/web/static/js/modules/sensitive.mjs +299 -0
  367. bpp/web/static/js/modules/settings-client.mjs +91 -0
  368. bpp/web/static/js/modules/share-tab.mjs +389 -0
  369. bpp/web/static/js/modules/sidebar-safety.mjs +265 -0
  370. bpp/web/static/js/modules/slideshow.mjs +390 -0
  371. bpp/web/static/js/modules/state.mjs +206 -0
  372. bpp/web/static/js/modules/tags-view.mjs +279 -0
  373. bpp/web/static/js/modules/tags.mjs +409 -0
  374. bpp/web/static/js/modules/text-format.mjs +78 -0
  375. bpp/web/static/js/modules/theme.mjs +62 -0
  376. bpp/web/static/js/modules/timeline.mjs +163 -0
  377. bpp/web/static/js/modules/toast.mjs +140 -0
  378. bpp/web/static/js/modules/toolbar.mjs +426 -0
  379. bpp/web/static/js/modules/tooltip.mjs +112 -0
  380. bpp/web/static/js/modules/tour.mjs +429 -0
  381. bpp/web/static/js/modules/ui-helpers.mjs +81 -0
  382. bpp/web/static/js/modules/update-checker.mjs +157 -0
  383. bpp/web/static/js/modules/use-context-options.mjs +52 -0
  384. bpp/web/static/js/modules/utils.mjs +450 -0
  385. bpp/web/static/js/modules/view-guard.mjs +132 -0
  386. bpp/web/static/js/modules/wizard.mjs +198 -0
  387. bpp/web/static/manifest.json +24 -0
  388. bpp/web/static/vendor/README.md +35 -0
  389. bpp/web/static/vendor/leaflet/LICENSE +25 -0
  390. bpp/web/static/vendor/leaflet/dist/images/layers-2x.png +0 -0
  391. bpp/web/static/vendor/leaflet/dist/images/layers.png +0 -0
  392. bpp/web/static/vendor/leaflet/dist/images/marker-icon-2x.png +0 -0
  393. bpp/web/static/vendor/leaflet/dist/images/marker-icon.png +0 -0
  394. bpp/web/static/vendor/leaflet/dist/images/marker-shadow.png +0 -0
  395. bpp/web/static/vendor/leaflet/dist/leaflet.css +661 -0
  396. bpp/web/static/vendor/leaflet/dist/leaflet.js +6 -0
  397. bpp/web/static/vendor/leaflet.markercluster/MIT-LICENCE.txt +21 -0
  398. bpp/web/static/vendor/leaflet.markercluster/dist/MarkerCluster.Default.css +60 -0
  399. bpp/web/static/vendor/leaflet.markercluster/dist/MarkerCluster.css +14 -0
  400. bpp/web/static/vendor/leaflet.markercluster/dist/leaflet.markercluster.js +2 -0
  401. bpp/web/templates/index.html +1266 -0
  402. bpp/web/templates/pair.html +207 -0
  403. bpp/web/thumbnails.py +239 -0
  404. bpp/web/update_checker.py +183 -0
  405. bpp/web/worker_pool.py +128 -0
  406. bpp/web/worker_registry.py +101 -0
  407. bppicker-0.1.0.dist-info/METADATA +698 -0
  408. bppicker-0.1.0.dist-info/RECORD +412 -0
  409. bppicker-0.1.0.dist-info/WHEEL +4 -0
  410. bppicker-0.1.0.dist-info/entry_points.txt +2 -0
  411. bppicker-0.1.0.dist-info/licenses/LICENSE +21 -0
  412. 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())
@@ -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
+ ]