Summary
• 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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user