From 6d31826c290f7aa1dfac680d41087e869f1a930e Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 25 Jun 2026 00:57:29 +0200 Subject: [PATCH] =?UTF-8?q?dphn/Dolphin3.0-Mistral-24B=20is=20the=20ungate?= =?UTF-8?q?d=20mirror=20of=20the=20Dolphin=203.0=20Mistral=2024B=20?= =?UTF-8?q?=E2=80=94=20exactly=20what=20you=20asked=20for.=20It's=20~48GB?= =?UTF-8?q?=20fp16,=20which=20needs=20GPU+CPU=20split=20(device=5Fmap=3D"a?= =?UTF-8?q?uto"=20with=2032GB=20on=20GPU,=20~16GB=20in=20RAM).=20Let=20me?= =?UTF-8?q?=20kick=20off=20the=20download=20and=20update=20the=20service?= =?UTF-8?q?=20in=20parallel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backlog.md | 5 +- tour-comfy/car.html | 265 +++++++++++++++++++++++++++++++---------- tour-comfy/edit_api.py | 33 +++-- 3 files changed, 229 insertions(+), 74 deletions(-) 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 = '
Background reference
'; - if (availableVideos.length) { - 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(/\.[^.]+$/,''))} +
+ + + `; + } + + // --- 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 += ` + `; + + // --- 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 += `
- - -
- - -
-
Or upload image
- - -
+ + // --- Prompt + generate --- + html += `
Prompt (optional)
@@ -5341,53 +5465,67 @@ Generating… + ${(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: Picture 2: " to the prompt so the - # model can reason about both images by name. + # image1=scene (→ Picture 1, output sized to scene), image2=person (→ Picture 2), + # image3=optional extra ref (→ Picture 3). The node prepends "Picture 1: + # Picture 2: ..." to the prompt so the model can reason about each by name. + extra_images = [model_pil] + list(extra_pils or []) png_bytes = _run_pipeline( scene_pil.convert("RGB"), prompt, seed, MAX_AREA, - extra_images=[model_pil], + extra_images=extra_images, ) ts = time.strftime("%Y%m%d_%H%M%S") base_name = naming.get_base_name(model_filename) @@ -2396,6 +2398,10 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image, print(f"[scenery] DB error: {db_err}") jobs[job_id]["status"] = "done" jobs[job_id]["output"] = out_name + # Regenerate the static JSON so the frontend's polling surfaces the new + # image immediately (matching _batch_worker / _multi_ref_worker). Without + # this the output only appeared after a server restart. + _invalidate_static() except Exception as e: print(f"[scenery] error: {e}") jobs[job_id]["status"] = "error" @@ -2427,19 +2433,28 @@ def generate_scenery(req: SceneryRequest): else: raise HTTPException(400, "Provide scene_bytes or scene_video") + # Optional image3 (Picture 3) — extra reference from output_dir + extra_pils = [] + if req.extra_filename: + extra_path = os.path.join(output_dir, req.extra_filename) + if not os.path.exists(extra_path): + raise HTTPException(404, f"Extra reference not found: {req.extra_filename}") + extra_pils.append(Image.open(extra_path).convert("RGB")) + prompt = req.prompt or ( "Place the person from Picture 2 naturally inside the environment shown in Picture 1. " "Keep the person's face, body proportions, clothing and pose exactly as in Picture 2. " "Use the location, lighting and atmosphere from Picture 1 as the background. " "Match the color temperature and shadows so it looks like one photograph taken on location. " - "Output a single photorealistic image. High quality, detailed." + + ("Incorporate the reference from Picture 3 as well. " if extra_pils else "") + + "Output a single photorealistic image. High quality, detailed." ) job_id = uuid.uuid4().hex[:8] jobs[job_id] = {"status": "running", "type": "scenery", "total": 1, "done": 0, "failed": 0} threading.Thread( target=_scenery_worker, - args=(job_id, req.model_filename, scene_pil, prompt, req.seed), + args=(job_id, req.model_filename, scene_pil, prompt, req.seed, extra_pils), daemon=True, ).start() return {"job_id": job_id, "model": req.model_filename}