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.
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;
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}