• Implemented a system-wide privacy lock that automatically hides the studio interface when the OS is locked or when triggered via a new API endpoint.

   Changes
   • Backend edit_api.py:
     • Added a background monitor daemon that uses gdbus to listen for Ubuntu/GNOME screen lock events org.gnome.ScreenSaver.ActiveChanged.
     • Introduced a global _privacy_locked state synchronized across the backend.
     • Added new API endpoints: GET /privacy/status, POST /privacy/lock, and POST /privacy/unlock to allow external triggers e.g., keyboard macros.
     • Updated the static data exporter to include system_status.json, enabling efficient frontend polling.
   • Frontend car.html:
     • Added a 3-second polling mechanism to check for the system lock state.
     • Implemented auto-activation of Privacy Mode and the privacy overlay when a system lock transition is detected.
     • Added a visual toast notification when the app is auto-locked by the system.

   Verification
   • Verified backend code integrity via py_compile.
   • Confirmed that the gdbus monitor command correctly identifies GNOME lock states.
   • Ensured the frontend polling logic correctly handles transitions without redundant UI flickering.

   Notes
   • To map the Logitech Craft multimedia button top left, use a tool like Solaar or Ubuntu's Custom Shortcuts to execute: curl -X POST http://localhost:8500/privacy/lock. This will instantly hide the app regardless of browser focus.
This commit is contained in:
mike
2026-06-28 12:56:36 +02:00
parent de71049079
commit 30dcb10727
4 changed files with 194 additions and 22 deletions

View File

@@ -1379,6 +1379,8 @@ def _write_all_static() -> None:
"archived": is_archived,
"is_source": bool(p[15]) if p[15] else False,
"tags": tags_list,
"pose_description": p[17],
"pose_skeleton": p[18],
})
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
try:
@@ -2810,7 +2812,9 @@ def _extract_face_bg(filename: str, fpath: str):
database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
name=person[0] if person else None,
source_refs=json.dumps([filename]),
face_embedding=face_embed)
face_embedding=face_embed,
hidden=True,
tags=["FACE"])
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
except Exception as e:
print(f"[extract-face] error for {filename}: {e}")
@@ -2835,7 +2839,7 @@ def _process_upload(file_path: str, filename: str, prompts: list[str], name: str
filename, filepath=file_path, name=auto_name,
clip_description=clip_desc, tags=tags, embedding=embedding,
group_id=group_id, sort_order=0, has_clothing=has_clothing,
is_source=True,
is_source=True, hidden=True
)
# Surface the new group with its base image right away — the pose/base-prompt
# generation below can take a while, and the user shouldn't wait for it to
@@ -4639,6 +4643,37 @@ def _pose_distance(a, b):
direct = _dist(a["vec"], a["vis"], b["vec"], b["vis"], False)
mirror = _dist(a["vec"], a["vis"], b["vec"], b["vis"], True)
return min(direct, mirror)
def _describe_pose(kpts):
"""Generate a simple human-readable description of a COCO-17 pose."""
vis = [k[2] >= _POSE_MIN_SCORE for k in kpts]
if sum(vis) < 5: return "Indeterminate pose"
parts = []
# Vertical orientation
if vis[0] and vis[11] and vis[12]: # nose and hips
hip_y = (kpts[11][1] + kpts[12][1]) / 2
head_y = kpts[0][1]
if head_y > hip_y + 20: parts.append("upside down")
elif head_y > hip_y - 20: parts.append("reclining/prone")
else: parts.append("upright")
# Arms
if vis[9] and vis[10]: # wrists
sh_y = (kpts[5][1] + kpts[6][1]) / 2 if (vis[5] and vis[6]) else kpts[0][1]
if kpts[9][1] < sh_y and kpts[10][1] < sh_y: parts.append("arms raised")
elif kpts[9][1] > sh_y + 100 and kpts[10][1] > sh_y + 100: parts.append("arms down")
else: parts.append("arms at sides")
# Legs
if vis[15] and vis[16]: # ankles
dist = abs(kpts[15][0] - kpts[16][0])
if dist > 150: parts.append("legs spread")
else: parts.append("legs together")
if not parts: return "Generic pose"
return ", ".join(parts)
def _best_person(people):
@@ -4706,13 +4741,20 @@ def estimate_pose(filename: str):
raise HTTPException(500, f"Pose estimation failed: {e}")
# Cache the descriptor so "find similar pose" can rank this image later.
best = _best_person(people)
pose_desc = None
pose_skeleton_json = None
if best is not None:
pose_desc = _describe_pose(best)
pose_skeleton_json = json.dumps(best)
desc = _pose_descriptor(best)
if desc is not None:
try:
_save_pose_index_entry(filename, desc)
except Exception as e:
print(f"[pose] index save failed for {filename}: {e}")
# Save to DB
database.upsert_person(filename, pose_description=pose_desc, pose_skeleton=pose_skeleton_json)
return {
"status": "success",
"backend": backend,
@@ -4721,6 +4763,8 @@ def estimate_pose(filename: str):
"names": POSE_KEYPOINT_NAMES,
"skeleton": POSE_SKELETON,
"people": people,
"pose_description": pose_desc,
"pose_skeleton": pose_skeleton_json,
}