This commit is contained in:
mike
2026-06-28 00:51:07 +02:00
parent 19e0656ccb
commit 3f91694491
5 changed files with 341 additions and 96 deletions

View File

@@ -2227,6 +2227,26 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"_turntable/up_6ed4293c/views/view_019_285deg.png",
"_turntable/up_6ed4293c/views/view_018_270deg.png",
"_turntable/up_6ed4293c/views/view_017_255deg.png",
"_turntable/up_6ed4293c/views/view_016_240deg.png",
"_turntable/up_6ed4293c/views/view_015_225deg.png",
"_turntable/up_6ed4293c/views/view_014_210deg.png",
"_turntable/up_6ed4293c/views/view_013_195deg.png",
"_turntable/up_6ed4293c/views/view_012_180deg.png",
"_turntable/up_6ed4293c/views/view_011_165deg.png",
"_turntable/up_6ed4293c/views/view_010_150deg.png",
"_turntable/up_6ed4293c/views/view_009_135deg.png",
"_turntable/up_6ed4293c/views/view_008_120deg.png",
"_turntable/up_6ed4293c/views/view_007_105deg.png",
"_turntable/up_6ed4293c/views/view_006_090deg.png",
"_turntable/up_6ed4293c/views/view_005_075deg.png",
"_turntable/up_6ed4293c/views/view_004_060deg.png",
"_turntable/up_6ed4293c/views/view_003_045deg.png",
"_turntable/up_6ed4293c/views/view_002_030deg.png",
"_turntable/up_6ed4293c/views/view_001_015deg.png",
"_turntable/up_6ed4293c/views/view_000_000deg.png",
"20260628_001216_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232502_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232843_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
@@ -2294,7 +2314,6 @@
"20260627_213202_9_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"20260627_213149_8_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png",
"_turntable/cg_74544975/views/view_004_060deg.png",
"_turntable/up_6ed4293c/views/view_019_285deg.png",
"_turntable/cg_74544975/views/view_001_015deg.png",
"_turntable/cg_74544975/views/view_000_000deg.png",
"20260627_213102_12_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
@@ -2326,25 +2345,6 @@
"20260627_212723_1_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212710_0_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
"_turntable/up_6ed4293c/views/view_018_270deg.png",
"_turntable/up_6ed4293c/views/view_017_255deg.png",
"_turntable/up_6ed4293c/views/view_016_240deg.png",
"_turntable/up_6ed4293c/views/view_015_225deg.png",
"_turntable/up_6ed4293c/views/view_014_210deg.png",
"_turntable/up_6ed4293c/views/view_013_195deg.png",
"_turntable/up_6ed4293c/views/view_012_180deg.png",
"_turntable/up_6ed4293c/views/view_011_165deg.png",
"_turntable/up_6ed4293c/views/view_010_150deg.png",
"_turntable/up_6ed4293c/views/view_009_135deg.png",
"_turntable/up_6ed4293c/views/view_008_120deg.png",
"_turntable/up_6ed4293c/views/view_007_105deg.png",
"_turntable/up_6ed4293c/views/view_006_090deg.png",
"_turntable/up_6ed4293c/views/view_005_075deg.png",
"_turntable/up_6ed4293c/views/view_004_060deg.png",
"_turntable/up_6ed4293c/views/view_003_045deg.png",
"_turntable/up_6ed4293c/views/view_002_030deg.png",
"_turntable/up_6ed4293c/views/view_001_015deg.png",
"_turntable/up_6ed4293c/views/view_000_000deg.png",
"20260627_204815_image.png",
"20260627_204804_image.png",
"20260627_204756_image.png",
@@ -6732,6 +6732,7 @@
let _sbPadBottom = 0;
let _sbPadLeft = 0;
let _sbPadOutpaint = false;
let _sbPadUniform = false;
const CAMERA_ANGLES = [
// Absolute camera positions
@@ -6890,33 +6891,126 @@
}
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);
if (v === undefined || v === null || v === '') return '0.00%';
const s = v.toString().trim().toLowerCase();
let val = parseFloat(s);
if (isNaN(val)) return '0.00%';
// If the user explicitly typed px
if (s.endsWith('px')) {
return val.toString(); // integer pixels
}
// If the user explicitly typed %
if (s.endsWith('%')) {
return val.toFixed(2) + '%';
}
// If contains a decimal, or if it's <= 10 (or < 1.0), it's percentage
if (s.includes('.') || val <= 10) {
// If it's a fraction like 0.1, convert to 10%
if (val > 0 && val < 1.0) {
return (val * 100).toFixed(2) + '%';
}
return val.toFixed(2) + '%';
}
// Otherwise, it's integer > 10, treat as pixels
return val.toString();
}
function syncPadFields(el) {
const id = el.id; // padLeft or sbPadLeft
let pairId = null;
if (id.endsWith('Left')) pairId = id.replace('Left', 'Right');
else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left');
else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom');
else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top');
const id = el.id; // padLeft, padTop, etc. or sbPadLeft, sbPadTop
const val = el.value;
const prefix = id.startsWith('sbPad') ? 'sbPad' : 'pad';
const isSb = id.startsWith('sbPad');
if (pairId) {
const pairEl = document.getElementById(pairId);
// only sync if the other side is 'empty' (0)
if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '')) {
pairEl.value = el.value;
// Check if Uniform mode is active for this context
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
const isUniform = uniformEl ? uniformEl.checked : false;
if (isUniform) {
// Set all four sides to the same value
['Top', 'Right', 'Bottom', 'Left'].forEach(side => {
const targetId = prefix + side;
const targetEl = document.getElementById(targetId);
if (targetEl && targetEl !== el) {
targetEl.value = val;
}
});
} else {
// Standard opposite-side sync if other side is 0
let pairId = null;
if (id.endsWith('Left')) pairId = id.replace('Left', 'Right');
else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left');
else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom');
else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top');
if (pairId) {
const pairEl = document.getElementById(pairId);
if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '0.00%' || pairEl.value === '')) {
pairEl.value = val;
}
}
}
if (id.startsWith('sbPad')) {
if (isSb) {
updateSbGenBtn();
}
updatePadPreview();
}
function togglePadUniform(cb) {
const isSb = cb.id.startsWith('sbPad');
const prefix = isSb ? 'sbPad' : 'pad';
if (isSb) _sbPadUniform = cb.checked;
if (cb.checked) {
// Find a non-zero value, or default to Top, and propagate
const sides = ['Top', 'Right', 'Bottom', 'Left'];
let val = '0.00%';
for (let side of sides) {
const el = document.getElementById(prefix + side);
if (el && el.value && el.value !== '0' && el.value !== '0.00' && el.value !== '0.00%') {
val = el.value;
break;
}
}
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = val;
});
updatePadPreview();
}
}
function applyPadPreset(presetVal, isSb) {
const prefix = isSb ? 'sbPad' : 'pad';
const sides = ['Top', 'Right', 'Bottom', 'Left'];
if (presetVal === '0') {
// Reset all
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = '0.00%';
});
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
if (uniformEl) uniformEl.checked = false;
if (isSb) _sbPadUniform = false;
} else {
// If it ends with %
let formatted = presetVal;
if (presetVal.endsWith('%')) {
formatted = parseFloat(presetVal).toFixed(2) + '%';
} else if (presetVal.endsWith('px')) {
formatted = parseFloat(presetVal).toString();
}
sides.forEach(side => {
const el = document.getElementById(prefix + side);
if (el) el.value = formatted;
});
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
if (uniformEl) uniformEl.checked = true;
if (isSb) _sbPadUniform = true;
}
if (isSb) {
updateSbGenBtn();
}
updatePadPreview();
@@ -7000,12 +7094,13 @@
function updatePadPreview() {
const viewer = document.getElementById('studioViewer');
if (viewer) {
// Clean up existing preview overlays
viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
}
const img = document.getElementById('lbImg');
if (!viewer || !img || img.style.display === 'none') return;
// Clean up existing preview overlays
viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
const rect = getImageDisplayRect(img);
if (!rect) return;
@@ -7015,17 +7110,37 @@
return el ? el.value : '0';
};
const fill = document.getElementById('padFill')?.value || 'transparent';
const sides = ['Left', 'Right', 'Top', 'Bottom'];
sides.forEach(side => {
const vStr = getV(side);
if (!vStr || vStr === '0' || vStr === '0.00') return;
if (!vStr || vStr === '0' || vStr === '0.00' || vStr === '0.00%') return;
const val = parseFloat(vStr);
if (isNaN(val) || val <= 0) return;
const isPercent = vStr.includes('%') || (vStr.indexOf('.') !== -1 && !isNaN(vStr));
const isPercent = vStr.includes('%');
const overlay = document.createElement('div');
overlay.className = 'pad-preview-overlay';
// Style based on selected fill mode to make preview professional & intuitive
if (isManual) {
if (fill === 'black') {
overlay.style.background = 'rgba(0, 0, 0, 0.7)';
overlay.style.borderColor = '#444';
} else if (fill === 'white') {
overlay.style.background = 'rgba(255, 255, 255, 0.7)';
overlay.style.borderColor = '#ccc';
} else {
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
} else {
// Sidebar is always transparent background before outpaint
overlay.style.background = 'rgba(245, 158, 11, 0.4)';
overlay.style.borderColor = '#f59e0b';
}
if (side === 'Left' || side === 'Right') {
const w = isPercent ? (rect.width * val / 100) : val;
@@ -7173,7 +7288,12 @@
const dimEl = document.getElementById('lbImgDims');
if (dimEl) dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`;
// Overlay re-sync is now handled by ResizeObserver
// Force layout recalculation and overlay sync when a new image loads
requestAnimationFrame(() => {
updatePadPreview();
const cb = document.getElementById('sbCheckerboard');
if (cb && cb.checked) toggleCheckerboard(true);
});
};
_fsModelFilename = fname; // keep faceswap/scenery/segment in sync
@@ -7194,6 +7314,10 @@
lbVideoEl.style.display = '';
lbVideoEl.poster = posterFor(lbUrls[lbIdx]);
lbVideoEl.src = lbUrls[lbIdx];
// Explicitly clean up pad previews and hide checkerboard when navigating to a video
updatePadPreview();
toggleCheckerboard(false);
} else {
lbVideoEl.style.display = 'none';
lbVideoEl.src = '';
@@ -7981,7 +8105,7 @@
const timeLabel = document.getElementById('pvmTimeLabel');
function pvmSetVideo(filename) {
vidEl.src = `${API}/output/${encodeURIComponent(filename)}`;
vidEl.src = IMAGE_FOLDER + filename;
vidEl.load();
vidEl.addEventListener('loadedmetadata', () => {
const dur = vidEl.duration || 10;
@@ -8316,15 +8440,24 @@
const numInput = (id, label) => {
const onInput = `syncPadFields(this)`;
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>`;
const onKeyDown = `if(event.key === 'Enter') { confirmManualPad(); event.preventDefault(); } if(event.key === 'Escape') { cancelManualPad(); event.preventDefault(); }`;
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}" onkeydown="${onKeyDown}" 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 =
`<span style="flex:1;font-size:11px;color:#aaa">Expand canvas (px or %):</span>`
`<span style="font-size:11px;color:#aaa">Preset:</span>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('10%', false)">+10%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('20%', false)">+20%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('50%', false)">+50%</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:4px;" onclick="applyPadPreset('100px', false)">+100px</button>`
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:8px;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', false)">Reset</button>`
+ `<span style="font-size:11px;color:#aaa;margin-right:4px;">Custom:</span>`
+ numInput('padTop','↑')
+ numInput('padRight','→')
+ numInput('padBottom','↓')
+ numInput('padLeft','←')
+ `<select id="padFill" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px">
+ `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc;cursor:pointer" title="Keep all padding values equal">`
+ `<input type="checkbox" id="padUniform" style="cursor:pointer" onchange="togglePadUniform(this)">Uniform</label>`
+ `<select id="padFill" onchange="updatePadPreview()" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px">
<option value="transparent">Transparent</option>
<option value="black">Black</option>
<option value="white">White</option>
@@ -8717,7 +8850,7 @@
} else if (jd.video_filename) {
// Depth-card MP4 fallback
const mp4 = jd.video_filename;
const src = mp4.includes('/') ? `${API}/output/${mp4}` : IMAGE_FOLDER + mp4;
const src = IMAGE_FOLDER + mp4;
const vid = document.getElementById('orbitVideo');
vid.src = src + '?t=' + Date.now();
vid.style.display = '';
@@ -8735,7 +8868,7 @@
// ---- Frame flipper engine ----
function _orbitStartFlipper(frameRelPaths) {
_orbitFlipperStop();
_orbitFrames = frameRelPaths.map(p => `${API}/output/${p}`);
_orbitFrames = frameRelPaths.map(p => IMAGE_FOLDER + p);
_orbitFlipperIdx = 0;
_orbitFlipperRunning = true;
@@ -8765,7 +8898,7 @@
_orbitFlipperStop();
document.getElementById('orbitFlipperPlayBtn').textContent = '▶ Play';
} else {
_orbitStartFlipper(_orbitFrames.map(u => u.replace(`${API}/output/`, '')));
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
document.getElementById('orbitFlipperPlayBtn').textContent = '⏸ Pause';
}
}
@@ -8775,7 +8908,7 @@
if (_orbitFlipperRunning && _orbitFrames.length > 0) {
// Restart with new interval
_orbitFlipperStop();
_orbitStartFlipper(_orbitFrames.map(u => u.replace(`${API}/output/`, '')));
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
}
}
@@ -8783,9 +8916,7 @@
function orbitDownload() {
if (!_orbitCurrentMp4) return;
const a = document.createElement('a');
a.href = _orbitCurrentMp4.includes('/')
? `${API}/output/${_orbitCurrentMp4}`
: IMAGE_FOLDER + _orbitCurrentMp4;
a.href = IMAGE_FOLDER + _orbitCurrentMp4;
a.download = _orbitCurrentMp4.split('/').pop();
a.click();
}
@@ -8904,7 +9035,7 @@
for (const t of _turntablesData) {
const label = t.group_name || t.group_id.slice(0, 8);
const thumb = t.frames.length > 0 ? `${API}/output/${t.frames[0]}` : '';
const thumb = t.frames.length > 0 ? IMAGE_FOLDER + t.frames[0] : '';
const compClass = t.completed ? ' complete' : '';
html += `<div class="orbit-tab-card${compClass}" onclick="_orbitTabOpen('${t.group_id}')" id="otc_${t.group_id}">
<div class="otc-img-wrap"><img id="otcImg_${t.group_id}" src="${thumb}" alt="${label}"></div>
@@ -8926,7 +9057,7 @@
function _orbitStartTabFlipper(groupId, frameRels) {
const imgEl = document.getElementById(`otcImg_${groupId}`);
if (!imgEl) return;
const urls = frameRels.map(f => `${API}/output/${f}`);
const urls = frameRels.map(f => IMAGE_FOLDER + f);
let idx = 0;
if (_orbitTabFlippers[groupId]) clearInterval(_orbitTabFlippers[groupId].timer);
const timer = setInterval(() => {
@@ -8947,7 +9078,7 @@
// Stop any existing full-player
if (viewer._flipperTimer) { clearTimeout(viewer._flipperTimer); viewer._flipperTimer = null; }
const urls = t.frames.map(f => `${API}/output/${f}`);
const urls = t.frames.map(f => IMAGE_FOLDER + f);
let idx = 0;
const label = t.group_name || t.group_id.slice(0, 8);
@@ -10208,6 +10339,7 @@
_sbPadBottom = document.getElementById('sbPadBottom')?.value ?? _sbPadBottom;
_sbPadLeft = document.getElementById('sbPadLeft')?.value ?? _sbPadLeft;
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked ?? _sbPadOutpaint;
_sbPadUniform = document.getElementById('sbPadUniform')?.checked ?? _sbPadUniform;
_fsSelectedPoses.forEach(name => {
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
if (el) _sbPoseEdits[name] = el.value;
@@ -10377,8 +10509,15 @@
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 %"
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
oninput="${onInput}" onblur="${onBlur}"></label>`;
oninput="${onInput}" onblur="${onBlur}"
onkeydown="if(event.key === 'Enter') { submitSbGenerate(); event.preventDefault(); }"></label>`;
}).join('')}
<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888;cursor:pointer" title="Keep all padding values equal">
<input type="checkbox" id="sbPadUniform" ${_sbPadUniform?'checked':''} onchange="togglePadUniform(this)" style="cursor:pointer">Uniform</label>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('10%', true)">+10%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('20%', true)">+20%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('50%', true)">+50%</button>
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', true)">Reset</button>
<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>
</div>

View File

@@ -3496,6 +3496,7 @@ def autocrop_image(filename: str):
cmin, cmax = np.where(cols)[0][[0, -1]]
cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1))
cropped.save(path, format="PNG")
_invalidate_static()
return {"status": "success", "filename": filename, "box": [int(cmin), int(rmin), int(cmax+1), int(rmax+1)]}
@@ -3558,8 +3559,7 @@ def manual_crop_image(filename: str, req: CropRequest):
cropped = img.crop((x1, y1, x2, y2))
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
cropped.save(path, format=fmt)
if req.as_copy:
_invalidate_static()
_invalidate_static()
return {"status": "success", "filename": filename, "new_filename": new_filename,
"new_url": f"/output/{new_filename}", "as_copy": req.as_copy,
"box": [x1, y1, x2, y2]}
@@ -3679,8 +3679,7 @@ def pad_image(filename: str, req: PadRequest):
if req.fill == "transparent":
fmt = "PNG" # JPEG cannot store alpha
padded.save(path, format=fmt)
if req.as_copy:
_invalidate_static()
_invalidate_static()
return {
"status": "success", "filename": filename, "new_filename": new_filename,
"new_url": f"/output/{new_filename}", "as_copy": req.as_copy,
@@ -3712,6 +3711,7 @@ def rotate_image(filename: str, req: RotateRequest):
img = Image.open(path).transpose(cw_to_transpose[deg])
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
img.save(path, format=fmt)
_invalidate_static()
return {"status": "success", "filename": filename, "degrees": deg}

View File

@@ -101,7 +101,10 @@
<div id="toast"></div>
<script>
const API = window.location.origin;
let API = window.location.origin;
if (API === 'null' || API.startsWith('file://')) {
API = 'http://127.0.0.1:8500';
}
function showToast(msg) {
const t = document.getElementById('toast');