Feature/photo selection two page f12b8b#2
Merged
rokas-ambrazevicius merged 22 commits intoJul 12, 2026
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.