outpaint orbit
This commit is contained in:
@@ -88,5 +88,6 @@ in car.html - studio view we have a action to pad the image we also have this fe
|
|||||||
2) 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.
|
2) 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.
|
||||||
3) 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.
|
3) 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.
|
||||||
4) 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.
|
4) 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.
|
||||||
|
5) remove the "set prompt"
|
||||||
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.
|
||||||
Use own insight for further refinement of these improvements.
|
Use own insight for further refinement of these improvements.
|
||||||
|
|||||||
@@ -3413,7 +3413,7 @@
|
|||||||
if (job.status === 'done') {
|
if (job.status === 'done') {
|
||||||
showToast('Pose generation done!', 'success');
|
showToast('Pose generation done!', 'success');
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast('Pose generation failed: ' + (job.error || 'unknown'), 'error');
|
showToast('Pose generation failed: ' + (job.error || 'unknown'), 'error');
|
||||||
}
|
}
|
||||||
@@ -3885,7 +3885,7 @@
|
|||||||
|
|
||||||
updateStudio();
|
updateStudio();
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
setTimeout(refreshNow, 500); // give server time to write static JSON
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast('Padded', 'success');
|
showToast('Padded', 'success');
|
||||||
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
||||||
@@ -3936,7 +3936,7 @@
|
|||||||
|
|
||||||
updateStudio();
|
updateStudio();
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
setTimeout(refreshNow, 500);
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast('Duplicate failed', 'error');
|
showToast('Duplicate failed', 'error');
|
||||||
}
|
}
|
||||||
@@ -3955,7 +3955,7 @@
|
|||||||
showToast('Background removed', 'success');
|
showToast('Background removed', 'success');
|
||||||
fileHasBg[fname] = false;
|
fileHasBg[fname] = false;
|
||||||
_bustStudioImage(fname); // cache-busts viewer + calls updateStudio()
|
_bustStudioImage(fname); // cache-busts viewer + calls updateStudio()
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast(`Failed to remove background for ${fname}`, 'error');
|
showToast(`Failed to remove background for ${fname}`, 'error');
|
||||||
}
|
}
|
||||||
@@ -3977,7 +3977,7 @@
|
|||||||
showToast('Transparency inverted', 'success');
|
showToast('Transparency inverted', 'success');
|
||||||
fileHasBg[fname] = false;
|
fileHasBg[fname] = false;
|
||||||
_bustStudioImage(fname);
|
_bustStudioImage(fname);
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast('Invert failed: ' + await r.text(), 'error');
|
showToast('Invert failed: ' + await r.text(), 'error');
|
||||||
}
|
}
|
||||||
@@ -4006,7 +4006,7 @@
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
showToast(`Video created: ${d.output}`, 'success');
|
showToast(`Video created: ${d.output}`, 'success');
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
} catch (e) { showToast('Video error: ' + e, 'error'); }
|
} catch (e) { showToast('Video error: ' + e, 'error'); }
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Make video'; }
|
if (btn) { btn.disabled = false; btn.textContent = 'Make video'; }
|
||||||
}
|
}
|
||||||
@@ -4659,7 +4659,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadImages() {
|
async function loadImages(bypassStatic = false) {
|
||||||
if (loadImages._inFlight) return;
|
if (loadImages._inFlight) return;
|
||||||
loadImages._inFlight = true;
|
loadImages._inFlight = true;
|
||||||
const gallery = document.getElementById('gallery');
|
const gallery = document.getElementById('gallery');
|
||||||
@@ -4671,6 +4671,7 @@
|
|||||||
|
|
||||||
// Primary: pre-generated static JSON (fast, no DB round-trip per request)
|
// Primary: pre-generated static JSON (fast, no DB round-trip per request)
|
||||||
try {
|
try {
|
||||||
|
if (bypassStatic) throw new Error("Bypass static requested");
|
||||||
const r = await fetch(`${API}/output/_data/images.json?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) });
|
const r = await fetch(`${API}/output/_data/images.json?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) });
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
@@ -4914,9 +4915,14 @@
|
|||||||
// Direct callers that need a response (pollJob completion, etc.) still use
|
// Direct callers that need a response (pollJob completion, etc.) still use
|
||||||
// loadImages() directly; everything else should go through refreshNow().
|
// loadImages() directly; everything else should go through refreshNow().
|
||||||
let _refreshTimer = null;
|
let _refreshTimer = null;
|
||||||
function refreshNow() {
|
let _refreshBypassStatic = false;
|
||||||
|
function refreshNow(bypass = false) {
|
||||||
|
if (bypass) _refreshBypassStatic = true;
|
||||||
clearTimeout(_refreshTimer);
|
clearTimeout(_refreshTimer);
|
||||||
_refreshTimer = setTimeout(loadImages, 80);
|
_refreshTimer = setTimeout(() => {
|
||||||
|
loadImages(_refreshBypassStatic || !!_followLatestGid);
|
||||||
|
_refreshBypassStatic = false;
|
||||||
|
}, 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
const API = 'http://127.0.0.1:8500';
|
const API = 'http://127.0.0.1:8500';
|
||||||
@@ -6010,7 +6016,7 @@
|
|||||||
const poll = () => setTimeout(async () => {
|
const poll = () => setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const s = await fetch(`${API}/batch/${job_id}`).then(r => r.json());
|
const s = await fetch(`${API}/batch/${job_id}`).then(r => r.json());
|
||||||
if (s.status === 'done') { refreshNow(); }
|
if (s.status === 'done') { refreshNow(true); }
|
||||||
else if (s.status !== 'error' && s.status !== 'cancelled') poll();
|
else if (s.status !== 'error' && s.status !== 'cancelled') poll();
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -6168,7 +6174,7 @@
|
|||||||
if (job.done > prevDone) {
|
if (job.done > prevDone) {
|
||||||
prevDone = job.done;
|
prevDone = job.done;
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
}
|
}
|
||||||
if (job.status === 'running') { pollGen(); return; }
|
if (job.status === 'running') { pollGen(); return; }
|
||||||
if (job.status === 'cancelled') {
|
if (job.status === 'cancelled') {
|
||||||
@@ -6188,7 +6194,7 @@
|
|||||||
_sbPoseEdits = {};
|
_sbPoseEdits = {};
|
||||||
_sbGenJobId = null;
|
_sbGenJobId = null;
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow();
|
refreshNow(true);
|
||||||
renderSidebarGenerate();
|
renderSidebarGenerate();
|
||||||
} else {
|
} else {
|
||||||
textEl.textContent = `Error: ${job.error || 'unknown'}`;
|
textEl.textContent = `Error: ${job.error || 'unknown'}`;
|
||||||
@@ -6445,7 +6451,7 @@
|
|||||||
if (btn) btn.textContent = 'Done ✓';
|
if (btn) btn.textContent = 'Done ✓';
|
||||||
showToast(`Faceswap complete: ${job.output || ''}`, 'success');
|
showToast(`Faceswap complete: ${job.output || ''}`, 'success');
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
setTimeout(() => { refreshNow(); }, 1800);
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
if (textEl) textEl.textContent = `Error: ${job.error || 'unknown'}`;
|
if (textEl) textEl.textContent = `Error: ${job.error || 'unknown'}`;
|
||||||
if (btn) { btn.disabled = false; btn.textContent = 'Retry'; }
|
if (btn) { btn.disabled = false; btn.textContent = 'Retry'; }
|
||||||
@@ -7068,6 +7074,7 @@
|
|||||||
const label = d.used_sam2 ? 'Done (SAM2)' : 'Done (rembg)';
|
const label = d.used_sam2 ? 'Done (SAM2)' : 'Done (rembg)';
|
||||||
if (statusEl) statusEl.textContent = label;
|
if (statusEl) statusEl.textContent = label;
|
||||||
showToast('Background removed', 'success');
|
showToast('Background removed', 'success');
|
||||||
|
refreshNow(true);
|
||||||
// Build URL same way as all gallery images: IMAGE_FOLDER + filename
|
// Build URL same way as all gallery images: IMAGE_FOLDER + filename
|
||||||
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
||||||
_nobgSidecars.set(fname, nobgUrl);
|
_nobgSidecars.set(fname, nobgUrl);
|
||||||
@@ -7116,6 +7123,7 @@
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
||||||
showToast('Background removed (rembg)', 'success');
|
showToast('Background removed (rembg)', 'success');
|
||||||
|
refreshNow(true);
|
||||||
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
||||||
_nobgSidecars.set(fname, nobgUrl);
|
_nobgSidecars.set(fname, nobgUrl);
|
||||||
const preload = new Image();
|
const preload = new Image();
|
||||||
@@ -7152,6 +7160,7 @@
|
|||||||
lbNames = [fname, ...otherNames];
|
lbNames = [fname, ...otherNames];
|
||||||
lbIdx = 0;
|
lbIdx = 0;
|
||||||
updateStudio();
|
updateStudio();
|
||||||
|
refreshNow(true);
|
||||||
// Fire-and-forget face extraction for face-book
|
// Fire-and-forget face extraction for face-book
|
||||||
fetch(`${API}/images/${encodeURIComponent(fname)}/extract-face`, { method: 'POST' })
|
fetch(`${API}/images/${encodeURIComponent(fname)}/extract-face`, { method: 'POST' })
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
|||||||
@@ -335,36 +335,14 @@ def _target_size(w: int, h: int, max_area: int) -> tuple[int, int]:
|
|||||||
def _prep_image(pil: Image.Image, max_area: int) -> tuple[Image.Image, int, int]:
|
def _prep_image(pil: Image.Image, max_area: int) -> tuple[Image.Image, int, int]:
|
||||||
"""
|
"""
|
||||||
Prepare image for ComfyUI:
|
Prepare image for ComfyUI:
|
||||||
1. If area > max_area, crop from bottom if height remains >= 256.
|
1. Scale (up or down) to fit area while preserving aspect.
|
||||||
2. Otherwise scale (up or down) to fit area while preserving aspect.
|
2. Ensure dimensions are rounded to 16.
|
||||||
3. Ensure dimensions are rounded to 16.
|
|
||||||
"""
|
"""
|
||||||
w, h = pil.width, pil.height
|
w, h = pil.width, pil.height
|
||||||
if w * h > max_area:
|
rw, rh = _target_size(w, h, max_area)
|
||||||
# Try to keep width and crop height from bottom
|
if rw != w or rh != h:
|
||||||
rw = _round16(w)
|
pil = pil.resize((rw, rh), resample=Image.LANCZOS)
|
||||||
th = max_area // rw
|
return pil, rw, rh
|
||||||
if th >= 256:
|
|
||||||
rh = (th // 16) * 16
|
|
||||||
if rh < 16: rh = 16
|
|
||||||
|
|
||||||
# To avoid black bars from .crop((0,0,rw,rh)) when rw > w,
|
|
||||||
# we crop to original w first, then resize to rw.
|
|
||||||
pil = pil.crop((0, 0, w, min(h, (rh * w) // rw)))
|
|
||||||
pil = pil.resize((rw, rh), resample=Image.LANCZOS)
|
|
||||||
return pil, rw, rh
|
|
||||||
else:
|
|
||||||
# Too wide to keep width and have decent height, scale both down
|
|
||||||
rw, rh = _target_size(w, h, max_area)
|
|
||||||
pil = pil.resize((rw, rh), resample=Image.LANCZOS)
|
|
||||||
return pil, rw, rh
|
|
||||||
else:
|
|
||||||
# Fits or is too small: scale UP to match the max_area budget
|
|
||||||
# (Legacy behavior that gives better model performance)
|
|
||||||
rw, rh = _target_size(w, h, max_area)
|
|
||||||
if rw != w or rh != h:
|
|
||||||
pil = pil.resize((rw, rh), resample=Image.LANCZOS)
|
|
||||||
return pil, rw, rh
|
|
||||||
|
|
||||||
|
|
||||||
def _comfy_upload(img_bytes: bytes, filename: str) -> str:
|
def _comfy_upload(img_bytes: bytes, filename: str) -> str:
|
||||||
@@ -1342,8 +1320,8 @@ def _invalidate_static() -> None:
|
|||||||
def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
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: int = 0, pad_right: int = 0,
|
pad_top: Any = 0, pad_right: Any = 0,
|
||||||
pad_bottom: int = 0, pad_left: int = 0, pad_fill: str = "black",
|
pad_bottom: Any = 0, pad_left: Any = 0, pad_fill: str = "black",
|
||||||
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:
|
||||||
@@ -1384,6 +1362,8 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
|||||||
cap.release()
|
cap.release()
|
||||||
if ret:
|
if ret:
|
||||||
pose_guide_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
pose_guide_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
||||||
|
if any([pad_top, pad_right, pad_bottom, pad_left]):
|
||||||
|
pose_guide_pil = _apply_manual_pad(pose_guide_pil, pad_top, pad_right, pad_bottom, pad_left, pad_fill)
|
||||||
print(f"[batch] using wireframe {wireframe_ref} frame {target_frame}/{total_frames}")
|
print(f"[batch] using wireframe {wireframe_ref} frame {target_frame}/{total_frames}")
|
||||||
except Exception as wf_err:
|
except Exception as wf_err:
|
||||||
print(f"[batch] wireframe extract error: {wf_err}")
|
print(f"[batch] wireframe extract error: {wf_err}")
|
||||||
@@ -1451,8 +1431,8 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
|||||||
|
|
||||||
def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], poses: list,
|
def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], poses: list,
|
||||||
seed: int, max_area: int,
|
seed: int, max_area: int,
|
||||||
pad_top: int = 0, pad_right: int = 0,
|
pad_top: Any = 0, pad_right: Any = 0,
|
||||||
pad_bottom: int = 0, pad_left: int = 0,
|
pad_bottom: Any = 0, pad_left: Any = 0,
|
||||||
pad_fill: str = "black", pad_outpaint: bool = False):
|
pad_fill: str = "black", pad_outpaint: bool = False):
|
||||||
"""Generate one output image per prompt using filenames[0] as primary and the rest as extra refs."""
|
"""Generate one output image per prompt using filenames[0] as primary and the rest as extra refs."""
|
||||||
output_dir = _load_output_dir()
|
output_dir = _load_output_dir()
|
||||||
@@ -1467,9 +1447,9 @@ def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], pos
|
|||||||
jobs[job_id]["status"] = "done"
|
jobs[job_id]["status"] = "done"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Apply padding to the primary image if requested
|
# Apply padding to images if requested
|
||||||
if any([pad_top, pad_right, pad_bottom, pad_left]):
|
if any([pad_top, pad_right, pad_bottom, pad_left]):
|
||||||
pils[0] = (pils[0][0], _apply_manual_pad(pils[0][1], pad_top, pad_right, pad_bottom, pad_left, pad_fill))
|
pils = [(name, _apply_manual_pad(pil, pad_top, pad_right, pad_bottom, pad_left, pad_fill)) for name, pil in pils]
|
||||||
|
|
||||||
# Output group: reuse shared group if all sources belong to the same one, else new group
|
# Output group: reuse shared group if all sources belong to the same one, else new group
|
||||||
source_groups = set()
|
source_groups = set()
|
||||||
|
|||||||
@@ -4479,3 +4479,4 @@ Anatomically precise, hyperrealistic, high detail, keep the characteristics of t
|
|||||||
# Upper Body Restraining
|
# Upper Body Restraining
|
||||||
|
|
||||||
Restrain more, push person into position on her toes, and chest forward out without restraining the chest, but via the arms and elbows. Use smart steel and restraining connections but enough to immobilize for maximum exposure.
|
Restrain more, push person into position on her toes, and chest forward out without restraining the chest, but via the arms and elbows. Use smart steel and restraining connections but enough to immobilize for maximum exposure.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user