diff --git a/backlog.md b/backlog.md
index ab0fc0c..0fff4a8 100644
--- a/backlog.md
+++ b/backlog.md
@@ -50,4 +50,7 @@ rating based pose, thumbs up/down find good/bad poses easier
when refresh page, we lose track of current jobs running.
-generating poses themself should use the adviced dimensions rather than the base image reference.
\ No newline at end of file
+generating poses themself should use the adviced dimensions rather than the base image reference.
+
+introduce a like/dislike base system for images/group to determine the sort order (and more)
+- would also dislike the pose
\ No newline at end of file
diff --git a/tour-comfy/car.html b/tour-comfy/car.html
index b3b8068..152c7d8 100644
--- a/tour-comfy/car.html
+++ b/tour-comfy/car.html
@@ -1513,6 +1513,7 @@
border-radius: 4px; color: #888; font-size: 11px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s, color 0.1s;
+ z-index: 2;
}
.sb-template-card:hover .sb-template-trim-btn { opacity: 1; }
.sb-template-trim-btn:hover { color: #fff; background: rgba(0,0,0,0.92); }
@@ -1578,7 +1579,41 @@
margin-bottom: 8px; position: relative;
}
.scene-frame-preview img { width:100%; height:100%; object-fit:contain; display:block; }
- .scene-scrubber { width: 100%; accent-color: #2563eb; margin: 4px 0; cursor: pointer; }
+ .scene-scrubber { width: 100%; accent-color: #2563eb; margin: 6px 0 2px; cursor: pointer; height: 18px; }
+ .scene-scrubber:disabled { opacity: 0.4; cursor: not-allowed; }
+ .scene-captured-badge {
+ position: absolute; top: 6px; left: 6px;
+ background: rgba(34,197,94,0.9); color: #04210f;
+ border-radius: 5px; padding: 2px 8px; font-size: 11px; font-weight: 600;
+ }
+ /* 3-slot reference summary (image1 / image2 / image3) */
+ .scene-slots { display: flex; gap: 6px; margin-bottom: 12px; }
+ .scene-slot {
+ flex: 1; min-width: 0; background: #141414; border: 1px solid #262626;
+ border-radius: 7px; overflow: hidden; text-align: center;
+ }
+ .scene-slot.filled { border-color: #2563eb; }
+ .scene-slot.empty { border-style: dashed; }
+ .scene-slot-thumb {
+ width: 100%; aspect-ratio: 1/1; background: #0c0c0c;
+ display: flex; align-items: center; justify-content: center;
+ }
+ .scene-slot-thumb img { width: 100%; height: 100%; object-fit: cover; }
+ .scene-slot-thumb .ph { font-size: 18px; color: #333; }
+ .scene-slot-cap { font-size: 8.5px; color: #888; padding: 3px 2px 2px; line-height: 1.25; }
+ .scene-slot-cap b { color: #bbb; display: block; font-size: 9px; }
+ /* compact filmstrip picker for image3 */
+ .scene-pick-row { display: flex; gap: 5px; overflow-x: auto; padding-bottom: 4px; }
+ .scene-pick {
+ flex: 0 0 auto; width: 46px; height: 46px; border-radius: 5px; overflow: hidden;
+ border: 2px solid transparent; cursor: pointer; position: relative; background: #0c0c0c;
+ }
+ .scene-pick img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
+ .scene-pick.sel { border-color: #2563eb; }
+ .scene-pick .badge3 {
+ position: absolute; bottom: 1px; right: 1px; background: #2563eb; color: #fff;
+ font-size: 9px; font-weight: 700; border-radius: 3px; padding: 0 3px;
+ }
/* ===================== SEGMENT TAB ===================== */
.segment-checker-hint { font-size: 10px; color: #444; margin-top: 6px; }
@@ -2321,7 +2356,7 @@
function openStudio(gid, startIdx) {
const data = groupData.get(gid);
if (!data) return;
- if (lbCurrentGid !== gid) _sbRefIndices = []; // clear refs when switching groups
+ if (lbCurrentGid !== gid) { _sbRefIndices = []; _sceneExtraRef = null; } // clear refs when switching groups
lbCurrentGid = gid;
lbUrls = data.urls;
lbNames = data.names;
@@ -5306,33 +5341,122 @@
// ---- scenery sidebar tab ----
let _sceneVideo = null, _sceneDuration = 0, _sceneFrameBytes = null, _sceneJobPollTimer = null;
+ let _sceneGridOpen = false; // whether the video picker grid is expanded
+ let _sceneExtraRef = null; // image3 — optional extra reference filename (from filmstrip)
+
function renderSidebarScenery() {
const panel = document.getElementById('sbPanelScenery');
if (!panel) return;
- let html = '
';
- availableVideos.forEach(v => {
- const vSafe = v.replace(/'/g, "\\'");
- html += _tplCardHTML(v, _sceneVideo === v, `sceneSelectVideo('${vSafe}')`);
+ const haveRef = _sceneVideo || _sceneFrameBytes;
+ // Auto-open the grid when nothing is chosen yet.
+ const gridOpen = _sceneGridOpen || !haveRef;
+
+ // Person (image2) = current studio image; extra (image3) = _sceneExtraRef
+ const personName = _fsModelFilename || lbNames[lbIdx] || '';
+ const personUrl = lbUrls[lbIdx] || '';
+ const personThumb = personName ? (isVideo(personName) ? posterFor(personUrl) : personUrl) : '';
+ const extraIdx = _sceneExtraRef ? lbNames.indexOf(_sceneExtraRef) : -1;
+ const extraUrl = extraIdx >= 0 ? lbUrls[extraIdx] : '';
+ const extraThumb = _sceneExtraRef ? (isVideo(_sceneExtraRef) ? posterFor(extraUrl) : extraUrl) : '';
+
+ // --- 3-slot reference summary (what feeds the multi-ref compose) ---
+ const slot = (cls, num, label, thumb) => `
+
+
${thumb
+ ? `
`
+ : `
${num} `}
+
Image ${num} ${label}
+
`;
+ let html = `
Compose: person + scene → one photo
+
+ ${slot('bg','1','Background', _sceneFrameBytes ? `data:image/png;base64,${_sceneFrameBytes}` : '')}
+ ${slot('person','2','Person', personThumb)}
+ ${slot('extra','3','Extra (opt)', extraThumb)}
+
`;
+
+ // --- IMAGE 1: Background scene (video scrub + capture, or upload) ---
+ html += `
Image 1 · Background scene
`;
+
+ // --- Selected video: player + slider + capture (always at top) ---
+ if (_sceneVideo) {
+ html += `
+
+
+
+
✓ frame captured
+
+
+
+ 0:00.0
+ ${escHtml(_sceneVideo.replace(/\.[^.]+$/,''))}
+
+
📷 Capture this frame
+
✖ Pick a different frame
+
+ ${gridOpen?'▲ Hide video list':'▼ Change background video'} `;
+ }
+
+ // --- Video picker grid (collapsible) ---
+ if (!_sceneVideo || gridOpen) {
+ html += '
Background video
';
+ if (availableVideos.length) {
+ html += '
';
+ availableVideos.forEach(v => {
+ const vSafe = v.replace(/'/g, "\\'");
+ html += _tplCardHTML(v, _sceneVideo === v, `sceneSelectVideo('${vSafe}')`);
+ });
+ html += '
';
+ } else {
+ html += '
No wireframe videos found
';
+ }
+ }
+
+ // Upload alternative for the background (still image 1)
+ html += `
+
⬆ Upload image as background `;
+
+ // --- IMAGE 2: Person (auto — current studio image) ---
+ html += `
+
Image 2 · Person · current image
`;
+ if (personThumb) {
+ html += `
+
+
${escHtml(personName)}
+
`;
+ } else {
+ html += `
No person image — open one from the gallery first.
`;
+ }
+
+ // --- IMAGE 3: Extra reference (optional, pick from this group) ---
+ html += `
+
Image 3 · Extra reference · optional · click to toggle
`;
+ const pickNames = lbNames.filter(n => n && n !== personName);
+ if (pickNames.length) {
+ html += '
';
+ pickNames.forEach(n => {
+ const idx = lbNames.indexOf(n);
+ const u = lbUrls[idx] || '';
+ const th = isVideo(n) ? posterFor(u) : u;
+ const sel = _sceneExtraRef === n;
+ const nSafe = n.replace(/'/g, "\\'");
+ html += `
+
+ ${sel?'
3
':''}
`;
});
html += '
';
+ } else {
+ html += `
No other images in this group.
`;
}
- html += `
-
-
-
-
-
✖ Change frame
-
-
Or upload image
-
-
Upload image
-
+
+ // --- Prompt + generate ---
+ html += `
Prompt (optional)
@@ -5341,53 +5465,67 @@
Generating…
Generate Scenery
+ ${(haveRef && personName)?'':'disabled'}>Create Scenery
`;
+
panel.innerHTML = html;
+
if (_sceneVideo) {
const vid = document.getElementById('sceneVideoEl');
vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`;
- vid.addEventListener('seeking', () => {
- if (_sceneFrameBytes) sceneReleaseFrame();
+ // Once metadata is known, size the slider so 1000 steps span the whole clip.
+ vid.addEventListener('loadedmetadata', () => {
+ _sceneDuration = vid.duration || 0;
+ const lbl = document.getElementById('sceneTimeLabel');
+ if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
+ }, { once: true });
+ // Keep the time label in sync if the frame is seeked any other way.
+ vid.addEventListener('seeked', () => {
+ const lbl = document.getElementById('sceneTimeLabel');
+ if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
});
}
- // Cards show static poster frames; videos mount only on hover (_tplPlay).
+ }
+
+ // Scrub the preview video live as the slider moves (no server round-trip).
+ function sceneScrub(val) {
+ const vid = document.getElementById('sceneVideoEl');
+ if (!vid || !vid.duration) return;
+ const t = (parseFloat(val) / 1000) * vid.duration;
+ vid.pause();
+ vid.currentTime = t;
+ const lbl = document.getElementById('sceneTimeLabel');
+ if (lbl) lbl.textContent = formatSecs(t);
+ }
+
+ function sceneToggleGrid() {
+ _sceneGridOpen = !_sceneGridOpen;
+ renderSidebarScenery();
+ }
+
+ // Toggle image3 (extra reference) selection from the in-group picker.
+ function sceneToggleExtraRef(name) {
+ _sceneExtraRef = (_sceneExtraRef === name) ? null : name;
+ renderSidebarScenery();
}
async function sceneSelectVideo(v) {
_sceneVideo = v; _sceneFrameBytes = null;
+ _sceneGridOpen = false; // collapse the picker so the player is front-and-centre
renderSidebarScenery();
}
function sceneReleaseFrame() {
_sceneFrameBytes = null;
- const vid = document.getElementById('sceneVideoEl');
- const img = document.getElementById('sceneFrameImg');
- const extractBtn = document.getElementById('sceneExtractBtn');
- const releaseBtn = document.getElementById('sceneReleaseBtn');
- const genBtn = document.getElementById('sceneGenBtn');
- if (vid) vid.style.display = '';
- if (img) img.style.display = 'none';
- if (extractBtn) extractBtn.style.display = '';
- if (releaseBtn) releaseBtn.style.display = 'none';
- if (genBtn) genBtn.disabled = !_sceneVideo;
+ renderSidebarScenery(); // re-render restores the live video + slider + capture button
}
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…'; }
+ const vid = document.getElementById('sceneVideoEl');
+ if (vid) vid.pause();
+ const t = vid ? vid.currentTime : 0;
+ const captureBtn = document.getElementById('sceneCaptureBtn');
+ if (captureBtn) { captureBtn.disabled = true; captureBtn.textContent = 'Extracting…'; }
try {
const r = await fetch(`${API}/wireframe/frame`, {
method: 'POST',
@@ -5396,22 +5534,16 @@
});
if (!r.ok) {
showToast('Frame extract failed: ' + (await r.text()), 'error');
+ if (captureBtn) { captureBtn.disabled = false; captureBtn.textContent = '📷 Capture this frame'; }
} else {
const d = await r.json();
_sceneFrameBytes = d.frame_b64;
- 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';
+ renderSidebarScenery(); // show the frozen frame + "pick a different frame"
}
} catch (e) {
showToast('Frame extract error: ' + e, 'error');
- if (btn) btn.textContent = 'Extract frame as reference';
+ if (captureBtn) { captureBtn.disabled = false; captureBtn.textContent = '📷 Capture this frame'; }
}
- if (btn) btn.disabled = false;
}
function sceneHandleUpload(event) {
@@ -5429,7 +5561,7 @@
async function submitGenerateScenery() {
if (!_fsModelFilename) return;
if (!_sceneVideo && !_sceneFrameBytes) { showToast('Pick a scene reference first', 'error'); return; }
- const scrubber = document.getElementById('sceneScrubber');
+ const sceneVid = document.getElementById('sceneVideoEl');
const prompt = (document.getElementById('scenePromptInput')?.value || '').trim() || null;
const btn = document.getElementById('sceneGenBtn');
const statusEl = document.getElementById('sceneJobStatus');
@@ -5438,10 +5570,11 @@
if (statusEl) statusEl.style.display = 'inline-flex';
if (textEl) textEl.textContent = 'Submitting…';
const payload = {
- model_filename: _fsModelFilename,
- scene_bytes: _sceneFrameBytes || null,
+ model_filename: _fsModelFilename, // image2 (Picture 2) — person
+ scene_bytes: _sceneFrameBytes || null, // image1 (Picture 1) — background
scene_video: _sceneVideo || null,
- scene_time: parseFloat(scrubber?.value || '0'),
+ scene_time: sceneVid ? sceneVid.currentTime : 0,
+ extra_filename: _sceneExtraRef || null, // image3 (Picture 3) — optional extra ref
prompt,
seed: -1,
};
@@ -5468,6 +5601,10 @@
showToast('Scenery generated!', 'success');
if (statusEl) statusEl.style.display = 'none';
if (btn) btn.disabled = false;
+ // Auto-follow so the open studio filmstrip jumps to the
+ // new image (matches the Generate tab); refreshNow() alone
+ // doesn't update an already-open studio's filmstrip.
+ _followLatestGid = lbCurrentGid;
refreshNow();
} else {
const errMsg = job.error || 'unknown error';
diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py
index 7384e46..ccdc884 100644
--- a/tour-comfy/edit_api.py
+++ b/tour-comfy/edit_api.py
@@ -2337,10 +2337,11 @@ def _extract_frame_at(video_path: str, t: float) -> Image.Image:
class SceneryRequest(BaseModel):
- model_filename: str # person image in output_dir
- scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene
+ model_filename: str # person image in output_dir → image2 (Picture 2)
+ scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene → image1
scene_video: str | None = None # wireframe video name to extract frame from
scene_time: float = 0.0 # timestamp (seconds) to extract from video
+ extra_filename: str | None = None # optional extra reference in output_dir → image3 (Picture 3)
prompt: str | None = None # override; auto-built if None
seed: int = -1
@@ -2361,18 +2362,19 @@ def _make_side_by_side(img1: Image.Image, img2: Image.Image,
def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
- prompt: str, seed: int):
+ prompt: str, seed: int, extra_pils: list | None = None):
output_dir = _load_output_dir()
try:
model_path = os.path.join(output_dir, model_filename)
model_pil = Image.open(model_path).convert("RGB")
- # image1=scene (→ Picture 1, output sized to scene), image2=person (→ Picture 2)
- # The node prepends "Picture 1: