Add calendar frame mode + server-side manage overlay (server)
Build and push server image / build-and-push (push) Successful in 42s
Build and push server image / build-and-push (push) Successful in 42s
calendar_feed.py/calendar_render.py: fetch/merge per-user ICS feeds, render agenda/week/month views. manage_overlay.py: composites the manage-button overlay server-side (QR, battery, location/date, share-QR, face labels), reused by every render mode. device.py/common.py wire both together: mode dispatch for /frame/image+advance+back, and the &manage=1 flag. Plus UI (frame_config.html Calendar card, settings calendar URL field) and the icalendar/recurring-ical-events deps.
This commit is contained in:
+19
-24
@@ -1,6 +1,6 @@
|
||||
"""Maps named faces (from Immich's own face recognition/People feature)
|
||||
onto their position in the final rendered 800x480 frame, for the
|
||||
manage-button overlay's escalated "who's in this photo" menu level.
|
||||
onto their position in the final rendered frame, for the manage-button
|
||||
overlay's named-face labels (see manage_overlay.py, which draws them).
|
||||
|
||||
No face detection or recognition happens here or anywhere else in this
|
||||
project -- Immich's GET /api/faces?id={assetId} already returns each
|
||||
@@ -15,32 +15,32 @@ import io
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from .image_pipeline import _has_bounding_box, _placement_transform, logical_render_size, logical_to_native
|
||||
from .image_pipeline import _has_bounding_box, _placement_transform, logical_render_size
|
||||
|
||||
# Small caps, not arbitrary: each label is its own malloc'd overlay
|
||||
# buffer on the device (see firmware/main/manage_qr_overlay.c), and the
|
||||
# four existing fixed corner regions already use a meaningful chunk of
|
||||
# the ESP32-C6's limited RAM. Capping at 4 short names keeps the total
|
||||
# overlay memory budget well clear of the WiFi/HTTP stack's own needs.
|
||||
MAX_LABELED_FACES = 4
|
||||
NAME_MAX_LEN = 10
|
||||
# Not a memory constraint anymore (the overlay renders server-side now,
|
||||
# not malloc'd per-label on the device) -- purely a legibility cap. A
|
||||
# photo with a dozen named people would just be visual clutter regardless
|
||||
# of what's rendering it.
|
||||
MAX_LABELED_FACES = 6
|
||||
|
||||
|
||||
def compute_face_labels(preview_bytes: bytes, faces: list[dict], display_mode: str,
|
||||
orientation: str = "landscape") -> list[dict]:
|
||||
"""Returns up to MAX_LABELED_FACES [{"name", "x", "y"}], x/y in native
|
||||
800x480 panel pixel space at each named face's bottom-center point.
|
||||
"""Returns up to MAX_LABELED_FACES [{"name", "x", "y"}], x/y in
|
||||
logical (pre-rotation) frame space at each named face's bottom-center
|
||||
point -- manage_overlay.compose() draws these directly onto the
|
||||
logical-space image before it's rotated into native panel space, so
|
||||
no rotation happens here (contrast with the old firmware-side
|
||||
version, which drew post-rotation and needed logical_to_native).
|
||||
Faces without an Immich-identified person name are skipped entirely.
|
||||
preview_bytes must be the same preview image render_frame() used for
|
||||
the currently-displayed frame, and display_mode/orientation must
|
||||
match the settings that were active then -- otherwise the placement
|
||||
and rotation computed here won't match what's actually on screen.
|
||||
computed here won't match what's actually on screen.
|
||||
|
||||
The placement math runs in logical (pre-rotation) space, matching
|
||||
render_frame()'s composition step (see image_pipeline._placement_transform,
|
||||
shared so the two can't drift apart); each anchor is then rotated
|
||||
into native panel coordinates via logical_to_native(), since the
|
||||
firmware draws labels in native space.
|
||||
The placement math matches render_frame()'s own composition step
|
||||
exactly (see image_pipeline._placement_transform, shared so the two
|
||||
can't drift apart).
|
||||
"""
|
||||
named = [face for face in faces if (face.get("person") or {}).get("name")]
|
||||
if not named:
|
||||
@@ -71,11 +71,6 @@ def compute_face_labels(preview_bytes: bytes, faces: list[dict], display_mode: s
|
||||
if not (0 <= frame_x <= logical_w and 0 <= frame_y <= logical_h):
|
||||
continue # this face got cropped out of the final frame entirely
|
||||
|
||||
name = face["person"]["name"]
|
||||
if len(name) > NAME_MAX_LEN:
|
||||
name = name[: NAME_MAX_LEN - 3] + "..."
|
||||
|
||||
native_x, native_y = logical_to_native(frame_x, frame_y, orientation)
|
||||
labels.append({"name": name, "x": native_x, "y": native_y})
|
||||
labels.append({"name": face["person"]["name"], "x": int(frame_x), "y": int(frame_y)})
|
||||
|
||||
return labels
|
||||
|
||||
Reference in New Issue
Block a user