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:
@@ -2034,7 +2034,8 @@
|
||||
<!-- 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 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('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>
|
||||
@@ -2108,6 +2109,14 @@
|
||||
<button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button>
|
||||
</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-actions" style="margin-bottom:8px">
|
||||
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
""")
|
||||
|
||||
@@ -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