updates UI

This commit is contained in:
mike
2026-06-30 01:07:54 +02:00
parent 61268de34b
commit ad9a2ae078
13 changed files with 1375 additions and 397 deletions

View File

@@ -2039,6 +2039,7 @@
<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>
<button onclick="switchFilmstripTab('video')" class="fs-tab" id="fsTabVideo" ondragover="event.preventDefault()">Videos</button>
</div>
<div style="display:flex;gap:6px;align-items:center" id="multiSelectControls">
<button onclick="toggleMultiSelectMode()" id="multiSelectToggleBtn" style="background:#222;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">Multi-select</button>
@@ -2137,13 +2138,24 @@
<span id="lbDeepPeopleCount" style="font-weight:bold;color:#f59e0b">-</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="color:#888">Completeness:</span>
<span style="color:#888">Anatomical Completeness:</span>
<span id="lbDeepCompleteness" style="font-weight:bold;color:#f59e0b">-</span>
</div>
<div id="lbDeepOutpaintRecommendationWrap" style="display:none;font-size:10px;background:rgba(245,158,11,0.1);border:1px dashed rgba(245,158,11,0.3);padding:6px;border-radius:4px;color:#f59e0b;line-height:1.3;margin-top:2px;margin-bottom:2px">
<span id="lbDeepOutpaintRecommendation"></span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="color:#888">Face Direction:</span>
<span style="color:#888">Camera Angle:</span>
<span id="lbDeepCameraAngle" style="font-weight:bold;color:#f59e0b">-</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="color:#888">Face & Gaze Direction:</span>
<span id="lbDeepFacialDirection" style="font-weight:bold;color:#f59e0b">-</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="color:#888">Face Status:</span>
<span id="lbDeepFaceStatus" style="font-weight:bold;color:#10b981">Not Detected</span>
</div>
<div style="margin-top:4px">
<div style="color:#888;margin-bottom:4px">Detected objects:</div>
<div id="lbDeepObjects" style="display:flex;flex-wrap:wrap;gap:4px"></div>
@@ -2334,9 +2346,161 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260619_040244_5_20260619_040135_image.png",
"20260629_212135_image.png",
"20260629_211901_dup_view_002_030deg.png",
"20260629_211841_dup_view_002_030deg.png",
"20260629_211821_dup_view_002_030deg.png",
"20260629_211613_dup_view_002_030deg.png",
"20260629_211543_dup_view_002_030deg.png",
"20260629_205647_dup_view_002_030deg.png",
"20260629_211323_test_upload_image.png",
"20260629_211213_dup_view_002_030deg.png",
"20260629_210320_dup_view_002_030deg.png",
"20260629_210225_dup_view_002_030deg.png",
"20260629_205723_dup_view_002_030deg.png",
"20260629_203653_view_022_330deg.png",
"_turntable/up_e7e12f38/views/view_023_345deg.png",
"_turntable/up_e7e12f38/views/view_022_330deg.png",
"_turntable/up_e7e12f38/views/view_021_315deg.png",
"_turntable/up_e7e12f38/views/view_020_300deg.png",
"_turntable/up_e7e12f38/views/view_019_285deg.png",
"_turntable/up_e7e12f38/views/view_018_270deg.png",
"_turntable/up_e7e12f38/views/view_017_255deg.png",
"_turntable/up_e7e12f38/views/view_016_240deg.png",
"cg_36a33bae_face.png",
"20260629_193733_image.png",
"20260629_193704_image.png",
"20260629_193611_image.png",
"_turntable/up_e7e12f38/views/view_015_225deg.png",
"_turntable/up_e7e12f38/views/view_014_210deg.png",
"_turntable/up_e7e12f38/views/view_013_195deg.png",
"_turntable/up_e7e12f38/views/view_012_180deg.png",
"_turntable/up_e7e12f38/views/view_011_165deg.png",
"20260629_193310_image.png",
"_turntable/up_e7e12f38/views/view_010_150deg.png",
"_turntable/up_e7e12f38/views/view_009_135deg.png",
"_turntable/up_e7e12f38/views/view_008_120deg.png",
"20260629_192810_image.png",
"_turntable/up_e7e12f38/views/view_007_105deg.png",
"_turntable/up_e7e12f38/views/view_006_090deg.png",
"_turntable/up_e7e12f38/views/view_005_075deg.png",
"_turntable/up_e7e12f38/views/view_004_060deg.png",
"_turntable/up_e7e12f38/views/view_003_045deg.png",
"_turntable/up_e7e12f38/views/view_002_030deg.png",
"_turntable/up_e7e12f38/views/view_001_015deg.png",
"_turntable/up_e7e12f38/views/view_000_000deg.png",
"20260629_192507_12_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192456_11_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192445_10_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192433_9_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192422_8_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192410_7_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192359_6_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192347_5_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192336_4_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192325_3_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192314_2_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192303_1_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192251_0_20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"20260629_192154_Screenshot_From_2026-06-29_18-54-57.png",
"_turntable/up_c385e55b/views/view_023_345deg.png",
"_turntable/up_c385e55b/views/view_022_330deg.png",
"_turntable/up_c385e55b/views/view_021_315deg.png",
"_turntable/up_c385e55b/views/view_020_300deg.png",
"_turntable/up_c385e55b/views/view_019_285deg.png",
"_turntable/up_c385e55b/views/view_018_270deg.png",
"_turntable/up_c385e55b/views/view_017_255deg.png",
"_turntable/up_c385e55b/views/view_016_240deg.png",
"_turntable/up_c385e55b/views/view_015_225deg.png",
"_turntable/up_c385e55b/views/view_014_210deg.png",
"_turntable/up_c385e55b/views/view_013_195deg.png",
"_turntable/up_c385e55b/views/view_012_180deg.png",
"_turntable/up_c385e55b/views/view_011_165deg.png",
"_turntable/up_c385e55b/views/view_010_150deg.png",
"_turntable/up_c385e55b/views/view_009_135deg.png",
"_turntable/up_c385e55b/views/view_008_120deg.png",
"_turntable/up_c385e55b/views/view_007_105deg.png",
"_turntable/up_c385e55b/views/view_006_090deg.png",
"_turntable/up_c385e55b/views/view_005_075deg.png",
"_turntable/up_c385e55b/views/view_004_060deg.png",
"_turntable/up_c385e55b/views/view_003_045deg.png",
"_turntable/up_c385e55b/views/view_002_030deg.png",
"_turntable/up_c385e55b/views/view_001_015deg.png",
"_turntable/up_c385e55b/views/view_000_000deg.png",
"up_c385e55b_face.png",
"20260629_174128_12_20260629_173620_image.png",
"20260629_174112_11_20260629_173620_image.png",
"20260629_174055_10_20260629_173620_image.png",
"20260629_174038_9_20260629_173620_image.png",
"20260629_174021_8_20260629_173620_image.png",
"20260629_174004_7_20260629_173620_image.png",
"20260629_173948_6_20260629_173620_image.png",
"20260629_173914_4_20260629_173620_image.png",
"20260629_173857_3_20260629_173620_image.png",
"20260629_173841_2_20260629_173620_image.png",
"20260629_173824_1_20260629_173620_image.png",
"20260629_173807_0_20260629_173620_image.png",
"20260629_173620_image.png",
"_turntable/up_5e5db95d/views/view_023_345deg.png",
"_turntable/up_5e5db95d/views/view_022_330deg.png",
"_turntable/up_5e5db95d/views/view_021_315deg.png",
"_turntable/up_5e5db95d/views/view_020_300deg.png",
"_turntable/up_5e5db95d/views/view_019_285deg.png",
"_turntable/up_5e5db95d/views/view_018_270deg.png",
"_turntable/up_5e5db95d/views/view_017_255deg.png",
"_turntable/up_5e5db95d/views/view_016_240deg.png",
"_turntable/up_5e5db95d/views/view_015_225deg.png",
"_turntable/up_5e5db95d/views/view_014_210deg.png",
"_turntable/up_5e5db95d/views/view_013_195deg.png",
"_turntable/up_5e5db95d/views/view_012_180deg.png",
"_turntable/up_5e5db95d/views/view_011_165deg.png",
"_turntable/up_5e5db95d/views/view_010_150deg.png",
"_turntable/up_5e5db95d/views/view_009_135deg.png",
"_turntable/up_5e5db95d/views/view_008_120deg.png",
"_turntable/up_5e5db95d/views/view_007_105deg.png",
"_turntable/up_5e5db95d/views/view_006_090deg.png",
"up_78ae5271_face.png",
"_turntable/up_5e5db95d/views/view_005_075deg.png",
"_turntable/up_5e5db95d/views/view_004_060deg.png",
"img_87.png_face.png",
"_turntable/up_5e5db95d/views/view_003_045deg.png",
"_turntable/up_5e5db95d/views/view_002_030deg.png",
"_turntable/up_5e5db95d/views/view_001_015deg.png",
"_turntable/up_5e5db95d/views/view_000_000deg.png",
"20260629_164213_12_20260629_163755_image.png",
"20260629_164202_11_20260629_163755_image.png",
"20260629_164151_10_20260629_163755_image.png",
"20260629_164140_9_20260629_163755_image.png",
"20260629_164129_8_20260629_163755_image.png",
"20260629_164118_7_20260629_163755_image.png",
"20260629_164107_6_20260629_163755_image.png",
"20260629_164056_5_20260629_163755_image.png",
"20260629_164045_4_20260629_163755_image.png",
"20260629_164031_3_20260629_163755_image.png",
"20260629_164020_2_20260629_163755_image.png",
"20260629_164003_1_20260629_163755_image.png",
"20260629_163944_0_20260629_163755_image.png",
"20260629_163755_image.png",
"20260629_152011_sc_image.png",
"20260629_151844_sc_image.png",
"20260629_151508_sc_image.png",
"20260629_151413_sc_image.png",
"20260629_151327_sc_image.png",
"20260629_151239_sc_view_000_000deg.png",
"20260629_150255_sc_image.png",
"20260629_150202_sc_image.png",
"20260629_150105_sc_dup_20260621_032709_image.nobg.nobg.png",
"20260629_145957_sc_image.nobg.png",
"20260629_114346_sc_sc_image.nobg.nobg.png",
"20260629_145638_sc_image.png",
"20260629_145454_sc_image.png",
"20260629_143122_sc_sc_1_20260619_040135_image.png",
"20260629_142746_sc_sc_1_20260619_040135_image.png",
"20260629_142612_sc_view_023_345deg.png",
"20260629_142432_sc_view_023_345deg.png",
"_turntable/cg_f85e7ae1/views/view_023_345deg.png",
"20260629_141838_sc_1_20260619_040135_image.png",
"cg_f85e7ae1_face.png",
"20260618_181804_8_20260618_181600_image.png",
"b1.png_face.png",
"_turntable/b1.png/views/view_023_345deg.nobg.png",
"cg_e6ba3260_face.png",
@@ -2350,7 +2514,6 @@
"20260629_131834_sc_1_20260619_123958_image.png",
"20260629_124948_sc_view_023_345deg.png",
"20260629_124523_sc_view_023_345deg.png",
"20260629_124433_sc_view_023_345deg.png",
"20260629_122907_fs_Step Sis will do anything to make me Delete this Videos_1633s-2269s_image.png_prev50.mp4",
"20260629_120459_fs_bitch insemination_dup_20260626_003125_dup_Pasted image (9).png.mp4",
"20260629_114451_sc_sc_image.nobg.png",
@@ -2442,14 +2605,11 @@
"20260629_063000_1_20260629_062932_image.png",
"20260629_062949_0_20260629_062932_image.png",
"20260629_062932_image.png",
"20260629_060926_12_20260629_060652_image.png",
"20260629_060915_11_20260629_060652_image.png",
"20260629_060904_10_20260629_060652_image.png",
"20260629_060853_9_20260629_060652_image.png",
"20260629_060842_8_20260629_060652_image.png",
"20260629_060831_7_20260629_060652_image.png",
"20260629_060820_6_20260629_060652_image.png",
"20260629_060809_5_20260629_060652_image.png",
"20260629_060747_3_20260629_060652_image.png",
"20260629_060736_2_20260629_060652_image.png",
"20260629_060725_1_20260629_060652_image.png",
@@ -2506,7 +2666,11 @@
"20260629_033631_sc_2_20260622_101128_image.nobg.png",
"20260629_032345_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_032136_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_032057_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_031800_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_031650_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_031559_mr_sc_dup_20260624_055327_image.nobg.png",
"20260629_031435_image.png",
"20260629_031259_image.png",
"20260629_030904_sc_dup_20260624_055327_image.nobg.png",
"20260629_030742_sc_dup_20260624_055327_image.nobg.png",
@@ -3190,7 +3354,6 @@
"20260627_204815_image.png",
"20260627_204804_image.png",
"20260627_204756_image.png",
"_turntable/cg_f85e7ae1/views/view_023_345deg.png",
"_turntable/cg_f85e7ae1/views/view_013_195deg.png",
"_turntable/cg_f85e7ae1/views/view_012_180deg.png",
"_turntable/cg_f85e7ae1/views/view_010_150deg.png",
@@ -4712,7 +4875,6 @@
"_turntable/20260618_181600_image.png/views/view_001_030deg.png",
"20260626_234334_dup_20260626_231655_sc_image.png",
"20260626_231655_sc_image.png",
"20260626_231349_sc_image.png",
"20260626_231127_image.png",
"_turntable/up_624995c3/views/view_011_330deg.png",
"_turntable/up_624995c3/views/view_010_300deg.png",
@@ -5468,8 +5630,8 @@
"20260625_025335_dup_20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260624_165109_0_20260622_091731_image.nobg.png",
"20260624_165109_0_20260622_091731_image.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.png",
"20260624_165109_0_20260622_091731_image.png",
"up_ab858b0a_face.png",
"20260625_022332_dup_20260625_022250_sc_image.png",
"20260625_022250_sc_image.png",
@@ -5668,8 +5830,8 @@
"20260624_182958_image.png",
"20260624_174900_image.png",
"20260624_173115_image.png",
"20260624_175711_dup_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_174442_image.png",
"20260624_172923_image.nobg.png",
@@ -5785,8 +5947,8 @@
"20260624_143700_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143647_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143635_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151613_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_144952_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_151613_dup_20260624_143523_Screenshot_From_2026-06-17_07-41-27.png",
"20260624_143024_mr_image.png",
"20260624_142959_mr_image.png",
"20260624_142923_mr_image.png",
@@ -6966,6 +7128,7 @@
"20260619_040319_8_20260619_040135_image.png",
"20260619_040307_7_20260619_040135_image.png",
"20260619_040255_6_20260619_040135_image.png",
"20260619_040244_5_20260619_040135_image.png",
"20260619_040233_4_20260619_040135_image.png",
"20260619_040221_3_20260619_040135_image.png",
"20260619_040135_image.png",
@@ -7632,7 +7795,7 @@
_activeFilmstripTab = tab;
// Highlight tab
['Active', 'Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
['Active', 'Group', 'Hidden', 'Source', 'Archived', 'Video'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === tab);
});
@@ -7647,11 +7810,12 @@
const data = groupData.get(lbCurrentGid);
if (data) {
if (tab === 'active') {
// Filter out hidden, source, and archived
// Filter out hidden, source, archived, and videos
const activeNames = [];
const activeUrls = [];
data.names.forEach((name, i) => {
if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name]) {
const isVid = isVideo(name) || fileContentType[name] === 'video';
if (!fileHidden[name] && !fileIsSource[name] && !fileArchived[name] && !isVid) {
activeNames.push(name);
activeUrls.push(data.urls[i]);
}
@@ -7669,6 +7833,7 @@
const isArchived = !!fileArchived[fname];
const isHidden = !!fileHidden[fname];
const isSource = !!fileIsSource[fname];
const isVid = isVideo(fname) || fileContentType[fname] === 'video';
if (fileGroups[fname] !== lbCurrentGid) {
return;
@@ -7680,6 +7845,8 @@
matched.push(fname);
} else if (tab === 'archived' && isArchived) {
matched.push(fname);
} else if (tab === 'video' && isVid && !isArchived) {
matched.push(fname);
}
});
@@ -8810,7 +8977,11 @@
const deepMetaWrap = document.getElementById('lbDeepMetaWrap');
const deepPeopleCount = document.getElementById('lbDeepPeopleCount');
const deepCompleteness = document.getElementById('lbDeepCompleteness');
const deepOutpaintRecommendationWrap = document.getElementById('lbDeepOutpaintRecommendationWrap');
const deepOutpaintRecommendation = document.getElementById('lbDeepOutpaintRecommendation');
const deepCameraAngle = document.getElementById('lbDeepCameraAngle');
const deepFacialDirection = document.getElementById('lbDeepFacialDirection');
const deepFaceStatus = document.getElementById('lbDeepFaceStatus');
const deepObjects = document.getElementById('lbDeepObjects');
if (deepMetaWrap) {
const count = filePeopleCount[fname];
@@ -8824,17 +8995,152 @@
deepPeopleCount.textContent = count !== null ? `${count} person(s)` : '-';
}
if (deepCompleteness) {
deepCompleteness.textContent = compl !== null ? (compl ? 'Complete / Full body' : 'Partial / Close-up') : '-';
if (compl !== null) {
deepCompleteness.textContent = compl ? 'Complete / Full body' : 'Partial / Close-up';
deepCompleteness.style.color = compl ? '#10b981' : '#f59e0b';
let outpaintRecText = '';
let outpaintRecStyle = 'rgba(245,158,11,0.1)';
let outpaintRecBorder = 'rgba(245,158,11,0.3)';
let outpaintRecColor = '#f59e0b';
if (!compl) {
const kpts = filePoseSkeleton[fname];
let parsedKpts = null;
if (kpts) {
try {
parsedKpts = typeof kpts === 'string' ? JSON.parse(kpts) : kpts;
} catch(e) {}
}
if (parsedKpts && parsedKpts.length >= 17) {
const minScore = 0.3;
const hasHead = parsedKpts.slice(0, 5).some(k => k && k[2] >= minScore);
const hasShoulders = (parsedKpts[5] && parsedKpts[5][2] >= minScore) || (parsedKpts[6] && parsedKpts[6][2] >= minScore);
const hasHips = (parsedKpts[11] && parsedKpts[11][2] >= minScore) || (parsedKpts[12] && parsedKpts[12][2] >= minScore);
const hasKnees = (parsedKpts[13] && parsedKpts[13][2] >= minScore) || (parsedKpts[14] && parsedKpts[14][2] >= minScore);
const hasAnkles = (parsedKpts[15] && parsedKpts[15][2] >= minScore) || (parsedKpts[16] && parsedKpts[16][2] >= minScore);
const imgEl = document.getElementById('lbImg');
const imgW = imgEl ? imgEl.naturalWidth : 0;
const imgH = imgEl ? imgEl.naturalHeight : 0;
let issues = [];
if (hasHead && !hasHips && !hasKnees && !hasAnkles) {
issues.push("Lower body is completely missing/cropped (portrait/bust view)");
} else if (hasHead && hasHips && !hasKnees && !hasAnkles) {
issues.push("Lower legs and knees are missing/cropped (waist-up/navel-up view)");
} else if (hasHead && hasHips && hasKnees && !hasAnkles) {
issues.push("Feet and ankles are missing/cropped (knee-up view)");
} else if (!hasHead && hasShoulders) {
issues.push("Head is cropped or out of frame");
}
if (imgW > 0 && imgH > 0) {
const headKpts = parsedKpts.slice(0, 5).filter(k => k && k[2] >= minScore);
if (headKpts.length > 0) {
const minY = Math.min(...headKpts.map(k => k[1]));
if (minY < imgH * 0.03) {
issues.push("Head/hair is cut off at the top edge");
}
}
const ankleKpts = parsedKpts.slice(15, 17).filter(k => k && k[2] >= minScore);
if (ankleKpts.length > 0) {
const maxY = Math.max(...ankleKpts.map(k => k[1]));
if (maxY > imgH * 0.97) {
issues.push("Feet are cut off at the bottom edge");
}
}
const wristKpts = parsedKpts.slice(9, 11).filter(k => k && k[2] >= minScore);
if (wristKpts.length > 0) {
const minX = Math.min(...wristKpts.map(k => k[0]));
const maxX = Math.max(...wristKpts.map(k => k[0]));
if (minX < imgW * 0.03 || maxX > imgW * 0.97) {
issues.push("Arms or hands are cut off at the side edges");
}
}
}
if (issues.length > 0) {
outpaintRecText = `💡 <strong>Recommendation:</strong> Crop detected: <strong>${issues.join(', ')}</strong>.<br/>`;
let suggestions = [];
if (issues.some(i => i.includes("Lower body") || i.includes("Lower legs") || i.includes("Feet") || i.includes("bottom"))) {
suggestions.push("expand the <strong>Bottom</strong> by 20% to 50% using <strong>Pad</strong> and enable <strong>Outpaint</strong>");
}
if (issues.some(i => i.includes("Head") || i.includes("top"))) {
suggestions.push("expand the <strong>Top</strong> by 15% to 25% using <strong>Pad</strong> and enable <strong>Outpaint</strong>");
}
if (issues.some(i => i.includes("side") || i.includes("Arms"))) {
suggestions.push("expand the <strong>Left/Right</strong> sides by 15% to 25% using <strong>Pad</strong> and enable <strong>Outpaint</strong>");
}
if (suggestions.length > 0) {
outpaintRecText += `To fix, ${suggestions.join(' and ')} to naturally complete the body and limbs.`;
} else {
outpaintRecText += `Expand canvas by 20% to 50% using <strong>Pad</strong> and enable <strong>Outpaint</strong> to render the missing anatomical structure.`;
}
}
}
if (!outpaintRecText) {
outpaintRecText = '💡 <strong>Recommendation:</strong> Anatomy is cropped/partial. Expand canvas by 20% to 50% using <strong>Pad</strong> and enable <strong>Outpaint</strong> to render missing limbs and complete the body anatomy.';
}
} else {
outpaintRecText = '✓ <strong>Recommendation:</strong> Anatomy is complete. No outpainting required!';
outpaintRecStyle = 'rgba(16,185,129,0.1)';
outpaintRecBorder = 'rgba(16,185,129,0.3)';
outpaintRecColor = '#10b981';
}
if (deepOutpaintRecommendationWrap && deepOutpaintRecommendation) {
deepOutpaintRecommendationWrap.style.display = 'block';
deepOutpaintRecommendationWrap.style.background = outpaintRecStyle;
deepOutpaintRecommendationWrap.style.borderColor = outpaintRecBorder;
deepOutpaintRecommendationWrap.style.color = outpaintRecColor;
deepOutpaintRecommendation.innerHTML = outpaintRecText;
}
} else {
deepCompleteness.textContent = '-';
deepCompleteness.style.color = '#ccc';
if (deepOutpaintRecommendationWrap) deepOutpaintRecommendationWrap.style.display = 'none';
}
}
if (deepCameraAngle) {
let camAngle = '-';
const promptLower = (filePrompts[fname] || '').toLowerCase();
if (promptLower.includes('front') || promptLower.includes('straight-on')) camAngle = 'Straight-on / Frontal';
else if (promptLower.includes('¾ left') || promptLower.includes('three-quarter left')) camAngle = '¾ Left View';
else if (promptLower.includes('¾ right') || promptLower.includes('three-quarter right')) camAngle = '¾ Right View';
else if (promptLower.includes('side l') || promptLower.includes('left profile')) camAngle = 'Left Profile / Side View';
else if (promptLower.includes('side r') || promptLower.includes('right profile')) camAngle = 'Right Profile / Side View';
else if (promptLower.includes('back') || promptLower.includes('rear')) camAngle = 'Rear / Back View';
else if (promptLower.includes('high angle') || promptLower.includes('bird\'s-eye')) camAngle = 'High / Bird\'s-eye View';
else if (promptLower.includes('low angle') || promptLower.includes('worm\'s-eye')) camAngle = 'Low / Worm\'s-eye View';
else if (filePoses[fname]) camAngle = filePoses[fname] + ' (Preset)';
deepCameraAngle.textContent = camAngle;
}
if (deepFacialDirection) {
deepFacialDirection.textContent = faceDir || '-';
deepFacialDirection.textContent = faceDir ? faceDir.toUpperCase() : '-';
}
if (deepFaceStatus) {
if (faceDir) {
deepFaceStatus.textContent = 'Detected & Gaze Analyzed';
deepFaceStatus.style.color = '#10b981';
} else {
deepFaceStatus.textContent = 'Not Detected / Pending';
deepFaceStatus.style.color = '#f59e0b';
}
}
if (deepObjects) {
if (objs.length > 0) {
deepObjects.innerHTML = objs.map(o => {
const tag = typeof o === 'string' ? o : (o.tag || '');
const score = typeof o === 'object' && o.score ? ` (${Math.round(o.score * 100)}%)` : '';
return `<span style="background:#27272a;border:1px solid #3f3f46;border-radius:4px;padding:2px 6px;margin:2px;font-size:10px;display:inline-block;color:#ccc">${escHtml(tag)}${score}</span>`;
const titleStr = (typeof o === 'object' && o.bbox) ? ` title="BBox: [${o.bbox.join(', ')}]"` : '';
return `<span style="background:#27272a;border:1px solid #3f3f46;border-radius:4px;padding:2px 6px;margin:2px;font-size:10px;display:inline-block;color:#ccc;cursor:help"${titleStr}>${escHtml(tag)}${score}</span>`;
}).join('');
} else {
deepObjects.innerHTML = '<span style="color:#555;font-size:10px">None detected</span>';
@@ -11757,6 +12063,13 @@
e.preventDefault();
return;
}
} else if (_activeSidebarTab === 'designer') {
const btn = document.getElementById('designerGenerateBtn');
if (btn && !btn.disabled) {
btn.click();
e.preventDefault();
return;
}
}
}
}
@@ -12274,7 +12587,12 @@
function showPromptSuggest(el, boxId = 'sbPromptSuggest') {
const box = document.getElementById(boxId);
if (!box) return;
const histKey = boxId === 'scenePromptSuggest' ? 'scenePromptHistory' : 'batchPromptHistory';
let histKey = 'batchPromptHistory';
if (boxId === 'scenePromptSuggest') {
histKey = 'scenePromptHistory';
} else if (boxId === 'designerPromptSuggest') {
histKey = 'designerPromptHistory';
}
const hist = JSON.parse(localStorage.getItem(histKey) || '[]');
const q = (el.value || '').trim().toLowerCase();
const matches = (q ? hist.filter(p => p.toLowerCase().includes(q) && p.toLowerCase() !== q) : hist).slice(0, 40);
@@ -12300,6 +12618,7 @@
el.value = item.dataset.val;
hidePromptSuggest(boxId);
if (inputId === 'sbGenPromptInput') updateSbGenBtn();
if (inputId === 'designerContextInput') _designerContext = el.value;
autoGrowPrompt(el);
el.focus();
}
@@ -12499,6 +12818,29 @@
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M12 8v8m-4-4h8"/>
</svg>
</button>
<button onclick="lbTogglePose()" title="Toggle body pose skeleton" style="color:#60a5fa">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<circle cx="12" cy="4" r="2"/>
<path d="M12 6v7m-3-4h6m-3 4l-2 5m2-5l2 5"/>
</svg>
</button>
<button onclick="lbDuplicate()" title="Duplicate image" style="color:#10b981">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<button onclick="startManualCrop()" title="Crop... (Manual crop)" style="color:#a78bfa">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<path d="M6 2v14a2 2 0 0 0 2 2h14M18 22V8a2 2 0 0 0-2-2H2"/>
</svg>
</button>
<button onclick="lbAutoCrop()" title="Crop (Auto-crop)" style="color:#8b5cf6">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<path d="M6 2v14a2 2 0 0 0 2 2h14M18 22V8a2 2 0 0 0-2-2H2"/>
<path d="M10 10l4 4m0-4l-4 4" stroke-width="1.5"/>
</svg>
</button>`;
}
@@ -12563,6 +12905,7 @@
});
if (r.ok) {
const { refined } = await r.json();
savePromptHistory(prompt);
// Append refined prompt to original
// User said: (original prompt+ gen_poses.py refined prompt)
el.value = (el.value.trim() + "\n\n" + refined).trim();
@@ -12621,6 +12964,7 @@
});
if (r.ok) {
const { refined } = await r.json();
savePromptHistory(prompt);
textarea.value = refined;
_sbPoseEdits[name] = refined;
textarea.style.height = 'auto';
@@ -14008,6 +14352,7 @@
});
if (r.ok) {
const { refined } = await r.json();
savePromptHistory(prompt, 'scenePromptHistory');
el.value = (el.value.trim() + "\n\n" + refined).trim();
autoGrowPrompt(el);
showToast('Prompt refined', 'success');
@@ -14398,13 +14743,23 @@
let _designerNum = 3;
let _designerUseActiveImg = true;
let _designerResults = null; // hold generated poses {"Name": "body"}
let _designerHistory = []; // array of {"role": "user"|"assistant", "content": string}
function renderSidebarDesigner() {
const panel = document.getElementById('sbPanelDesigner');
if (!panel) return;
const savedScrollTop = panel.scrollTop;
let html = `<div class="sb-label">Prompt Designer</div>
let turnCount = "";
if (_designerHistory && _designerHistory.length > 0) {
const turn = Math.ceil(_designerHistory.length / 2) + 1;
turnCount = `<span style="font-size:9px;background:#8b5cf6;color:white;padding:2px 6px;border-radius:4px;margin-left:auto;font-weight:bold">Turn ${turn}</span>`;
}
let html = `<div style="display:flex;align-items:center;margin-bottom:4px">
<div class="sb-label" style="margin:0">Prompt Designer</div>
${turnCount}
</div>
<div style="font-size:11px;color:#777;margin-bottom:10px;line-height:1.4">
Generate multiple custom adult photographic pose prompts using the uncensored Mistral-24B model based on specific context guidelines or active image features.
</div>`;
@@ -14415,15 +14770,33 @@
style="margin-bottom:10px;background:#111;color:#ccc;border:1px solid #2a2a2a"
onchange="_designerNum = parseInt(this.value) || 3">`;
// Specific Context inputs
html += `<div class="sb-label">Context / Guidelines</div>
<textarea id="designerContextInput" class="sb-input" rows="4"
placeholder="e.g. standing pose, legs spread, hands bound behind back, explicit photography..."
style="resize:vertical;min-height:60px;margin-bottom:10px;background:#111;color:#ccc;border:1px solid #2a2a2a"
oninput="_designerContext = this.value">${escHtml(_designerContext)}</textarea>`;
// Specific Context inputs / guidelines
const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
let seedBtns = [];
if (activeFn && filePrompts[activeFn]) {
seedBtns.push(`<button class="sb-btn" style="font-size:9px;padding:1px 5px;margin:0;height:16px;line-height:1;background:#f59e0b;color:#111" onclick="designerSeedFromActive()" title="Seed the input with the active image's generation prompt">Seed from Active</button>`);
}
if (_designerContext) {
seedBtns.push(`<button class="sb-btn" style="font-size:9px;padding:1px 5px;margin:0;height:16px;line-height:1;background:#333;color:#ccc" onclick="designerClearContext()" title="Clear guidelines">Clear</button>`);
}
const buttonsHtml = seedBtns.length ? `<div style="display:flex;gap:4px;margin-left:auto">${seedBtns.join('')}</div>` : '';
html += `<div style="display:flex;align-items:center;margin-bottom:4px">
<div class="sb-label" style="margin:0">Context / Guidelines</div>
${buttonsHtml}
</div>
<div class="sb-prompt-wrap" style="position:relative;margin-bottom:10px">
<textarea id="designerContextInput" class="sb-input" rows="4" autocomplete="off"
placeholder="e.g. standing pose, legs spread, hands bound behind back, explicit photography..."
style="resize:vertical;min-height:60px;margin-bottom:0;background:#111;color:#ccc;border:1px solid #2a2a2a"
oninput="_designerContext = this.value;showPromptSuggest(this,'designerPromptSuggest')"
onfocus="showPromptSuggest(this,'designerPromptSuggest')"
onblur="setTimeout(()=>hidePromptSuggest('designerPromptSuggest'),150)"
onkeydown="if(event.key==='Escape')hidePromptSuggest('designerPromptSuggest')">${escHtml(_designerContext)}</textarea>
<div id="designerPromptSuggest" style="display:none;position:absolute;left:0;right:0;z-index:50;max-height:180px;overflow-y:auto;background:#18181b;border:1px solid #2a2a2a;border-radius:6px;margin-top:2px;box-shadow:0 6px 20px rgba(0,0,0,0.6)"></div>
</div>`;
// Checkbox for active image context
const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
if (activeFn) {
const checkedAttr = _designerUseActiveImg ? ' checked' : '';
html += `<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:#ccc;margin-bottom:12px;cursor:pointer">
@@ -14435,10 +14808,21 @@
html += `<div style="font-size:10px;color:#555;margin-bottom:12px">No active image selected to extract context.</div>`;
}
// Generate Button
html += `<button class="sb-btn amber" id="designerGenerateBtn" onclick="submitDesignerGenerate()" style="width:100%;margin-bottom:15px">
Generate Poses
</button>`;
// Generate or Follow up Buttons
if (_designerHistory && _designerHistory.length > 0) {
html += `<div style="display:flex;gap:4px;margin-bottom:15px">
<button class="sb-btn amber" id="designerGenerateBtn" onclick="submitDesignerGenerate()" style="flex:1;margin:0">
Follow up Poses
</button>
<button class="sb-btn" onclick="designerResetSession()" style="background:#333;color:#ccc;margin:0" title="Start a fresh design session (clears chat history)">
Reset
</button>
</div>`;
} else {
html += `<button class="sb-btn amber" id="designerGenerateBtn" onclick="submitDesignerGenerate()" style="width:100%;margin-bottom:15px">
Generate Poses
</button>`;
}
// Results Container
html += `<div id="designerResultsContainer">`;
@@ -14450,12 +14834,12 @@
const divId = 'designer_pose_' + name.replace(/\s+/g, '_');
html += `
<div class="sb-pose-edit" id="${divId}" style="border:1px solid #2a2a2a;background:#141414;border-radius:6px;padding:8px;margin-bottom:10px">
<div style="font-weight:bold;color:#f59e0b;font-size:11px;margin-bottom:4px">${nameEsc}</div>
<div class="sb-pose-name" style="font-weight:bold;color:#f59e0b;font-size:11px;margin-bottom:4px">${nameEsc}</div>
<textarea class="sb-input sb-pose-text" rows="5" style="font-size:10px;background:#1a1a1a;color:#ccc;border:1px solid #333;margin-bottom:6px;resize:vertical;width:100%">${bodyEsc}</textarea>
<div style="display:flex;gap:4px;flex-wrap:wrap">
<button class="sb-btn" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerLoadToCustomPrompt('${nameEsc}', this)" title="Copy this pose prompt into the custom prompt box">Copy to Custom</button>
<button class="sb-btn teal" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerSaveToLibrary('${nameEsc}', this)" title="Add this pose permanently to the poses.md library">Save to Library</button>
<button class="sb-btn purple" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerLoadAndSwitch('${nameEsc}', this)" title="Copy prompt and switch to Generate tab instantly">Use & Switch</button>
<button class="sb-btn" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerLoadToCustomPrompt(this)" title="Copy this pose prompt into the custom prompt box">Copy to Custom</button>
<button class="sb-btn teal" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerSaveToLibrary(this)" title="Add this pose permanently to the poses.md library">Save to Library</button>
<button class="sb-btn purple" style="font-size:9px;padding:2px 6px;margin:0" onclick="designerLoadAndSwitch(this)" title="Copy prompt and switch to Generate tab instantly">Use & Switch</button>
</div>
</div>`;
});
@@ -14468,6 +14852,38 @@
panel.scrollTop = savedScrollTop;
}
function designerSeedFromActive() {
const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
if (activeFn && filePrompts[activeFn]) {
_designerContext = filePrompts[activeFn];
const input = document.getElementById('designerContextInput');
if (input) {
input.value = _designerContext;
}
renderSidebarDesigner();
showToast('Seeded designer with active image prompt', 'success');
} else {
showToast('No active image prompt available', 'error');
}
}
function designerClearContext() {
_designerContext = "";
const input = document.getElementById('designerContextInput');
if (input) {
input.value = "";
}
renderSidebarDesigner();
}
function designerResetSession() {
_designerHistory = [];
_designerResults = null;
_designerContext = "";
renderSidebarDesigner();
showToast('Designer session reset', 'info');
}
async function submitDesignerGenerate() {
const btn = document.getElementById('designerGenerateBtn');
if (!btn) return;
@@ -14476,10 +14892,16 @@
btn.innerHTML = '<span class="sb-spinner" style="width:10px;height:10px;border-width:2px;margin:0"></span> Designing Poses…';
const activeFn = (lbIdx >= 0 && lbNames[lbIdx]) ? lbNames[lbIdx] : null;
if (_designerContext && _designerContext.trim()) {
savePromptHistory(_designerContext, 'designerPromptHistory');
}
const payload = {
n: _designerNum,
context: _designerContext,
filename: _designerUseActiveImg ? activeFn : null
filename: _designerUseActiveImg ? activeFn : null,
messages: _designerHistory.length > 0 ? _designerHistory : null
};
try {
@@ -14490,7 +14912,17 @@
});
if (r.ok) {
const data = await r.json();
// Build/update history
if (_designerHistory.length === 0) {
_designerHistory.push({ "role": "user", "content": _designerContext || "Generate poses based on active image features." });
} else if (_designerContext) {
_designerHistory.push({ "role": "user", "content": _designerContext });
}
_designerHistory.push({ "role": "assistant", "content": data.raw });
_designerResults = data.poses;
_designerContext = ""; // clear after generation
showToast(`Successfully designed ${Object.keys(data.poses).length} custom poses`, 'success');
} else {
showToast('Designer failed: ' + await r.text(), 'error');
@@ -14504,9 +14936,11 @@
}
}
function designerLoadToCustomPrompt(name, btn) {
function designerLoadToCustomPrompt(btn) {
const box = btn.closest('.sb-pose-edit');
if (!box) return;
const nameEl = box.querySelector('.sb-pose-name');
const name = nameEl ? nameEl.textContent.trim() : 'Designed Pose';
const textarea = box.querySelector('.sb-pose-text');
if (!textarea) return;
const promptVal = textarea.value.trim();
@@ -14522,9 +14956,15 @@
}
}
async function designerSaveToLibrary(name, btn) {
async function designerSaveToLibrary(btn) {
const box = btn.closest('.sb-pose-edit');
if (!box) return;
const nameEl = box.querySelector('.sb-pose-name');
const name = nameEl ? nameEl.textContent.trim() : '';
if (!name) {
showToast('Pose name not found.', 'error');
return;
}
const textarea = box.querySelector('.sb-pose-text');
if (!textarea) return;
const promptVal = textarea.value.trim();
@@ -14562,8 +15002,8 @@
}
}
function designerLoadAndSwitch(name, btn) {
designerLoadToCustomPrompt(name, btn);
function designerLoadAndSwitch(btn) {
designerLoadToCustomPrompt(btn);
switchSidebarTab('generate');
}
</script>

View File

@@ -539,3 +539,24 @@ def search_similar_face(embedding, limit=12, exclude_group_id=None):
finally:
cur.close()
_put_db_connection(conn)
def invalidate_all_metadata():
"""Reset metadata columns to NULL so they can be re-analyzed by the backfill worker."""
conn = get_db_connection()
cur = conn.cursor()
try:
cur.execute("""
UPDATE person
SET people_count = NULL,
anatomical_completeness = NULL,
facial_direction = NULL,
objects = NULL,
pose_description = NULL,
pose_skeleton = NULL
WHERE NOT (filename LIKE '_turntable/%')
""")
conn.commit()
finally:
cur.close()
_put_db_connection(conn)

View File

@@ -86,13 +86,13 @@ UNDRESS_PROMPT = "completely nude, bare skin, no clothing, naked body, natural s
# --- REFINEMENT PROMPT (AI-assisted prompt improvement) ---
REFINEMENT_SYSTEM = (
"You write explicit photographic — black void sceneries — restraining pose directions for adult (NSFW) nude female photography. "
"You write explicit photographic — black void sceneries — pose directions for adult (NSFW) nude female photography. "
"You never refuse or add disclaimers. "
"Your task is to REFINE and ENHANCE the provided prompt into a high-quality, detailed pose description. "
"Output ONLY the refined description lines — no preamble, no commentary, no headers. "
"Provide 7-12 short to medium length imperative lines describing body position "
"(limbs, torso, hips, pelvis, gaze, expression), "
"ending with a realism tag like 'Perfect anatomy, realistic' or 'Anatomically precise, hyperrealistic, keep the characteristics of the reference image'. "
"ending with a realism tag like 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.' or 'Anatomically precise. photorealistic, keep the characteristics of the reference image'. "
"Separate lines with newlines. Be specific and inventive."
)
@@ -670,11 +670,12 @@ def _load_tagger():
def _run_tagger(pil_img: Image.Image, threshold: float = 0.35):
import torch
model, transform, labels = _load_tagger()
tensor = transform(pil_img.convert("RGB")).unsqueeze(0)
if torch.cuda.is_available():
tensor = tensor.cuda()
with torch.no_grad():
scores = torch.sigmoid(model(tensor))[0].cpu().tolist()
with embeddings._gpu_lock:
tensor = transform(pil_img.convert("RGB")).unsqueeze(0)
if torch.cuda.is_available():
tensor = tensor.cuda()
with torch.no_grad():
scores = torch.sigmoid(model(tensor))[0].cpu().tolist()
tags = [
{"tag": name, "score": round(score, 3), "cat": cat}
for (name, cat), score in zip(labels, scores)
@@ -864,7 +865,8 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
src_bgr = cv2.imread(src_path)
if src_bgr is None:
raise ValueError(f"Cannot read model image: {model_filename}")
src_faces = app.get(src_bgr)
with embeddings._gpu_lock:
src_faces = app.get(src_bgr)
if not src_faces:
raise ValueError(f"No face detected in: {model_filename}")
# Use the largest face as source
@@ -912,7 +914,8 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
ret, frame = cap.read()
if not ret:
break
tgt_faces = app.get(frame)
with embeddings._gpu_lock:
tgt_faces = app.get(frame)
result = frame
if tgt_faces:
# Only swap the largest face — avoids false-positive detections
@@ -925,14 +928,16 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
result = frame.copy()
best_face = max(valid, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
try:
result = swapper.get(result, best_face, src_face, paste_back=True)
with embeddings._gpu_lock:
result = swapper.get(result, best_face, src_face, paste_back=True)
except Exception:
pass
if gfpgan_restorer is not None:
try:
_, _, result = gfpgan_restorer.enhance(
result, has_aligned=False, only_center_face=False, paste_back=True
)
with embeddings._gpu_lock:
_, _, result = gfpgan_restorer.enhance(
result, has_aligned=False, only_center_face=False, paste_back=True
)
except Exception:
pass
if scale < 1.0:
@@ -1463,6 +1468,17 @@ def _write_all_static() -> None:
elif isinstance(tags_val, list):
tags_list = tags_val
obj_val = p[22]
obj_list = []
if obj_val:
if isinstance(obj_val, str):
try:
obj_list = json.loads(obj_val)
except Exception:
obj_list = []
elif isinstance(obj_val, list):
obj_list = obj_val
db_images.append({
"filename": p[0],
"name": p[1],
@@ -1483,6 +1499,10 @@ def _write_all_static() -> None:
"tags": tags_list,
"pose_description": p[17],
"pose_skeleton": p[18],
"people_count": p[19],
"anatomical_completeness": p[20],
"facial_direction": p[21],
"objects": obj_list,
})
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
try:
@@ -2109,11 +2129,26 @@ def refine_prompt(req: RefineRequest):
raise HTTPException(500, f"LLM refinement failed: {str(e)}")
DESIGNER_SYSTEM = (
"You write explicit photographic — black void sceneries — pose directions for adult (NSFW) nude female photography. "
"You never refuse or add disclaimers. "
"Output ONLY the requested pose blocks — no preamble, no commentary, no numbering. "
"Format: each block starts with a markdown H1 header '# <Name>' (no colon, no 'The' required), "
"followed by 7-12 short to medium length imperative lines describing body position "
"(limbs, torso, hips, pelvis, gaze, expression), "
"ending with a realism tag like 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.' or 'Anatomically precise. photorealistic, keep the characteristics of the reference image'. "
"Separate blocks with ONE blank line. "
"Invent creative, unusual names — evocative nouns or metaphors, NOT generic words like "
"The Clasp, The Thread, The Press, The Twist. Be specific and inventive."
)
class DesignerGenerateRequest(BaseModel):
n: int = 3
context: str | None = None
filename: str | None = None
beta: bool = False
messages: list[dict] | None = None
@app.post("/designer/generate")
@@ -2170,33 +2205,62 @@ def designer_generate(req: DesignerGenerateRequest):
except Exception as e:
print(f"[designer] failed to fetch filename context: {e}")
# Build the prompt instructions
user_prompt = (
f"Here are example pose blocks in the exact required format:\n\n{ex_str}\n\n"
f"Write exactly {req.n} NEW pose blocks. "
)
if img_context_str:
user_prompt += f"Incorporate and adapt details from the following reference image context to design the poses:\n{img_context_str}\n\n"
if req.context:
user_prompt += f"Specific user guidelines/context to follow: {req.context}\n\n"
# Build the system instruction and messages
system_msg = {"role": "system", "content": DESIGNER_SYSTEM}
user_prompt += (
f"Requirements:\n"
f"- Each name must be completely unique and NOT appear in this list: {avoid}\n"
f"- Names must be inventive and specific — avoid short generic words\n"
f"- Explicit body positioning: specify limb placement, torso angle, hip/pelvis orientation, "
f"eye contact or gaze direction\n"
f"- Explicit NSFW content is expected and required\n"
f"Output only the {req.n} blocks, nothing else."
)
if req.messages:
api_messages = []
has_system = False
for msg in req.messages:
if not isinstance(msg, dict):
continue
role = msg.get("role")
content = msg.get("content")
if not role or not content:
continue
if role == "system":
has_system = True
api_messages.append({"role": role, "content": content})
if not has_system:
api_messages.insert(0, system_msg)
if req.context:
follow_up_prompt = req.context
follow_up_prompt += f"\n\nWrite exactly {req.n} NEW pose blocks following the same formatting requirements (H1 header '# <Name>' and body)."
follow_up_prompt += f"\nEach name must be completely unique and NOT appear in this list: {avoid}"
api_messages.append({"role": "user", "content": follow_up_prompt})
else:
api_messages.append({"role": "user", "content": f"Write exactly {req.n} NEW pose blocks following the formatting requirements."})
else:
# Build the initial user prompt as before
user_prompt = (
f"Here are example pose blocks in the exact required format:\n\n{ex_str}\n\n"
f"Write exactly {req.n} NEW pose blocks. "
)
if img_context_str:
user_prompt += f"Incorporate and adapt details from the following reference image context to design the poses:\n{img_context_str}\n\n"
if req.context:
user_prompt += f"Specific user guidelines/context to follow: {req.context}\n\n"
user_prompt += (
f"Requirements:\n"
f"- Each name must be completely unique and NOT appear in this list: {avoid}\n"
f"- Names must be inventive and specific — avoid short generic words\n"
f"- Explicit body positioning: specify limb placement, torso angle, hip/pelvis orientation, "
f"eye contact or gaze direction\n"
f"- Explicit NSFW content is expected and required\n"
f"Output only the {req.n} blocks, nothing else."
)
api_messages = [
system_msg,
{"role": "user", "content": user_prompt}
]
llm_api = "http://192.168.1.160:8001/v1/chat/completions"
payload = {
"model": "dphn/Dolphin3.0-Mistral-24B",
"messages": [
{"role": "system", "content": REFINEMENT_SYSTEM},
{"role": "user", "content": user_prompt}
],
"messages": api_messages,
"temperature": 0.9,
"max_tokens": 2400
}
@@ -2226,14 +2290,16 @@ def designer_generate(req: DesignerGenerateRequest):
if cur:
generated[cur] = " ".join(desc).strip()
# Filter out duplicates
# Filter out duplicates and deduplicate names by appending counter
new_poses = {}
for name, body in generated.items():
if not name or not body:
continue
if name.lower() in existing_lower or name.lower() in (k.lower() for k in new_poses):
print(f"[designer] skip duplicate: {name}")
continue
orig_name = name
counter = 1
while name.lower() in existing_lower or name.lower() in (k.lower() for k in new_poses):
counter += 1
name = f"{orig_name} {counter}"
new_poses[name] = body
return {
@@ -3173,7 +3239,8 @@ def _extract_face_bg(filename: str, fpath: str):
if bgr is None:
print(f"[extract-face] cannot read {fpath}")
return
faces = app_fa.get(bgr)
with embeddings._gpu_lock:
faces = app_fa.get(bgr)
if not faces:
print(f"[extract-face] no face detected in {filename}")
return
@@ -3806,7 +3873,8 @@ def _face_index_worker():
bgr = cv2.imread(fpath)
if bgr is None:
continue
faces = app_fa.get(bgr)
with embeddings._gpu_lock:
faces = app_fa.get(bgr)
if not faces:
continue
face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
@@ -4389,12 +4457,13 @@ def _apply_transparency_sam2(png_bytes: bytes) -> bytes:
box = np.array([[int(w * 0.01), int(h * 0.01),
int(w * 0.99), int(h * 0.99)]], dtype=np.float32)
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(
box=box,
multimask_output=True,
)
with embeddings._gpu_lock:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(
box=box,
multimask_output=True,
)
if masks is None or len(masks) == 0:
print("[sam2] no masks returned, falling back to rembg")
@@ -4500,9 +4569,10 @@ def _apply_transparency_black_bg(png_bytes: bytes) -> bytes:
if predictor is not False:
box = np.array([[x1, y1, x2, y2]], dtype=np.float32)
try:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(box=box, multimask_output=True)
with embeddings._gpu_lock:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(box=box, multimask_output=True)
if masks is not None and len(masks) > 0:
best = masks[int(np.argmax(scores))]
@@ -5144,33 +5214,109 @@ def _pose_distance(a, b):
return min(direct, mirror)
def _describe_pose(kpts):
"""Generate a simple human-readable description of a COCO-17 pose."""
"""Generate a highly detailed human-readable description of a COCO-17 pose,
including body orientation (standing, sitting, laying down), facing direction,
arms position, and legs posture.
"""
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")
# Extract some key positions
head_x = kpts[0][0] if vis[0] else ((kpts[5][0] + kpts[6][0])/2 if (vis[5] and vis[6]) else None)
head_y = kpts[0][1] if vis[0] else ((kpts[5][1] + kpts[6][1])/2 if (vis[5] and vis[6]) else None)
# 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")
hip_x = (kpts[11][0] + kpts[12][0])/2 if (vis[11] and vis[12]) else (kpts[11][0] if vis[11] else (kpts[12][0] if vis[12] else None))
hip_y = (kpts[11][1] + kpts[12][1])/2 if (vis[11] and vis[12]) else (kpts[11][1] if vis[11] else (kpts[12][1] if vis[12] else None))
sh_y = (kpts[5][1] + kpts[6][1])/2 if (vis[5] and vis[6]) else (kpts[0][1] if vis[0] else None)
torso_h = abs(hip_y - sh_y) if (hip_y is not None and sh_y is not None) else 100.0
# 1. Posture (standing, sitting, kneeling/crouching, laying down/reclining)
posture = "upright"
if head_y is not None and hip_y is not None:
if head_y > hip_y + 30:
posture = "upside down"
else:
# Check horizontal vs vertical distance to identify lying down
dx = abs(head_x - hip_x) if head_x is not None and hip_x is not None else 0
dy = abs(head_y - hip_y)
if dx > 1.2 * dy:
posture = "lying down/reclining"
if posture == "upright":
has_hips = vis[11] or vis[12]
has_knees = vis[13] or vis[14]
has_ankles = vis[15] or vis[16]
if has_hips and has_knees:
h_y = hip_y
h_x = hip_x
k_y = (kpts[13][1] + kpts[14][1])/2 if (vis[13] and vis[14]) else (kpts[13][1] if vis[13] else kpts[14][1])
k_x = (kpts[13][0] + kpts[14][0])/2 if (vis[13] and vis[14]) else (kpts[13][0] if vis[13] else kpts[14][0])
thigh_dy = abs(k_y - h_y)
thigh_dx = abs(k_x - h_x)
if has_ankles:
a_y = (kpts[15][1] + kpts[16][1])/2 if (vis[15] and vis[16]) else (kpts[15][1] if vis[15] else kpts[16][1])
a_x = (kpts[15][0] + kpts[16][0])/2 if (vis[15] and vis[16]) else (kpts[15][0] if vis[15] else kpts[16][0])
shin_dy = abs(a_y - k_y)
shin_dx = abs(a_x - k_x)
# Sitting: thigh horizontal, shin vertical
if thigh_dy < 0.6 * thigh_dx and shin_dy > 1.2 * shin_dx:
posture = "sitting"
elif thigh_dy < 0.45 * torso_h and shin_dy > 0.5 * torso_h:
posture = "sitting"
# Crouching/Kneeling: hips close to ankles/ground
elif abs(h_y - a_y) < 0.85 * torso_h:
posture = "crouching/kneeling"
else:
posture = "standing"
else:
# Ankles not visible
if thigh_dy < 0.5 * torso_h:
posture = "sitting"
else:
posture = "standing"
parts.append(posture)
# 2. Body Orientation / Facing Direction
if vis[5] and vis[6]:
sh_dist = abs(kpts[5][0] - kpts[6][0])
# Profile view check (shoulders compressed horizontally)
if sh_dist < 0.25 * torso_h:
parts.append("turned sideways (profile view)")
# Back view check
elif kpts[5][0] < kpts[6][0]:
parts.append("facing away (back view)")
else:
parts.append("facing forward (front view)")
# 3. Arms Posture
if vis[9] and vis[10]: # wrists
sh_y_val = sh_y if sh_y is not None else kpts[0][1]
if kpts[9][1] < sh_y_val and kpts[10][1] < sh_y_val:
parts.append("arms raised")
elif kpts[9][1] > sh_y_val + torso_h * 0.8 and kpts[10][1] > sh_y_val + torso_h * 0.8:
# Check if close to hips (hands on hips)
if (vis[11] and abs(kpts[9][0] - kpts[11][0]) < torso_h * 0.2) or (vis[12] and abs(kpts[10][0] - kpts[12][0]) < torso_h * 0.2):
parts.append("hands on hips")
else:
parts.append("arms down")
else:
parts.append("arms at sides")
# 4. Legs Posture
if vis[15] and vis[16]: # ankles
ankle_dist = abs(kpts[15][0] - kpts[16][0])
if ankle_dist > torso_h * 0.6:
parts.append("legs spread")
else:
parts.append("legs together")
if not parts: return "Generic pose"
return ", ".join(parts)
@@ -6109,67 +6255,309 @@ def _detect_people_count(keypoints: list) -> int:
return 1 if keypoints else 0
def _detect_anatomical_completeness(keypoints: list) -> bool:
def _detect_anatomical_completeness(keypoints: list, width: int = None, height: int = None) -> bool:
"""Detect if the person has complete anatomical structure.
Returns True if all major body parts are visible (head, torso, arms, legs).
Uses pose keypoint visibility to determine completeness.
Returns True if all major body parts are visible (head, torso, arms, legs)
and are fully contained within the frame (not cropped at boundaries).
"""
if not keypoints or len(keypoints) < 17:
return False
# Minimum visibility threshold for each keypoint
MIN_VISIBILITY = 0.3
# Key keypoints that indicate anatomical completeness
# Head (0), shoulders (5,6), hips (11,12), elbows (7,8), wrists (9,10), knees (13,14), ankles (15,16)
keypoint_indices = [0, 5, 6, 11, 12, 7, 8, 9, 10, 13, 14, 15, 16]
# 1. Presence of major body segments (requires visibility >= 0.3)
has_head = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [0, 1, 2, 3, 4])
has_shoulders = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [5, 6])
has_hips = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [11, 12])
has_knees = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [13, 14])
has_ankles = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [15, 16])
visible_count = 0
for idx in keypoint_indices:
if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY:
visible_count += 1
# If more than half of the key keypoints are visible, consider it complete
return visible_count > len(keypoint_indices) * 0.5
# If any major body segment is completely missing, the anatomy is not complete (e.g., cropped above knees/hips/chest)
if not (has_head and has_shoulders and has_hips and has_knees and has_ankles):
return False
# 2. Boundary cropping check (if image dimensions are provided)
if width and height:
# Check if head is too close to top edge
head_kpts = [keypoints[idx] for idx in [0, 1, 2, 3, 4] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if head_kpts:
min_head_y = min(kp[1] for kp in head_kpts)
if min_head_y < height * 0.02: # head is cropped at top
return False
# Check if ankles (feet) are too close to bottom edge
ankle_kpts = [keypoints[idx] for idx in [15, 16] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if ankle_kpts:
max_ankle_y = max(kp[1] for kp in ankle_kpts)
if max_ankle_y > height * 0.98: # ankles/feet are cropped at bottom
return False
# Check if wrists (hands) are too close to left/right edge
wrist_kpts = [keypoints[idx] for idx in [9, 10] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if wrist_kpts:
min_wrist_x = min(kp[0] for kp in wrist_kpts)
max_wrist_x = max(kp[0] for kp in wrist_kpts)
if min_wrist_x < width * 0.02 or max_wrist_x > width * 0.98: # wrists/hands are cropped at sides
return False
return True
def _detect_facial_direction(keypoints: list) -> str:
"""Detect the facial direction from keypoints.
"""Detect the facial direction from keypoints, including fine-grained gaze details (up/down/left/right).
Returns a string describing the head orientation.
Returns a string describing the head/gaze orientation.
"""
if not keypoints or len(keypoints) < 17:
return "unknown"
# Key points for face direction detection
# Nose (0), left ear (3), right ear (4)
# Nose (0), left eye (1), right eye (2), left ear (3), right ear (4)
nose = keypoints[0] if len(keypoints) > 0 and keypoints[0][2] >= 0.3 else None
l_eye = keypoints[1] if len(keypoints) > 1 and keypoints[1][2] >= 0.3 else None
r_eye = keypoints[2] if len(keypoints) > 2 and keypoints[2][2] >= 0.3 else None
l_ear = keypoints[3] if len(keypoints) > 3 and keypoints[3][2] >= 0.3 else None
r_ear = keypoints[4] if len(keypoints) > 4 and keypoints[4][2] >= 0.3 else None
if not nose:
return "unknown"
# Determine face direction based on ear positions
# 1. Determine horizontal direction
horiz = "forward"
if l_ear and r_ear:
ear_mid_x = (l_ear[0] + r_ear[0]) / 2
dx = nose[0] - ear_mid_x
if dx < -0.05:
return "looking left"
elif dx > 0.05:
return "looking right"
else:
return "looking forward"
# Normalize dx by distance between ears to make it scale invariant
ear_dist = abs(l_ear[0] - r_ear[0])
if ear_dist > 0:
norm_dx = dx / ear_dist
if norm_dx < -0.06:
horiz = "left"
elif norm_dx > 0.06:
horiz = "right"
elif l_ear and not r_ear:
return "looking strongly right"
horiz = "strongly right"
elif r_ear and not l_ear:
return "looking strongly left"
else:
return "looking forward"
horiz = "strongly left"
# 2. Determine vertical direction (up/down)
vert = "level"
# Try using eyes first as they are closer to the nose
if l_eye and r_eye:
eye_y = (l_eye[1] + r_eye[1]) / 2
eye_dist = abs(l_eye[0] - r_eye[0])
if eye_dist > 0:
v_ratio = (nose[1] - eye_y) / eye_dist
if v_ratio < 0.15:
vert = "up"
elif v_ratio > 0.65:
vert = "down"
elif l_ear and r_ear:
ear_y = (l_ear[1] + r_ear[1]) / 2
ear_dist = abs(l_ear[0] - r_ear[0])
if ear_dist > 0:
v_ratio = (nose[1] - ear_y) / ear_dist
if v_ratio < -0.1:
vert = "up"
elif v_ratio > 0.3:
vert = "down"
# 3. Combine horizontal and vertical
if horiz == "forward":
if vert == "level":
return "looking forward"
elif vert == "up":
return "looking forward and up"
elif vert == "down":
return "looking forward and down"
elif horiz == "left":
if vert == "level":
return "looking left"
elif vert == "up":
return "looking left and up"
elif vert == "down":
return "looking left and down"
elif horiz == "right":
if vert == "level":
return "looking right"
elif vert == "up":
return "looking right and up"
elif vert == "down":
return "looking right and down"
elif horiz == "strongly left":
if vert == "level":
return "looking strongly left"
elif vert == "up":
return "looking strongly left and up"
elif vert == "down":
return "looking strongly left and down"
elif horiz == "strongly right":
if vert == "level":
return "looking strongly right"
elif vert == "up":
return "looking strongly right and up"
elif vert == "down":
return "looking strongly right and down"
return "looking forward"
def _detect_objects(pil_img: Image.Image) -> list:
def _estimate_bbox_for_tag(tag: str, keypoints: list, width: int, height: int, alpha_bbox: list = None) -> list:
"""Estimate a bounding box for a given tag using pose keypoints or alpha bounding box.
Returns a list [x1, y1, x2, y2] of pixel coordinates, or None.
"""
import math
if not keypoints or len(keypoints) < 17:
if alpha_bbox:
return [int(v) for v in alpha_bbox]
return [0, 0, width, height]
tag_lower = tag.lower().replace("_", " ")
# Define terms lists mapped to anatomical structures
head_terms = ["hair", "head", "face", "eye", "eyes", "nose", "ear", "ears", "mouth", "makeup", "eyebrow", "eyebrows", "glasses", "sunglasses", "earrings", "jewelry", "blush", "necklace", "collar", "hat", "cap", "crown", "smile", "gaze", "cheek", "teeth", "lips"]
chest_terms = ["breast", "nipple", "nipples", "breasts", "chest", "cleavage", "bra", "bikini top", "top", "shirt", "collarbone", "pendant"]
stomach_terms = ["navel", "stomach", "belly", "abs", "midriff", "waist", "panties", "underwear", "pelvis", "bikini bottom", "hips", "hip"]
arm_terms = ["arm", "arms", "hand", "hands", "wrist", "wrists", "elbow", "elbows", "finger", "fingers", "sleeve", "sleeves", "glove", "gloves"]
leg_terms = ["leg", "legs", "thigh", "thighs", "knee", "knees", "calf", "calves", "foot", "feet", "ankle", "ankles", "shoe", "shoes", "socks", "sock", "boots", "boot"]
bbox = None
# 1. Head/Face
if any(term in tag_lower for term in head_terms):
head_kpts = [keypoints[i] for i in range(5) if i < len(keypoints) and keypoints[i][2] >= 0.3]
if head_kpts:
xs = [kp[0] for kp in head_kpts]
ys = [kp[1] for kp in head_kpts]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
# Determine padding based on shoulder distance if available
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
shoulder_dist = math.dist(keypoints[5][:2], keypoints[6][:2])
pad_x = shoulder_dist * 0.35
pad_y = shoulder_dist * 0.45
else:
pad_x = max(max_x - min_x, width * 0.08)
pad_y = max(max_y - min_y, height * 0.08)
bbox = [
min_x - pad_x,
min_y - pad_y * 1.3, # pull top higher to cover hair/hats
max_x + pad_x,
max_y + pad_y * 0.7
]
# 2. Chest/Breasts
elif any(term in tag_lower for term in chest_terms):
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
sh_mid_x = (keypoints[5][0] + keypoints[6][0]) / 2
sh_mid_y = (keypoints[5][1] + keypoints[6][1]) / 2
sh_dist = math.dist(keypoints[5][:2], keypoints[6][:2])
if len(keypoints) > 12 and keypoints[11][2] >= 0.3 and keypoints[12][2] >= 0.3:
hip_mid_y = (keypoints[11][1] + keypoints[12][1]) / 2
torso_h = hip_mid_y - sh_mid_y
else:
torso_h = sh_dist * 1.2
chest_center_y = sh_mid_y + torso_h * 0.28
chest_w = sh_dist * 0.95
chest_h = torso_h * 0.42
bbox = [
sh_mid_x - chest_w / 2,
chest_center_y - chest_h / 2,
sh_mid_x + chest_w / 2,
chest_center_y + chest_h / 2
]
# 3. Midriff/Pelvis/Hips/Underwear
elif any(term in tag_lower for term in stomach_terms):
if len(keypoints) > 12 and keypoints[11][2] >= 0.3 and keypoints[12][2] >= 0.3:
hip_mid_x = (keypoints[11][0] + keypoints[12][0]) / 2
hip_mid_y = (keypoints[11][1] + keypoints[12][1]) / 2
hip_dist = math.dist(keypoints[11][:2], keypoints[12][:2])
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
sh_mid_y = (keypoints[5][1] + keypoints[6][1]) / 2
torso_h = hip_mid_y - sh_mid_y
else:
torso_h = hip_dist * 1.5
if any(term in tag_lower for term in ["navel", "stomach", "belly", "abs", "midriff", "waist"]):
center_y = hip_mid_y - torso_h * 0.22
box_h = torso_h * 0.32
box_w = hip_dist * 1.15
else: # panties, underwear, pelvis, bikini bottom, hips
center_y = hip_mid_y + torso_h * 0.05
box_h = torso_h * 0.42
box_w = hip_dist * 1.25
bbox = [
hip_mid_x - box_w / 2,
center_y - box_h / 2,
hip_mid_x + box_w / 2,
center_y + box_h / 2
]
# 4. Arms
elif any(term in tag_lower for term in arm_terms):
arm_indices = [5, 6, 7, 8, 9, 10]
xs = [keypoints[i][0] for i in arm_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
ys = [keypoints[i][1] for i in arm_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
if xs:
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.12 + 15
pad_y = (max_y - min_y) * 0.12 + 15
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
# 5. Legs
elif any(term in tag_lower for term in leg_terms):
leg_indices = [11, 12, 13, 14, 15, 16]
xs = [keypoints[i][0] for i in leg_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
ys = [keypoints[i][1] for i in leg_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
if xs:
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.12 + 15
pad_y = (max_y - min_y) * 0.12 + 15
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
# 6. Fallback or general full-body
if bbox is None:
visible_kpts = [k for k in keypoints if k[2] >= 0.3]
if visible_kpts:
xs = [k[0] for k in visible_kpts]
ys = [k[1] for k in visible_kpts]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.15 + 20
pad_y = (max_y - min_y) * 0.12 + 20
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
elif alpha_bbox:
bbox = list(alpha_bbox)
else:
bbox = [0, 0, width, height]
# Clip to image bounds and format
x1 = max(0, min(int(bbox[0]), width))
y1 = max(0, min(int(bbox[1]), height))
x2 = max(0, min(int(bbox[2]), width))
y2 = max(0, min(int(bbox[3]), height))
# Ensure some valid size
if x2 <= x1:
x2 = min(x1 + 10, width)
if y2 <= y1:
y2 = min(y1 + 10, height)
return [x1, y1, x2, y2]
def _detect_objects(pil_img: Image.Image, keypoints: list = None) -> list:
"""Detect objects in the image using WD tagger.
Returns a list of detected objects with bounding box coordinates.
@@ -6180,14 +6568,20 @@ def _detect_objects(pil_img: Image.Image) -> list:
# Filter for object-related tags (general and character categories)
objects = []
width, height = pil_img.size
alpha_bbox = None
if pil_img.mode == 'RGBA':
alpha = pil_img.split()[-1]
alpha_bbox = alpha.getbbox()
for t in tags:
if t["cat"] in (0, 4): # general and character categories
# For simplicity, we'll return just the tag name with confidence
# In a more advanced implementation, we could extract bounding boxes from the model
bbox = _estimate_bbox_for_tag(t["tag"], keypoints, width, height, alpha_bbox)
objects.append({
"tag": t["tag"],
"score": t["score"],
"bbox": None # No bounding box available from WD tagger
"bbox": bbox
})
return objects
except Exception as e:
@@ -6226,8 +6620,9 @@ def _process_image_for_metadata(filename: str):
best_person = _best_person(people)
# Extract metadata
width, height = pil_img.size
people_count = _detect_people_count(best_person)
anatomical_completeness = _detect_anatomical_completeness(best_person)
anatomical_completeness = _detect_anatomical_completeness(best_person, width, height)
facial_direction = _detect_facial_direction(best_person)
# ALSO extract pose description and pose skeleton
@@ -6244,7 +6639,7 @@ def _process_image_for_metadata(filename: str):
print(f"[pose] index save failed for {filename}: {e}")
# Detect objects
objects = _detect_objects(pil_img)
objects = _detect_objects(pil_img, keypoints=best_person)
# Update database with new metadata
database.upsert_person(
@@ -6276,6 +6671,7 @@ def _process_image_for_metadata(filename: str):
class BackfillMetadataRequest(BaseModel):
filenames: list[str] | None = None # If None, process all images in DB
force: bool = False
import asyncio
@@ -6283,6 +6679,21 @@ from concurrent.futures import ThreadPoolExecutor as _ThreadPoolExecutor
_metadata_executor = _ThreadPoolExecutor(max_workers=1, thread_name_prefix="metadata")
@app.post("/images/invalidate-metadata")
def invalidate_metadata():
"""Invalidate all metadata records by setting their columns to NULL.
This enables full reprocessing via backfill or the background idle loop.
"""
try:
database.invalidate_all_metadata()
_failed_backfill_filenames.clear()
_invalidate_static()
return {"status": "success", "message": "All earlier metadata has been invalidated successfully."}
except Exception as e:
print(f"[metadata] Invalidation error: {e}")
raise HTTPException(500, f"Invalidation failed: {str(e)}")
@app.post("/images/backfill-metadata")
async def backfill_metadata(req: BackfillMetadataRequest):
"""Backfill metadata for existing images in the database.
@@ -6291,6 +6702,9 @@ async def backfill_metadata(req: BackfillMetadataRequest):
people count, anatomical completeness, facial direction, and objects.
"""
try:
if req.force:
_failed_backfill_filenames.clear()
# Get list of all image files
if req.filenames is not None:
filenames = req.filenames

View File

@@ -10,6 +10,7 @@ _model = None
_preprocess = None
_device = None
_lock = threading.Lock()
_gpu_lock = threading.Lock()
def get_model():
global _model, _preprocess, _device
@@ -31,12 +32,13 @@ def get_model():
def generate_embedding(image_path):
model, preprocess, device = get_model()
try:
with Image.open(image_path) as img:
image = preprocess(img.convert("RGB")).unsqueeze(0).to(device)
with torch.no_grad():
image_features = model.encode_image(image)
image_features /= image_features.norm(dim=-1, keepdim=True)
return image_features.cpu().numpy()[0].tolist()
with _gpu_lock:
with Image.open(image_path) as img:
image = preprocess(img.convert("RGB")).unsqueeze(0).to(device)
with torch.no_grad():
image_features = model.encode_image(image)
image_features /= image_features.norm(dim=-1, keepdim=True)
return image_features.cpu().numpy()[0].tolist()
except Exception as e:
print(f"Error generating embedding for {image_path}: {e}")
return None

View File

@@ -799,5 +799,308 @@ class TestAPIRegression(unittest.TestCase):
except Exception:
pass
def test_14_static_serialization_of_deep_metadata(self):
import edit_api
import database
import json
import os
from PIL import Image
# Create dummy image and person record
fn = "test_deep_meta_serialization_123.png"
fpath = os.path.join(self.output_dir, fn)
Image.new("RGB", (10, 10)).save(fpath)
conn = database.get_db_connection()
cur = conn.cursor()
try:
cur.execute("""
INSERT INTO person (filename, filepath, group_id, people_count, anatomical_completeness, facial_direction, objects)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (fn, fpath, "test_group", 2, False, "front", json.dumps([{"tag": "hat", "score": 0.9}])))
conn.commit()
finally:
cur.close()
database._put_db_connection(conn)
try:
# Trigger write static images.json
edit_api._write_all_static()
# Read static file and check that the deep metadata properties are serialized correctly
static_file = os.path.join(self.output_dir, "_data", "images.json")
self.assertTrue(os.path.exists(static_file))
with open(static_file, "r") as f:
data = json.load(f)
imgs = data.get("images", [])
matched = [x for x in imgs if x["filename"] == fn]
self.assertEqual(len(matched), 1)
item = matched[0]
# Assert that the serialized dictionary has all of the deep metadata fields
self.assertEqual(item.get("people_count"), 2)
self.assertEqual(item.get("anatomical_completeness"), False)
self.assertEqual(item.get("facial_direction"), "front")
self.assertEqual(item.get("objects"), [{"tag": "hat", "score": 0.9}])
finally:
# Clean up
if os.path.exists(fpath):
os.remove(fpath)
conn = database.get_db_connection()
cur = conn.cursor()
try:
cur.execute("DELETE FROM person WHERE filename = %s", (fn,))
conn.commit()
finally:
cur.close()
database._put_db_connection(conn)
# Re-generate static to clean up the dummy entry
edit_api._write_all_static()
def test_15_anatomical_completeness_refinement(self):
import edit_api
# 17 keypoints matching COCO-17 format: [x, y, score]
# Nose (0), shoulders (5,6), elbows (7,8), wrists (9,10), hips (11,12), knees (13,14), ankles (15,16)
# All of them are visible with score >= 0.3, and well within bounds [0.1, 0.9]
complete_kpts = [
[50, 10, 0.9], # Nose (0)
[48, 11, 0.9], # L_Eye (1)
[52, 11, 0.9], # R_Eye (2)
[46, 12, 0.9], # L_Ear (3)
[54, 12, 0.9], # R_Ear (4)
[40, 20, 0.9], # L_Shoulder (5)
[60, 20, 0.9], # R_Shoulder (6)
[35, 35, 0.9], # L_Elbow (7)
[65, 35, 0.9], # R_Elbow (8)
[30, 50, 0.9], # L_Wrist (9)
[70, 50, 0.9], # R_Wrist (10)
[42, 55, 0.9], # L_Hip (11)
[58, 55, 0.9], # R_Hip (12)
[40, 75, 0.9], # L_Knee (13)
[60, 75, 0.9], # R_Knee (14)
[40, 90, 0.9], # L_Ankle (15)
[60, 90, 0.9], # R_Ankle (16)
]
# Test complete pose
res1 = edit_api._detect_anatomical_completeness(complete_kpts, 100, 100)
self.assertTrue(res1)
# Test partial pose: missing knees and ankles
partial_kpts = [list(kp) for kp in complete_kpts]
for idx in [13, 14, 15, 16]:
partial_kpts[idx][2] = 0.1 # Hide them
res2 = edit_api._detect_anatomical_completeness(partial_kpts, 100, 100)
self.assertFalse(res2)
# Test cropped pose: ankles too close to bottom edge (y=99 in height=100)
cropped_bottom_kpts = [list(kp) for kp in complete_kpts]
cropped_bottom_kpts[15][1] = 99
cropped_bottom_kpts[16][1] = 99
res3 = edit_api._detect_anatomical_completeness(cropped_bottom_kpts, 100, 100)
self.assertFalse(res3)
def test_16_object_bbox_estimation(self):
import edit_api
# Define some realistic keypoints
kpts = [
[50, 10, 0.9], # Nose (0)
[48, 11, 0.9], # L_Eye (1)
[52, 11, 0.9], # R_Eye (2)
[46, 12, 0.9], # L_Ear (3)
[54, 12, 0.9], # R_Ear (4)
[40, 20, 0.9], # L_Shoulder (5)
[60, 20, 0.9], # R_Shoulder (6)
[35, 35, 0.9], # L_Elbow (7)
[65, 35, 0.9], # R_Elbow (8)
[30, 50, 0.9], # L_Wrist (9)
[70, 50, 0.9], # R_Wrist (10)
[42, 55, 0.9], # L_Hip (11)
[58, 55, 0.9], # R_Hip (12)
[40, 75, 0.9], # L_Knee (13)
[60, 75, 0.9], # R_Knee (14)
[40, 90, 0.9], # L_Ankle (15)
[60, 90, 0.9], # R_Ankle (16)
]
# Test head / hair tag bbox estimation
bbox_hair = edit_api._estimate_bbox_for_tag("long_hair", kpts, 100, 100)
self.assertIsNotNone(bbox_hair)
self.assertEqual(len(bbox_hair), 4)
# Bounding box should surround the head/face region, which is y-centered around 10-12.
# So upper y (bbox_hair[1]) should be relatively small (close to 0/top)
self.assertLess(bbox_hair[1], 15)
# Test chest tag bbox estimation
bbox_breasts = edit_api._estimate_bbox_for_tag("breasts", kpts, 100, 100)
self.assertIsNotNone(bbox_breasts)
self.assertEqual(len(bbox_breasts), 4)
# Should be below shoulders (y=20) and above hips (y=55). Center around 30.
self.assertGreater(bbox_breasts[1], 15)
self.assertLess(bbox_breasts[3], 55)
# Test stomach tag bbox estimation
bbox_navel = edit_api._estimate_bbox_for_tag("navel", kpts, 100, 100)
self.assertIsNotNone(bbox_navel)
self.assertEqual(len(bbox_navel), 4)
# Should be around the stomach area, i.e. between 35 and 55.
self.assertGreater(bbox_navel[1], 25)
# Test arms tag bbox estimation
bbox_arms = edit_api._estimate_bbox_for_tag("sleeves", kpts, 100, 100)
self.assertIsNotNone(bbox_arms)
self.assertEqual(len(bbox_arms), 4)
# Test legs tag bbox estimation
bbox_legs = edit_api._estimate_bbox_for_tag("thighs", kpts, 100, 100)
self.assertIsNotNone(bbox_legs)
self.assertEqual(len(bbox_legs), 4)
def test_17_gaze_and_pose_description_enhancements(self):
import edit_api
# Test 1: Standard upright front facing pose with direct gaze
standing_kpts = [
[50, 12.5, 0.9], # Nose (0)
[52, 11, 0.9], # L_Eye (1)
[48, 11, 0.9], # R_Eye (2)
[54, 12, 0.9], # L_Ear (3)
[46, 12, 0.9], # R_Ear (4)
[60, 20, 0.9], # L_Shoulder (5)
[40, 20, 0.9], # R_Shoulder (6)
[65, 35, 0.9], # L_Elbow (7)
[35, 35, 0.9], # R_Elbow (8)
[70, 50, 0.9], # L_Wrist (9)
[30, 50, 0.9], # R_Wrist (10)
[58, 55, 0.9], # L_Hip (11)
[42, 55, 0.9], # R_Hip (12)
[60, 75, 0.9], # L_Knee (13)
[40, 75, 0.9], # R_Knee (14)
[60, 90, 0.9], # L_Ankle (15)
[40, 90, 0.9], # R_Ankle (16)
]
pose_desc = edit_api._describe_pose(standing_kpts)
self.assertIn("standing", pose_desc)
self.assertIn("facing forward", pose_desc)
gaze = edit_api._detect_facial_direction(standing_kpts)
self.assertEqual(gaze, "looking forward")
# Test 2: Gaze look left and up
left_up_kpts = [list(kp) for kp in standing_kpts]
# To look left: nose (0) moves to the left (smaller X than ear midpoint 50)
left_up_kpts[0][0] = 45 # Nose X
# To look up: nose (0) moves up (y-level close to eye level y=11)
left_up_kpts[0][1] = 11.2 # Nose Y
gaze_left_up = edit_api._detect_facial_direction(left_up_kpts)
self.assertEqual(gaze_left_up, "looking left and up")
# Test 3: Lying down / reclining pose
lying_kpts = [list(kp) for kp in standing_kpts]
# In lying down, nose is at y=10, hips are at y=12 (very horizontal)
# Head X = 10, Hip X = 80
lying_kpts[0][0] = 10 # Head X
lying_kpts[0][1] = 10 # Head Y
lying_kpts[11][0] = 80 # Hip X
lying_kpts[11][1] = 12 # Hip Y
lying_kpts[12][0] = 80
lying_kpts[12][1] = 12
pose_desc_lying = edit_api._describe_pose(lying_kpts)
self.assertIn("lying down", pose_desc_lying)
def test_18_designer_enhancements(self):
import unittest.mock as mock
import edit_api
# 1. Test Deduplication of Pose Names
# We will mock the external requests.post LLM call to return some mock poses,
# one of which collides with an existing pose name in poses.md (e.g., "The Clasp" or "standing")
mock_raw_response = (
"# standing\n"
"This is a standing pose description.\n"
"Perfect anatomy, photorealistic.\n\n"
"# Custom Pose\n"
"This is a custom pose description.\n"
"Anatomically precise, photorealistic.\n"
)
mock_response = mock.Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"choices": [
{
"message": {
"content": mock_raw_response
}
}
]
}
with mock.patch("requests.post", return_value=mock_response) as mock_post:
# Let's override _load_poses to return {"standing": "existing text"} to force a name collision
with mock.patch("edit_api._load_poses", return_value={"standing": {"text": "existing text", "beta": False}}):
payload = {
"n": 2,
"context": "standing and custom pose instructions",
"filename": None,
"beta": False,
"messages": None
}
response = self.client.post("/designer/generate", json=payload)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["status"], "success")
poses = data["poses"]
# The duplicate "standing" name should be renamed to "standing 2" instead of skipped!
self.assertIn("standing 2", poses)
self.assertIn("Custom Pose", poses)
self.assertEqual(poses["standing 2"], "This is a standing pose description. Perfect anatomy, photorealistic.")
self.assertEqual(poses["Custom Pose"], "This is a custom pose description. Anatomically precise, photorealistic.")
# 2. Test Multi-Turn Conversation/History Payload Building
with mock.patch("requests.post", return_value=mock_response) as mock_post:
with mock.patch("edit_api._load_poses", return_value={}):
# Send history messages in the request
history_messages = [
{"role": "user", "content": "Initial prompt"},
{"role": "assistant", "content": "# Some Pose\nbody text"}
]
payload_with_history = {
"n": 1,
"context": "make them sit down",
"filename": None,
"beta": False,
"messages": history_messages
}
response = self.client.post("/designer/generate", json=payload_with_history)
self.assertEqual(response.status_code, 200)
# Check that requests.post was called with the correct messages payload
self.assertTrue(mock_post.called)
call_args = mock_post.call_args
called_json = call_args[1]["json"]
called_messages = called_json["messages"]
# The payload should contain:
# 1. System message (DESIGNER_SYSTEM)
# 2. Initial user prompt
# 3. Assistant response
# 4. New user follow-up prompt incorporating "make them sit down"
self.assertEqual(len(called_messages), 4)
self.assertEqual(called_messages[0]["role"], "system")
self.assertEqual(called_messages[1]["role"], "user")
self.assertEqual(called_messages[1]["content"], "Initial prompt")
self.assertEqual(called_messages[2]["role"], "assistant")
self.assertEqual(called_messages[2]["content"], "# Some Pose\nbody text")
self.assertEqual(called_messages[3]["role"], "user")
self.assertIn("make them sit down", called_messages[3]["content"])
if __name__ == "__main__":
unittest.main()

View File

@@ -94,6 +94,15 @@
<h2>Untracked Files <span class="info">(On disk, but not in DB)</span></h2>
<div id="untrackedList">No untracked files found.</div>
</div>
<div class="section">
<h2>Metadata & Pose Deep Backfill <span class="info">(Re-analyze pose, anatomical completeness, gaze, and objects with detailed geometry rules)</span></h2>
<div style="display:flex;gap:12px;margin-top:10px">
<button class="btn danger" onclick="invalidateMetadata()">Invalidate All Old Metadata</button>
<button class="btn success" onclick="triggerBackfill()">Trigger Deep Backfill Now</button>
</div>
<div id="backfillStatus" style="margin-top:12px;font-size:0.9em;color:#aaa"></div>
</div>
<div class="info" id="timestamp"></div>
</div>
@@ -240,6 +249,37 @@
`).join('');
}
async function invalidateMetadata() {
if (!confirm("Are you sure you want to invalidate all earlier metadata? This will clear all calculated completeness/pose/gaze/object details, enabling them to be clean-reprocessed by the background worker or manual backfiller.")) return;
try {
const r = await fetch(`${API}/images/invalidate-metadata`, { method: 'POST' });
const d = await r.json();
showToast(d.message || "Metadata invalidated successfully");
document.getElementById('backfillStatus').textContent = "All metadata reset. Background idle backfill will now start processing them one by one.";
} catch (e) {
console.error(e);
showToast('Failed to invalidate metadata');
}
}
async function triggerBackfill() {
document.getElementById('backfillStatus').textContent = "Triggering manual deep backfill on all images in background...";
try {
const r = await fetch(`${API}/images/backfill-metadata`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: true })
});
const d = await r.json();
showToast("Deep backfill complete!");
document.getElementById('backfillStatus').textContent = `Backfill complete! Processed: ${d.processed}, Failed: ${d.failed}, Total: ${d.total}.`;
} catch (e) {
console.error(e);
showToast('Backfill failed');
document.getElementById('backfillStatus').textContent = "Manual backfill failed or timed out.";
}
}
loadInconsistencies();
</script>
</body>