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 @@
- Group Variants
+ Group Active
+ Group Variant
Hidden
Source
Archived
@@ -2108,6 +2109,14 @@
🔞 21+
+
+
Pose information
+
+
+
+
+
+
Actions
Re-tag
@@ -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,
}