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