dphn/Dolphin3.0-Mistral-24B is the ungated mirror of the Dolphin 3.0 Mistral 24B — exactly what you asked for. It's ~48GB fp16, which needs GPU+CPU split (device_map="auto" with 32GB on GPU, ~16GB in RAM). Let me kick off the download and update the service in parallel.

This commit is contained in:
mike
2026-06-25 00:57:29 +02:00
parent ee7569f38c
commit 6d31826c29
3 changed files with 229 additions and 74 deletions

View File

@@ -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. when refresh page, we lose track of current jobs running.
generating poses themself should use the adviced dimensions rather than the base image reference. 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

View File

@@ -1513,6 +1513,7 @@
border-radius: 4px; color: #888; font-size: 11px; border-radius: 4px; color: #888; font-size: 11px;
cursor: pointer; display: flex; align-items: center; justify-content: center; cursor: pointer; display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s, color 0.1s; opacity: 0; transition: opacity 0.15s, color 0.1s;
z-index: 2;
} }
.sb-template-card:hover .sb-template-trim-btn { opacity: 1; } .sb-template-card:hover .sb-template-trim-btn { opacity: 1; }
.sb-template-trim-btn:hover { color: #fff; background: rgba(0,0,0,0.92); } .sb-template-trim-btn:hover { color: #fff; background: rgba(0,0,0,0.92); }
@@ -1578,7 +1579,41 @@
margin-bottom: 8px; position: relative; margin-bottom: 8px; position: relative;
} }
.scene-frame-preview img { width:100%; height:100%; object-fit:contain; display:block; } .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 TAB ===================== */
.segment-checker-hint { font-size: 10px; color: #444; margin-top: 6px; } .segment-checker-hint { font-size: 10px; color: #444; margin-top: 6px; }
@@ -2321,7 +2356,7 @@
function openStudio(gid, startIdx) { function openStudio(gid, startIdx) {
const data = groupData.get(gid); const data = groupData.get(gid);
if (!data) return; 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; lbCurrentGid = gid;
lbUrls = data.urls; lbUrls = data.urls;
lbNames = data.names; lbNames = data.names;
@@ -5306,33 +5341,122 @@
// ---- scenery sidebar tab ---- // ---- scenery sidebar tab ----
let _sceneVideo = null, _sceneDuration = 0, _sceneFrameBytes = null, _sceneJobPollTimer = null; 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() { function renderSidebarScenery() {
const panel = document.getElementById('sbPanelScenery'); const panel = document.getElementById('sbPanelScenery');
if (!panel) return; if (!panel) return;
let html = '<div class="sb-label">Background reference</div>'; const haveRef = _sceneVideo || _sceneFrameBytes;
if (availableVideos.length) { // Auto-open the grid when nothing is chosen yet.
html += '<div class="sb-template-grid">'; const gridOpen = _sceneGridOpen || !haveRef;
availableVideos.forEach(v => {
const vSafe = v.replace(/'/g, "\\'"); // Person (image2) = current studio image; extra (image3) = _sceneExtraRef
html += _tplCardHTML(v, _sceneVideo === v, `sceneSelectVideo('${vSafe}')`); 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) => `
<div class="scene-slot ${thumb?'filled':'empty'}">
<div class="scene-slot-thumb">${thumb
? `<img src="${thumb}" onerror="this.style.opacity=.3">`
: `<span class="ph">${num}</span>`}</div>
<div class="scene-slot-cap"><b>Image ${num}</b>${label}</div>
</div>`;
let html = `<div class="sb-label">Compose: person + scene → one photo</div>
<div class="scene-slots">
${slot('bg','1','Background', _sceneFrameBytes ? `data:image/png;base64,${_sceneFrameBytes}` : '')}
${slot('person','2','Person', personThumb)}
${slot('extra','3','Extra (opt)', extraThumb)}
</div>`;
// --- IMAGE 1: Background scene (video scrub + capture, or upload) ---
html += `<div class="sb-label">Image 1 · Background scene</div>`;
// --- Selected video: player + slider + capture (always at top) ---
if (_sceneVideo) {
html += `
<div class="scene-frame-preview" id="sceneFramePreview" style="display:block">
<video id="sceneVideoEl" muted preload="auto" playsinline
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;height:100%;object-fit:contain"></video>
<img id="sceneFrameImg" style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;height:100%;object-fit:contain"
${_sceneFrameBytes?`src="data:image/png;base64,${_sceneFrameBytes}"`:''}/>
<div class="scene-captured-badge" id="sceneCapturedBadge" style="${_sceneFrameBytes?'display:block':'display:none'}">✓ frame captured</div>
</div>
<input type="range" class="scene-scrubber" id="sceneScrubber" min="0" max="1000" value="0"
oninput="sceneScrub(this.value)" ${_sceneFrameBytes?'disabled':''}>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span id="sceneTimeLabel" style="font-size:11px;color:#888">0:00.0</span>
<span style="font-size:10px;color:#555">${escHtml(_sceneVideo.replace(/\.[^.]+$/,''))}</span>
</div>
<button class="sb-btn primary" id="sceneCaptureBtn" onclick="sceneExtractFrame()"
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;margin-bottom:8px">📷 Capture this frame</button>
<button class="sb-btn" id="sceneReleaseBtn" onclick="sceneReleaseFrame()"
style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;margin-bottom:8px">✖ Pick a different frame</button>
<button class="sb-btn" onclick="sceneToggleGrid()" style="width:100%;margin-bottom:10px">
${gridOpen?'▲ Hide video list':'▼ Change background video'}</button>`;
}
// --- Video picker grid (collapsible) ---
if (!_sceneVideo || gridOpen) {
html += '<div class="sb-label">Background video</div>';
if (availableVideos.length) {
html += '<div class="sb-template-grid">';
availableVideos.forEach(v => {
const vSafe = v.replace(/'/g, "\\'");
html += _tplCardHTML(v, _sceneVideo === v, `sceneSelectVideo('${vSafe}')`);
});
html += '</div>';
} else {
html += '<div style="font-size:11px;color:#555;padding:6px 0">No wireframe videos found</div>';
}
}
// Upload alternative for the background (still image 1)
html += `<input type="file" id="sceneUploadInput" accept="image/*" style="display:none" onchange="sceneHandleUpload(event)">
<button class="sb-btn" onclick="document.getElementById('sceneUploadInput').click()" style="width:100%;margin-bottom:10px">⬆ Upload image as background</button>`;
// --- IMAGE 2: Person (auto — current studio image) ---
html += `<div class="sb-sep"></div>
<div class="sb-label">Image 2 · Person <span style="color:#555;font-weight:400;font-size:9px">· current image</span></div>`;
if (personThumb) {
html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<img src="${personThumb}" style="width:46px;height:46px;object-fit:cover;border-radius:5px;border:1px solid #2563eb"
onerror="this.style.opacity=.3">
<span style="font-size:10px;color:#888;word-break:break-all">${escHtml(personName)}</span>
</div>`;
} else {
html += `<div style="font-size:11px;color:#a55;padding:4px 0 10px">No person image — open one from the gallery first.</div>`;
}
// --- IMAGE 3: Extra reference (optional, pick from this group) ---
html += `<div class="sb-sep"></div>
<div class="sb-label">Image 3 · Extra reference <span style="color:#555;font-weight:400;font-size:9px">· optional · click to toggle</span></div>`;
const pickNames = lbNames.filter(n => n && n !== personName);
if (pickNames.length) {
html += '<div class="scene-pick-row">';
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 += `<div class="scene-pick ${sel?'sel':''}" title="${escHtml(n)}"
onclick="sceneToggleExtraRef('${nSafe}')">
<img src="${th}" loading="lazy" onerror="this.style.opacity=.3">
${sel?'<div class="badge3">3</div>':''}</div>`;
}); });
html += '</div>'; html += '</div>';
} else {
html += `<div style="font-size:11px;color:#555;padding:4px 0">No other images in this group.</div>`;
} }
html += `<div class="scene-frame-preview" id="sceneFramePreview" style="${_sceneVideo||_sceneFrameBytes?'':'display:none'}">
<video id="sceneVideoEl" muted preload="auto" playsinline loop controls // --- Prompt + generate ---
style="${_sceneVideo&&!_sceneFrameBytes?'':'display:none'};width:100%;height:100%;object-fit:contain"></video> html += `<div class="sb-sep"></div>
<img id="sceneFrameImg" style="${_sceneFrameBytes?'':'display:none'};width:100%;height:100%;object-fit:contain"
${_sceneFrameBytes?`src="data:image/png;base64,${_sceneFrameBytes}"`:''}/>
</div>
<button class="sb-btn" id="sceneExtractBtn" onclick="sceneExtractFrame()"
style="${_sceneVideo&&!_sceneFrameBytes?'':'display:none'};margin-bottom:8px">Extract frame as reference</button>
<button class="sb-btn" id="sceneReleaseBtn" onclick="sceneReleaseFrame()"
style="${_sceneFrameBytes?'':'display:none'};margin-bottom:8px">&#10006; Change frame</button>
<div class="sb-sep"></div>
<div class="sb-label">Or upload image</div>
<input type="file" id="sceneUploadInput" accept="image/*" style="display:none" onchange="sceneHandleUpload(event)">
<button class="sb-btn" onclick="document.getElementById('sceneUploadInput').click()" style="margin-bottom:10px">Upload image</button>
<div class="sb-sep"></div>
<div class="sb-label">Prompt (optional)</div> <div class="sb-label">Prompt (optional)</div>
<input type="text" class="sb-input" id="scenePromptInput" <input type="text" class="sb-input" id="scenePromptInput"
placeholder="Auto: place person in scene…" style="margin-bottom:10px"> placeholder="Auto: place person in scene…" style="margin-bottom:10px">
@@ -5341,53 +5465,67 @@
<span class="sb-spinner"></span><span id="sceneJobText">Generating…</span> <span class="sb-spinner"></span><span id="sceneJobText">Generating…</span>
</span> </span>
<button class="sb-btn primary" id="sceneGenBtn" onclick="submitGenerateScenery()" <button class="sb-btn primary" id="sceneGenBtn" onclick="submitGenerateScenery()"
${(_sceneVideo||_sceneFrameBytes)?'':'disabled'}>Generate Scenery</button> ${(haveRef && personName)?'':'disabled'}>Create Scenery</button>
</div>`; </div>`;
panel.innerHTML = html; panel.innerHTML = html;
if (_sceneVideo) { if (_sceneVideo) {
const vid = document.getElementById('sceneVideoEl'); const vid = document.getElementById('sceneVideoEl');
vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`; vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`;
vid.addEventListener('seeking', () => { // Once metadata is known, size the slider so 1000 steps span the whole clip.
if (_sceneFrameBytes) sceneReleaseFrame(); 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) { async function sceneSelectVideo(v) {
_sceneVideo = v; _sceneFrameBytes = null; _sceneVideo = v; _sceneFrameBytes = null;
_sceneGridOpen = false; // collapse the picker so the player is front-and-centre
renderSidebarScenery(); renderSidebarScenery();
} }
function sceneReleaseFrame() { function sceneReleaseFrame() {
_sceneFrameBytes = null; _sceneFrameBytes = null;
const vid = document.getElementById('sceneVideoEl'); renderSidebarScenery(); // re-render restores the live video + slider + capture button
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;
} }
async function sceneExtractFrame() { async function sceneExtractFrame() {
const scrubber = document.getElementById('sceneScrubber'); const vid = document.getElementById('sceneVideoEl');
const t = parseFloat(scrubber?.value || '0'); if (vid) vid.pause();
const btn = document.getElementById('sceneExtractBtn'); const t = vid ? vid.currentTime : 0;
const img = document.getElementById('sceneFrameImg'); const captureBtn = document.getElementById('sceneCaptureBtn');
// If the debounced preview already shows this frame, just lock it — no re-fetch if (captureBtn) { captureBtn.disabled = true; captureBtn.textContent = 'Extracting…'; }
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 { try {
const r = await fetch(`${API}/wireframe/frame`, { const r = await fetch(`${API}/wireframe/frame`, {
method: 'POST', method: 'POST',
@@ -5396,22 +5534,16 @@
}); });
if (!r.ok) { if (!r.ok) {
showToast('Frame extract failed: ' + (await r.text()), 'error'); showToast('Frame extract failed: ' + (await r.text()), 'error');
if (captureBtn) { captureBtn.disabled = false; captureBtn.textContent = '📷 Capture this frame'; }
} else { } else {
const d = await r.json(); const d = await r.json();
_sceneFrameBytes = d.frame_b64; _sceneFrameBytes = d.frame_b64;
const vid = document.getElementById('sceneVideoEl'); renderSidebarScenery(); // show the frozen frame + "pick a different frame"
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 (e) { } catch (e) {
showToast('Frame extract error: ' + e, 'error'); 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) { function sceneHandleUpload(event) {
@@ -5429,7 +5561,7 @@
async function submitGenerateScenery() { async function submitGenerateScenery() {
if (!_fsModelFilename) return; if (!_fsModelFilename) return;
if (!_sceneVideo && !_sceneFrameBytes) { showToast('Pick a scene reference first', 'error'); 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 prompt = (document.getElementById('scenePromptInput')?.value || '').trim() || null;
const btn = document.getElementById('sceneGenBtn'); const btn = document.getElementById('sceneGenBtn');
const statusEl = document.getElementById('sceneJobStatus'); const statusEl = document.getElementById('sceneJobStatus');
@@ -5438,10 +5570,11 @@
if (statusEl) statusEl.style.display = 'inline-flex'; if (statusEl) statusEl.style.display = 'inline-flex';
if (textEl) textEl.textContent = 'Submitting…'; if (textEl) textEl.textContent = 'Submitting…';
const payload = { const payload = {
model_filename: _fsModelFilename, model_filename: _fsModelFilename, // image2 (Picture 2) — person
scene_bytes: _sceneFrameBytes || null, scene_bytes: _sceneFrameBytes || null, // image1 (Picture 1) — background
scene_video: _sceneVideo || null, 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, prompt,
seed: -1, seed: -1,
}; };
@@ -5468,6 +5601,10 @@
showToast('Scenery generated!', 'success'); showToast('Scenery generated!', 'success');
if (statusEl) statusEl.style.display = 'none'; if (statusEl) statusEl.style.display = 'none';
if (btn) btn.disabled = false; 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(); refreshNow();
} else { } else {
const errMsg = job.error || 'unknown error'; const errMsg = job.error || 'unknown error';

View File

@@ -2337,10 +2337,11 @@ def _extract_frame_at(video_path: str, t: float) -> Image.Image:
class SceneryRequest(BaseModel): class SceneryRequest(BaseModel):
model_filename: str # person image in output_dir model_filename: str # person image in output_dir → image2 (Picture 2)
scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene 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_video: str | None = None # wireframe video name to extract frame from
scene_time: float = 0.0 # timestamp (seconds) to extract from video 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 prompt: str | None = None # override; auto-built if None
seed: int = -1 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, 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() output_dir = _load_output_dir()
try: try:
model_path = os.path.join(output_dir, model_filename) model_path = os.path.join(output_dir, model_filename)
model_pil = Image.open(model_path).convert("RGB") model_pil = Image.open(model_path).convert("RGB")
# image1=scene (→ Picture 1, output sized to scene), image2=person (→ Picture 2) # image1=scene (→ Picture 1, output sized to scene), image2=person (→ Picture 2),
# The node prepends "Picture 1: <img> Picture 2: <img>" to the prompt so the # image3=optional extra ref (→ Picture 3). The node prepends "Picture 1: <img>
# model can reason about both images by name. # Picture 2: <img> ..." to the prompt so the model can reason about each by name.
extra_images = [model_pil] + list(extra_pils or [])
png_bytes = _run_pipeline( png_bytes = _run_pipeline(
scene_pil.convert("RGB"), prompt, seed, MAX_AREA, 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") ts = time.strftime("%Y%m%d_%H%M%S")
base_name = naming.get_base_name(model_filename) 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}") print(f"[scenery] DB error: {db_err}")
jobs[job_id]["status"] = "done" jobs[job_id]["status"] = "done"
jobs[job_id]["output"] = out_name 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: except Exception as e:
print(f"[scenery] error: {e}") print(f"[scenery] error: {e}")
jobs[job_id]["status"] = "error" jobs[job_id]["status"] = "error"
@@ -2427,19 +2433,28 @@ def generate_scenery(req: SceneryRequest):
else: else:
raise HTTPException(400, "Provide scene_bytes or scene_video") 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 ( prompt = req.prompt or (
"Place the person from Picture 2 naturally inside the environment shown in Picture 1. " "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. " "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. " "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. " "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] job_id = uuid.uuid4().hex[:8]
jobs[job_id] = {"status": "running", "type": "scenery", "total": 1, "done": 0, "failed": 0} jobs[job_id] = {"status": "running", "type": "scenery", "total": 1, "done": 0, "failed": 0}
threading.Thread( threading.Thread(
target=_scenery_worker, 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, daemon=True,
).start() ).start()
return {"job_id": job_id, "model": req.model_filename} return {"job_id": job_id, "model": req.model_filename}