diff --git a/tour-comfy/car.html b/tour-comfy/car.html index d040a02..d4fe42d 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -2246,8 +2246,9 @@ const prefBtn = document.getElementById('lbPreferredBtn'); if (prefBtn) prefBtn.style.display = multi && lbIdx > 0 ? '' : 'none'; - // Film strip + // Film strip — always visible (override old display:none CSS) const strip = document.getElementById('lbVariantStrip'); + strip.style.display = 'flex'; strip.innerHTML = lbUrls.map((url, i) => { const n = lbNames[i]; const pose = filePoses[n] || ''; @@ -2735,6 +2736,21 @@ updateCardSelection(); 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 studio is open, sync lbUrls/lbNames from freshly-loaded groupData + const studioEl = document.getElementById('studio'); + if (studioEl?.classList.contains('open') && lbCurrentGid) { + const fresh = groupData.get(lbCurrentGid); + if (fresh) { + const curName = lbNames[lbIdx]; + const newIdx = fresh.names.indexOf(curName); + lbUrls = fresh.urls; + lbNames = fresh.names; + lbIdx = newIdx >= 0 ? newIdx : Math.min(lbIdx, fresh.names.length - 1); + updateStudio(); + } + } + statusDot.classList.remove('updating'); } @@ -3361,9 +3377,9 @@ function renderSidebarGenerate() { const panel = document.getElementById('sbPanelGenerate'); if (!panel) return; + // Use lbNames (current open group) to find already-generated poses const donePoses = new Set(); - const gData = groupData.get(lbCurrentGid); - if (gData) gData.names.forEach(n => { if (filePoses[n]) donePoses.add(filePoses[n]); }); + lbNames.forEach(n => { if (filePoses[n]) donePoses.add(filePoses[n]); }); let html = '
Poses
'; if (!availablePoses || Object.keys(availablePoses).length === 0) { @@ -3780,10 +3796,11 @@ html += '
'; } html += `
- - + +
_sceneShowFrame(t), 300); } - async function sceneExtractFrame() { - const scrubber = document.getElementById('sceneScrubber'); - const t = parseFloat(scrubber?.value || '0'); - const btn = document.getElementById('sceneExtractBtn'); - if (btn) btn.textContent = 'Extracting…'; + async function _sceneShowFrame(t) { + if (!_sceneVideo) return; try { const r = await fetch(`${API}/wireframe/frame`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ video_name: _sceneVideo, time: t }), }); - if (r.ok) { + if (!r.ok) return; + const d = await r.json(); + const vid = document.getElementById('sceneVideoEl'); + const img = document.getElementById('sceneFrameImg'); + if (!img) return; + if (vid) vid.style.display = 'none'; + img.src = 'data:image/png;base64,' + d.frame_b64; + img.style.display = ''; + img.dataset.pendingFrame = d.frame_b64; // ready to capture without re-fetch + } catch (_) { /* video stays visible on error */ } + } + + async function sceneExtractFrame() { + const scrubber = document.getElementById('sceneScrubber'); + const t = parseFloat(scrubber?.value || '0'); + const btn = document.getElementById('sceneExtractBtn'); + const img = document.getElementById('sceneFrameImg'); + // If the debounced preview already shows this frame, just lock it — no re-fetch + if (img?.dataset.pendingFrame) { + _sceneFrameBytes = img.dataset.pendingFrame; + delete img.dataset.pendingFrame; + if (btn) btn.textContent = '✓ Frame captured'; + const genBtn = document.getElementById('sceneGenBtn'); + if (genBtn) genBtn.disabled = false; + return; + } + if (btn) { btn.disabled = true; btn.textContent = 'Extracting…'; } + try { + const r = await fetch(`${API}/wireframe/frame`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ video_name: _sceneVideo, time: t }), + }); + if (!r.ok) { + showToast('Frame extract failed: ' + (await r.text()), 'error'); + } else { const d = await r.json(); _sceneFrameBytes = d.frame_b64; - const img = document.getElementById('sceneFrameImg'); - if (img) { img.src = 'data:image/png;base64,' + _sceneFrameBytes; img.style.display = ''; } + const vid = document.getElementById('sceneVideoEl'); + const imgEl = document.getElementById('sceneFrameImg'); + if (vid) vid.style.display = 'none'; + if (imgEl) { imgEl.src = 'data:image/png;base64,' + _sceneFrameBytes; imgEl.style.display = ''; } const genBtn = document.getElementById('sceneGenBtn'); if (genBtn) genBtn.disabled = false; + if (btn) btn.textContent = '✓ Frame captured'; } - } catch (_) {} - if (btn) btn.textContent = 'Extract frame as reference'; + } catch (e) { + showToast('Frame extract error: ' + e, 'error'); + if (btn) btn.textContent = 'Extract frame as reference'; + } + if (btn) btn.disabled = false; } function sceneHandleUpload(event) { @@ -3920,10 +3992,12 @@ if (btn) btn.disabled = false; refreshNow(); } else { - if (textEl) textEl.textContent = 'Error: ' + (job.error || 'unknown'); + const errMsg = job.error || 'unknown error'; + if (textEl) textEl.textContent = 'Error: ' + errMsg; if (btn) btn.disabled = false; + showToast('Scenery failed: ' + errMsg, 'error'); } - } catch (_) { pollScene(); } + } catch (e) { pollScene(); } }, 2500); }; pollScene(); diff --git a/tour-comfy/watcher.log b/tour-comfy/watcher.log index a4a2a65..d336670 100644 --- a/tour-comfy/watcher.log +++ b/tour-comfy/watcher.log @@ -2094,3 +2094,6 @@ SyntaxError: broken PNG file (chunk b'END\xae') 2026-06-21 11:04:28,350 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... 2026-06-21 11:04:28,350 - INFO - Output directory: /mnt/zim/tour-comfy/output 2026-06-21 11:04:28,350 - INFO - API URL: http://127.0.0.1:8500/edit +2026-06-21 22:09:31,709 - INFO - Watcher started. Monitoring /mnt/zim/tour-comfy/stage... +2026-06-21 22:09:31,709 - INFO - Output directory: /mnt/zim/tour-comfy/output +2026-06-21 22:09:31,709 - INFO - API URL: http://127.0.0.1:8500/edit