203 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-19 17:31:12 +00:00
1 parent 279159581d
commit 99467464e4
203 files changed
+2035 -241

No files matched your search

+59 -4
View File
@@ -52,6 +52,29 @@ _SKIP_SEARCH_LIMIT = 30
_MAX_RENDER_ATTEMPTS = 10
# Fixed floor (in items) for the pairing cooldown exclusion zone, applied on
# each side of the current index in addition to the percentage-based radius.
# Keeps tiny albums from pairing consecutive/near-consecutive shots even when
# a small percentage rounds down to zero.
_PAIR_GAP_FLOOR = 2
def _pair_exclusion_radius(n: int, min_gap_percent: float) -> int:
"""Items to exclude on each side of the current index for pairing.
Combines ``_PAIR_GAP_FLOOR`` with a percentage of the album size so tiny
albums still get some separation while large albums scale
proportionally. A ``min_gap_percent`` of 0 disables the cooldown
entirely. The result is capped well below ``n / 2`` so eligible
candidates always remain, even for small albums (a ring buffer of
``n`` items only has ``n - 1`` non-self candidates to begin with).
"""
if n <= 3 or min_gap_percent <= 0:
return 0
radius = max(_PAIR_GAP_FLOOR, round(n * (min_gap_percent / 100.0)))
max_radius = (n - 3) // 2
return max(0, min(radius, max_radius))
@dataclass(frozen=True, slots=True)
class _NavigationCursor:
@@ -315,6 +338,7 @@ class AlbumSlideshowCamera(Camera):
"aspect_ratio": self.store.aspect_ratio,
"pair_divider_px": int(self.store.pair_divider_px),
"pair_divider_color": self.store.pair_divider_color,
"pair_min_gap_percent": int(self.store.pair_min_gap_percent),
"frame_id": self._frame_id,
"navigation_buffer_size": self._buffer_depth,
"previous_frames_cached": len(self._previous_frames),
@@ -1129,6 +1153,27 @@ class AlbumSlideshowCamera(Camera):
) -> tuple[Image.Image, MediaItem] | None:
"""Find an image with the opposite orientation of the canvas.
By default (``pair_min_gap_percent`` = 0) this is the original
search: walk forward from the very next item, nearest candidate
wins, fully deterministic. Setting ``pair_min_gap_percent`` above 0
opts into a cooldown search instead - candidates within that
percentage of the current index (wrapping on both sides, since the
album is a ring buffer) are excluded so the partner comes from a
meaningfully different part of the collection, and the remaining
candidates are shuffled before scanning so the same nearby partner
isn't picked deterministically every time. Small albums where the
cooldown would leave no candidates fall back to the deterministic
search (see ``_pair_exclusion_radius``); no eligible partner at all
(orientation, recent-repeat, or download failures) is a normal
outcome and simply returns ``None``, which the caller renders as a
single unpaired image.
This is a pure function of ``self._index``, ``self._recent_urls``
and ``self._rng``'s state, which is exactly what the navigation
buffer's cursor snapshots capture - so replaying a cached cursor
(Previous/Next) always reproduces the same paired frame; only
rendering a slide for the first time consumes new randomness.
Uses metadata wherever possible - only candidates without width/height
metadata are downloaded and decoded for their orientation. The returned
PIL image is the caller's to close. The matching ``MediaItem`` is
@@ -1137,12 +1182,22 @@ class AlbumSlideshowCamera(Camera):
if not items:
return None
n = len(items)
radius = _pair_exclusion_radius(n, self.store.pair_min_gap_percent)
if radius > 0:
excluded = {(self._index + d) % n for d in range(-radius, radius + 1)}
candidates = [idx for idx in range(n) if idx not in excluded]
self._rng.shuffle(candidates)
else:
candidates = (
(self._index + offset) % n
for offset in range(1, min(n, limit + 1))
)
tries = 0
offset = 1
while tries < limit and offset < n:
idx = (self._index + offset) % n
for idx in candidates:
if tries >= limit:
break
it = items[idx]
offset += 1
tries += 1
if it.url in self._recent_urls: