This commit is contained in:
mike
2026-06-27 15:07:38 +02:00
parent 08a8cc9b82
commit 35306327f7
2 changed files with 114 additions and 14 deletions

View File

@@ -37,18 +37,19 @@ Scenery:
- ✅ OUTPAINT feature/ crop/ pad call qwen - ✅ OUTPAINT feature/ crop/ pad call qwen
- the pad/crop i think should allow also percentage values, since we looking in scaled images. - the pad/crop i think should allow also percentage values, since we looking in scaled images.
- valid multi-input way to facilite the 2.5/3d orbit - valid multi-input way to facilite the 2.5/3d orbit
- Refine a POSE, so we have the ref (source) image + pose prompt. use the tour GPU + api to refine the pose with user instruction. Then use the source image + updated prompt to generate a new image. - Refine a POSE, so we have the ref (source) image + pose prompt. use the tour GPU + api to refine the pose with user instruction. Then use the source image + updated prompt to generate a new image.
- introduce custom padding features - auto crop afterwards - introduce in the custom padding features + auto crop afterwards
- in car.html - studio view we have a action to pad the image we also have this feature in the "Generate" tab. - in car.html - studio view we have a action to pad the image we also have this feature in the "Generate" tab.
- add a checkbox (or similar) to instruct the image-model to outpaint the padded area. Also implement the outpainting functionality in edit_api.py, we can instruct qwen-image. We can have the same functionality in the "Generate" tab when no prompt is given but a padding is given. - add a checkbox (or similar) to instruct the image-model to outpaint the padded area. Also implement the outpainting functionality in edit_api.py, we can instruct qwen-image. We can have the same functionality in the "Generate" tab when no prompt is given but a padding is given.
- ✅ we have a bug that when pressing the ui-action "duplicate" and "pad" the browser needs to refresh before we see the result. Other actions like generating from pose do not hae this bug, fix the bug and boy scout adjecnt actions if they are missing the required technical follow up. Fix the bug for the features. - ✅ we have a bug that when pressing the ui-action "duplicate" and "pad" the browser needs to refresh before we see the result. Other actions like generating from pose do not hae this bug, fix the bug and boy scout adjecnt actions if they are missing the required technical follow up. Fix the bug for the features.
- ✅ In the "Generate" tab, the CUSTOM prompt is shown twice, above POSES and statically always in view in the right bottom. Remove the statically CUSTOM prompt from the UI. - ✅ In the "Generate" tab, the CUSTOM prompt is shown twice, above POSES and statically always in view in the right bottom. Remove the statically CUSTOM prompt from the UI.
- The Generate button is always visible in the "Generate" tab, however we cannot jump to it via keyboard. Add a keyboard shortcut to jump to the Generate button "G" or simialler. - The Generate button is always visible in the "Generate" tab, however we cannot jump to it via keyboard. Add a keyboard shortcut to jump to the Generate button "G" or simialler.
- remove the "set prompt" - remove the "set prompt"
- after pose generation, sometimes small black area's occur in the subject. ( presumably the _apply_transparency_black_bg addition) - after pose generation, sometimes small black area's occur in the subject. ( presumably the _apply_transparency_black_bg addition)
- studio view - orbit tab - view sometimes seems accelerated, liek the tiems is set twice. And most of all a step in the turn seems in in the wrong direction. maybe its an image-ordering bug, but i think its because the image-model generated the step 240deg into the wrong direction - studio view - orbit tab - view sometimes seems accelerated, liek the tiems is set twice. And most of all a step in the turn seems in in the wrong direction. maybe its an image-ordering bug, but i think its because the image-model generated the step 240deg into the wrong direction
- Improve the UI/UX for the end-user experience. Where effort is low and impact is high. - Improve the UI/UX for the end-user experience. Where effort is low and impact is high.
- add an action + key-bind to move a the current selected image in the filmstrip to the end of the filmstrip. ( so also do the actual reordering of the image )
-
- animate limbs along keyframe curves (true motion) - animate limbs along keyframe curves (true motion)
- 3D-aware rotation / "circling around" with depth estimation - 3D-aware rotation / "circling around" with depth estimation
- camera-orbit batch with consistent lighting across views - camera-orbit batch with consistent lighting across views
@@ -59,7 +60,9 @@ Scenery:
- after orbit generation, dont generate the mp4, but do show the orbit orbit-full-img-wrap and render the alpha images 1fps ourselves. - after orbit generation, dont generate the mp4, but do show the orbit orbit-full-img-wrap and render the alpha images 1fps ourselves.
- allow re-ordering of the images in the orbit generated frames - allow re-ordering of the images in the orbit generated frames
- sometimes the UI renders two orbits in the same orbitFullViewer, must be a timer not being cleared in the ui - sometimes the UI renders two orbits in the same orbitFullViewer, must be a timer not being cleared in the ui
- - The UI wants to car.html:2817 GET file:///mnt/zim/tour-comfy/output/_turntable/up_34e48ffb/turntable.jpg?t=1782562827857 net::ERR_FILE_NOT_FOUND, often want to show the wireframe - pose
- Jun 27 14:38:03 MIKE bash[3607206]: INFO: 127.0.0.1:53442 - "GET /output/_turntable/up_a1104f04/views/view_023_345deg.png HTTP/1.1" 304 Not Modified those files should be statically served from filesystem, not via server
## refine ## refine
- when refresh page, we lose track of current jobs running. - when refresh page, we lose track of current jobs running.

View File

@@ -126,6 +126,19 @@
opacity: 1; opacity: 1;
} }
.image-card.highlight-prev {
border: 2px solid #f59e0b !important;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.4);
}
.pad-preview-overlay {
position: absolute;
background: rgba(245,158,11,0.25);
border: 1px dashed #f59e0b;
pointer-events: none;
z-index: 5;
}
.action-btn { .action-btn {
background: rgba(0,0,0,0.6); background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
@@ -1902,6 +1915,7 @@
<div class="studio-topbar"> <div class="studio-topbar">
<div class="studio-topbar-left"> <div class="studio-topbar-left">
<button class="btn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button> <button class="btn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button>
<button class="btn" id="lbBackBtn" onclick="jumpToPrevGroup()" style="font-size:11px;padding:4px 10px;display:none" title="Go back to previously opened group">↺ Back</button>
</div> </div>
<div class="studio-topbar-center"> <div class="studio-topbar-center">
<input type="text" id="lbGroupNameInput" <input type="text" id="lbGroupNameInput"
@@ -2426,8 +2440,11 @@
let currentFiles = []; let currentFiles = [];
let currentGroups = []; let currentGroups = [];
const groupData = new Map(); // gid → { urls[], names[] } const groupData = new Map(); // gid → { urls[], names[] }
const fileGroups = {}; // filename → gid (for jumpToImage)
const cycleTimers = new Map(); // gid → intervalId const cycleTimers = new Map(); // gid → intervalId
const cycleIdx = new Map(); // gid → current index const cycleIdx = new Map(); // gid → current index
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
let _prevGid = null; // for "Back" button in studio
let imageNames = {}; // filename → human name (from /names) let imageNames = {}; // filename → human name (from /names)
let clipDescriptions = {}; // filename → description let clipDescriptions = {}; // filename → description
let customGroups = {}; // filename → groupId (from /groups) let customGroups = {}; // filename → groupId (from /groups)
@@ -2510,6 +2527,7 @@
} }
if (!map.has(gid)) map.set(gid, []); if (!map.has(gid)) map.set(gid, []);
map.get(gid).push(f); map.get(gid).push(f);
fileGroups[f.cleanName] = gid;
} }
// Sort each group: by explicit sort_order first, then by filename length // Sort each group: by explicit sort_order first, then by filename length
for (const files of map.values()) { for (const files of map.values()) {
@@ -2573,6 +2591,7 @@
const data = groupData.get(gid); const data = groupData.get(gid);
if (!data) return; if (!data) return;
if (lbCurrentGid !== gid) { // clear refs when switching groups if (lbCurrentGid !== gid) { // clear refs when switching groups
_prevGid = lbCurrentGid;
_sbRefIndices = []; _sbRefIndices = [];
_sceneRefs = [null, null, null]; _sceneRefs = [null, null, null];
_scenePersonInit = false; _scenePersonInit = false;
@@ -2580,6 +2599,8 @@
stopOrbitFast(); stopOrbitFast();
} }
lbCurrentGid = gid; lbCurrentGid = gid;
_lastSelectedGid = gid;
localStorage.setItem('lastSelectedGid', gid);
lbUrls = data.urls; lbUrls = data.urls;
lbNames = data.names; lbNames = data.names;
lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0); lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0);
@@ -2613,6 +2634,60 @@
openStudio(gid, idx); openStudio(gid, idx);
} }
function jumpToPrevGroup() {
if (_prevGid) openStudio(_prevGid);
}
function formatPadValue(v) {
if (v === undefined || v === null || v === '') return '0.00';
let val = parseFloat(v);
if (isNaN(val)) return '0.00';
const s = v.toString();
// User requested: show 0.00 if it contains a decimal see it as percentage.
// if integer and its greater than 10 its pixel value
if (s.includes('.') || s.includes('%')) return val.toFixed(2);
if (val > 10) return val.toString();
return val.toFixed(2);
}
function updatePadPreview() {
const viewer = document.getElementById('studioViewer');
if (!viewer) return;
// Clean up existing preview overlays
viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
const isManual = !!document.getElementById('padBar');
const getV = (side) => {
const el = document.getElementById(isManual ? 'pad' + side : 'sbPad' + side);
return el ? el.value : '0';
};
const sides = ['Left', 'Right'];
sides.forEach(side => {
const vStr = getV(side);
if (!vStr || vStr === '0' || vStr === '0.00') return;
const val = parseFloat(vStr);
if (isNaN(val) || val <= 0) return;
const isPercent = vStr.includes('%') || vStr.includes('.');
const width = isPercent ? (val + '%') : (val + 'px');
const overlay = document.createElement('div');
overlay.className = 'pad-preview-overlay';
overlay.style.top = '0';
overlay.style.bottom = '0';
if (side === 'Left') {
overlay.style.left = '0';
overlay.style.width = width;
} else {
overlay.style.right = '0';
overlay.style.width = width;
}
viewer.appendChild(overlay);
});
}
function closeStudio() { function closeStudio() {
document.getElementById('studio').classList.remove('open'); document.getElementById('studio').classList.remove('open');
document.body.style.overflow = ''; document.body.style.overflow = '';
@@ -2749,6 +2824,9 @@
lbImgEl.src = lbUrls[lbIdx]; lbImgEl.src = lbUrls[lbIdx];
} }
const backBtn = document.getElementById('lbBackBtn');
if (backBtn) backBtn.style.display = _prevGid ? '' : 'none';
const groupNameInput = document.getElementById('lbGroupNameInput'); const groupNameInput = document.getElementById('lbGroupNameInput');
groupNameInput.value = groupNames[lbCurrentGid] || ''; groupNameInput.value = groupNames[lbCurrentGid] || '';
@@ -3853,10 +3931,13 @@
const bar = document.createElement('div'); const bar = document.createElement('div');
bar.id = 'padBar'; bar.id = 'padBar';
bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.85);z-index:101;justify-content:flex-end;align-items:center;flex-wrap:wrap;'; bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.85);z-index:101;justify-content:flex-end;align-items:center;flex-wrap:wrap;';
const numInput = (id, label) => const numInput = (id, label) => {
`<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`; const onInput = `updatePadPreview()`;
const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`;
return `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0.00" oninput="${onInput}" onblur="${onBlur}" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
};
bar.innerHTML = bar.innerHTML =
`<span style="flex:1;font-size:11px;color:#aaa">Pad canvas (px):</span>` `<span style="flex:1;font-size:11px;color:#aaa">Pad canvas (px or %):</span>`
+ numInput('padTop','↑') + numInput('padTop','↑')
+ numInput('padRight','→') + numInput('padRight','→')
+ numInput('padBottom','↓') + numInput('padBottom','↓')
@@ -3873,10 +3954,13 @@
+ `<button class="sb-btn" onclick="cancelManualPad()">Cancel</button>` + `<button class="sb-btn" onclick="cancelManualPad()">Cancel</button>`
+ `<button class="sb-btn primary" id="padConfirmBtn" onclick="confirmManualPad()">Pad</button>`; + `<button class="sb-btn primary" id="padConfirmBtn" onclick="confirmManualPad()">Pad</button>`;
viewer.appendChild(bar); viewer.appendChild(bar);
updatePadPreview();
} }
function cancelManualPad() { function cancelManualPad() {
document.getElementById('padBar')?.remove(); document.getElementById('padBar')?.remove();
// Clean up preview overlays
document.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
} }
async function confirmManualPad() { async function confirmManualPad() {
@@ -4880,7 +4964,7 @@
: `<img src="${firstVisUrl}" alt="${label}" loading="${isRecent ? 'eager' : 'lazy'}" onerror="this.style.opacity='0.2'">`; : `<img src="${firstVisUrl}" alt="${label}" loading="${isRecent ? 'eager' : 'lazy'}" onerror="this.style.opacity='0.2'">`;
return ` return `
<div class="image-card ${isNew ? 'new' : ''}" data-group="${safeGid}" onclick="handleCardClick(this)"> <div class="image-card ${isNew ? 'new' : ''} ${_lastSelectedGid === gid ? 'highlight-prev' : ''}" data-group="${safeGid}" onclick="handleCardClick(this)">
<div class="image-actions"> <div class="image-actions">
${!isVideoCard ? `<div class="action-btn" title="Remove Background for Group" onclick="removeBgGroup(this, event)"> ${!isVideoCard ? `<div class="action-btn" title="Remove Background for Group" onclick="removeBgGroup(this, event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -5435,6 +5519,11 @@
if ((e.key === 'p' || e.key === 'P') && tag !== 'INPUT' && tag !== 'TEXTAREA') { if ((e.key === 'p' || e.key === 'P') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
togglePrivacyMode(); return; togglePrivacyMode(); return;
} }
if ((e.key === 'a' || e.key === 'A') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
startManualPad(); return;
}
}
if ((e.key === 'g' || e.key === 'G') && tag !== 'INPUT' && tag !== 'TEXTAREA') { if ((e.key === 'g' || e.key === 'G') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (_activeSidebarTab === 'generate') { if (_activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn'); const btn = document.getElementById('sbGenBtn');
@@ -5831,10 +5920,12 @@
<span style="font-size:11px;color:#666" title="Expand canvas before generation — gives the model space to outpaint">Pad:</span> <span style="font-size:11px;color:#666" title="Expand canvas before generation — gives the model space to outpaint">Pad:</span>
${['Top','Right','Bottom','Left'].map(side => { ${['Top','Right','Bottom','Left'].map(side => {
const val = side==='Top'?_sbPadTop:side==='Right'?_sbPadRight:side==='Bottom'?_sbPadBottom:_sbPadLeft; const val = side==='Top'?_sbPadTop:side==='Right'?_sbPadRight:side==='Bottom'?_sbPadBottom:_sbPadLeft;
const onInput = `updateSbGenBtn(); updatePadPreview()`;
const onBlur = `this.value = formatPadValue(this.value); ${onInput}`;
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]} return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]}
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %" <input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
oninput="updateSbGenBtn()"></label>`; oninput="${onInput}" onblur="${onBlur}"></label>`;
}).join('')} }).join('')}
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area"> <label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label> <input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
@@ -6087,7 +6178,13 @@
bar.innerHTML = overlay.map(a => { bar.innerHTML = overlay.map(a => {
const pSafe = a.prompt.replace(/'/g, "\\'"); const pSafe = a.prompt.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`; return `<button onclick="submitSingleAngle('${pSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
}).join(''); }).join('') + `
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>
<path d="M12 8v8m-4-4h8"/>
</svg>
</button>`;
} }
function updateSbGenBtn() { function updateSbGenBtn() {