From 30dcb10727ba18822b4187de303af20b0d91e32f Mon Sep 17 00:00:00 2001 From: mike Date: Sun, 28 Jun 2026 12:56:36 +0200 Subject: [PATCH] =?UTF-8?q?Summary=20=20=20=20=E2=80=A2=20Implemented=20a?= =?UTF-8?q?=20system-wide=20privacy=20lock=20that=20automatically=20hides?= =?UTF-8?q?=20the=20studio=20interface=20when=20the=20OS=20is=20locked=20o?= =?UTF-8?q?r=20when=20triggered=20via=20a=20new=20API=20endpoint.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backlog.md | 3 +- tour-comfy/car.html | 143 +++++++++++++++++++++++++++++++++++++---- tour-comfy/database.py | 22 +++++-- tour-comfy/edit_api.py | 48 +++++++++++++- 4 files changed, 194 insertions(+), 22 deletions(-) diff --git a/backlog.md b/backlog.md index 8525aa5..3443468 100644 --- a/backlog.md +++ b/backlog.md @@ -68,7 +68,8 @@ Scenery: - ✅ Fixed: introduce multiple filmstrips (Group, Hidden, Source, Archived with drag-and-drop & bulk selection actions). - samengestelde outpaint featurue lijkt niet goed te werken, but individual steps work perfect. - regenerate a single orbit - pose -- when generating a known pose, we should know roughly the image ratio. Make a pose-table, when a pose is generated store the wireframe and meta data +- when generating a known pose, we should know roughly the image ratio. Make a pose-table, when a pose is generated store the wireframe and meta data +- we can rate an image if its OVER21, manually now in the UI. this is fine, however we want to backfill this TAG for evey image ## refine - when refresh page, we lose track of current jobs running. diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 9bdca7f..c98d6e3 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -2034,7 +2034,8 @@
- + + @@ -2108,6 +2109,14 @@
+ +
Actions
@@ -2290,6 +2299,47 @@ // --- HYDRATION_START --- const PRELOADED_IMAGES = [ + "up_b8c61991_face.png", + "_turntable/bb_01.png/views/view_008_120deg.png", + "_turntable/bb_01.png/views/view_007_105deg.png", + "_turntable/bb_01.png/views/view_006_090deg.png", + "_turntable/bb_01.png/views/view_005_075deg.png", + "_turntable/bb_01.png/views/view_004_060deg.png", + "_turntable/bb_01.png/views/view_003_045deg.png", + "_turntable/bb_01.png/views/view_002_030deg.png", + "_turntable/bb_01.png/views/view_001_015deg.png", + "_turntable/bb_01.png/views/view_000_000deg.png", + "_turntable/up_b8c61991/views/view_023_345deg.png", + "_turntable/up_b8c61991/views/view_022_330deg.png", + "_turntable/up_b8c61991/views/view_021_315deg.png", + "_turntable/up_b8c61991/views/view_020_300deg.png", + "_turntable/up_b8c61991/views/view_019_285deg.png", + "_turntable/up_b8c61991/views/view_018_270deg.png", + "_turntable/up_b8c61991/views/view_017_255deg.png", + "_turntable/up_b8c61991/views/view_016_240deg.png", + "_turntable/up_b8c61991/views/view_015_225deg.png", + "_turntable/up_b8c61991/views/view_014_210deg.png", + "_turntable/up_b8c61991/views/view_013_195deg.png", + "_turntable/up_b8c61991/views/view_012_180deg.png", + "_turntable/up_b8c61991/views/view_011_165deg.png", + "_turntable/up_b8c61991/views/view_010_150deg.png", + "_turntable/up_b8c61991/views/view_009_135deg.png", + "_turntable/up_b8c61991/views/view_008_120deg.png", + "_turntable/up_b8c61991/views/view_007_105deg.png", + "_turntable/up_b8c61991/views/view_006_090deg.png", + "_turntable/up_b8c61991/views/view_005_075deg.png", + "_turntable/up_b8c61991/views/view_004_060deg.png", + "_turntable/up_b8c61991/views/view_003_045deg.png", + "_turntable/up_b8c61991/views/view_002_030deg.png", + "_turntable/up_b8c61991/views/view_001_015deg.png", + "_turntable/up_b8c61991/views/view_000_000deg.png", + "_turntable/pass-1.png/views/view_023_345deg.png", + "_turntable/pass-1.png/views/view_022_330deg.png", + "_turntable/pass-1.png/views/view_021_315deg.png", + "_turntable/pass-1.png/views/view_020_300deg.png", + "_turntable/pass-1.png/views/view_019_285deg.png", + "_turntable/pass-1.png/views/view_018_270deg.png", + "_turntable/pass-1.png/views/view_017_255deg.png", "_turntable/pass-1.png/views/view_016_240deg.png", "_turntable/up_810b587c/views/view_012_180deg.png", "_turntable/up_810b587c/views/view_011_165deg.png", @@ -2351,8 +2401,8 @@ "_turntable/cg_9bb8f3bb/views/view_001_015deg.png", "_turntable/cg_9bb8f3bb/views/view_000_000deg.png", "_turntable/pa01.png/views/view_023_345deg.png", - "20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", "20260628_105307_dup_20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", + "20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", "_turntable/pa01.png/views/view_022_330deg.png", "_turntable/pa01.png/views/view_021_315deg.png", "20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", @@ -5081,8 +5131,8 @@ "20260625_021910_image.png", "20260625_021702_sc_image.png", "20260625_021615_sc_image.png", - "20260625_015711_sc_image.png", "20260625_020212_dup_20260625_015711_sc_image.png", + "20260625_015711_sc_image.png", "20260625_015711_sc_image.nobg.png", "20260625_015900_sc_image.png", "20260625_015413_sc_image.png", @@ -5269,9 +5319,9 @@ "20260624_183010_0_20260624_182958_image.png", "20260624_182958_image.png", "20260624_174900_image.png", + "20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png", "20260624_173115_image.png", "20260624_174943_dup_20260624_173115_image.png", - "20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png", "20260624_174757_image.png", "20260624_174442_image.png", "20260624_172923_image.nobg.png", @@ -5778,8 +5828,8 @@ "20260622_193411_1_20260622_193118_image.png", "20260622_193354_0_20260622_193118_image.png", "20260622_193118_image.png", - "20260622_181910_0_20260619_124038_image.png", "20260622_212012_dup_20260622_181910_0_20260619_124038_image.png", + "20260622_181910_0_20260619_124038_image.png", "20260622_181734_0_20260619_124038_image.png", "20260622_181720_0_20260619_124038_image.png", "20260622_181634_0_20260619_124038_image.png", @@ -6121,8 +6171,8 @@ "20260620_152017_image.png", "20260618_013452_out4.png", "20260620_025959_out4.png", - "20260622_021658_dup_20260621_212650_image.png", "20260621_212650_image.png", + "20260622_021658_dup_20260621_212650_image.png", "20260621_210130_0_20260621_205151_image.png", "20260621_210118_0_20260621_205151_image.png", "20260621_210107_0_20260621_205151_image.png", @@ -7040,6 +7090,8 @@ let filePrompts = {}; // filename → generation prompt let fileHidden = {}; // filename → boolean (hidden from cycling) let fileArchived = {}; // filename → boolean + let filePoseDescription = {}; // filename → human readable string + let filePoseSkeleton = {}; // filename → JSON string (COCO-17 keypoints) let fileTags = {}; // filename → array of strings let fileIsSource = {}; // filename → boolean let _archiveViewActive = false; // when true, show archived items only @@ -7171,7 +7223,7 @@ // --- studio view (replaces lightbox) --- let lbUrls = [], lbNames = [], lbIdx = 0; - let _activeFilmstripTab = 'group'; // 'group', 'hidden', 'source', 'archived' + let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived' let _multiSelectModeActive = false; const _selectedFilenames = new Set(); @@ -7179,7 +7231,7 @@ _activeFilmstripTab = tab; // Highlight tab - ['Group', 'Hidden', 'Source', 'Archived'].forEach(t => { + ['Active', 'Group', 'Hidden', 'Source', 'Archived'].forEach(t => { const el = document.getElementById(`fsTab${t}`); if (el) el.classList.toggle('active', t.toLowerCase() === tab); }); @@ -7190,11 +7242,25 @@ const currentImg = lbNames[lbIdx]; - if (tab === 'group') { + if (tab === 'active' || tab === 'group') { const data = groupData.get(lbCurrentGid); if (data) { - lbUrls = data.urls; - lbNames = data.names; + if (tab === 'active') { + // Filter out hidden, source, and archived + const activeNames = []; + const activeUrls = []; + data.names.forEach((name, i) => { + if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name]) { + activeNames.push(name); + activeUrls.push(data.urls[i]); + } + }); + lbNames = activeNames; + lbUrls = activeUrls; + } else { + lbUrls = data.urls; + lbNames = data.names; + } } } else { const matched = []; @@ -7325,7 +7391,7 @@ if (r.ok) { showToast(`Moved ${fname} to ${targetStrip}`); - if (targetStrip === 'VISIBLE') { + if (targetStrip === 'VISIBLE' || targetStrip === 'ACTIVE') { fileArchived[fname] = false; fileHidden[fname] = false; if (fileTags[fname]) { @@ -7945,6 +8011,8 @@ const faceBook = document.getElementById('lbFaceBook'); if (faceBook) faceBook.style.display = 'none'; + const poseInfoWrap = document.getElementById('lbPoseInfoWrap'); + if (poseInfoWrap) poseInfoWrap.style.display = 'none'; updatePadPreview(); toggleCheckerboard(false); @@ -8134,6 +8202,21 @@ } else if (faceBook) { faceBook.style.display = 'none'; } + + const poseInfoWrap = document.getElementById('lbPoseInfoWrap'); + const poseDescEl = document.getElementById('lbPoseDescription'); + const poseCanvas = document.getElementById('lbPoseInfoCanvas'); + if (poseInfoWrap && poseDescEl && poseCanvas) { + const desc = filePoseDescription[fname]; + const kpts = filePoseSkeleton[fname]; + if (desc || kpts) { + poseInfoWrap.style.display = 'block'; + poseDescEl.textContent = desc || 'Pose detected'; + _renderPoseToCanvas(poseCanvas, kpts); + } else { + poseInfoWrap.style.display = 'none'; + } + } } // Alias for old callsites @@ -8622,6 +8705,40 @@ }); } + const POSE_SKELETON = [[5,7],[7,9],[6,8],[8,10],[11,13],[13,15],[12,14],[14,16],[5,6],[11,12],[5,11],[6,12],[0,1],[0,2],[1,3],[2,4],[0,5],[0,6]]; + function _renderPoseToCanvas(canvas, kptsJson) { + if (!canvas || !kptsJson) return; + let kpts; + try { kpts = typeof kptsJson === 'string' ? JSON.parse(kptsJson) : kptsJson; } catch(e) { return; } + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Auto-scale to fit + const xs = kpts.filter(k => k[2] >= POSE_MIN_SCORE).map(k => k[0]); + const ys = kpts.filter(k => k[2] >= POSE_MIN_SCORE).map(k => k[1]); + if (!xs.length) return; + const minX = Math.min(...xs), maxX = Math.max(...xs); + const minY = Math.min(...ys), maxY = Math.max(...ys); + const w = maxX - minX, h = maxY - minY; + const margin = 10; + const scale = Math.min((canvas.width - margin*2) / (w || 1), (canvas.height - margin*2) / (h || 1)); + const offX = margin - minX * scale + (canvas.width - margin*2 - w * scale) / 2; + const offY = margin - minY * scale + (canvas.height - margin*2 - h * scale) / 2; + const cx = x => offX + x * scale, cy = y => offY + y * scale; + + ctx.lineWidth = 2; ctx.strokeStyle = '#3b82f6'; + POSE_SKELETON.forEach(([a, b]) => { + const pa = kpts[a], pb = kpts[b]; + if (!pa || !pb || pa[2] < POSE_MIN_SCORE || pb[2] < POSE_MIN_SCORE) return; + ctx.beginPath(); ctx.moveTo(cx(pa[0]), cy(pa[1])); ctx.lineTo(cx(pb[0]), cy(pb[1])); ctx.stroke(); + }); + kpts.forEach((p, i) => { + if (!p || p[2] < POSE_MIN_SCORE) return; + ctx.beginPath(); ctx.arc(cx(p[0]), cy(p[1]), 2, 0, Math.PI * 2); + ctx.fillStyle = '#fff'; ctx.fill(); + }); + } + function _poseHitTest(mx, my) { const st = window._poseState; if (!st) return null; const cx = x => st.offX + x * st.scale, cy = y => st.offY + y * st.scale; @@ -10073,6 +10190,8 @@ fileSortOrders[img.filename] = img.sort_order ?? null; fileHidden[img.filename] = !!img.hidden; fileArchived[img.filename] = !!img.archived; + filePoseDescription[img.filename] = img.pose_description || null; + filePoseSkeleton[img.filename] = img.pose_skeleton || null; fileTags[img.filename] = img.tags || []; fileIsSource[img.filename] = !!img.is_source; fileHasBg[img.filename] = img.has_background !== false; diff --git a/tour-comfy/database.py b/tour-comfy/database.py index 4eec61d..432ef81 100644 --- a/tour-comfy/database.py +++ b/tour-comfy/database.py @@ -113,6 +113,8 @@ def migrate_schema(): "ALTER TABLE person ADD COLUMN IF NOT EXISTS archived BOOLEAN DEFAULT FALSE", "ALTER TABLE person ADD COLUMN IF NOT EXISTS face_embedding vector(512)", "ALTER TABLE person ADD COLUMN IF NOT EXISTS is_source BOOLEAN DEFAULT FALSE", + "ALTER TABLE person ADD COLUMN IF NOT EXISTS pose_description TEXT", + "ALTER TABLE person ADD COLUMN IF NOT EXISTS pose_skeleton TEXT", ]: cur.execute(sql) conn.commit() @@ -206,7 +208,8 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None, sort_order=None, group_name=None, hidden=None, has_background=None, source_refs=None, has_clothing=None, content_type=None, faceswap_source_video=None, archived=None, - face_embedding=None, is_source=None): + face_embedding=None, is_source=None, + pose_description=None, pose_skeleton=None): conn = get_db_connection() cur = conn.cursor() face_embedding_str = ("[" + ",".join(map(str, face_embedding)) + "]") if face_embedding is not None else None @@ -215,8 +218,9 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None, INSERT INTO person (filename, filepath, name, group_id, tags, embedding, clip_description, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing, - content_type, faceswap_source_video, archived, face_embedding, is_source) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + content_type, faceswap_source_video, archived, face_embedding, is_source, + pose_description, pose_skeleton) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (filename) DO UPDATE SET filepath = COALESCE(EXCLUDED.filepath, person.filepath), name = COALESCE(EXCLUDED.name, person.name), @@ -236,12 +240,15 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None, faceswap_source_video = COALESCE(EXCLUDED.faceswap_source_video, person.faceswap_source_video), archived = COALESCE(EXCLUDED.archived, person.archived), face_embedding = COALESCE(EXCLUDED.face_embedding, person.face_embedding), - is_source = COALESCE(EXCLUDED.is_source, person.is_source); + is_source = COALESCE(EXCLUDED.is_source, person.is_source), + pose_description = COALESCE(EXCLUDED.pose_description, person.pose_description), + pose_skeleton = COALESCE(EXCLUDED.pose_skeleton, person.pose_skeleton); """, (filename, filepath, name, group_id, json.dumps(tags) if tags else None, embedding, clip_description, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing, - content_type, faceswap_source_video, archived, face_embedding_str, is_source)) + content_type, faceswap_source_video, archived, face_embedding_str, is_source, + pose_description, pose_skeleton)) conn.commit() finally: cur.close() @@ -299,7 +306,7 @@ def get_person(filename): cur.execute(""" SELECT name, group_id, tags, embedding, clip_description, filepath, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, - has_clothing, is_source + has_clothing, is_source, pose_description, pose_skeleton FROM person WHERE filename = %s """, (filename,)) return cur.fetchone() @@ -315,7 +322,8 @@ def list_persons(include_archived=False): cur.execute(f""" SELECT filename, name, group_id, clip_description, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, - has_clothing, content_type, faceswap_source_video, archived, is_source, tags + has_clothing, content_type, faceswap_source_video, archived, is_source, tags, + pose_description, pose_skeleton FROM person {where} """) diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index d9b7fbe..cb13f61 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -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, }