This commit is contained in:
mike
2026-06-27 23:36:41 +02:00
parent 2ada5fb559
commit bf3773bfd4
3 changed files with 74 additions and 20 deletions

View File

@@ -2004,9 +2004,9 @@
<div class="sidebar-tabs"> <div class="sidebar-tabs">
<button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')">Info</button> <button class="sb-tab active" data-tab="info" onclick="switchSidebarTab('info')">Info</button>
<button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Generate</button> <button class="sb-tab" data-tab="generate" onclick="switchSidebarTab('generate')">Generate</button>
<button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">Faceswap</button> <button class="sb-tab" data-tab="faceswap" onclick="switchSidebarTab('faceswap')">Face</button>
<button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">Scenery</button> <button class="sb-tab" data-tab="scenery" onclick="switchSidebarTab('scenery')">Scene</button>
<button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">Segment</button> <button class="sb-tab" data-tab="segment" onclick="switchSidebarTab('segment')">SAM</button>
<button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit">Orbit</button> <button class="sb-tab" data-tab="orbit" onclick="switchSidebarTab('orbit')" id="sbTabOrbit">Orbit</button>
</div> </div>
<div class="sb-panels"> <div class="sb-panels">
@@ -2220,6 +2220,18 @@
// --- HYDRATION_START --- // --- HYDRATION_START ---
const PRELOADED_IMAGES = [ const PRELOADED_IMAGES = [
"20260627_232843_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232502_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232526_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
"20260627_232302_pad_20260627_232133_image.png",
"20260627_232238_pad_20260627_232133_image.png",
"up_6ed4293c_face.png",
"20260627_232209_image.png",
"20260627_232151_image.png",
"20260627_232133_image.png",
"20260627_230405_mr_image.png",
"20260627_222806_mr_image.png",
"20260627_230301_7_20260627_201343_image.nobg.png",
"20260627_201629_7_20260627_201343_image.nobg.png", "20260627_201629_7_20260627_201343_image.nobg.png",
"20260627_201629_7_20260627_201343_image.png", "20260627_201629_7_20260627_201343_image.png",
"20260627_225051_mr_image.png", "20260627_225051_mr_image.png",
@@ -2234,7 +2246,6 @@
"20260627_222926_image.png", "20260627_222926_image.png",
"20260627_222915_image.png", "20260627_222915_image.png",
"20260627_222904_image.png", "20260627_222904_image.png",
"20260627_222806_mr_image.png",
"20260627_222321_image.png", "20260627_222321_image.png",
"20260627_222310_image.png", "20260627_222310_image.png",
"20260627_222259_image.png", "20260627_222259_image.png",
@@ -6733,6 +6744,7 @@
let _sbGenJobId = null; let _sbGenJobId = null;
let _sbGenJobPollTimer= null; let _sbGenJobPollTimer= null;
let _followLatestGid = null; // set to gid after generation to auto-jump to newest image let _followLatestGid = null; // set to gid after generation to auto-jump to newest image
let _followLatestFilename = null;
let _slideshowActive = false; let _slideshowActive = false;
let _slideshowTimer = null; let _slideshowTimer = null;
let _slideshowInterval= 4000; let _slideshowInterval= 4000;
@@ -6881,6 +6893,27 @@
return val.toFixed(2); return val.toFixed(2);
} }
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');
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;
}
}
if (id.startsWith('sbPad')) {
updateSbGenBtn();
}
updatePadPreview();
}
function updatePadPreview() { function updatePadPreview() {
const viewer = document.getElementById('studioViewer'); const viewer = document.getElementById('studioViewer');
if (!viewer) return; if (!viewer) return;
@@ -8163,7 +8196,7 @@
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) => {
const onInput = `updatePadPreview()`; const onInput = `syncPadFields(this)`;
const onBlur = `this.value = formatPadValue(this.value); 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>`; 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>`;
}; };
@@ -8174,9 +8207,9 @@
+ numInput('padBottom','↓') + numInput('padBottom','↓')
+ numInput('padLeft','←') + numInput('padLeft','←')
+ `<select id="padFill" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px"> + `<select id="padFill" 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="black">Black</option>
<option value="white">White</option> <option value="white">White</option>
<option value="transparent">Transparent</option>
</select>` </select>`
+ `<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#ccc;cursor:pointer" title="Keep original and pad a copy">` + `<label style="display:flex;align-items:center;gap:4px;font-size:11px;color:#ccc;cursor:pointer" title="Keep original and pad a copy">`
+ `<input type="checkbox" id="padAsCopy" checked style="cursor:pointer">Copy</label>` + `<input type="checkbox" id="padAsCopy" checked style="cursor:pointer">Copy</label>`
@@ -8204,7 +8237,7 @@
const isZero = (v) => !v || v === '0' || v === '0%' || v === '0px'; const isZero = (v) => !v || v === '0' || v === '0%' || v === '0px';
if (isZero(top) && isZero(right) && isZero(bottom) && isZero(left)) { showToast('Enter at least one padding value', 'info'); return; } if (isZero(top) && isZero(right) && isZero(bottom) && isZero(left)) { showToast('Enter at least one padding value', 'info'); return; }
const fill = document.getElementById('padFill')?.value || 'black'; const fill = document.getElementById('padFill')?.value || 'transparent';
const asCopy = document.getElementById('padAsCopy')?.checked ?? true; const asCopy = document.getElementById('padAsCopy')?.checked ?? true;
const outpaint = document.getElementById('padOutpaint')?.checked ?? false; const outpaint = document.getElementById('padOutpaint')?.checked ?? false;
const btn = document.getElementById('padConfirmBtn'); const btn = document.getElementById('padConfirmBtn');
@@ -8241,6 +8274,7 @@
} }
updateStudio(); updateStudio();
_followLatestFilename = newFn;
_followLatestGid = lbCurrentGid; _followLatestGid = lbCurrentGid;
refreshNow(true); refreshNow(true);
} else { } else {
@@ -9184,8 +9218,15 @@
const visibleIdx = names.map((n, i) => i).filter(i => !fileHidden[names[i]]); const visibleIdx = names.map((n, i) => i).filter(i => !fileHidden[names[i]]);
groupData.set(gid, { urls, names, visibleIdx }); groupData.set(gid, { urls, names, visibleIdx });
// Auto-follow: if a generation just finished for this group, jump to the last (newest) image // Auto-follow: if a generation just finished for this group, jump to the image
if (_followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) { if (_followLatestFilename && names.includes(_followLatestFilename) && _isStudioOpen() && lbCurrentGid === gid) {
lbIdx = names.indexOf(_followLatestFilename);
_followLatestFilename = null;
_followLatestGid = null;
lbUrls = urls;
lbNames = names;
updateStudio();
} else if (_followLatestGid === gid && _isStudioOpen() && lbCurrentGid === gid && names.length > 0) {
_followLatestGid = null; _followLatestGid = null;
lbIdx = names.length - 1; lbIdx = names.length - 1;
lbUrls = urls; lbUrls = urls;
@@ -9777,6 +9818,15 @@
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 === 'c' || e.key === 'C') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
const cb = document.getElementById('sbCheckerboard');
const viewer = document.getElementById('studioViewer');
const cur = viewer ? viewer.classList.contains('show-checker') : (cb ? cb.checked : false);
toggleCheckerboard(!cur);
return;
}
}
if ((e.key === 'a' || e.key === 'A') && tag !== 'INPUT' && tag !== 'TEXTAREA') { if ((e.key === 'a' || e.key === 'A') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) { if (document.getElementById('studio').classList.contains('open')) {
startManualPad(); return; startManualPad(); return;
@@ -10178,7 +10228,7 @@
<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 onInput = `syncPadFields(this)`;
const onBlur = `this.value = formatPadValue(this.value); ${onInput}`; 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 %"
@@ -11485,6 +11535,7 @@
function renderSidebarSegment() { function renderSidebarSegment() {
const panel = document.getElementById('sbPanelSegment'); const panel = document.getElementById('sbPanelSegment');
if (!panel) return; if (!panel) return;
const isChecked = document.getElementById('studioViewer')?.classList.contains('show-checker');
panel.innerHTML = ` panel.innerHTML = `
<div style="font-size:11px;color:#666;margin-bottom:12px;line-height:1.6"> <div style="font-size:11px;color:#666;margin-bottom:12px;line-height:1.6">
Remove background using SAM2 or rembg fallback. The original file is Remove background using SAM2 or rembg fallback. The original file is
@@ -11499,7 +11550,7 @@
<div id="sbSegStatus" style="font-size:11px;color:#aaa;min-height:16px;margin-bottom:10px"></div> <div id="sbSegStatus" style="font-size:11px;color:#aaa;min-height:16px;margin-bottom:10px"></div>
<div class="sb-sep"></div> <div class="sb-sep"></div>
<label style="display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer;margin-bottom:6px"> <label style="display:flex;align-items:center;gap:8px;font-size:12px;cursor:pointer;margin-bottom:6px">
<input type="checkbox" id="sbCheckerboard" onchange="toggleCheckerboard(this.checked)"> <input type="checkbox" id="sbCheckerboard" onchange="toggleCheckerboard(this.checked)" ${isChecked ? 'checked' : ''}>
<span>Checkerboard (show transparency)</span> <span>Checkerboard (show transparency)</span>
</label> </label>
<div style="font-size:10px;color:#555;line-height:1.4"> <div style="font-size:10px;color:#555;line-height:1.4">
@@ -11598,6 +11649,8 @@
function toggleCheckerboard(on) { function toggleCheckerboard(on) {
const viewer = document.getElementById('studioViewer'); const viewer = document.getElementById('studioViewer');
if (viewer) viewer.classList.toggle('show-checker', on); if (viewer) viewer.classList.toggle('show-checker', on);
const cb = document.getElementById('sbCheckerboard');
if (cb) cb.checked = on;
} }
// ---- set preferred ---- // ---- set preferred ----

View File

@@ -1456,7 +1456,7 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
seed: int, max_area: int, group_id: str | None = None, seed: int, max_area: int, group_id: str | None = None,
wireframe_ref: str | None = None, wireframe_time: float = 0.5, wireframe_ref: str | None = None, wireframe_time: float = 0.5,
pad_top: Any = 0, pad_right: Any = 0, pad_top: Any = 0, pad_right: Any = 0,
pad_bottom: Any = 0, pad_left: Any = 0, pad_fill: str = "black", pad_bottom: Any = 0, pad_left: Any = 0, pad_fill: str = "transparent",
pad_outpaint: bool = False): pad_outpaint: bool = False):
output_dir = _load_output_dir() output_dir = _load_output_dir()
for fname in filenames: for fname in filenames:
@@ -3571,13 +3571,13 @@ class PadRequest(BaseModel):
bottom: int | float | str = 0 bottom: int | float | str = 0
left: int | float | str = 0 left: int | float | str = 0
as_copy: bool = True as_copy: bool = True
fill: str = "black" # "black", "white", "transparent" fill: str = "transparent" # "black", "white", "transparent"
outpaint: bool = False outpaint: bool = False
prompt: str | None = None prompt: str | None = None
def _apply_manual_pad(pil: Image.Image, top, right, bottom, left, def _apply_manual_pad(pil: Image.Image, top, right, bottom, left,
fill: str = "black") -> Image.Image: fill: str = "transparent") -> Image.Image:
"""Expand canvas by padding pixels on each side. Fill with black, white, or transparency. """Expand canvas by padding pixels on each side. Fill with black, white, or transparency.
Supports pixel values (int) or percentages (str like "10%" or float < 1.0). Supports pixel values (int) or percentages (str like "10%" or float < 1.0).
""" """

View File

@@ -55,12 +55,13 @@ Keep full characteristics of reference image.
# Three-quarter (18) # Three-quarter (18)
You are in a black empty void. You are in a black empty void, transparent background.
high quality, masterpiece, detailed, detailed skin. masterpiece, high quality, detailed, detailed skin.
Head-on a right-front detailed full-nude-body. Top-to-toe photo. Head-on a right-front.
female teenage, photorealistic, sharp focus, transparent background. Detailed full-nude-body.
Identical person from reference image. Top-to-toe photo.
Keep characteristics of reference image. female teenage, hyper realistic.
Anatomically precise. Keep characteristics of reference image.
# Three-quarter # Three-quarter