diff --git a/backlog.md b/backlog.md index c2122c2..303ee15 100644 --- a/backlog.md +++ b/backlog.md @@ -12,7 +12,13 @@ groups.json config videos +prompts should be viisible in studio view + copied artefacts should cary references flag beta poses in ui delete button gets disabled after a delete + +extract a frame from a clip (scenery) +faceswap defaults nothing enabled, see jobs even when refresh + diff --git a/tour-comfy/bad-poses.md b/tour-comfy/bad-poses.md index f89b963..da066ee 100644 --- a/tour-comfy/bad-poses.md +++ b/tour-comfy/bad-poses.md @@ -3,13 +3,6 @@ A realistic artistic nude female. The model kneels on all fours with hands planted firmly under the shoulders and knees directly under the hips. One arm and the opposite leg are lifted completely off the ground, creating a powerful diagonal stretch across the body. Hips are lowered toward the ground. The composition emphasizes the muscular tension in the lifted limbs and the continuous curved lines of the arched back and torso. Strength and dynamic energy radiate from the pose. Studio lighting, anatomically precise, no background, plain neutral backdrop, high detail, professional figure reference photography. -# The Butterfly2: -Lays down with knees bent and feet touching, soles of feet pressed together in butterfly pose. -Clasp your hands around your legs and pull your feet up towards your chest. -Extend one leg over the other. -Head on top. -Realistic, perfect anatomy - # The Kneeling (Dynamic): kneeling on all fours, hands under shoulders, knees under hips. one arm and opposite leg slightly lifted off the ground, creating diagonal tension. @@ -25,3 +18,43 @@ legs extended straight behind, toes pointed. head lowered, face turned down toward the ground, chin tucked. Looking down, not at camera. Regal, poised. Perfect anatomy, realistic + +# The Ascension (beta): +lying on back. +arms extended overhead, hands clasped or reaching past the head. +legs lifted straight up together, fully extended toward ceiling. +lower back pressed flat to the ground, shoulders relaxed. +vertical line from shoulders through heels. +Looking directly into camera. Weightless, transcendent. +Perfect anatomy, realistic + + +# The Sickle (beta): +lying on back, legs lifted and bent. +knees drawing toward the chest, then extending and lowering toward the head. +feet arched, toes pointing toward the ground behind the head. +arms wrapped around the thighs or extended to the sides. +body folded tightly, spine compressed. +Looking at the knees, then into camera. Curved, hooked. +Perfect anatomy, realistic + +--- + +# The Hogtie: +lying on stomach, wrists bound behind the back. +ankles bound together, drawn toward the bound wrists. +body arched backward, chest and thighs lifted slightly off the ground. +head turned to the side, cheek on the ground. +Looking directly into camera. Contorted, helpless. +Perfect anatomy, realistic + + +--- + +# The Spread Eagle (Restrained): +lying on back, arms extended straight out to the sides, wrists bound to implied anchors. +legs spread wide, ankles bound to implied anchors. +back flat on the ground, hips centered. +head tilted back, throat exposed. +Looking upward, not at camera. Vulnerable, displayed. +Perfect anatomy, realistic diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 24d4a85..91dcf17 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -1183,6 +1183,7 @@ border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0; + will-change: transform; } .fs-options { display: flex; @@ -1400,6 +1401,7 @@ border-top-color: #2563eb; border-radius: 50%; animation: spin 0.8s linear infinite; display: inline-block; margin-right: 4px; vertical-align: middle; + will-change: transform; } .sb-source-refs { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } .sb-source-refs img { @@ -1765,6 +1767,10 @@ onblur="updateName()" onkeydown="if(event.key==='Enter')this.blur()" style="margin-bottom:8px">
+
Order & visibility
@@ -1783,8 +1789,10 @@
+ + Download - +
@@ -2123,6 +2131,9 @@ let availableVideos = []; // populated from /videos let _fsActiveTab = 'swap'; let _fsSelectedPoses = new Set(); + let _sbPoseEdits = {}; // poseName → edited text + let _sbGenJobId = null; + let _sbGenJobPollTimer= null; let _fsTrimVideo = null; function escHtml(s) { @@ -2177,6 +2188,8 @@ function startGroupCycle(base) { cycleIdx.set(base, 0); // index into visibleIdx array const id = setInterval(() => { + // Don't update gallery cards while studio is open — saves GPU and avoids distraction + if (document.getElementById('studio').classList.contains('open')) return; const data = groupData.get(base); if (!data || !data.visibleIdx || data.visibleIdx.length < 2) { clearInterval(id); cycleTimers.delete(base); return; } const card = [...document.querySelectorAll('.image-card')].find(c => c.dataset.group === base); @@ -2194,6 +2207,8 @@ // --- studio view (replaces lightbox) --- let lbUrls = [], lbNames = [], lbIdx = 0; let _activeSidebarTab = localStorage.getItem('studioSidebarTab') || 'info'; + // Maps fname → nobg sidecar URL; viewer shows sidecar while this entry exists + const _nobgSidecars = new Map(); function openLightbox(gid, startIdx) { // kept for compat openStudio(gid, startIdx); @@ -2209,6 +2224,8 @@ lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0); updateStudio(); document.getElementById('studio').classList.add('open'); + // Hide main page scrollbar — nothing in studio scrolls the page + document.body.style.overflow = 'hidden'; // Restore sidebar state const sidebar = document.getElementById('studioSidebar'); const btn = document.getElementById('sidebarInnerToggle'); @@ -2221,6 +2238,7 @@ function closeStudio() { document.getElementById('studio').classList.remove('open'); + document.body.style.overflow = ''; } function lbNav(dir) { @@ -2261,6 +2279,12 @@ const clipEl = document.getElementById('lbClipDesc'); if (clipEl) clipEl.textContent = clipDescriptions[fname] || ''; + const genPromptWrap = document.getElementById('lbGenPromptWrap'); + const genPromptEl = document.getElementById('lbGenPrompt'); + const genPrompt = filePrompts[fname] || ''; + if (genPromptWrap) genPromptWrap.style.display = genPrompt ? '' : 'none'; + if (genPromptEl) genPromptEl.textContent = genPrompt; + const pose = filePoses[fname]; const poseTag = document.getElementById('lbPoseTag'); if (pose) { poseTag.textContent = '⛷ ' + pose; poseTag.style.display = ''; } @@ -2278,6 +2302,8 @@ if (noBgBtn) noBgBtn.style.display = (!isVid && fileHasBg[fname] !== false) ? '' : 'none'; if (undressBtn) undressBtn.style.display = (!isVid && fileHasClothing[fname] === true) ? '' : 'none'; if (faceswapBtn)faceswapBtn.style.display = !isVid ? '' : 'none'; + const cropBtn = document.getElementById('lbCropBtn'); + if (cropBtn) cropBtn.style.display = (!isVid && fileHasBg[fname] === false) ? '' : 'none'; // Source refs const refs = fileSourceRefs[fname]; @@ -2447,24 +2473,96 @@ } } - async function lbDelete() { + // Two-step inline confirmation: first click arms the button, second confirms. + let _deleteArmed = false, _deleteArmTimer = null; + function lbDeleteArm() { + const btn = document.getElementById('lbDeleteBtn'); + if (!btn) return; + if (_deleteArmed) { lbDeleteConfirm(); return; } + _deleteArmed = true; + btn.textContent = 'Confirm delete'; + btn.style.background = '#dc2626'; + clearTimeout(_deleteArmTimer); + _deleteArmTimer = setTimeout(() => { + _deleteArmed = false; + if (btn) { btn.textContent = 'Delete'; btn.style.background = ''; } + }, 3500); + } + + async function lbDeleteConfirm() { + _deleteArmed = false; + clearTimeout(_deleteArmTimer); + const btn = document.getElementById('lbDeleteBtn'); + if (btn) { btn.textContent = 'Delete'; btn.style.background = ''; btn.disabled = true; } const fname = lbNames[lbIdx]; - if (!confirm(`Are you sure you want to delete this image? (${fname})`)) return; - try { const r = await fetch(`${API}/images/${fname}`, { method: 'DELETE' }); if (r.ok) { - showToast(`Deleted ${fname}`, 'success'); - closeLightbox(); - refreshNow(); + showToast(`Deleted`, 'success'); + if (btn) btn.disabled = false; + // Remove from current group view + lbUrls.splice(lbIdx, 1); + lbNames.splice(lbIdx, 1); + if (lbNames.length === 0) { + closeStudio(); + refreshNow(); + } else { + lbIdx = Math.min(lbIdx, lbNames.length - 1); + updateStudio(); + refreshNow(); + } } else { - showToast(`Failed to delete ${fname}`, 'error'); + showToast(`Failed to delete`, 'error'); + if (btn) btn.disabled = false; } } catch (e) { showToast(`Failed to delete: ${e}`, 'error'); + if (btn) btn.disabled = false; } } + async function lbAutoCrop() { + const fname = lbNames[lbIdx]; + if (!fname) return; + const btn = document.getElementById('lbCropBtn'); + if (btn) { btn.disabled = true; btn.textContent = 'Cropping…'; } + try { + const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/autocrop`, { method: 'POST' }); + if (r.ok) { + showToast('Cropped', 'success'); + lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now(); + updateStudio(); + } else { + showToast('Crop failed: ' + await r.text(), 'error'); + } + } catch (e) { showToast('Crop failed: ' + e, 'error'); } + if (btn) { btn.disabled = false; btn.textContent = 'Crop'; } + } + + async function lbDuplicate() { + const fname = lbNames[lbIdx]; + if (!fname) return; + const btn = document.getElementById('lbDuplicateBtn'); + if (btn) btn.disabled = true; + showToast('Duplicating…'); + try { + const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/duplicate`, { method: 'POST' }); + if (r.ok) { + const d = await r.json(); + showToast('Duplicated', 'success'); + // Insert the duplicate right after the current image in the studio strip + lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + d.new_filename + '?t=' + Date.now()); + lbNames.splice(lbIdx + 1, 0, d.new_filename); + lbIdx = lbIdx + 1; + updateStudio(); + refreshNow(); + } else { + showToast('Duplicate failed', 'error'); + } + } catch (e) { showToast('Duplicate failed: ' + e, 'error'); } + if (btn) btn.disabled = false; + } + async function lbRemoveBg() { const fname = lbNames[lbIdx]; showToast(`Removing background for ${fname}...`); @@ -3455,7 +3553,13 @@ function renderSidebarGenerate() { const panel = document.getElementById('sbPanelGenerate'); if (!panel) return; - // Use lbNames (current open group) to find already-generated poses + // Preserve live input values before re-render + const prevPromptVal = document.getElementById('sbGenPromptInput')?.value ?? ''; + _fsSelectedPoses.forEach(name => { + const el = document.getElementById('sbPoseEdit_' + CSS.escape(name)); + if (el) _sbPoseEdits[name] = el.value; + }); + const donePoses = new Set(); lbNames.forEach(n => { if (filePoses[n]) donePoses.add(filePoses[n]); }); @@ -3473,21 +3577,44 @@ return ``; }).join(''); } - html += ` -
+ html += ``; + + // Fine-tune section: one editable textarea per selected pose + if (_fsSelectedPoses.size > 0) { + html += `
Fine-tune poses
`; + _fsSelectedPoses.forEach(name => { + const origText = String(availablePoses[name]?.text ?? availablePoses[name] ?? name); + const curText = (_sbPoseEdits[name] !== undefined) ? _sbPoseEdits[name] : origText; + const idSafe = escHtml(name).replace(/"/g, '"'); + html += `
${escHtml(name)}
+ `; + }); + } + + html += `
Custom prompt
-
-