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

@@ -51,3 +51,6 @@ 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.
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;
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">&#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>
// --- 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';

View File

@@ -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}