updates UI
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user