update poses
This commit is contained in:
@@ -1675,6 +1675,7 @@
|
||||
<button class="btn" onclick="setIntervalTime(30)">30s</button>
|
||||
<button class="btn" onclick="setIntervalTime(120)">2m</button>
|
||||
<button class="btn primary" onclick="refreshNow()">Refresh Now</button>
|
||||
<button class="btn" id="archiveViewBtn" onclick="toggleArchiveView()" title="View archived items">📦 Archive</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" onclick="cleanupDb()" title="Remove DB records for deleted/missing files" style="font-size:11px;opacity:0.6">Clean DB</button>
|
||||
@@ -1745,6 +1746,16 @@
|
||||
</div>
|
||||
<!-- Film strip / variant thumbnails -->
|
||||
<div class="studio-filmstrip" id="lbVariantStrip"></div>
|
||||
<!-- Slideshow controls -->
|
||||
<div id="slideshowBar" style="display:flex;align-items:center;gap:6px;padding:4px 8px;background:#111;border-top:1px solid #1a1a1a;flex-shrink:0">
|
||||
<button onclick="toggleSlideshow()" id="slideshowBtn" style="background:none;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">▶ Slideshow</button>
|
||||
<select id="slideshowInterval" onchange="updateSlideshowInterval()" style="background:#111;border:1px solid #333;color:#888;font-size:11px;padding:2px 4px;border-radius:4px">
|
||||
<option value="2000">2s</option>
|
||||
<option value="4000" selected>4s</option>
|
||||
<option value="8000">8s</option>
|
||||
<option value="15000">15s</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar toolbox -->
|
||||
@@ -1791,7 +1802,9 @@
|
||||
<button class="sb-btn" id="lbNoBgBtn" onclick="lbRemoveBg()">No BG</button>
|
||||
<button class="sb-btn" id="lbCropBtn" style="display:none" onclick="lbAutoCrop()" title="Crop away transparent border">Crop</button>
|
||||
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group">Duplicate</button>
|
||||
<button class="sb-btn" id="lbManualCropBtn" onclick="startManualCrop()" title="Drag to crop image">Crop…</button>
|
||||
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none">Download</a>
|
||||
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b" title="Move to archive (recoverable)">Archive</button>
|
||||
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" title="Click once to arm, then again to confirm">Delete</button>
|
||||
</div>
|
||||
<div id="lbSourceRefs" class="sb-source-refs" style="display:none"></div>
|
||||
@@ -1821,6 +1834,7 @@
|
||||
<button class="btn" onclick="deselectAll()">None</button>
|
||||
<button class="btn" id="groupBtn" onclick="groupSelected()">Group</button>
|
||||
<button class="btn" id="multiRefBtn" onclick="processMultiRef()" style="display:none" title="Use selected (2–3) as combined references → single output">Multi-ref</button>
|
||||
<span id="multiRefLegend" style="display:none;font-size:10px;color:#888;white-space:nowrap"></span>
|
||||
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
|
||||
<span class="batch-progress" id="batchProgress"></span>
|
||||
<button class="btn" onclick="toggleSelectMode()">Done</button>
|
||||
@@ -2116,6 +2130,8 @@
|
||||
let filePoses = {}; // filename → pose name
|
||||
let filePrompts = {}; // filename → generation prompt
|
||||
let fileHidden = {}; // filename → boolean (hidden from cycling)
|
||||
let fileArchived = {}; // filename → boolean
|
||||
let _archiveViewActive = false; // when true, show archived items only
|
||||
let fileHasBg = {}; // filename → boolean (has background, default true)
|
||||
let fileSourceRefs = {}; // filename → array of source filenames
|
||||
let fileHasClothing = {}; // filename → boolean|null (null = unknown)
|
||||
@@ -2134,6 +2150,10 @@
|
||||
let _sbPoseEdits = {}; // poseName → edited text
|
||||
let _sbGenJobId = null;
|
||||
let _sbGenJobPollTimer= null;
|
||||
let _followLatestGid = null; // set to gid after generation to auto-jump to newest image
|
||||
let _slideshowActive = false;
|
||||
let _slideshowTimer = null;
|
||||
let _slideshowInterval= 4000;
|
||||
let _fsTrimVideo = null;
|
||||
|
||||
function escHtml(s) {
|
||||
@@ -2239,6 +2259,36 @@
|
||||
function closeStudio() {
|
||||
document.getElementById('studio').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
stopSlideshow();
|
||||
}
|
||||
|
||||
function toggleSlideshow() {
|
||||
if (_slideshowActive) stopSlideshow();
|
||||
else startSlideshow();
|
||||
}
|
||||
|
||||
function startSlideshow() {
|
||||
if (_slideshowTimer) clearInterval(_slideshowTimer);
|
||||
_slideshowActive = true;
|
||||
const btn = document.getElementById('slideshowBtn');
|
||||
if (btn) { btn.textContent = '⏸ Pause'; btn.style.color = '#60a5fa'; }
|
||||
_slideshowTimer = setInterval(() => {
|
||||
if (lbUrls.length > 1) { lbIdx = (lbIdx + 1) % lbUrls.length; updateStudio(); }
|
||||
}, _slideshowInterval);
|
||||
}
|
||||
|
||||
function stopSlideshow() {
|
||||
_slideshowActive = false;
|
||||
clearInterval(_slideshowTimer);
|
||||
_slideshowTimer = null;
|
||||
const btn = document.getElementById('slideshowBtn');
|
||||
if (btn) { btn.textContent = '▶ Slideshow'; btn.style.color = ''; }
|
||||
}
|
||||
|
||||
function updateSlideshowInterval() {
|
||||
const sel = document.getElementById('slideshowInterval');
|
||||
if (sel) _slideshowInterval = parseInt(sel.value, 10);
|
||||
if (_slideshowActive) startSlideshow(); // restart with new interval
|
||||
}
|
||||
|
||||
function lbNav(dir) {
|
||||
@@ -2304,6 +2354,8 @@
|
||||
if (faceswapBtn)faceswapBtn.style.display = !isVid ? '' : 'none';
|
||||
const cropBtn = document.getElementById('lbCropBtn');
|
||||
if (cropBtn) cropBtn.style.display = (!isVid && fileHasBg[fname] === false) ? '' : 'none';
|
||||
const archiveBtn = document.getElementById('lbArchiveBtn');
|
||||
if (archiveBtn) archiveBtn.textContent = fileArchived[fname] ? 'Restore' : 'Archive';
|
||||
|
||||
// Source refs
|
||||
const refs = fileSourceRefs[fname];
|
||||
@@ -2521,6 +2573,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function lbArchive() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
const btn = document.getElementById('lbArchiveBtn');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const endpoint = fileArchived[fname] ? 'unarchive' : 'archive';
|
||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/${endpoint}`, { method: 'POST' });
|
||||
if (r.ok) {
|
||||
fileArchived[fname] = !fileArchived[fname];
|
||||
const action = fileArchived[fname] ? 'Archived' : 'Restored';
|
||||
showToast(`${action}: ${fname}`, 'success');
|
||||
// Remove from current filmstrip view and navigate
|
||||
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('Archive failed', 'error');
|
||||
}
|
||||
} catch (e) { showToast('Archive error: ' + e, 'error'); }
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
function toggleArchiveView() {
|
||||
_archiveViewActive = !_archiveViewActive;
|
||||
const btn = document.getElementById('archiveViewBtn');
|
||||
if (btn) {
|
||||
btn.textContent = _archiveViewActive ? '📦 Back to Gallery' : '📦 Archive';
|
||||
btn.style.color = _archiveViewActive ? '#f59e0b' : '';
|
||||
}
|
||||
// Show/hide archive banner
|
||||
let banner = document.getElementById('archiveBanner');
|
||||
if (_archiveViewActive) {
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'archiveBanner';
|
||||
banner.style.cssText = 'background:#7c3aed22;border:1px solid #7c3aed;border-radius:6px;padding:8px 14px;font-size:12px;color:#a78bfa;margin:8px 0;text-align:center';
|
||||
banner.textContent = '📦 Viewing archived items — these are hidden from the main gallery';
|
||||
const gallery = document.getElementById('gallery');
|
||||
if (gallery) gallery.parentNode.insertBefore(banner, gallery);
|
||||
}
|
||||
} else {
|
||||
if (banner) banner.remove();
|
||||
}
|
||||
refreshNow();
|
||||
}
|
||||
|
||||
async function lbAutoCrop() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
@@ -2539,6 +2645,120 @@
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
|
||||
}
|
||||
|
||||
function startManualCrop() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to crop', 'info'); return; }
|
||||
const viewer = document.getElementById('studioViewer');
|
||||
const img = document.getElementById('lbImg');
|
||||
if (!img || img.style.display === 'none') { showToast('No image displayed', 'info'); return; }
|
||||
if (document.getElementById('cropCanvas')) return; // already active
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'cropCanvas';
|
||||
canvas.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair;z-index:100;';
|
||||
viewer.appendChild(canvas);
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'cropBar';
|
||||
bar.style.cssText = 'position:absolute;bottom:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.75);z-index:101;justify-content:flex-end;align-items:center;';
|
||||
bar.innerHTML = '<span style="flex:1;font-size:11px;color:#aaa">Drag to select crop area</span>'
|
||||
+ '<button class="sb-btn" onclick="cancelManualCrop()">Cancel</button>'
|
||||
+ '<button class="sb-btn primary" id="cropConfirmBtn" disabled onclick="confirmManualCrop()">Crop</button>';
|
||||
viewer.appendChild(bar);
|
||||
|
||||
const st = window._cropState = { canvas, viewer, img, fname, x1: 0, y1: 0, x2: 0, y2: 0, dragging: false };
|
||||
|
||||
function resize() { canvas.width = viewer.clientWidth; canvas.height = viewer.clientHeight; draw(); }
|
||||
function draw() {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const w = Math.abs(st.x2 - st.x1), h = Math.abs(st.y2 - st.y1);
|
||||
if (w < 2 || h < 2) return;
|
||||
const rx = Math.min(st.x1, st.x2), ry = Math.min(st.y1, st.y2);
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.clearRect(rx, ry, w, h);
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.strokeRect(rx + 0.5, ry + 0.5, w - 1, h - 1);
|
||||
}
|
||||
st.draw = draw;
|
||||
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
st.x1 = st.x2 = e.clientX - r.left;
|
||||
st.y1 = st.y2 = e.clientY - r.top;
|
||||
st.dragging = true;
|
||||
const cb = document.getElementById('cropConfirmBtn');
|
||||
if (cb) cb.disabled = true;
|
||||
});
|
||||
canvas.addEventListener('mousemove', e => {
|
||||
if (!st.dragging) return;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
st.x2 = e.clientX - r.left;
|
||||
st.y2 = e.clientY - r.top;
|
||||
draw();
|
||||
});
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
st.dragging = false;
|
||||
draw();
|
||||
const w = Math.abs(st.x2 - st.x1), h = Math.abs(st.y2 - st.y1);
|
||||
const cb = document.getElementById('cropConfirmBtn');
|
||||
if (cb) cb.disabled = (w < 5 || h < 5);
|
||||
});
|
||||
|
||||
resize();
|
||||
}
|
||||
|
||||
function cancelManualCrop() {
|
||||
document.getElementById('cropCanvas')?.remove();
|
||||
document.getElementById('cropBar')?.remove();
|
||||
window._cropState = null;
|
||||
}
|
||||
|
||||
async function confirmManualCrop() {
|
||||
const st = window._cropState;
|
||||
if (!st) return;
|
||||
const { canvas, img, fname } = st;
|
||||
|
||||
// Convert canvas rect → image pixel coords (account for object-fit: contain letterbox)
|
||||
const cW = canvas.width, cH = canvas.height;
|
||||
const iW = img.naturalWidth, iH = img.naturalHeight;
|
||||
const scale = Math.min(cW / iW, cH / iH);
|
||||
const dW = iW * scale, dH = iH * scale;
|
||||
const offX = (cW - dW) / 2, offY = (cH - dH) / 2;
|
||||
|
||||
const toImg = (cx, cy) => ({
|
||||
x: Math.round(Math.max(0, Math.min(iW, (cx - offX) / scale))),
|
||||
y: Math.round(Math.max(0, Math.min(iH, (cy - offY) / scale))),
|
||||
});
|
||||
const p1 = toImg(Math.min(st.x1, st.x2), Math.min(st.y1, st.y2));
|
||||
const p2 = toImg(Math.max(st.x1, st.x2), Math.max(st.y1, st.y2));
|
||||
if (p2.x - p1.x < 2 || p2.y - p1.y < 2) { showToast('Crop area too small', 'info'); return; }
|
||||
|
||||
const btn = document.getElementById('cropConfirmBtn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Cropping…'; }
|
||||
try {
|
||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/crop`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
|
||||
});
|
||||
if (r.ok) {
|
||||
showToast('Cropped', 'success');
|
||||
cancelManualCrop();
|
||||
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
||||
updateStudio();
|
||||
} else {
|
||||
showToast('Crop failed: ' + await r.text(), 'error');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Crop error: ' + e, 'error');
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Crop'; }
|
||||
}
|
||||
}
|
||||
|
||||
async function lbDuplicate() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
@@ -2671,11 +2891,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type} show`;
|
||||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||
setTimeout(() => toast.classList.remove('show'), duration);
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
@@ -2747,7 +2967,8 @@
|
||||
|
||||
// Primary: live list from API (always up to date, works after batch jobs)
|
||||
try {
|
||||
const r = await fetch(`${API}/images`, { signal: AbortSignal.timeout(12000) });
|
||||
const url = _archiveViewActive ? `${API}/images?archived=true` : `${API}/images`;
|
||||
const r = await fetch(url, { signal: AbortSignal.timeout(12000) });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
files = data.images.map(img => img.filename);
|
||||
@@ -2760,6 +2981,7 @@
|
||||
if (img.group_name && img.group_id) groupNames[img.group_id] = img.group_name;
|
||||
fileSortOrders[img.filename] = img.sort_order ?? null;
|
||||
fileHidden[img.filename] = !!img.hidden;
|
||||
fileArchived[img.filename] = !!img.archived;
|
||||
fileHasBg[img.filename] = img.has_background !== false;
|
||||
fileHasClothing[img.filename] = img.has_clothing ?? null;
|
||||
fileContentType[img.filename] = img.content_type || 'image';
|
||||
@@ -2840,6 +3062,15 @@
|
||||
const visibleIdx = names.map((n, i) => i).filter(i => !fileHidden[names[i]]);
|
||||
groupData.set(gid, { urls, names, visibleIdx });
|
||||
|
||||
// Auto-follow: if a generation just finished for this group, jump to the last (newest) image
|
||||
if (_followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) {
|
||||
_followLatestGid = null;
|
||||
lbIdx = names.length - 1;
|
||||
lbUrls = urls;
|
||||
lbNames = names;
|
||||
updateStudio();
|
||||
}
|
||||
|
||||
const isNew = gf.some(f => newFileNames.has(f.cleanName));
|
||||
const isRecent = gIdx < 3;
|
||||
const count = gf.length;
|
||||
@@ -3137,7 +3368,23 @@
|
||||
const n = selectedFiles.size;
|
||||
document.getElementById('batchCount').textContent = `${n} group${n !== 1 ? 's' : ''} selected`;
|
||||
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
|
||||
document.getElementById('multiRefBtn').style.display = (n >= 2 && n <= 3) ? '' : 'none';
|
||||
const showMR = (n >= 2 && n <= 3);
|
||||
document.getElementById('multiRefBtn').style.display = showMR ? '' : 'none';
|
||||
const legend = document.getElementById('multiRefLegend');
|
||||
if (legend) {
|
||||
if (showMR) {
|
||||
const names = [...selectedFiles].map((gid, i) => {
|
||||
const data = groupData.get(gid);
|
||||
const idx = selectedVariants.get(gid) ?? 0;
|
||||
const fname = data?.names[idx] ?? gid;
|
||||
return `ref${i+1}=${fname.split('/').pop().replace(/\?.*$/,'').slice(0,24)}`;
|
||||
});
|
||||
legend.textContent = names.join(' · ');
|
||||
legend.style.display = '';
|
||||
} else {
|
||||
legend.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
@@ -3226,6 +3473,12 @@
|
||||
if (prompt) { prompts.push(prompt); poses.push(null); savePromptHistory(prompt); }
|
||||
poseEntries.forEach(e => { prompts.push(e.text); poses.push(e.name); });
|
||||
|
||||
// Show which filename maps to which ref slot
|
||||
const shortName = f => f.split('/').pop().replace(/\?.*$/, '');
|
||||
let refLabel = `Ref1: ${shortName(filenames[0])} Ref2: ${shortName(filenames[1])}`;
|
||||
if (filenames[2]) refLabel += ` Ref3: ${shortName(filenames[2])}`;
|
||||
showToast(refLabel, 'info', 5000);
|
||||
|
||||
document.getElementById('batchProgress').textContent = 'Submitting…';
|
||||
try {
|
||||
const r = await fetch(`${API}/multi-ref`, {
|
||||
@@ -3697,8 +3950,12 @@
|
||||
const job = await jr.json();
|
||||
const finished = job.done + job.failed;
|
||||
textEl.textContent = `Generating… ${finished}/${total}`;
|
||||
// Refresh UI as each task completes
|
||||
if (job.done > prevDone) { prevDone = job.done; refreshNow(); }
|
||||
// Refresh UI as each task completes; auto-follow to newest image
|
||||
if (job.done > prevDone) {
|
||||
prevDone = job.done;
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow();
|
||||
}
|
||||
if (job.status === 'running') { pollGen(); return; }
|
||||
if (job.status === 'cancelled') {
|
||||
statusEl.style.display = 'none';
|
||||
@@ -3715,6 +3972,7 @@
|
||||
_fsSelectedPoses.clear();
|
||||
_sbPoseEdits = {};
|
||||
_sbGenJobId = null;
|
||||
_followLatestGid = lbCurrentGid;
|
||||
refreshNow();
|
||||
renderSidebarGenerate();
|
||||
} else {
|
||||
@@ -3804,7 +4062,7 @@
|
||||
const vSafe = v.replace(/'/g, "\\'");
|
||||
const sel = _fsSelectedVideo === v ? ' selected' : '';
|
||||
html += `<div class="sb-template-card${sel}" onclick="sbSelectTemplate('${vSafe}')"
|
||||
onmouseenter="this.querySelector('video').play()" onmouseleave="this.querySelector('video').pause()">
|
||||
onmouseenter="const v=this.querySelector('video');v&&v.play().catch(()=>{})" onmouseleave="const v=this.querySelector('video');v&&v.pause()">
|
||||
<video src="${API}/wireframe/${encodeURIComponent(v)}" muted loop playsinline
|
||||
preload="metadata" style="pointer-events:none;width:100%;height:100%;object-fit:cover"></video>
|
||||
<div class="sb-template-label">${escHtml(stem)}</div>
|
||||
@@ -3825,6 +4083,17 @@
|
||||
<span>FaceFusion</span>
|
||||
<span id="sbFsHairBadge" style="font-size:10px;color:#f87;background:#2a1a1a;padding:1px 5px;border-radius:3px">not installed</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer" title="Generate at lower resolution for quick preview">
|
||||
<input type="checkbox" id="sbFsPreview" onchange="document.getElementById('sbFsPreviewOpts').style.display=this.checked?'flex':'none'">
|
||||
<span>Preview mode</span>
|
||||
</label>
|
||||
<div id="sbFsPreviewOpts" style="display:none;align-items:center;gap:6px;padding-left:20px">
|
||||
<span style="font-size:11px;color:#888">Scale:</span>
|
||||
<select id="sbFsPreviewScale" style="font-size:11px;background:#1a1a1a;color:#ccc;border:1px solid #333;border-radius:3px;padding:1px 4px">
|
||||
<option value="0.5">50%</option>
|
||||
<option value="0.25">25%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span id="sbFsJobStatus" style="flex:1;font-size:11px;color:#888;display:none">
|
||||
@@ -3865,21 +4134,26 @@
|
||||
|
||||
async function submitFaceswap() {
|
||||
if (!_fsModelFilename || !_fsSelectedVideo) return;
|
||||
const enhance = document.getElementById('sbFsEnhance')?.checked ?? true;
|
||||
const hairEl = document.getElementById('sbFsHair');
|
||||
const hair = (hairEl?.checked && !hairEl?.disabled) ?? false;
|
||||
const enhance = document.getElementById('sbFsEnhance')?.checked ?? true;
|
||||
const hairEl = document.getElementById('sbFsHair');
|
||||
const hair = (hairEl?.checked && !hairEl?.disabled) ?? false;
|
||||
const previewCb = document.getElementById('sbFsPreview');
|
||||
const previewScale = (previewCb?.checked)
|
||||
? parseFloat(document.getElementById('sbFsPreviewScale')?.value ?? '0.5')
|
||||
: 1.0;
|
||||
const btn = document.getElementById('sbFsStartBtn');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Starting…'; }
|
||||
const statusEl = document.getElementById('sbFsJobStatus');
|
||||
const textEl = document.getElementById('sbFsJobText');
|
||||
if (statusEl) statusEl.style.display = 'inline-flex';
|
||||
const modeLabel = hair ? 'FaceFusion + hair' : (enhance ? 'insightface + enhance' : 'insightface');
|
||||
const prevTag = previewScale < 1.0 ? ` preview ${Math.round(previewScale * 100)}%` : '';
|
||||
const modeLabel = (hair ? 'FaceFusion + hair' : (enhance ? 'insightface + enhance' : 'insightface')) + prevTag;
|
||||
if (textEl) textEl.textContent = `Starting (${modeLabel})…`;
|
||||
try {
|
||||
const r = await fetch(`${API}/faceswap`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model_filename: _fsModelFilename, video_name: _fsSelectedVideo, enhance, hair }),
|
||||
body: JSON.stringify({ model_filename: _fsModelFilename, video_name: _fsSelectedVideo, enhance, hair, preview_scale: previewScale }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
if (textEl) textEl.textContent = `Error: ${await r.text()}`;
|
||||
@@ -4038,7 +4312,7 @@
|
||||
const vSafe = v.replace(/'/g, "\\'");
|
||||
const sel = _sceneVideo === v ? ' selected' : '';
|
||||
html += `<div class="sb-template-card${sel}" onclick="sceneSelectVideo('${vSafe}')"
|
||||
onmouseenter="this.querySelector('video').play()" onmouseleave="this.querySelector('video').pause()">
|
||||
onmouseenter="const v=this.querySelector('video');v&&v.play().catch(()=>{})" onmouseleave="const v=this.querySelector('video');v&&v.pause()">
|
||||
<video src="${API}/wireframe/${encodeURIComponent(v)}" muted loop playsinline
|
||||
preload="metadata" style="pointer-events:none;width:100%;height:100%;object-fit:cover"></video>
|
||||
<div class="sb-template-label">${escHtml(stem)}</div>
|
||||
@@ -4270,7 +4544,8 @@
|
||||
simply shows the original again without any file change.
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
|
||||
<button class="sb-btn" id="sbSam2Btn" onclick="sam2RemoveBg()">Remove BG</button>
|
||||
<button class="sb-btn" id="sbSam2Btn" onclick="sam2RemoveBg()" title="SAM2 segmentation — preserves hair/glasses/accessories; falls back to rembg if unavailable">Remove BG (SAM2)</button>
|
||||
<button class="sb-btn" id="sbRembgBtn" onclick="rembgRemoveBg()" title="rembg (U2Net) — fast background removal without SAM2">Remove BG (rembg)</button>
|
||||
<button class="sb-btn" id="sbRestoreBgBtn" onclick="restoreBg()">Restore BG</button>
|
||||
</div>
|
||||
<div id="sbSegStatus" style="font-size:11px;color:#aaa;min-height:16px;margin-bottom:10px"></div>
|
||||
@@ -4281,7 +4556,8 @@
|
||||
</label>
|
||||
<div style="font-size:10px;color:#555;line-height:1.4">
|
||||
SAM2 merges all non-border masks (preserves hair, glasses, sandals).<br>
|
||||
Requires sam2_checkpoint in config.json.
|
||||
rembg (U2Net) is faster and works without SAM2 checkpoint.<br>
|
||||
Requires sam2_checkpoint in config.json for SAM2 path.
|
||||
</div>`;
|
||||
// Check SAM2 availability
|
||||
fetch(`${API}/sam2/check`).then(r => r.json()).then(d => {
|
||||
@@ -4339,6 +4615,36 @@
|
||||
showToast('Original image shown', 'success');
|
||||
}
|
||||
|
||||
async function rembgRemoveBg() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
const btn = document.getElementById('sbRembgBtn');
|
||||
const statusEl = document.getElementById('sbSegStatus');
|
||||
if (btn) btn.disabled = true;
|
||||
if (statusEl) statusEl.textContent = 'Removing background (rembg)…';
|
||||
try {
|
||||
const r = await fetch(`${API}/remove-background/${encodeURIComponent(fname)}`, { method: 'POST' });
|
||||
if (!r.ok) {
|
||||
if (statusEl) statusEl.textContent = 'Error: ' + await r.text();
|
||||
} else {
|
||||
const d = await r.json();
|
||||
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
||||
showToast('Background removed (rembg)', 'success');
|
||||
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
||||
_nobgSidecars.set(fname, nobgUrl);
|
||||
const preload = new Image();
|
||||
preload.onload = () => {
|
||||
const img = document.getElementById('lbImg');
|
||||
if (img) img.src = nobgUrl;
|
||||
const cb = document.getElementById('sbCheckerboard');
|
||||
if (cb && !cb.checked) { cb.checked = true; toggleCheckerboard(true); }
|
||||
};
|
||||
preload.src = nobgUrl;
|
||||
}
|
||||
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
function toggleCheckerboard(on) {
|
||||
const viewer = document.getElementById('studioViewer');
|
||||
if (viewer) viewer.classList.toggle('show-checker', on);
|
||||
|
||||
Reference in New Issue
Block a user