• 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

@@ -68,7 +68,8 @@ Scenery:
- ✅ Fixed: introduce multiple filmstrips (Group, Hidden, Source, Archived with drag-and-drop & bulk selection actions). - ✅ 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. - samengestelde outpaint featurue lijkt niet goed te werken, but individual steps work perfect.
- regenerate a single orbit - pose - 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 ## refine
- when refresh page, we lose track of current jobs running. - when refresh page, we lose track of current jobs running.

View File

@@ -2034,7 +2034,8 @@
<!-- Film strip selector tabs --> <!-- Film strip selector tabs -->
<div class="filmstrip-selector-tabs" style="display:flex;gap:4px;padding:4px 8px;background:#151515;border-top:1px solid #1a1a1a;align-items:center;justify-content:space-between;flex-shrink:0"> <div class="filmstrip-selector-tabs" style="display:flex;gap:4px;padding:4px 8px;background:#151515;border-top:1px solid #1a1a1a;align-items:center;justify-content:space-between;flex-shrink:0">
<div style="display:flex;gap:4px"> <div style="display:flex;gap:4px">
<button onclick="switchFilmstripTab('group')" class="fs-tab active" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variants</button> <button onclick="switchFilmstripTab('active')" class="fs-tab active" id="fsTabActive" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ACTIVE')">Group Active</button>
<button onclick="switchFilmstripTab('group')" class="fs-tab" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variant</button>
<button onclick="switchFilmstripTab('hidden')" class="fs-tab" id="fsTabHidden" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'HIDDEN')">Hidden</button> <button onclick="switchFilmstripTab('hidden')" class="fs-tab" id="fsTabHidden" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'HIDDEN')">Hidden</button>
<button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button> <button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button>
<button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button> <button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button>
@@ -2108,6 +2109,14 @@
<button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button> <button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button>
</div> </div>
<div class="sb-sep"></div> <div class="sb-sep"></div>
<div id="lbPoseInfoWrap" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:4px">Pose information</div>
<div id="lbPoseDescription" style="font-size:11px;color:#888;margin-bottom:6px;line-height:1.4"></div>
<div style="display:flex;justify-content:center;background:#000;border-radius:6px;border:1px solid #333;overflow:hidden;position:relative;height:120px">
<canvas id="lbPoseInfoCanvas" width="120" height="120" style="max-height:100%;object-fit:contain"></canvas>
</div>
</div>
<div class="sb-sep"></div>
<div class="sb-label">Actions</div> <div class="sb-label">Actions</div>
<div class="sb-actions" style="margin-bottom:8px"> <div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button> <button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
@@ -2290,6 +2299,47 @@
// --- HYDRATION_START --- // --- HYDRATION_START ---
const PRELOADED_IMAGES = [ 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/pass-1.png/views/view_016_240deg.png",
"_turntable/up_810b587c/views/view_012_180deg.png", "_turntable/up_810b587c/views/view_012_180deg.png",
"_turntable/up_810b587c/views/view_011_165deg.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_001_015deg.png",
"_turntable/cg_9bb8f3bb/views/view_000_000deg.png", "_turntable/cg_9bb8f3bb/views/view_000_000deg.png",
"_turntable/pa01.png/views/view_023_345deg.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_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_022_330deg.png",
"_turntable/pa01.png/views/view_021_315deg.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", "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_021910_image.png",
"20260625_021702_sc_image.png", "20260625_021702_sc_image.png",
"20260625_021615_sc_image.png", "20260625_021615_sc_image.png",
"20260625_015711_sc_image.png",
"20260625_020212_dup_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_015711_sc_image.nobg.png",
"20260625_015900_sc_image.png", "20260625_015900_sc_image.png",
"20260625_015413_sc_image.png", "20260625_015413_sc_image.png",
@@ -5269,9 +5319,9 @@
"20260624_183010_0_20260624_182958_image.png", "20260624_183010_0_20260624_182958_image.png",
"20260624_182958_image.png", "20260624_182958_image.png",
"20260624_174900_image.png", "20260624_174900_image.png",
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
"20260624_173115_image.png", "20260624_173115_image.png",
"20260624_174943_dup_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_174757_image.png",
"20260624_174442_image.png", "20260624_174442_image.png",
"20260624_172923_image.nobg.png", "20260624_172923_image.nobg.png",
@@ -5778,8 +5828,8 @@
"20260622_193411_1_20260622_193118_image.png", "20260622_193411_1_20260622_193118_image.png",
"20260622_193354_0_20260622_193118_image.png", "20260622_193354_0_20260622_193118_image.png",
"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_212012_dup_20260622_181910_0_20260619_124038_image.png",
"20260622_181910_0_20260619_124038_image.png",
"20260622_181734_0_20260619_124038_image.png", "20260622_181734_0_20260619_124038_image.png",
"20260622_181720_0_20260619_124038_image.png", "20260622_181720_0_20260619_124038_image.png",
"20260622_181634_0_20260619_124038_image.png", "20260622_181634_0_20260619_124038_image.png",
@@ -6121,8 +6171,8 @@
"20260620_152017_image.png", "20260620_152017_image.png",
"20260618_013452_out4.png", "20260618_013452_out4.png",
"20260620_025959_out4.png", "20260620_025959_out4.png",
"20260622_021658_dup_20260621_212650_image.png",
"20260621_212650_image.png", "20260621_212650_image.png",
"20260622_021658_dup_20260621_212650_image.png",
"20260621_210130_0_20260621_205151_image.png", "20260621_210130_0_20260621_205151_image.png",
"20260621_210118_0_20260621_205151_image.png", "20260621_210118_0_20260621_205151_image.png",
"20260621_210107_0_20260621_205151_image.png", "20260621_210107_0_20260621_205151_image.png",
@@ -7040,6 +7090,8 @@
let filePrompts = {}; // filename → generation prompt let filePrompts = {}; // filename → generation prompt
let fileHidden = {}; // filename → boolean (hidden from cycling) let fileHidden = {}; // filename → boolean (hidden from cycling)
let fileArchived = {}; // filename → boolean 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 fileTags = {}; // filename → array of strings
let fileIsSource = {}; // filename → boolean let fileIsSource = {}; // filename → boolean
let _archiveViewActive = false; // when true, show archived items only let _archiveViewActive = false; // when true, show archived items only
@@ -7171,7 +7223,7 @@
// --- studio view (replaces lightbox) --- // --- studio view (replaces lightbox) ---
let lbUrls = [], lbNames = [], lbIdx = 0; let lbUrls = [], lbNames = [], lbIdx = 0;
let _activeFilmstripTab = 'group'; // 'group', 'hidden', 'source', 'archived' let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived'
let _multiSelectModeActive = false; let _multiSelectModeActive = false;
const _selectedFilenames = new Set(); const _selectedFilenames = new Set();
@@ -7179,7 +7231,7 @@
_activeFilmstripTab = tab; _activeFilmstripTab = tab;
// Highlight tab // Highlight tab
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => { ['Active', 'Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
const el = document.getElementById(`fsTab${t}`); const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === tab); if (el) el.classList.toggle('active', t.toLowerCase() === tab);
}); });
@@ -7190,11 +7242,25 @@
const currentImg = lbNames[lbIdx]; const currentImg = lbNames[lbIdx];
if (tab === 'group') { if (tab === 'active' || tab === 'group') {
const data = groupData.get(lbCurrentGid); const data = groupData.get(lbCurrentGid);
if (data) { if (data) {
lbUrls = data.urls; if (tab === 'active') {
lbNames = data.names; // 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 { } else {
const matched = []; const matched = [];
@@ -7325,7 +7391,7 @@
if (r.ok) { if (r.ok) {
showToast(`Moved ${fname} to ${targetStrip}`); showToast(`Moved ${fname} to ${targetStrip}`);
if (targetStrip === 'VISIBLE') { if (targetStrip === 'VISIBLE' || targetStrip === 'ACTIVE') {
fileArchived[fname] = false; fileArchived[fname] = false;
fileHidden[fname] = false; fileHidden[fname] = false;
if (fileTags[fname]) { if (fileTags[fname]) {
@@ -7945,6 +8011,8 @@
const faceBook = document.getElementById('lbFaceBook'); const faceBook = document.getElementById('lbFaceBook');
if (faceBook) faceBook.style.display = 'none'; if (faceBook) faceBook.style.display = 'none';
const poseInfoWrap = document.getElementById('lbPoseInfoWrap');
if (poseInfoWrap) poseInfoWrap.style.display = 'none';
updatePadPreview(); updatePadPreview();
toggleCheckerboard(false); toggleCheckerboard(false);
@@ -8134,6 +8202,21 @@
} else if (faceBook) { } else if (faceBook) {
faceBook.style.display = 'none'; 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 // 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) { function _poseHitTest(mx, my) {
const st = window._poseState; if (!st) return null; const st = window._poseState; if (!st) return null;
const cx = x => st.offX + x * st.scale, cy = y => st.offY + y * st.scale; 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; fileSortOrders[img.filename] = img.sort_order ?? null;
fileHidden[img.filename] = !!img.hidden; fileHidden[img.filename] = !!img.hidden;
fileArchived[img.filename] = !!img.archived; fileArchived[img.filename] = !!img.archived;
filePoseDescription[img.filename] = img.pose_description || null;
filePoseSkeleton[img.filename] = img.pose_skeleton || null;
fileTags[img.filename] = img.tags || []; fileTags[img.filename] = img.tags || [];
fileIsSource[img.filename] = !!img.is_source; fileIsSource[img.filename] = !!img.is_source;
fileHasBg[img.filename] = img.has_background !== false; fileHasBg[img.filename] = img.has_background !== false;

View File

@@ -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 archived BOOLEAN DEFAULT FALSE",
"ALTER TABLE person ADD COLUMN IF NOT EXISTS face_embedding vector(512)", "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 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) cur.execute(sql)
conn.commit() 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, sort_order=None, group_name=None, hidden=None,
has_background=None, source_refs=None, has_clothing=None, has_background=None, source_refs=None, has_clothing=None,
content_type=None, faceswap_source_video=None, archived=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() conn = get_db_connection()
cur = conn.cursor() cur = conn.cursor()
face_embedding_str = ("[" + ",".join(map(str, face_embedding)) + "]") if face_embedding is not None else None 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, INSERT INTO person (filename, filepath, name, group_id, tags, embedding,
clip_description, prompt, pose, sort_order, group_name, hidden, clip_description, prompt, pose, sort_order, group_name, hidden,
has_background, source_refs, has_clothing, has_background, source_refs, has_clothing,
content_type, faceswap_source_video, archived, face_embedding, is_source) 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) 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 ON CONFLICT (filename) DO UPDATE
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath), SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
name = COALESCE(EXCLUDED.name, person.name), 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), faceswap_source_video = COALESCE(EXCLUDED.faceswap_source_video, person.faceswap_source_video),
archived = COALESCE(EXCLUDED.archived, person.archived), archived = COALESCE(EXCLUDED.archived, person.archived),
face_embedding = COALESCE(EXCLUDED.face_embedding, person.face_embedding), 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, """, (filename, filepath, name, group_id,
json.dumps(tags) if tags else None, json.dumps(tags) if tags else None,
embedding, clip_description, prompt, pose, sort_order, group_name, hidden, embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
has_background, source_refs, has_clothing, 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() conn.commit()
finally: finally:
cur.close() cur.close()
@@ -299,7 +306,7 @@ def get_person(filename):
cur.execute(""" cur.execute("""
SELECT name, group_id, tags, embedding, clip_description, filepath, SELECT name, group_id, tags, embedding, clip_description, filepath,
prompt, pose, sort_order, group_name, hidden, has_background, source_refs, 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 FROM person WHERE filename = %s
""", (filename,)) """, (filename,))
return cur.fetchone() return cur.fetchone()
@@ -315,7 +322,8 @@ def list_persons(include_archived=False):
cur.execute(f""" cur.execute(f"""
SELECT filename, name, group_id, clip_description, SELECT filename, name, group_id, clip_description,
prompt, pose, sort_order, group_name, hidden, has_background, source_refs, 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 FROM person
{where} {where}
""") """)

View File

@@ -1379,6 +1379,8 @@ def _write_all_static() -> None:
"archived": is_archived, "archived": is_archived,
"is_source": bool(p[15]) if p[15] else False, "is_source": bool(p[15]) if p[15] else False,
"tags": tags_list, "tags": tags_list,
"pose_description": p[17],
"pose_skeleton": p[18],
}) })
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived") print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
try: 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, database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
name=person[0] if person else None, name=person[0] if person else None,
source_refs=json.dumps([filename]), 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 "")) print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
except Exception as e: except Exception as e:
print(f"[extract-face] error for {filename}: {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, filename, filepath=file_path, name=auto_name,
clip_description=clip_desc, tags=tags, embedding=embedding, clip_description=clip_desc, tags=tags, embedding=embedding,
group_id=group_id, sort_order=0, has_clothing=has_clothing, 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 # 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 # 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) direct = _dist(a["vec"], a["vis"], b["vec"], b["vis"], False)
mirror = _dist(a["vec"], a["vis"], b["vec"], b["vis"], True) mirror = _dist(a["vec"], a["vis"], b["vec"], b["vis"], True)
return min(direct, mirror) 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): def _best_person(people):
@@ -4706,13 +4741,20 @@ def estimate_pose(filename: str):
raise HTTPException(500, f"Pose estimation failed: {e}") raise HTTPException(500, f"Pose estimation failed: {e}")
# Cache the descriptor so "find similar pose" can rank this image later. # Cache the descriptor so "find similar pose" can rank this image later.
best = _best_person(people) best = _best_person(people)
pose_desc = None
pose_skeleton_json = None
if best is not None: if best is not None:
pose_desc = _describe_pose(best)
pose_skeleton_json = json.dumps(best)
desc = _pose_descriptor(best) desc = _pose_descriptor(best)
if desc is not None: if desc is not None:
try: try:
_save_pose_index_entry(filename, desc) _save_pose_index_entry(filename, desc)
except Exception as e: except Exception as e:
print(f"[pose] index save failed for {filename}: {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 { return {
"status": "success", "status": "success",
"backend": backend, "backend": backend,
@@ -4721,6 +4763,8 @@ def estimate_pose(filename: str):
"names": POSE_KEYPOINT_NAMES, "names": POSE_KEYPOINT_NAMES,
"skeleton": POSE_SKELETON, "skeleton": POSE_SKELETON,
"people": people, "people": people,
"pose_description": pose_desc,
"pose_skeleton": pose_skeleton_json,
} }