From 8df588e594a6e37929da62a2bd44e0b1dee09c36 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 24 Jun 2026 13:10:07 +0200 Subject: [PATCH] =?UTF-8?q?dphn/Dolphin3.0-Mistral-24B=20is=20the=20ungate?= =?UTF-8?q?d=20mirror=20of=20the=20Dolphin=203.0=20Mistral=2024B=20?= =?UTF-8?q?=E2=80=94=20exactly=20what=20you=20asked=20for.=20It's=20~48GB?= =?UTF-8?q?=20fp16,=20which=20needs=20GPU+CPU=20split=20(device=5Fmap=3D"a?= =?UTF-8?q?uto"=20with=2032GB=20on=20GPU,=20~16GB=20in=20RAM).=20Let=20me?= =?UTF-8?q?=20kick=20off=20the=20download=20and=20update=20the=20service?= =?UTF-8?q?=20in=20parallel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tour-comfy/car.html | 430 +++++++++++++++++++++++++++++++++++----- tour-comfy/database.py | 112 +++++++++-- tour-comfy/edit_api.py | 220 +++++++++++++++++++- tour-comfy/gen_poses.py | 42 +++- tour-comfy/poses.md | 407 ++++++++++++++++++++++++++++++++----- 5 files changed, 1081 insertions(+), 130 deletions(-) diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 4c71b3e..54656a1 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -1439,7 +1439,26 @@ text-align: center; transition: all 0.12s; } .sb-angle-btn:hover { border-color: #444; color: #ccc; } + .sb-angle-btn.done { border-color: #14532d; background: #052e16; color: #4ade80; position: relative; } + .sb-angle-btn.done::after { content: '✓'; position: absolute; top: 1px; right: 3px; font-size: 9px; color: #4ade80; opacity: 0.9; } .sb-angle-btn.selected { border-color: #0e7490; background: #082f49; color: #7dd3fc; font-weight: 600; } + + /* Generate panel: sticky footer (custom prompt + Generate) and selected-ref thumbnails */ + .sb-gen-footer { + position: sticky; bottom: 0; margin: 8px -13px -12px; padding: 10px 13px; + background: #0d0d10; border-top: 1px solid #1f1f24; + } + .sb-gen-refs { display: flex; gap: 5px; margin-bottom: 8px; flex-wrap: wrap; } + .sb-gen-refs .sb-gen-ref { position: relative; width: 40px; height: 40px; flex-shrink: 0; } + .sb-gen-refs .sb-gen-ref img { + width: 40px; height: 40px; object-fit: cover; border-radius: 4px; + border: 1px solid #3d2008; background: #111; + } + .sb-gen-refs .sb-gen-ref-badge { + position: absolute; top: -4px; left: -4px; background: #f97316; color: #111; + font-size: 9px; font-weight: 700; border-radius: 50%; width: 15px; height: 15px; + display: flex; align-items: center; justify-content: center; line-height: 1; + } #studioAngleBar { position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%); display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s; @@ -1829,6 +1848,8 @@ + + Download @@ -2175,6 +2196,7 @@ let _fsSelectedPoses = new Set(); let _sbSelectedAngles = new Set(); // selected camera angle names let _sbRefIndices = []; // ordered filmstrip indices selected as multi-ref (#1, #2, #3) + let _sbRefMode = 'combine'; // with 2–3 refs: 'combine' → 1 multi-ref output; 'each' → one edit per ref (cross product) let _sbWireframeRef = ''; // wireframe video name for pose guide let _sbWireframeTime = 0.5; // normalized frame time 0–1 let _sbPoseEdits = {}; // poseName → edited text @@ -2484,10 +2506,7 @@ const faceBook = document.getElementById('lbFaceBook'); const faceThumb = document.getElementById('lbFaceThumb'); if (faceBook && faceThumb && lbCurrentGid && lbIdx === 0 && !isVid) { - const faceFname = lbCurrentGid.replace(/\//g, '_') + '_face.png'; - faceThumb.src = IMAGE_FOLDER + faceFname + '?t=' + Date.now(); - faceThumb.onerror = () => { faceBook.style.display = 'none'; }; - faceThumb.onload = () => { faceBook.style.display = ''; }; + loadFaceThumb(lbCurrentGid); } else if (faceBook) { faceBook.style.display = 'none'; } @@ -2641,11 +2660,13 @@ lbNames.splice(lbIdx, 1); if (lbNames.length === 0) { closeStudio(); - refreshNow(); + // Add a small delay to allow static regeneration to complete before refresh + setTimeout(refreshNow, 500); } else { lbIdx = Math.min(lbIdx, lbNames.length - 1); updateStudio(); - refreshNow(); + // Add a small delay to allow static regeneration to complete before refresh + setTimeout(refreshNow, 500); } } else { showToast(`Failed to delete`, 'error'); @@ -2674,11 +2695,13 @@ lbNames.splice(lbIdx, 1); if (lbNames.length === 0) { closeStudio(); - refreshNow(); + // Add a small delay to allow static regeneration to complete before refresh + setTimeout(refreshNow, 500); } else { lbIdx = Math.min(lbIdx, lbNames.length - 1); updateStudio(); - refreshNow(); + // Add a small delay to allow static regeneration to complete before refresh + setTimeout(refreshNow, 500); } } else { showToast('Archive failed', 'error'); @@ -2729,6 +2752,29 @@ if (btn) { btn.disabled = false; btn.textContent = 'Crop'; } } + async function lbRotate(degrees) { + const fname = lbNames[lbIdx]; + if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to rotate', 'info'); return; } + const btns = [document.getElementById('lbRotateLeftBtn'), document.getElementById('lbRotateRightBtn')]; + btns.forEach(b => { if (b) b.disabled = true; }); + try { + const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/rotate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ degrees }), + }); + if (r.ok) { + showToast('Rotated', 'success'); + lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now(); + updateStudio(); + refreshNow(); + } else { + showToast('Rotate failed: ' + await r.text(), 'error'); + } + } catch (e) { showToast('Rotate failed: ' + e, 'error'); } + btns.forEach(b => { if (b) b.disabled = false; }); + } + function startManualCrop() { const fname = lbNames[lbIdx]; if (!fname || !/\.(png|jpg|jpeg|webp)$/i.test(fname)) { showToast('Select an image to crop', 'info'); return; } @@ -2744,8 +2790,11 @@ const bar = document.createElement('div'); bar.id = 'cropBar'; - bar.style.cssText = 'position:absolute;bottom:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.75);z-index:101;justify-content:flex-end;align-items:center;'; + // Anchored at the TOP so the buttons never block cropping to the bottom edge. + bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.75);z-index:101;justify-content:flex-end;align-items:center;'; bar.innerHTML = 'Drag to select crop area' + + '' + '' + ''; viewer.appendChild(bar); @@ -2820,19 +2869,31 @@ const p2 = toImg(Math.max(st.x1, st.x2), Math.max(st.y1, st.y2)); if (p2.x - p1.x < 2 || p2.y - p1.y < 2) { showToast('Crop area too small', 'info'); return; } + const asCopy = document.getElementById('cropAsCopy')?.checked ?? false; const btn = document.getElementById('cropConfirmBtn'); if (btn) { btn.disabled = true; btn.textContent = 'Cropping…'; } try { const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/crop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), + body: JSON.stringify({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y, as_copy: asCopy }), }); if (r.ok) { - showToast('Cropped', 'success'); + const d = await r.json().catch(() => ({})); cancelManualCrop(); - lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now(); - updateStudio(); + if (asCopy && d.new_filename) { + showToast('Cropped to copy', 'success'); + // Insert the cropped copy right after the original in the studio strip. + lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + d.new_filename + '?t=' + Date.now()); + lbNames.splice(lbIdx + 1, 0, d.new_filename); + lbIdx = lbIdx + 1; + updateStudio(); + refreshNow(); + } else { + showToast('Cropped', 'success'); + lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now(); + updateStudio(); + } } else { showToast('Crop failed: ' + await r.text(), 'error'); if (btn) { btn.disabled = false; btn.textContent = 'Crop'; } @@ -2901,7 +2962,8 @@ const r = await fetch(`${API}/groups/${gid}`, { method: 'DELETE' }); if (r.ok) { showToast(`Group ${gid} deleted`, 'success'); - refreshNow(); + // Add a small delay to allow static regeneration to complete before refresh + setTimeout(refreshNow, 500); } else { showToast(`Failed to delete group ${gid}`, 'error'); } @@ -3078,7 +3140,7 @@ // Primary: pre-generated static JSON (fast, no DB round-trip per request) try { - const r = await fetch(`${API}/output/_data/images.json`, { signal: AbortSignal.timeout(4000) }); + const r = await fetch(`${API}/output/_data/images.json?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) }); if (r.ok) { const data = await r.json(); let imgs = data.images || []; @@ -3780,6 +3842,16 @@ if (tag === 'INPUT' || tag === 'TEXTAREA') return; if (e.key === 'ArrowLeft') { lbNav(-1); e.preventDefault(); } if (e.key === 'ArrowRight') { lbNav(1); e.preventDefault(); } + if (e.key === 'Home') { + lbIdx = 0; + updateStudio(); + e.preventDefault(); + } + if (e.key === 'End') { + lbIdx = lbUrls.length - 1; + updateStudio(); + e.preventDefault(); + } if (e.key === ' ') { const vid = document.getElementById('lbVideo'); if (vid && vid.style.display !== 'none') { @@ -3788,6 +3860,22 @@ } } if ((e.key === 'h' || e.key === 'H') && tag !== 'INPUT') { lbToggleHidden(); } + if (e.key === 'F' || e.key === 'f') { + lbSetPreferred(); + e.preventDefault(); + } + if (e.key === 'Delete') { + lbArchive(); + e.preventDefault(); + } + if (e.key === 'ArrowUp') { + lbMoveInGroup(-1); + e.preventDefault(); + } + if (e.key === 'ArrowDown') { + lbMoveInGroup(1); + e.preventDefault(); + } }); }); @@ -3838,6 +3926,33 @@ document.getElementById('privacyOverlay').style.display = 'none'; } + // Load the face-book thumbnail for a group. Face extraction runs + // asynchronously after "set preferred", so we ask the API whether the + // crop exists (works over file://, unlike probing with an that + // logs a 404 during the race) and poll a few times while it's pending. + async function loadFaceThumb(gid, retries = 0) { + const faceBook = document.getElementById('lbFaceBook'); + const faceThumb = document.getElementById('lbFaceThumb'); + if (!faceBook || !faceThumb || !gid) return; + try { + const r = await fetch(`${API}/faces/${encodeURIComponent(gid)}`); + const d = await r.json(); + // Ignore stale responses if the user navigated away. + if (lbCurrentGid !== gid || lbIdx !== 0) return; + if (d.exists) { + faceThumb.onerror = () => { faceBook.style.display = 'none'; }; + faceThumb.src = IMAGE_FOLDER + d.filename + '?t=' + Date.now(); + faceBook.style.display = ''; + } else if (retries > 0) { + setTimeout(() => loadFaceThumb(gid, retries - 1), 1500); + } else { + faceBook.style.display = 'none'; + } + } catch (e) { + faceBook.style.display = 'none'; + } + } + async function setAsPreferred(filename, btn) { try { const r = await fetch(`/images/${encodeURIComponent(filename)}/set-preferred`, {method:'POST'}); @@ -3848,6 +3963,8 @@ fileSortOrders[filename] = 0; // Re-open picker to reflect new order showVariantPicker(filename); + // Poll for the freshly-extracted face crop (async on the server). + loadFaceThumb(gid, 8); } catch(e) { console.error(e); } } @@ -3930,18 +4047,20 @@ const relAngles = CAMERA_ANGLES.filter(a => a.relative); let html = '
Camera — absolute
'; html += absAngles.map(a => { - const sel = _sbSelectedAngles.has(a.name) ? ' selected' : ''; + const sel = _sbSelectedAngles.has(a.name) ? ' selected' : ''; + const done = donePoses.has(a.name) ? ' done' : ''; const nSafe = a.name.replace(/'/g, "\\'"); - return ``; + return ``; }).join(''); html += '
'; // Relative rotations html += '
Camera — relative
'; html += relAngles.map(a => { - const sel = _sbSelectedAngles.has(a.name) ? ' selected' : ''; + const sel = _sbSelectedAngles.has(a.name) ? ' selected' : ''; + const done = donePoses.has(a.name) ? ' done' : ''; const nSafe = a.name.replace(/'/g, "\\'"); - return ``; + return ``; }).join(''); html += '
'; @@ -3981,43 +4100,135 @@ } html += ``; - // Fine-tune section: one editable textarea per selected pose + // Fine-tune section: one editable textarea per selected pose. + // "Save" persists the edit to poses.md; "Delete" removes the pose entirely. if (_fsSelectedPoses.size > 0) { - html += `
Fine-tune poses
`; + html += `
Fine-tune poses + · Save persists to poses.md
`; _fsSelectedPoses.forEach(name => { const origText = String(availablePoses[name]?.text ?? availablePoses[name] ?? name); const curText = (_sbPoseEdits[name] !== undefined) ? _sbPoseEdits[name] : origText; const idSafe = escHtml(name).replace(/"/g, '"'); - html += `
${escHtml(name)}
- `; + oninput="sbSavePoseEdit('${name.replace(/'/g,"\\'")}')">${escHtml(curText)} + `; }); } + // Add a brand-new pose (persisted to poses.md). html += `
-
Custom prompt
`; +
+ + New pose + + + + +
`; + + // Sticky footer — always visible without scrolling: refs + custom prompt + Generate + html += ``; panel.innerHTML = html; updateSbGenBtn(); + const promptEl = document.getElementById('sbGenPromptInput'); + if (promptEl) autoGrowPrompt(promptEl); + } + + // Grow the custom-prompt textarea to fit its content (capped by max-height in CSS). + function autoGrowPrompt(el) { + if (!el) return; + el.style.height = 'auto'; + el.style.height = Math.min(el.scrollHeight, 160) + 'px'; + } + + // History autocomplete for the custom-prompt textarea. can't bind to a + //