Skip to content

Feature/photo selection two page f12b8b#2

Merged
rokas-ambrazevicius merged 22 commits into
mainfrom
feature/photo-selection-two-page-f12b8b
Jul 12, 2026
Merged

Feature/photo selection two page f12b8b#2
rokas-ambrazevicius merged 22 commits into
mainfrom
feature/photo-selection-two-page-f12b8b

Conversation

@rokas-ambrazevicius

Copy link
Copy Markdown
Contributor

No description provided.

rokas-ambrazevicius and others added 22 commits July 12, 2026 09:44
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nalyzer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feature-print near-duplicate clustering for best-N photo curation.

- CurationCandidate (PhotoBookCore, pure): quality/captureDate/clusterID/isUtility
- CurationAnalyzer (PhotoBookImport): pure union-find clusters() with
  time-bounded sliding window (undated photos count-capped at 32), plus
  Vision VNGenerateImageFeaturePrintRequest production entry candidates()
- ImageContentAnalyzer.analyzeWithScores surfaces per-photo scores; analyze
  delegates to it (no behavior change)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add CurationStepModel (target picking, best-N analysis via Vision,
review/adjust) and a thin CurationStepView, wired into NewBookSetupView
between photo selection and preset choice. PhotoCurator.resolvedPhotoCount
extracted so the UI can preview "≈ N photos" for a pages target.
generate() skips its own analysis pass when curation already stamped
importance, avoiding a duplicate Vision run.
The 3,000-photo curation flow decoded every thumbnail twice (scoring,
then feature-print clustering); on PhotoKit/iCloud the second pass could
re-hit the network.

- ImageContentAnalyzer.analyzeWithScores now also returns per-photo
  FeaturePrints, computed on the same decoded thumbnail (includePrints
  flag; plain analyze passes false so non-curation callers pay nothing)
- FeaturePrint: public @unchecked Sendable box over the immutable
  VNFeaturePrintObservation, with a distance(to:) helper
- CurationAnalyzer.candidates is now synchronous and takes precomputed
  prints; its own thumbnail pipeline is dropped (the wizard was the only
  caller, so no self-computing fallback)
- CurationStepModel.startAnalysis: one honest progress pass (total = N,
  not 2N); clustering runs inline on the precomputed prints
- Parity test: candidates' clusterIDs match the pure core run on the
  same prints' distances

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hero-full (1-photo full-canvas spread), split-two-thirds /
split-two-thirds-3 (2:1 big+stacked-small split), center-columns-3 /
center-columns-5 (center column + side columns), panorama-band-3
(horizontal band + top/bottom strips). Dominant frame listed first in
each template's photoFrames array, matching BookEngine's convention of
binding photos to slots in array order.

Tests verify real geometry (tiling area + pairwise overlap, not just
counts) plus hero-full's slice() behavior across the gutter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hero-full was a pixel-identical duplicate of the pre-existing
spread-panorama; nothing binds template ids yet so removal is safe.
New template ids gain the spread- prefix for consistency with the
existing entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spreads always rendered framed because buildSpread hardcoded
style.pageMargin and style.gutter, ignoring style.edgeStyle. Mirror
JustifiedProvider: drop the outer margin unless .framed, drop the gutter
under .borderless. Book-level edgeStyle is the source (spreads are built
before member pages, so no per-page override to resolve). Unblocks B4
hero full-bleed spreads reaching the paper edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bias a straddling spread slot's crop so a photo's salient center projects
outside an 8%-of-spread band centered on the spine. Pure GutterSafeCrop
returns a slot-aspect-matched crop (exact aspect-fill, no re-crop);
buildSpread applies it per bound photo, consuming no seeds/ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A lone photo with importance >= ImportanceWeight.heroThreshold AND
aspect >= heroAspectThreshold (1.2) is now auto-promoted to a full
2-page hero spread in makeBook, alongside the existing panorama path.

Caps: at most one hero per time-cluster, and >= heroSpreadMinSpacing (6)
standard pages between hero spreads (first hero has no spacing gate).
Panorama spreads are a separate mechanism — they neither consume a
hero-cluster slot nor reset the spacing counter. Promotion routes to the
existing buildSpread (zero-crop, gutter-safe, EdgeStyle-aware).

Determinism: the branch is derived purely from photo data + emission
order and the group seed is consumed unconditionally, so books with no
qualifying hero stay byte-identical (SHA256 regression test).

repaginate/repaginateBook are unchanged: like panoramas, hero promotion
happens only at makeBook; existing spread members bound reflow runs and
are copied verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BookEngine.spreadLayoutOptions/applySpreadTemplate let a spread be
re-laid to any bundled template matching its current photo count:
photos rebind in slot order, gutter-safe crop is recomputed per
straddling slot, and member pages are re-sliced — all deterministic
(same salt as buildSpread, so re-applying a template is byte-identical).

ModelLayer exposes spreadTemplateOptions + applySpreadTemplate, wired
through the same apply()/undo funnel as every other edit. BookBrowserView
now shows a spread-template strip (2x page aspect wireframes) instead of
the page-layout strip when the selection is a spread member.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spread.slotIDGenerator(for:) is now the single source of the slot-id
salt (stableSeed &+ 0x5170A11); buildSpread and applySpreadTemplate
both call it, so the byte-stability coupling is a symbol, not a prose
comment. Spread strip tooltip shows "layout N" instead of the raw
template slug (slug stays in the accessibility identifier).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
convertToSpread mints fresh member-page IDs, leaving selectedPageID
dead after convertSelectedSpread — refreshAlternatives' guard then
cleared spreadTemplateOptions and no strip rendered. Re-point selection
at the new spread's left member (found via the appended spread, not a
photo scan — the cover duplicates the lead photo, which can misdirect
reselectPhoto) and refresh again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rokas-ambrazevicius rokas-ambrazevicius merged commit 519c0d4 into main Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant