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:
@@ -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 = '<div class="sb-label">Background reference</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}')`);
|
||||
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) => `
|
||||
<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>';
|
||||
} 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
|
||||
style="${_sceneVideo&&!_sceneFrameBytes?'':'display:none'};width:100%;height:100%;object-fit:contain"></video>
|
||||
<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">✖ 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>
|
||||
|
||||
// --- Prompt + generate ---
|
||||
html += `<div class="sb-sep"></div>
|
||||
<div class="sb-label">Prompt (optional)</div>
|
||||
<input type="text" class="sb-input" id="scenePromptInput"
|
||||
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>
|
||||
<button class="sb-btn primary" id="sceneGenBtn" onclick="submitGenerateScenery()"
|
||||
${(_sceneVideo||_sceneFrameBytes)?'':'disabled'}>Generate Scenery</button>
|
||||
${(haveRef && personName)?'':'disabled'}>Create Scenery</button>
|
||||
</div>`;
|
||||
|
||||
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';
|
||||
|
||||
@@ -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: <img> Picture 2: <img>" 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: <img>
|
||||
# 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(
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user