updates UI

This commit is contained in:
mike
2026-07-01 05:42:20 +02:00
parent 251d9b1cc8
commit 94419d7673
5 changed files with 298 additions and 95 deletions

View File

@@ -97,6 +97,10 @@ Scenery:
- in order to decouple the landingpage/dashboard from the studio view (editor) we are going to generate group "shoot" specific html pages. Start by just generate the studio portion of car.html into the group specifc html pages. It does not need to be synchronous, but when data of the group is changing we should update the group specifc data json file too, we copy the goup files to the output folder next to car.html - in order to decouple the landingpage/dashboard from the studio view (editor) we are going to generate group "shoot" specific html pages. Start by just generate the studio portion of car.html into the group specifc html pages. It does not need to be synchronous, but when data of the group is changing we should update the group specifc data json file too, we copy the goup files to the output folder next to car.html
- in /mnt/zim/tour-comfy/output/_turntable we have data regarding orbit generations. We want to store the orbit data in the database, start by adding the corresponding tags and meta info to the images (currently stored in the person table (but actually are images with metadata)) - in /mnt/zim/tour-comfy/output/_turntable we have data regarding orbit generations. We want to store the orbit data in the database, start by adding the corresponding tags and meta info to the images (currently stored in the person table (but actually are images with metadata))
- in the ui - studio view we have an orbit filter. Currently the filter is filtering too many files, also references of orbit files are being marked orbit. When an orbit file is used, the metadata should not include the orbit tag. - in the ui - studio view we have an orbit filter. Currently the filter is filtering too many files, also references of orbit files are being marked orbit. When an orbit file is used, the metadata should not include the orbit tag.
- pose-prompt has at least three parts, [scenery ,pose, art-form] allow in the ui to compound the prompt with these, scene default = "You are in a black void" art-form= "photo-realistic keep identical person from reference" also allow inherit scenery to as option.
## refine ## refine
#file:///mnt/zim/tour-comfy/output/shoot_cg_7ec17537.html #file:///mnt/zim/tour-comfy/output/shoot_cg_7ec17537.html
- when refresh page, we lose track of current jobs running. - when refresh page, we lose track of current jobs running.

View File

@@ -1330,10 +1330,20 @@
inset: 0; inset: 0;
background: #0a0a0a; background: #0a0a0a;
z-index: 500; z-index: 500;
flex-direction: column; flex-direction: row; /* row layout to support full height sidebar */
overflow: hidden;
} }
.studio.open { display: flex; } .studio.open { display: flex; }
.studio-left {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
height: 100%;
overflow: hidden;
}
.studio-topbar { .studio-topbar {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
@@ -1348,10 +1358,6 @@
.studio-topbar-center{ flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; overflow: hidden; } .studio-topbar-center{ flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; overflow: hidden; }
.studio-topbar-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } .studio-topbar-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.studio-body {
flex: 1; min-height: 0;
display: flex; overflow: hidden;
}
.studio-main { .studio-main {
flex: 1; min-width: 0; flex: 1; min-width: 0;
display: flex; flex-direction: column; overflow: hidden; display: flex; flex-direction: column; overflow: hidden;
@@ -1464,7 +1470,7 @@
.sidebar-inner { .sidebar-inner {
flex: 1; min-height: 0; flex: 1; min-height: 0;
display: flex; flex-direction: column; display: flex; flex-direction: column;
padding-top: 36px; overflow: hidden; padding-top: 8px; overflow: hidden;
transition: opacity 0.15s; transition: opacity 0.15s;
} }
.studio-sidebar.collapsed .sidebar-inner { opacity: 0; pointer-events: none; } .studio-sidebar.collapsed .sidebar-inner { opacity: 0; pointer-events: none; }
@@ -1936,17 +1942,6 @@
</style> </style>
</head> </head>
<body> <body>
<!-- Floating services status container -->
<div class="services-status-container" style="position: fixed; bottom: 12px; left: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);">
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="backendStatus" title="Backend: Offline (API server is down)">
<span class="status-dot-led offline" id="backendDot"></span>
<span>API</span>
</div>
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="modelStatus" title="Model-Engine: Status unknown (API server is down)">
<span class="status-dot-led offline" id="modelDot"></span>
<span>Model</span>
</div>
</div>
<div id="privacyOverlay" class="privacy-overlay" onclick="if(privacyMode)disablePrivacyOverlay()"> <div id="privacyOverlay" class="privacy-overlay" onclick="if(privacyMode)disablePrivacyOverlay()">
<div class="priv-topbar"> <div class="priv-topbar">
<div class="priv-logo">S</div> <div class="priv-logo">S</div>
@@ -2069,12 +2064,23 @@
</div> </div>
<div class="header"> <div class="header">
<div class="controls"> <div class="controls" style="display:flex;align-items:center;gap:8px">
<span class="count" id="count">0 images</span> <span class="count" id="count">0 images</span>
<button class="btn" id="archiveViewBtn" onclick="toggleArchiveView()" title="View archived items">📦 Archive</button> <button class="btn" id="archiveViewBtn" onclick="toggleArchiveView()" title="View archived items">📦 Archive</button>
<a href="trash.html" class="btn" title="View database inconsistencies" style="text-decoration:none;display:inline-flex;align-items:center;">🗑️ Trash</a> <a href="trash.html" class="btn" title="View database inconsistencies" style="text-decoration:none;display:inline-flex;align-items:center;">🗑️ Trash</a>
<button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button> <button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button>
<button class="btn" id="privacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)">🔓</button> <button class="btn" id="privacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)">🔓</button>
<!-- Inline Services LEDs on Dashboard -->
<div class="services-status-container" style="display:flex;align-items:center;gap:10px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:4px 10px;border-radius:15px;font-family:monospace;font-size:10px;color:#888">
<div style="display:flex;align-items:center;gap:4px;cursor:help" id="backendStatus" title="Backend: Offline (API server is down)">
<span class="status-dot-led offline" id="backendDot"></span>
<span>API</span>
</div>
<div style="display:flex;align-items:center;gap:4px;cursor:help" id="modelStatus" title="Model-Engine: Status unknown (API server is down)">
<span class="status-dot-led offline" id="modelDot"></span>
<span>COMFY</span>
</div>
</div>
</div> </div>
<div class="upload-zone"> <div class="upload-zone">
<input type="file" id="uploadInput" multiple onchange="handleUpload(event)" /> <input type="file" id="uploadInput" multiple onchange="handleUpload(event)" />
@@ -2103,30 +2109,65 @@
<!-- Studio view (replaces old lightbox + faceswap modal) --> <!-- Studio view (replaces old lightbox + faceswap modal) -->
<div id="studio" class="studio"> <div id="studio" class="studio">
<!-- Topbar --> <!-- Left area: topbar + main viewer + filmstrip -->
<div class="studio-topbar"> <div class="studio-left">
<div class="studio-topbar-left"> <!-- Topbar -->
<button class="btn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button> <div class="studio-topbar">
<button class="btn" id="lbBackBtn" onclick="jumpToPrevGroup()" style="font-size:11px;padding:4px 10px;display:none" title="Go back to previously opened group">↺ Back</button> <div class="studio-topbar-left">
</div> <a href="./car.html" id="studioDashboardLink" class="btn" style="font-size:11px;padding:4px 10px;text-decoration:none;display:none;align-items:center;gap:4px">← Dashboard</a>
<div class="studio-topbar-center"> <button class="btn" id="studioGalleryBtn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button>
<input type="text" id="lbGroupNameInput" <button class="btn" id="lbBackBtn" onclick="jumpToPrevGroup()" style="font-size:11px;padding:4px 10px;display:none" title="Go back to previously opened group">↺ Back</button>
style="background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#fff;font-size:13px;font-weight:600;padding:2px 5px;min-width:80px;max-width:200px;outline:none" </div>
placeholder="Group name…" <div class="studio-topbar-center" style="display:flex;align-items:center;gap:6px">
onblur="updateGroupName()" onkeydown="if(event.key==='Enter')this.blur()" <input type="text" id="lbGroupNameInput"
title="Group name (applies to all images in this group)" /> style="background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#fff;font-size:13px;font-weight:600;padding:2px 5px;min-width:80px;max-width:200px;outline:none"
<span id="lbPoseTag" style="font-size:11px;color:#f59e0b;display:none"></span> placeholder="Group name…"
<span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span> onblur="updateGroupName()" onkeydown="if(event.key==='Enter')this.blur()"
<span id="lbCounter" style="font-size:12px;color:#555"></span> title="Group name (applies to all images in this group)" />
</div>
<div class="studio-topbar-right" style="display:flex;align-items:center;gap:6px"> <span style="color:#444;font-size:13px">/</span>
<button class="btn" id="studioPrivacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)" style="font-size:12px;padding:4px 8px">🔓</button>
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button> <input type="text" id="lbTopbarImageNameInput"
</div> style="background:transparent;border:none;border-bottom:1px solid #2a2a2a;color:#ccc;font-size:12px;padding:2px 5px;min-width:80px;max-width:180px;outline:none"
</div> placeholder="Image name…"
onblur="updateTopbarImageName()" onkeydown="if(event.key==='Enter')this.blur()"
title="Image name / Character name" />
<div style="display:flex;gap:4px;margin-left:4px">
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:3px;height:20px;border-radius:4px" title="Copy raw filename (ID) to clipboard">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>ID
</button>
<button class="sb-btn" onclick="copyFilepath()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:3px;height:20px;border-radius:4px" title="Copy absolute file path to clipboard">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>Path
</button>
</div>
<!-- Body: main viewer + sidebar --> <span id="lbPoseTag" style="font-size:11px;color:#f59e0b;display:none"></span>
<div class="studio-body"> <span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span>
<span id="lbCounter" style="font-size:12px;color:#555"></span>
</div>
<div class="studio-topbar-right" style="display:flex;align-items:center;gap:6px">
<!-- Inline Services LEDs on Studio Topbar -->
<div class="services-status-container" style="display:flex;align-items:center;gap:10px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);padding:4px 10px;border-radius:15px;font-family:monospace;font-size:10px;color:#888;margin-right:6px">
<div style="display:flex;align-items:center;gap:4px;cursor:help" id="studioBackendStatus" title="Backend: Status unknown">
<span class="status-dot-led offline" id="studioBackendDot"></span>
<span>API</span>
</div>
<div style="display:flex;align-items:center;gap:4px;cursor:help" id="studioModelStatus" title="Model-Engine: Status unknown">
<span class="status-dot-led offline" id="studioModelDot"></span>
<span>COMFY</span>
</div>
</div>
<button class="btn" id="studioPrivacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)" style="font-size:12px;padding:4px 8px">🔓</button>
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button>
</div>
</div>
<!-- Main area: viewer + filmstrip --> <!-- Main area: viewer + filmstrip -->
<div class="studio-main"> <div class="studio-main">
@@ -2179,19 +2220,20 @@
<option value="15000">15s</option> <option value="15000">15s</option>
</select> </select>
</div> </div>
</div> </div> <!-- Close .studio-main -->
</div> <!-- Close .studio-left -->
<!-- Sidebar toolbox --> <!-- Sidebar toolbox -->
<div id="studioSidebar" class="studio-sidebar"> <div id="studioSidebar" class="studio-sidebar">
<div id="sidebarInner" class="sidebar-inner"> <div id="sidebarInner" class="sidebar-inner">
<div class="sidebar-tabs"> <div class="sidebar-tabs">
<button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')">Info</button> <button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')" title="Info"></button>
<button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Ceate</button> <button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')" title="Create">🪄</button>
<button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">👤</button> <button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')" title="Faceswap">👤</button>
<button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">🎬</button> <button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')" title="Scenery">🎬</button>
<button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">α</button> <button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')" title="Segment / Background">α</button>
<button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit"></button> <button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit" title="Orbit 3D"></button>
<button class="sb-tab" data-tab="designer" onclick="switchSidebarTab('designer')" id="sbTabDesigner">Design</button> <button class="sb-tab" data-tab="designer" onclick="switchSidebarTab('designer')" id="sbTabDesigner" title="Design / Refine">🎨</button>
</div> </div>
<div class="sb-panels"> <div class="sb-panels">
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works --> <!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
@@ -2355,7 +2397,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Orbit preview modal --> <!-- Orbit preview modal -->
<div id="orbitModal"> <div id="orbitModal">
@@ -2503,6 +2544,36 @@
// --- HYDRATION_START --- // --- HYDRATION_START ---
const PRELOADED_IMAGES = [ const PRELOADED_IMAGES = [
"20260701_051754_image.png",
"20260701_051737_image.png",
"20260701_051721_image.png",
"20260701_051704_image.png",
"20260701_051647_image.png",
"20260701_051614_image.png",
"20260701_051541_image.png",
"20260701_051520_image.png",
"20260701_050249_image.png",
"20260701_050232_image.png",
"20260701_050215_image.png",
"20260701_050158_image.png",
"20260701_050141_image.png",
"20260701_050124_image.png",
"20260701_050108_image.png",
"20260701_050051_image.png",
"20260701_050035_image.png",
"20260701_050018_image.png",
"20260701_050001_image.png",
"20260701_045945_image.png",
"20260701_045928_image.png",
"20260701_045912_image.png",
"20260701_045838_image.png",
"20260701_045822_image.png",
"20260701_045805_image.png",
"20260701_045749_image.png",
"20260701_045732_image.png",
"20260701_045716_image.png",
"20260701_045659_1_20260619_202924_image.png",
"20260620_014953_image.png",
"20260630_130454_sc_0_20260622_101128_image.nobg.png", "20260630_130454_sc_0_20260622_101128_image.nobg.png",
"20260630_131638_sc_2_20260622_101128_image.nobg.nobg.png", "20260630_131638_sc_2_20260622_101128_image.nobg.nobg.png",
"20260630_151502_sc_0_20260622_101128_image.nobg.png", "20260630_151502_sc_0_20260622_101128_image.nobg.png",
@@ -7232,7 +7303,6 @@
"20260620_020313_2_20260618_172500_image.png", "20260620_020313_2_20260618_172500_image.png",
"20260620_020302_2_20260618_172500_image.png", "20260620_020302_2_20260618_172500_image.png",
"20260620_020251_2_20260618_172500_image.png", "20260620_020251_2_20260618_172500_image.png",
"20260620_014953_image.png",
"20260620_014646_image.png", "20260620_014646_image.png",
"20260620_014349_image.png", "20260620_014349_image.png",
"20260620_014239_img_77.png", "20260620_014239_img_77.png",
@@ -7943,6 +8013,7 @@
// ============================================ // ============================================
let autoRefreshTimer = null; let autoRefreshTimer = null;
let isInitialLoad = true;
let knownFiles = new Set(); let knownFiles = new Set();
let currentFiles = []; let currentFiles = [];
let currentGroups = []; let currentGroups = [];
@@ -8175,6 +8246,7 @@
// --- studio view (replaces lightbox) --- // --- studio view (replaces lightbox) ---
let lbUrls = [], lbNames = [], lbIdx = 0; let lbUrls = [], lbNames = [], lbIdx = 0;
let _lastRenderedFilename = null;
let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived' let _activeFilmstripTab = 'active'; // 'active', 'group', 'hidden', 'source', 'archived'
let _multiSelectModeActive = false; let _multiSelectModeActive = false;
const _selectedFilenames = new Set(); const _selectedFilenames = new Set();
@@ -9123,6 +9195,23 @@
function updateStudio() { function updateStudio() {
const fname = lbNames[lbIdx]; const fname = lbNames[lbIdx];
if (fname && fname !== _lastRenderedFilename) {
// Image changed, force cancel any active text areas/edits
_lastRenderedFilename = fname;
const genPromptEl = document.getElementById('lbGenPrompt');
if (genPromptEl) {
genPromptEl.innerHTML = '';
genPromptEl.onclick = () => editMetadataField('prompt');
}
const descEl = document.getElementById('lbDescription');
if (descEl) {
descEl.innerHTML = '';
descEl.onclick = () => editMetadataField('description');
}
} else if (!fname) {
_lastRenderedFilename = null;
}
if (document.getElementById('poseCanvas')) _removePoseOverlay(); // skeleton is per-image if (document.getElementById('poseCanvas')) _removePoseOverlay(); // skeleton is per-image
document.getElementById('poseResults')?.remove(); document.getElementById('poseResults')?.remove();
@@ -9256,6 +9345,9 @@
const nameInput = document.getElementById('lbNameInput'); const nameInput = document.getElementById('lbNameInput');
if (nameInput) { nameInput.value = imageNames[fname] || ''; nameInput.placeholder = fname; } if (nameInput) { nameInput.value = imageNames[fname] || ''; nameInput.placeholder = fname; }
const topbarNameInput = document.getElementById('lbTopbarImageNameInput');
if (topbarNameInput) { topbarNameInput.value = imageNames[fname] || ''; topbarNameInput.placeholder = fname; }
const clipEl = document.getElementById('lbClipDesc'); const clipEl = document.getElementById('lbClipDesc');
if (clipEl) clipEl.textContent = clipDescriptions[fname] || ''; if (clipEl) clipEl.textContent = clipDescriptions[fname] || '';
@@ -9682,6 +9774,8 @@
}); });
if (r.ok) { if (r.ok) {
imageNames[fname] = newName; imageNames[fname] = newName;
const topbarNameInput = document.getElementById('lbTopbarImageNameInput');
if (topbarNameInput) topbarNameInput.value = newName;
showToast(`Name updated`); showToast(`Name updated`);
} }
} catch (e) { } catch (e) {
@@ -9689,6 +9783,18 @@
} }
} }
async function updateTopbarImageName() {
const fname = lbNames[lbIdx];
const topbarNameInput = document.getElementById('lbTopbarImageNameInput');
if (!topbarNameInput) return;
const newName = topbarNameInput.value;
const nameInput = document.getElementById('lbNameInput');
if (nameInput) nameInput.value = newName;
await updateName();
}
async function updateGroupName() { async function updateGroupName() {
const gid = lbCurrentGid; const gid = lbCurrentGid;
if (!gid) return; if (!gid) return;
@@ -11375,10 +11481,21 @@
async function _pollTurntableBadge() { async function _pollTurntableBadge() {
try { try {
let d = null; let d = null;
try { const gid = lbCurrentGid;
const r = await fetch(`${API}/output/_data/turntable_status.json?t=` + Date.now(), { signal: AbortSignal.timeout(2000) });
if (r.ok) d = await r.json(); if (gid) {
} catch (_) {} try {
const r = await fetch(`${API}/turntable/status/${encodeURIComponent(gid)}`);
if (r.ok) d = await r.json();
} catch (_) {}
}
if (!d) {
try {
const r = await fetch(`${API}/output/_data/turntable_status.json?t=` + Date.now(), { signal: AbortSignal.timeout(2000) });
if (r.ok) d = await r.json();
} catch (_) {}
}
if (!d) { if (!d) {
const r = await fetch(`${API}/turntable/status`); const r = await fetch(`${API}/turntable/status`);
if (r.ok) d = await r.json(); if (r.ok) d = await r.json();
@@ -11389,7 +11506,7 @@
const tbsDot = document.getElementById('tbsDot'); const tbsDot = document.getElementById('tbsDot');
const tbsText = document.getElementById('tbsText'); const tbsText = document.getElementById('tbsText');
const total = d.n_total || 0; const total = d.n_total || 0;
const done = d.n_complete || 0; const done = (d.n_complete !== undefined ? d.n_complete : d.n_done) || 0;
if (total === 0) { badge.style.display = 'none'; } if (total === 0) { badge.style.display = 'none'; }
else if (done < total) { badge.textContent = `${done}/${total}`; badge.style.display = ''; } else if (done < total) { badge.textContent = `${done}/${total}`; badge.style.display = ''; }
else { badge.style.display = 'none'; } else { badge.style.display = 'none'; }
@@ -11413,8 +11530,15 @@
// Refresh orbit tab badge on the tab button // Refresh orbit tab badge on the tab button
const tabBtn = document.getElementById('sbTabOrbit'); const tabBtn = document.getElementById('sbTabOrbit');
if (tabBtn && done > 0) { if (tabBtn) {
tabBtn.textContent = `Orbit${done < total ? ' (' + done + '/' + total + ')' : ' ✓'}`; tabBtn.title = `Orbit 3D (${done}/${total})`;
if (total > 0 && done < total) {
tabBtn.innerHTML = `⚛<span style="font-size: 8px; vertical-align: super; background: #fbbf24; color: #1e1b4b; padding: 1px 3px; border-radius: 6px; margin-left: 1px; display: inline-block; line-height: 1; font-weight: bold;">${done}/${total}</span>`;
} else if (total > 0 && done === total) {
tabBtn.innerHTML = `⚛<span style="font-size: 8px; vertical-align: super; background: #10b981; color: #fff; padding: 1px 3px; border-radius: 6px; margin-left: 1px; display: inline-block; line-height: 1; font-weight: bold;">✓</span>`;
} else {
tabBtn.innerHTML = ``;
}
} }
} catch(_) {} } catch(_) {}
} }
@@ -12155,7 +12279,8 @@
} }
document.getElementById('count').textContent = `${currentGroups.length} group${currentGroups.length !== 1 ? 's' : ''} · ${fileObjects.length} images`; document.getElementById('count').textContent = `${currentGroups.length} group${currentGroups.length !== 1 ? 's' : ''} · ${fileObjects.length} images`;
if (newFileNames.size > 0) showToast(`${newFileNames.size} new image${newFileNames.size !== 1 ? 's' : ''} detected`, 'success'); if (newFileNames.size > 0 && !isInitialLoad) showToast(`${newFileNames.size} new image${newFileNames.size !== 1 ? 's' : ''} detected`, 'success');
isInitialLoad = false;
// If studio is open, sync lbUrls/lbNames from freshly-loaded groupData // If studio is open, sync lbUrls/lbNames from freshly-loaded groupData
const studioEl = document.getElementById('studio'); const studioEl = document.getElementById('studio');
@@ -12419,18 +12544,36 @@
_backendOnline = true; _backendOnline = true;
_modelEngineOnline = data.model_engine.online; _modelEngineOnline = data.model_engine.online;
const backendClass = data.backend.busy ? 'status-dot-led busy' : 'status-dot-led online';
const backendTitle = data.backend.busy ? 'Backend: Active task running' : 'Backend: Idle (Online)';
const modelClass = data.model_engine.online ? (data.model_engine.busy ? 'status-dot-led busy' : 'status-dot-led online') : 'status-dot-led offline';
const modelTitle = data.model_engine.online
? (data.model_engine.busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)')
: 'Model-Engine: Offline (ComfyUI backend is down)';
// Dashboard dots
const backendDot = document.getElementById('backendDot'); const backendDot = document.getElementById('backendDot');
if (backendDot) { if (backendDot) {
backendDot.className = 'status-dot-led ' + (data.backend.busy ? 'busy' : 'online'); backendDot.className = backendClass;
backendDot.parentElement.title = data.backend.busy ? 'Backend: Active task running' : 'Backend: Idle (Online)'; backendDot.parentElement.title = backendTitle;
} }
const modelDot = document.getElementById('modelDot'); const modelDot = document.getElementById('modelDot');
if (modelDot) { if (modelDot) {
modelDot.className = 'status-dot-led ' + (data.model_engine.online ? (data.model_engine.busy ? 'busy' : 'online') : 'offline'); modelDot.className = modelClass;
modelDot.parentElement.title = data.model_engine.online modelDot.parentElement.title = modelTitle;
? (data.model_engine.busy ? 'Model-Engine: Active inference running' : 'Model-Engine: Idle (Online)') }
: 'Model-Engine: Offline (ComfyUI backend is down)';
// Studio topbar dots
const studioBackendDot = document.getElementById('studioBackendDot');
if (studioBackendDot) {
studioBackendDot.className = backendClass;
studioBackendDot.parentElement.title = backendTitle;
}
const studioModelDot = document.getElementById('studioModelDot');
if (studioModelDot) {
studioModelDot.className = modelClass;
studioModelDot.parentElement.title = modelTitle;
} }
} else { } else {
throw new Error("API responded with error"); throw new Error("API responded with error");
@@ -12439,15 +12582,32 @@
_backendOnline = false; _backendOnline = false;
_modelEngineOnline = false; _modelEngineOnline = false;
const offlineClass = 'status-dot-led offline';
const backendOfflineTitle = 'Backend: Offline (API server is down)';
const modelOfflineTitle = 'Model-Engine: Status unknown (API server is down)';
// Dashboard dots
const backendDot = document.getElementById('backendDot'); const backendDot = document.getElementById('backendDot');
if (backendDot) { if (backendDot) {
backendDot.className = 'status-dot-led offline'; backendDot.className = offlineClass;
backendDot.parentElement.title = 'Backend: Offline (API server is down)'; backendDot.parentElement.title = backendOfflineTitle;
} }
const modelDot = document.getElementById('modelDot'); const modelDot = document.getElementById('modelDot');
if (modelDot) { if (modelDot) {
modelDot.className = 'status-dot-led offline'; modelDot.className = offlineClass;
modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)'; modelDot.parentElement.title = modelOfflineTitle;
}
// Studio topbar dots
const studioBackendDot = document.getElementById('studioBackendDot');
if (studioBackendDot) {
studioBackendDot.className = offlineClass;
studioBackendDot.parentElement.title = backendOfflineTitle;
}
const studioModelDot = document.getElementById('studioModelDot');
if (studioModelDot) {
studioModelDot.className = offlineClass;
studioModelDot.parentElement.title = modelOfflineTitle;
} }
} }
@@ -13007,6 +13167,14 @@
document.title = PRIV_TITLE; document.title = PRIV_TITLE;
showPrivacyOverlay(); showPrivacyOverlay();
} }
// Direct-to-dashboard back link for group specific pages
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
const dashLink = document.getElementById('studioDashboardLink');
const galBtn = document.getElementById('studioGalleryBtn');
if (dashLink) dashLink.style.display = 'inline-flex';
if (galBtn) galBtn.style.display = 'none';
}
}); });
async function toggleTagAction(tagName) { async function toggleTagAction(tagName) {
@@ -13095,7 +13263,7 @@
fileDescriptions[fname] = newDesc; fileDescriptions[fname] = newDesc;
showToast('Successfully updated both prompt and description!', 'success'); showToast('Successfully updated both prompt and description!', 'success');
updateStudio(); updateStudio();
refreshNow(); refreshNow(true);
} else { } else {
showToast('Failed to update prompt or description', 'error'); showToast('Failed to update prompt or description', 'error');
} }
@@ -13120,17 +13288,19 @@
const el = document.getElementById(contentId); const el = document.getElementById(contentId);
if (!el || el.querySelector('textarea')) return; if (!el || el.querySelector('textarea')) return;
const buttonLabel = currentVal.trim() === '' ? '✨ Create' : '✨ Refine';
el.onclick = null; el.onclick = null;
el.innerHTML = ` el.innerHTML = `
<textarea id="editFieldArea" style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px">${currentVal}</textarea> <textarea id="editFieldArea_${field}" style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px">${currentVal}</textarea>
<div style="display:flex;gap:6px;justify-content:flex-end"> <div style="display:flex;gap:6px;justify-content:flex-end">
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="cancelEditField(event, '${field}')">Cancel</button> <button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="cancelEditField(event, '${field}')">Cancel</button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, '${field}')">✨ Refine</button> <button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, '${field}')">${buttonLabel}</button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px;background:#10b981;color:#fff" onclick="saveEditField(event, '${field}')">Save</button> <button class="sb-btn" style="padding:2px 8px;font-size:10px;background:#10b981;color:#fff" onclick="saveEditField(event, '${field}')">Save</button>
</div> </div>
`; `;
const ta = document.getElementById('editFieldArea'); const ta = document.getElementById(`editFieldArea_${field}`);
if (ta) { if (ta) {
ta.focus(); ta.focus();
ta.setSelectionRange(ta.value.length, ta.value.length); ta.setSelectionRange(ta.value.length, ta.value.length);
@@ -13151,7 +13321,7 @@
const fname = lbNames[lbIdx]; const fname = lbNames[lbIdx];
if (!fname) return; if (!fname) return;
const ta = document.getElementById('editFieldArea'); const ta = document.getElementById(`editFieldArea_${field}`);
if (!ta) return; if (!ta) return;
const newVal = ta.value.trim(); const newVal = ta.value.trim();
@@ -13174,7 +13344,7 @@
showToast('Character & scenery description updated successfully', 'success'); showToast('Character & scenery description updated successfully', 'success');
} }
updateStudio(); updateStudio();
refreshNow(); refreshNow(true);
} else { } else {
showToast('Failed to save update', 'error'); showToast('Failed to save update', 'error');
} }
@@ -13189,24 +13359,30 @@
const fname = lbNames[lbIdx]; const fname = lbNames[lbIdx];
if (!fname) return; if (!fname) return;
const ta = document.getElementById('editFieldArea'); const ta = document.getElementById(`editFieldArea_${field}`);
if (!ta) return; if (!ta) return;
const currentVal = ta.value.trim(); const currentVal = ta.value.trim();
const instruction = prompt(`Enter instructions to refine this ${field}:`, "Make it more detailed..."); const actionVerb = currentVal === '' ? 'create' : 'refine';
const instruction = prompt(`Enter instructions to ${actionVerb} this ${field}:`, actionVerb === 'create' ? "Write a detailed description of..." : "Make it more detailed...");
if (!instruction) return; if (!instruction) return;
showToast('AI is refining your text...', 'info'); showToast(`AI is generating your ${field}...`, 'info');
try { try {
const r = await fetch(`${API}/refine-prompt`, { const r = await fetch(`${API}/refine-prompt`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: currentVal, user_instruction: instruction }) body: JSON.stringify({
prompt: currentVal,
user_instruction: instruction,
filename: fname,
field: field
})
}); });
if (r.ok) { if (r.ok) {
const data = await r.json(); const data = await r.json();
ta.value = data.refined; ta.value = data.refined;
showToast('Refined successfully! Click Save to apply.', 'success'); showToast('Successfully updated! Click Save to apply.', 'success');
} else { } else {
showToast('Failed to refine field: ' + await r.text(), 'error'); showToast('Failed to refine field: ' + await r.text(), 'error');
} }

View File

@@ -422,7 +422,8 @@ def list_persons(include_archived=False):
SELECT filename, name, group_id, clip_description, SELECT filename, name, group_id, clip_description,
prompt, pose, sort_order, group_name, hidden, has_background, source_refs, prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
has_clothing, content_type, faceswap_source_video, archived, is_source, tags, has_clothing, content_type, faceswap_source_video, archived, is_source, tags,
pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects, description pose_description, pose_skeleton, people_count, anatomical_completeness, facial_direction, objects, description,
filepath
FROM person FROM person
{where} {where}
""") """)

View File

@@ -1508,6 +1508,8 @@ def _write_all_static() -> None:
"anatomical_completeness": p[20], "anatomical_completeness": p[20],
"facial_direction": p[21], "facial_direction": p[21],
"objects": obj_list, "objects": obj_list,
"description": p[23],
"filepath": p[24],
}) })
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived") print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
try: try:
@@ -2144,16 +2146,15 @@ def start_multi_ref(req: MultiRefRequest):
class RefineRequest(BaseModel): class RefineRequest(BaseModel):
prompt: str prompt: str = ""
filename: str | None = None filename: str | None = None
user_instruction: str | None = None
field: str | None = None
@app.post("/refine-prompt") @app.post("/refine-prompt")
def refine_prompt(req: RefineRequest): def refine_prompt(req: RefineRequest):
"""Refine a prompt using the external uncensored chat LLM.""" """Refine or create a prompt/description using the external uncensored chat LLM."""
if not req.prompt:
raise HTTPException(400, "Prompt is required")
context_str = "" context_str = ""
if req.filename: if req.filename:
try: try:
@@ -2197,9 +2198,22 @@ def refine_prompt(req: RefineRequest):
except Exception as db_err: except Exception as db_err:
print(f"[refine-prompt] database error for {req.filename}: {db_err}") print(f"[refine-prompt] database error for {req.filename}: {db_err}")
user_content = f"Refine this pose: {req.prompt}" field_label = req.field if req.field else "text"
if req.prompt:
if req.user_instruction:
user_content = f"Refine this {field_label} according to the following instructions:\n{req.user_instruction}\n\nOriginal {field_label}:\n{req.prompt}"
else:
user_content = f"Refine this {field_label} to make it more descriptive, detailed, and high-quality.\n\nOriginal {field_label}:\n{req.prompt}"
else:
# Prompt/text is empty, so we are creating one from scratch!
if req.user_instruction:
user_content = f"Create a new {field_label} from scratch according to these instructions:\n{req.user_instruction}"
else:
user_content = f"Create a new detailed and high-quality {field_label} from scratch."
if context_str: if context_str:
user_content += f"\n\nUse the following image context details to ensure the refined prompt matches the reference characteristics closely:\n{context_str}" user_content += f"\n\nUse the following image context details to ensure the output matches the reference characteristics closely:\n{context_str}"
# Use the same API as gen_poses.py # Use the same API as gen_poses.py
llm_api = "http://192.168.1.160:8001/v1/chat/completions" llm_api = "http://192.168.1.160:8001/v1/chat/completions"
@@ -2221,7 +2235,8 @@ def refine_prompt(req: RefineRequest):
try: try:
database.save_db_prompt("refine", refined, { database.save_db_prompt("refine", refined, {
"original": req.prompt, "original": req.prompt,
"filename": req.filename "filename": req.filename,
"field": req.field
}) })
except Exception as db_err: except Exception as db_err:
print(f"[refine-prompt] failed to save to prompt table: {db_err}") print(f"[refine-prompt] failed to save to prompt table: {db_err}")
@@ -2712,6 +2727,7 @@ def list_images(archived: bool = False, bypass_static: bool = False):
"facial_direction": p[21], "facial_direction": p[21],
"objects": obj_list, "objects": obj_list,
"description": p[23] if len(p) > 23 else None, "description": p[23] if len(p) > 23 else None,
"filepath": p[24] if len(p) > 24 else None,
}) })
db_images.sort( db_images.sort(
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1], key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],

View File

@@ -878,6 +878,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t
# Lantern: # Lantern:
you are in a black void.
kneeling, knees apart, sitting back on the heels. kneeling, knees apart, sitting back on the heels.
torso upright, spine long. torso upright, spine long.
arms extended overhead, hands clasped or holding an imaginary object. arms extended overhead, hands clasped or holding an imaginary object.
@@ -889,6 +890,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t
# Compass: # Compass:
you are in a black void.
seated, one leg extended straight forward. seated, one leg extended straight forward.
other leg bent, foot placed on the inner thigh of the extended leg. other leg bent, foot placed on the inner thigh of the extended leg.
torso folded forward over the extended leg. torso folded forward over the extended leg.
@@ -900,6 +902,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t
# Vessel (beta): # Vessel (beta):
you are in a black void.
lying on back, knees bent, feet flat on the ground. lying on back, knees bent, feet flat on the ground.
arms extended overhead, hands clasped. arms extended overhead, hands clasped.
hips lifted slightly, lower back arched. hips lifted slightly, lower back arched.
@@ -911,6 +914,7 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t
# Quiver (beta): # Quiver (beta):
you are in a black void.
lying on stomach, legs together, arms at the sides. lying on stomach, legs together, arms at the sides.
torso lifted in a backbend, chest and shoulders arching upward. torso lifted in a backbend, chest and shoulders arching upward.
head thrown back, neck stretched. head thrown back, neck stretched.
@@ -922,6 +926,7 @@ Perfect anatomy, hyperrealistic
# Loom: # Loom:
you are in a black void.
seated, legs extended straight forward, feet flexed. seated, legs extended straight forward, feet flexed.
torso upright, spine long. torso upright, spine long.
arms extended forward, hands reaching toward the feet. arms extended forward, hands reaching toward the feet.
@@ -3051,7 +3056,7 @@ You are standing in black void.
A single solid steel device, polished and seamless. A single solid steel device, polished and seamless.
An internal plug, contoured and smooth, connected by a short rigid bar to a small external shield covering your vulva. An internal plug, contoured and smooth, connected by a short rigid bar to a small external shield covering your vulva.
From the shield, a thin rigid steel bar rises up your abdomen and sternum, terminating in a collar locked around your neck. From the shield, a thin rigid steel bar rises up your abdomen and sternum, terminating in a collar locked around your neck.
One continuous piece — internal plug, connecting bar, collar. One continuous piece — internal plug, connecting bar, collar.
Your wrists cuffed to the vertical bar at your hips. Your wrists cuffed to the vertical bar at your hips.
Your ankles cuffed together. Your ankles cuffed together.
The device occupies and seals simultaneously — internal and external denial in one form. The device occupies and seals simultaneously — internal and external denial in one form.
@@ -4630,11 +4635,12 @@ sheen of perspiration, accentuating every muscle contour. Anatomically precise,
# Integrated Collar Arms Restraint # Integrated Collar Arms Restraint
A subject kneels, your body bent into a powerful, inviting stance. A subject kneels, your body bent into a powerful, inviting stance.
Your arms are restrained behind your back, firmly secured with an integral collar restraint, a sleek device that encircles your neck, connecting to your wrists with rigid steel bars. Your arms are restrained behind your back, firmly secured with an integral collar restraint, a sleek device that encircles your neck, connecting to your wrists with rigid steel bars.
Your hands are clasped together, adding to the restricted pose. Your knees are positioned under your hips, providing stability. Your hands are clasped together, adding to the restricted pose. Your knees are positioned under your hips, providing stability.
The back is arched slightly upward, head lifted, drawing attention to the exposed shoulders and back. The back is arched slightly upward, head lifted, drawing attention to the exposed shoulders and back.
Your gaze is locked onto the viewer, a look of defiance and anticipation. The pose conveys a mix of sultry energy and Your gaze is locked onto the viewer, a look of defiance and anticipation. The pose conveys a mix of sultry energy and
power, the steel bars creating an asymmetrical silhouette. Perfect anatomy, realistic. power, the steel bars creating an asymmetrical silhouette. Perfect anatomy, realistic.
# nice pose # nice pose
standing, feet apart, hands on the hips. torso twisted, one shoulder forward, one back. weight shifted to one leg, opposite hip popped. waist compressed, creating a pronounced hourglass silhouette. head turned to look over the shoulder. Looking back into camera. Pinched, exaggerated. Perfect anatomy, realistic standing, feet apart, hands on the hips. torso twisted, one shoulder forward, one back. weight shifted to one leg, opposite hip popped. waist compressed, creating a pronounced hourglass silhouette. head turned to look over the shoulder. Looking back into camera. Pinched, exaggerated. Perfect anatomy, realistic