Portrait handling improvements#342
Draft
artyfarty wants to merge 5 commits into
Draft
Conversation
Temporal clustering of the Immich asset pool: sort by localDateTime, group assets within a configurable gap, play one random representative per cluster. Bursts and photo-heavy single events collapse to one slot each so they don't dominate against long-span collection albums. Albums matching an optional regex (e.g. film scans, where timestamps are scanning-session artefacts) bypass clustering and are sampled by a configurable keep-percent. Settings live under a new "Smart slideshow" category in the Immich source screen; disabled by default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the smart slideshow picked one representative per temporal
cluster at fetch time and froze that choice for the lifetime of the
playlist — across an entire session the same cluster always showed the
same photo, with the other members completely hidden until the next
provider refresh.
Now every cluster's other members travel with the representative on the
AerialMedia object (new `clusterAlternates: List<Uri>` field). When the
slot comes up for rendering, ImagePlayerView picks one of
{primary} ∪ {alternates} uniformly at random. Over repeated playlist
loops the user sees the whole cluster rotate through the same slot,
while the clustering's airtime-evening effect on album-vs-album mix is
preserved.
Attached metadata (album name, source pool, date, location) stays that
of the representative — cluster members are by definition within a
small time/location window so the numbers remain representative enough
for overlay captions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Portrait photos shown in a landscape viewport normally get center-cropped and lose the top/bottom third of the image. This adds a third option to the existing photo scale preference — a slow vertical pan that shows the full image over the slideshow slot duration. * New PhotoScale.KEN_BURNS_VERTICAL value (selectable from the existing Portrait / Landscape scale prefs; safely falls back to center-crop when applied to images that don't actually overflow vertically). * ImagePlayerView sets scaleType MATRIX and animates imageMatrix Y translation from 0 to -(overflow) over the slot duration, alternating direction per slot for variety. Pause/resume respected via ValueAnimator.pause/resume. * Works uniformly for all portrait assets — does not depend on any subject/face metadata. Landscape assets with this mode set fall back to center-crop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in "Face-aware portrait crop" toggle that uses Immich's
ML-detected face bounding boxes to bias how portrait photos are framed.
* New pref `photo_scale_face_aware` (off by default) in the existing
Appearance → Scaling screen.
* `Asset` gains nullable `width/height/people` fields. `people` is only
populated by `GET /api/assets/{id}` (in Immich 2.7.5 batch/search
endpoints return an empty people array even when withPeople=true).
* `ImmichRepository.enrichPortraitsWithFaces(...)` parallel-fetches the
per-asset detail endpoint for portrait-aspect assets and extracts the
largest face's bounding box, normalized against the ML-preview frame
(`asset_face.imageWidth/imageHeight` — NOT `asset.width/height`).
* `AerialMediaMetadata.subjectRect: NormalizedRect?` carries that rect
through to the renderer.
* `ImagePlayerView.setScaleMode`, for portraits only:
- CENTER_CROP + face → MATRIX with face's y-center at the rule-of-thirds
line (~33 % from top of visible band).
- KEN_BURNS_VERTICAL + face → pan range is narrowed so face + 7 % margin
stays visible throughout, and pan direction starts from the side of
the image where the face sits (upper-face → start at top pan down;
lower-face → start at bottom pan up).
- Face bigger than visible band → static center on face, no pan.
* Landscape and square images are untouched — face-aware code paths
short-circuit on aspect mismatch, preserving existing behavior.
Cost: one `GET /api/assets/{id}` per portrait on each fetch cycle.
On LAN with 10 parallel requests this adds ~1-3 s to initial load for
a library of ~900 portraits. Feature is opt-in so users who don't want
the delay keep the old behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the screensaver starts up, a slideshow fetch can take several seconds. Instead of a black rectangle behind the "Loading…" text, paint a JPEG snapshot of a photo shown during a recent session. * New SplashCache helper: saves/loads a single JPEG in the app's cacheDir (system may evict, which is fine — we just fall back to black). No schema, no DB, no migration. * ImagePlayerView saves a snapshot every 50th rendered photo (~20 min at slideshow_speed=25s), overwriting the previous file. Gentle on flash; across sessions the captured photo naturally varies so users don't see the same splash every launch. * ScreenController loads the file (if present) into the foreground ImageView before the media fetch is launched. The existing loading overlay sits on top; when the real first photo arrives via the normal load path, it replaces the splash seamlessly. No new preferences — the feature is always on when a cached file exists. Overlays/controls don't apply to the splash, matching the intent of a pure background placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Three portrait-oriented UX improvements bundled in one branch because they share the same renderer plumbing and two of them share data flow:
instead of crop-to-center losing the top/bottom thirds. Landscape images with the mode selected fall back to center-crop.
- CENTER_CROP: MATRIX-positioned so the face's y-center sits at the rule-of-thirds line.
- KEN_BURNS_VERTICAL: pan range is narrowed so the largest face stays in view throughout, direction chosen so pan starts from whichever side the face lives. Configurable "face allowed off-screen percent at pan extremes" (0–40%, default 0 = strict, higher = wider more
cinematic pan).
Why this branch is based on smart-slideshow (#341), not upstream/master
Commit 2 requires the clusterAlternates data model introduced by #341. When the Immich smart slideshow collapses a temporal burst into one slot, the cluster members now each need to carry their own face bbox (cluster gap can be up to 1 day, so alternates are often entirely
different scenes). This branch refactors clusterAlternates: List into List where each entry carries an optional NormalizedRect.
If #341 is merged first, the apparent size of this PR will shrink by those 2 commits. If there's a different preference — e.g. rebase commit 1 and 3 onto master since they don't depend on #341, and leave commit 2 stacked — happy to split.
Immich API quirk encountered
people[].faces[] is only populated by GET /api/assets/{id} in Immich 2.7.5. The batch /api/albums/{id} and /api/search/metadata endpoints return the field with an empty array even when withPeople: true or withFaces: true is passed. So enrichment makes one detail call per
portrait asset — parallelised, this adds ~1–3 s of startup time for ~900 portraits on LAN. Feature is opt-in to make the cost optional.
Face bounding boxes come from asset_face.imageWidth/imageHeight (the ML-preview frame Immich decoded on), not asset.width/height. Normalizing against the wrong dimensions gives consistently shifted boxes — documented inline.
Testing status
Expected adjustments before removing draft status