QIE-Bbox-Studio / app.py
prithivMLmods's picture
update app [cleaned] ✅
6108a5c verified
raw
history blame
81.5 kB
import gradio as gr
import numpy as np
import random
import torch
import spaces
import base64
import json
from io import BytesIO
from PIL import Image, ImageDraw
from diffusers import FlowMatchEulerDiscreteScheduler
from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
MAX_SEED = np.iinfo(np.int32).max
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
pipe = QwenImageEditPlusPipeline.from_pretrained(
"Qwen/Qwen-Image-Edit-2511",
transformer=QwenImageTransformer2DModel.from_pretrained(
"prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
torch_dtype=dtype,
device_map="cuda",
),
torch_dtype=dtype,
).to(device)
try:
pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
print("Flash Attention 3 Processor set successfully.")
except Exception as e:
print(f"Warning: Could not set FA3 processor: {e}")
ADAPTER_SPECS = {
"Object-Remover": {
"repo": "prithivMLmods/QIE-2511-Object-Remover-Bbox-v3",
"weights": "QIE-2511-Object-Remover-Bbox-v3-10000.safetensors",
"adapter_name": "object-remover",
},
"Design-Adder": {
"repo": "prithivMLmods/QIE-2511-Outfit-Design-Layout",
"weights": "QIE-2511-Outfit-Design-Layout-3000.safetensors",
"adapter_name": "design-adder",
},
"Object-Mover": {
"repo": "prithivMLmods/QIE-2511-Object-Mover-Bbox",
"weights": "QIE-2511-Object-Mover-Bbox-5000.safetensors",
"adapter_name": "object-mover",
},
}
loaded_adapters = set()
current_adapter = None
DEFAULT_PROMPTS = {
"Object-Remover": "Remove the red highlighted object from the scene",
"Design-Adder": "Add the design pattern inside the red highlighted bounding box area",
"Object-Mover": "Move the object highlighted in the red box to the location indicated by the other red box in the scene",
}
def b64_to_pil(b64_str):
if not b64_str or not b64_str.startswith("data:image"):
return None
try:
_, data = b64_str.split(',', 1)
image_data = base64.b64decode(data)
return Image.open(BytesIO(image_data)).convert("RGB")
except Exception as e:
print(f"Error decoding image: {e}")
return None
def burn_boxes_onto_image(pil_image, boxes_json_str):
if not pil_image:
return pil_image
try:
boxes = json.loads(boxes_json_str) if boxes_json_str and boxes_json_str.strip() else []
except Exception:
boxes = []
if not boxes:
return pil_image
img = pil_image.copy().convert("RGB")
w, h = img.size
draw = ImageDraw.Draw(img)
bw = max(3, w // 250)
for b in boxes:
x1 = int(b["x1"] * w)
y1 = int(b["y1"] * h)
x2 = int(b["x2"] * w)
y2 = int(b["y2"] * h)
lx, rx = min(x1, x2), max(x1, x2)
ty, by_ = min(y1, y2), max(y1, y2)
draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
return img
@spaces.GPU
def infer_bbox_task(
b64_str,
boxes_json,
prompt,
adapter_choice,
seed=0,
randomize_seed=True,
guidance_scale=1.0,
num_inference_steps=4,
height=1024,
width=1024,
):
global loaded_adapters, current_adapter
progress = gr.Progress(track_tqdm=True)
if not prompt or prompt.strip() == "":
raise gr.Error("⚠ Prompt cannot be empty. Please enter a prompt describing the edit you want.")
spec = ADAPTER_SPECS.get(adapter_choice)
if not spec:
raise gr.Error(f"Unknown adapter: {adapter_choice}")
adapter_name = spec["adapter_name"]
source_image = b64_to_pil(b64_str)
if source_image is None:
raise gr.Error("Please upload an image first using the canvas area.")
try:
boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else []
except Exception:
boxes = []
if not boxes:
raise gr.Error("Please draw at least one bounding box on the image.")
if adapter_choice == "Object-Mover":
if len(boxes) != 2:
raise gr.Error(
f"⚠ Object Mover requires exactly 2 bounding boxes (Source + Target). "
f"You have {len(boxes)} box{'es' if len(boxes) != 1 else ''}. "
f"Draw one box on the object to move, and another on the target location."
)
if adapter_name not in loaded_adapters:
progress(0.1, desc=f"Loading {adapter_choice} adapter...")
pipe.load_lora_weights(
spec["repo"],
weight_name=spec["weights"],
adapter_name=adapter_name,
)
loaded_adapters.add(adapter_name)
if current_adapter != adapter_name:
progress(0.2, desc=f"Switching to {adapter_choice}...")
pipe.set_adapters([adapter_name], adapter_weights=[1.0])
current_adapter = adapter_name
progress(0.3, desc="Burning red boxes onto image...")
marked = burn_boxes_onto_image(source_image, boxes_json)
progress(0.5, desc=f"Running {adapter_choice} inference...")
if randomize_seed:
seed = random.randint(0, MAX_SEED)
generator = torch.Generator(device=device).manual_seed(seed)
result = pipe(
image=[marked],
prompt=prompt,
height=height if height != 0 else None,
width=width if width != 0 else None,
num_inference_steps=num_inference_steps,
generator=generator,
guidance_scale=guidance_scale,
num_images_per_prompt=1,
).images[0]
return result, seed, marked
def update_dimensions_on_upload(b64_str):
image = b64_to_pil(b64_str)
if image is None:
return 1024, 1024
original_width, original_height = image.size
if original_width > original_height:
new_width = 1024
aspect_ratio = original_height / original_width
new_height = int(new_width * aspect_ratio)
else:
new_height = 1024
aspect_ratio = original_width / original_height
new_width = int(new_height * aspect_ratio)
new_width = (new_width // 8) * 8
new_height = (new_height // 8) * 8
return new_width, new_height
css = r"""
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
body,.gradio-container{
background:#0f0f13!important;
font-family:'Inter',system-ui,-apple-system,sans-serif!important;
font-size:14px!important;
color:#e4e4e7!important;
min-height:100vh;
}
.dark body,.dark .gradio-container{
background:#0f0f13!important;
color:#e4e4e7!important;
}
footer{display:none!important}
.hidden-input{
display:none!important;
height:0!important;
overflow:hidden!important;
margin:0!important;
padding:0!important;
}
/* ── Toast Notifications ── */
.toast-container{
position:fixed;
top:20px;
right:20px;
z-index:999999;
display:flex;
flex-direction:column;
gap:10px;
pointer-events:none;
max-width:420px;
}
.toast{
pointer-events:auto;
display:flex;
align-items:flex-start;
gap:10px;
padding:14px 18px;
border-radius:12px;
font-family:'Inter',system-ui,sans-serif;
font-size:13px;
line-height:1.5;
color:#e4e4e7;
background:rgba(30,18,28,.95);
border-left:4px solid #FF1493;
backdrop-filter:blur(16px);
box-shadow:0 8px 32px rgba(0,0,0,.5),
0 0 0 1px rgba(255,20,147,.1);
animation:toastSlideIn .35s cubic-bezier(.21,1.02,.73,1) forwards;
transform:translateX(120%);
opacity:0;
}
.toast.toast-exit{
animation:toastSlideOut .3s ease forwards;
}
@keyframes toastSlideIn{
from{transform:translateX(120%);opacity:0}
to{transform:translateX(0);opacity:1}
}
@keyframes toastSlideOut{
from{transform:translateX(0);opacity:1}
to{transform:translateX(120%);opacity:0}
}
.toast-warning{
background:rgba(30,18,28,.95);
border-left:4px solid #FF69B4;
}
.toast-error{
background:rgba(30,15,20,.95);
border-left:4px solid #FF1493;
}
.toast-success{
background:rgba(20,22,28,.95);
border-left:4px solid #FFB6C1;
}
.toast-info{
background:rgba(25,18,30,.95);
border-left:4px solid #FF69B4;
}
.toast-icon{
font-size:18px;
line-height:1;
flex-shrink:0;
margin-top:1px;
color:#FF69B4;
}
.toast-warning .toast-icon{color:#FF69B4}
.toast-error .toast-icon{color:#FF1493}
.toast-success .toast-icon{color:#FFB6C1}
.toast-info .toast-icon{color:#FF69B4}
.toast-body{
display:flex;
flex-direction:column;
gap:2px;
flex:1;
min-width:0;
}
.toast-title{
font-weight:700;
font-size:13px;
color:#FF69B4;
}
.toast-warning .toast-title{color:#FF69B4}
.toast-error .toast-title{color:#FF1493}
.toast-success .toast-title{color:#FFB6C1}
.toast-info .toast-title{color:#FF69B4}
.toast-msg{
font-size:12.5px;
color:#a1a1aa;
word-break:break-word;
}
.toast-close{
flex-shrink:0;
background:none;
border:none;
color:#71717a;
cursor:pointer;
font-size:16px;
line-height:1;
padding:0 0 0 4px;
transition:color .15s;
}
.toast-close:hover{color:#FF69B4}
/* ── Main Container ── */
.app-shell{
background:#18181b;
border:1px solid #27272a;
border-radius:16px;
margin:12px auto;
max-width:1400px;
overflow:hidden;
box-shadow:0 25px 50px -12px rgba(0,0,0,.6),
0 0 0 1px rgba(255,255,255,.03);
}
/* ── Header Bar ── */
.app-header{
background:linear-gradient(135deg,#18181b 0%,#1e1e24 100%);
border-bottom:1px solid #27272a;
padding:14px 24px;
display:flex;
align-items:center;
justify-content:space-between;
}
.app-header-left{
display:flex;
align-items:center;
gap:12px;
}
.app-logo{
width:36px;height:36px;
background:linear-gradient(135deg,#FF1493,#FF69B4,#FFB6C1);
border-radius:10px;
display:flex;align-items:center;justify-content:center;
font-size:18px;font-weight:800;color:#fff;
box-shadow:0 4px 12px rgba(255,20,147,.35);
}
.app-title{
font-size:18px;font-weight:700;
background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
-webkit-background-clip:text;
-webkit-text-fill-color:transparent;
letter-spacing:-.3px;
}
.app-badge{
font-size:11px;
font-weight:600;
padding:3px 10px;
border-radius:20px;
background:rgba(255,20,147,.15);
color:#FF69B4;
border:1px solid rgba(255,20,147,.25);
letter-spacing:.3px;
}
/* ── Mode Switcher ── */
.mode-switcher{
display:flex;
gap:4px;
background:#09090b;
border:1px solid #27272a;
border-radius:10px;
padding:3px;
}
.mode-btn{
display:inline-flex;align-items:center;justify-content:center;
gap:6px;
padding:6px 16px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:13px;
font-weight:600;
font-family:'Inter',sans-serif;
color:#71717a;
background:transparent;
transition:all .2s ease;
white-space:nowrap;
}
.mode-btn:hover{
color:#a1a1aa;
background:rgba(255,20,147,.08);
}
.mode-btn.active{
color:#fff!important;
-webkit-text-fill-color:#fff!important;
background:linear-gradient(135deg,#FF1493,#C71585)!important;
box-shadow:0 2px 8px rgba(255,20,147,.35);
}
.mode-btn.active .mode-icon{
color:#fff!important;
-webkit-text-fill-color:#fff!important;
}
.mode-btn .mode-icon{
font-size:15px;line-height:1;
}
/* ── Toolbar ── */
.app-toolbar{
background:#18181b;
border-bottom:1px solid #27272a;
padding:8px 16px;
display:flex;
gap:4px;
align-items:center;
flex-wrap:wrap;
}
.tb-sep{
width:1px;height:28px;
background:#27272a;
margin:0 8px;
}
.modern-tb-btn{
display:inline-flex;align-items:center;justify-content:center;
gap:6px;
min-width:32px;height:34px;
background:transparent;
border:1px solid transparent;
border-radius:8px;
cursor:pointer;
font-size:13px;
font-weight:600;
padding:0 12px;
font-family:'Inter',sans-serif;
color:#ffffff!important;
transition:all .15s ease;
}
.modern-tb-btn:hover{
background:rgba(255,20,147,.15);
color:#ffffff!important;
border-color:rgba(255,20,147,.3);
}
.modern-tb-btn:active,.modern-tb-btn.active{
background:rgba(255,20,147,.25);
color:#ffffff!important;
border-color:rgba(255,20,147,.45);
}
.modern-tb-btn .tb-icon{
font-size:15px;
line-height:1;
color:#ffffff!important;
}
.modern-tb-btn .tb-label{
font-size:13px;
color:#ffffff!important;
font-weight:600;
}
/* ── Main Layout ── */
.app-main-row{
display:flex;
gap:0;
flex:1;
overflow:hidden;
}
.app-main-left{
flex:1;
display:flex;
flex-direction:column;
min-width:0;
border-right:1px solid #27272a;
}
.app-main-right{
width:420px;
display:flex;
flex-direction:column;
flex-shrink:0;
background:#18181b;
}
/* ── Canvas Area ── */
#bbox-draw-wrap{
position:relative;
background:#09090b;
margin:0;
min-height:440px;
overflow:hidden;
cursor:crosshair;
}
#bbox-draw-canvas{display:block;margin:0 auto}
#bbox-status{
position:absolute;top:12px;left:12px;
background:rgba(255,0,0,.9);
color:#fff;
padding:4px 12px;
font-family:'JetBrains Mono',monospace;
font-size:12px;
font-weight:500;
border-radius:6px;
z-index:10;display:none;
pointer-events:none;
backdrop-filter:blur(8px);
}
#bbox-count{
position:absolute;top:12px;right:12px;
background:rgba(24,24,27,.9);
color:#FF1493;
padding:4px 12px;
font-family:'JetBrains Mono',monospace;
font-size:12px;
font-weight:600;
border-radius:6px;
border:1px solid rgba(255,20,147,.3);
z-index:10;display:none;
backdrop-filter:blur(8px);
}
/* ── Mover Mode Indicator ── */
#mover-box-hint{
position:absolute;bottom:12px;left:50%;
transform:translateX(-50%);
background:rgba(24,24,27,.92);
color:#FF69B4;
padding:6px 16px;
font-family:'JetBrains Mono',monospace;
font-size:12px;
font-weight:600;
border-radius:8px;
border:1px solid rgba(255,20,147,.3);
z-index:10;display:none;
pointer-events:none;
backdrop-filter:blur(8px);
white-space:nowrap;
}
/* ── Upload Prompt (icon only) ── */
.upload-prompt-modern{
position:absolute;
top:50%;left:50%;
transform:translate(-50%,-50%);
z-index:20;
}
.upload-click-area{
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
cursor:pointer;
padding:36px 44px;
border:2px dashed #3f3f46;
border-radius:16px;
background:rgba(255,20,147,.03);
transition:all .2s ease;
}
.upload-click-area:hover{
background:rgba(255,20,147,.08);
border-color:#FF1493;
transform:scale(1.03);
}
.upload-click-area:active{
background:rgba(255,20,147,.12);
transform:scale(.98);
}
.upload-click-area svg{
width:80px;height:80px;
}
/* ── Hint Bar ── */
.hint-bar{
background:rgba(255,20,147,.06);
border-top:1px solid #27272a;
border-bottom:1px solid #27272a;
padding:10px 20px;
font-size:13px;
color:#a1a1aa;
line-height:1.7;
}
.hint-bar b{color:#FFB6C1;font-weight:600}
.hint-bar kbd{
display:inline-block;
padding:1px 6px;
background:#27272a;
border:1px solid #3f3f46;
border-radius:4px;
font-family:'JetBrains Mono',monospace;
font-size:11px;
color:#a1a1aa;
}
.hint-bar .hint-mover-tag{
display:inline-block;
padding:1px 8px;
background:rgba(255,20,147,.15);
border:1px solid rgba(255,20,147,.3);
border-radius:4px;
font-size:11px;
font-weight:600;
color:#FF69B4;
}
/* ── JSON Panel ── */
.json-panel{
background:#18181b;
border-top:1px solid #27272a;
display:flex;
flex-direction:column;
height:160px;
max-height:160px;
min-height:160px;
}
.json-panel-title{
padding:8px 16px;
font-size:12px;
font-weight:600;
color:#71717a;
text-transform:uppercase;
letter-spacing:.8px;
border-bottom:1px solid #27272a;
display:flex;
align-items:center;
gap:8px;
flex-shrink:0;
}
.json-panel-title::before{
content:'{ }';
font-family:'JetBrains Mono',monospace;
font-size:11px;
color:#FF1493;
background:rgba(255,20,147,.12);
padding:2px 6px;
border-radius:4px;
}
.json-panel-content{
background:#09090b;
margin:0;
padding:12px 16px;
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:#a1a1aa;
flex:1;
overflow-y:auto;
overflow-x:hidden;
word-break:break-all;
white-space:pre-wrap;
line-height:1.6;
}
.json-panel-content::-webkit-scrollbar{width:8px}
.json-panel-content::-webkit-scrollbar-track{background:#09090b}
.json-panel-content::-webkit-scrollbar-thumb{
background:#27272a;
border-radius:4px;
}
.json-panel-content::-webkit-scrollbar-thumb:hover{background:#3f3f46}
/* ── Right Panel Cards ── */
.panel-card{
border-bottom:1px solid #27272a;
}
.panel-card-title{
padding:12px 20px;
font-size:12px;
font-weight:600;
color:#71717a;
text-transform:uppercase;
letter-spacing:.8px;
border-bottom:1px solid rgba(39,39,42,.6);
}
.panel-card-body{
padding:16px 20px;
display:flex;
flex-direction:column;
gap:8px;
}
.modern-label{
font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block;
}
.modern-textarea{
width:100%;
background:#09090b;
border:1px solid #27272a;
border-radius:8px;
padding:10px 14px;
font-family:'Inter',sans-serif;
font-size:14px;
color:#e4e4e7;
resize:vertical;
outline:none;
min-height:42px;
transition:border-color .2s;
}
.modern-textarea:focus{
border-color:#FF1493;
box-shadow:0 0 0 3px rgba(255,20,147,.15);
}
.modern-textarea::placeholder{color:#3f3f46}
.modern-textarea.prompt-error{
border-color:#FF1493!important;
box-shadow:0 0 0 3px rgba(255,20,147,.25)!important;
animation:promptShake .4s ease;
}
@keyframes promptShake{
0%,100%{transform:translateX(0)}
20%{transform:translateX(-4px)}
40%{transform:translateX(4px)}
60%{transform:translateX(-3px)}
80%{transform:translateX(3px)}
}
/* ── Primary Button — Force white text & SVG in ALL themes ── */
.btn-run{
display:flex;align-items:center;justify-content:center;gap:8px;
width:100%;
background:linear-gradient(135deg,#FF1493,#C71585);
border:none;
border-radius:10px;
padding:12px 24px;
cursor:pointer;
font-size:15px;
font-weight:600;
font-family:'Inter',sans-serif;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
transition:all .2s ease;
box-shadow:0 4px 16px rgba(255,20,147,.3),
inset 0 1px 0 rgba(255,255,255,.1);
letter-spacing:-.2px;
}
.btn-run:hover{
background:linear-gradient(135deg,#FF69B4,#FF1493);
box-shadow:0 6px 24px rgba(255,20,147,.45),
inset 0 1px 0 rgba(255,255,255,.15);
transform:translateY(-1px);
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run:active{
transform:translateY(0);
box-shadow:0 2px 8px rgba(255,20,147,.3);
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run:focus{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run svg{
width:18px;height:18px;
fill:#ffffff!important;
color:#ffffff!important;
}
.btn-run svg path{
fill:#ffffff!important;
color:#ffffff!important;
}
.btn-run span,
.btn-run #run-btn-label,
#run-btn-label{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run.design-mode{
background:linear-gradient(135deg,#FF1493,#DB7093)!important;
box-shadow:0 4px 16px rgba(255,20,147,.3),
inset 0 1px 0 rgba(255,255,255,.1)!important;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run.design-mode:hover{
background:linear-gradient(135deg,#FF69B4,#FF1493)!important;
box-shadow:0 6px 24px rgba(255,20,147,.45),
inset 0 1px 0 rgba(255,255,255,.15)!important;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run.design-mode span,
.btn-run.design-mode #run-btn-label,
.btn-run.design-mode svg,
.btn-run.design-mode svg path{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
.btn-run.mover-mode{
background:linear-gradient(135deg,#C71585,#FF1493)!important;
box-shadow:0 4px 16px rgba(255,20,147,.3),
inset 0 1px 0 rgba(255,255,255,.1)!important;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run.mover-mode:hover{
background:linear-gradient(135deg,#FF69B4,#C71585)!important;
box-shadow:0 6px 24px rgba(255,20,147,.45),
inset 0 1px 0 rgba(255,255,255,.15)!important;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.btn-run.mover-mode span,
.btn-run.mover-mode #run-btn-label,
.btn-run.mover-mode svg,
.btn-run.mover-mode svg path{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
/* ── Comprehensive white-force for run button across every theme ── */
#custom-run-btn,
#custom-run-btn *,
#custom-run-btn span,
#custom-run-btn svg,
#custom-run-btn svg *,
#custom-run-btn svg path,
#custom-run-btn #run-btn-label,
.btn-run *{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
/* ── Mode button active state — white in ALL themes ── */
.mode-btn.active,
.mode-btn.active *,
.mode-btn.active span,
.mode-btn.active .mode-icon,
#mode-remover.active,
#mode-remover.active *,
#mode-remover.active span,
#mode-remover.active .mode-icon,
#mode-designer.active,
#mode-designer.active *,
#mode-designer.active span,
#mode-designer.active .mode-icon,
#mode-mover.active,
#mode-mover.active *,
#mode-mover.active span,
#mode-mover.active .mode-icon{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
/* ── Output Frames ── */
.output-frame{
border-bottom:1px solid #27272a;
display:flex;
flex-direction:column;
position:relative;
}
.output-frame .out-title{
padding:10px 20px;
font-size:13px;
font-weight:700;
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
text-transform:uppercase;
letter-spacing:.8px;
border-bottom:1px solid rgba(39,39,42,.6);
display:flex;
align-items:center;
justify-content:space-between;
}
.output-frame .out-title span{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.output-frame .out-body{
flex:1;
background:#09090b;
display:flex;
align-items:center;
justify-content:center;
overflow:hidden;
min-height:180px;
position:relative;
}
.output-frame .out-body img{
max-width:100%;max-height:460px;
image-rendering:auto;
}
.output-frame .out-placeholder{
color:#3f3f46;
font-size:13px;
text-align:center;
padding:20px;
}
.out-download-btn{
display:none;
align-items:center;
justify-content:center;
background:rgba(255,20,147,.1);
border:1px solid rgba(255,20,147,.2);
border-radius:6px;
cursor:pointer;
padding:3px 10px;
font-size:11px;
font-weight:500;
color:#FFB6C1!important;
gap:4px;
height:24px;
transition:all .15s;
}
.out-download-btn:hover{
background:rgba(255,20,147,.2);
border-color:rgba(255,20,147,.35);
color:#ffffff!important;
}
.out-download-btn.visible{display:inline-flex}
.out-download-btn svg{width:12px;height:12px;fill:#FFB6C1}
/* ── Loader ── */
.modern-loader{
display:none;
position:absolute;
top:0;left:0;right:0;bottom:0;
background:rgba(9,9,11,.92);
z-index:15;
flex-direction:column;
align-items:center;
justify-content:center;
gap:16px;
backdrop-filter:blur(4px);
}
.modern-loader.active{display:flex}
.modern-loader .loader-spinner{
width:36px;height:36px;
border:3px solid #27272a;
border-top-color:#FF1493;
border-radius:50%;
animation:spin .8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
.modern-loader .loader-text{
font-size:13px;
color:#a1a1aa;
font-weight:500;
}
.loader-bar-track{
width:200px;height:4px;
background:#27272a;
border-radius:2px;
overflow:hidden;
}
.loader-bar-fill{
height:100%;
background:linear-gradient(90deg,#FF1493,#FF69B4,#FF1493);
background-size:200% 100%;
animation:shimmer 1.5s ease-in-out infinite;
border-radius:2px;
}
@keyframes shimmer{
0%{background-position:200% 0}
100%{background-position:-200% 0}
}
/* ── Settings ── */
.settings-group{
border:1px solid #27272a;
border-radius:10px;
margin:12px 16px;
padding:0;
overflow:hidden;
}
.settings-group-title{
font-size:12px;
font-weight:600;
color:#71717a;
text-transform:uppercase;
letter-spacing:.8px;
padding:10px 16px;
border-bottom:1px solid #27272a;
background:rgba(24,24,27,.5);
}
.settings-group-body{
padding:14px 16px;
display:flex;
flex-direction:column;
gap:12px;
}
.slider-row{
display:flex;
align-items:center;
gap:10px;
min-height:28px;
}
.slider-row label,.slider-row .dim-label{
font-size:13px;
font-weight:500;
color:#a1a1aa;
min-width:72px;
flex-shrink:0;
}
.slider-row input[type="range"]{
flex:1;
-webkit-appearance:none;
appearance:none;
height:6px;
background:#27272a;
border-radius:3px;
outline:none;
min-width:0;
}
.slider-row input[type="range"]::-webkit-slider-thumb{
-webkit-appearance:none;
appearance:none;
width:16px;height:16px;
background:linear-gradient(135deg,#FF1493,#C71585);
border-radius:50%;
cursor:pointer;
box-shadow:0 2px 6px rgba(255,20,147,.4);
transition:transform .15s;
}
.slider-row input[type="range"]::-webkit-slider-thumb:hover{
transform:scale(1.2);
}
.slider-row input[type="range"]::-moz-range-thumb{
width:16px;height:16px;
background:linear-gradient(135deg,#FF1493,#C71585);
border-radius:50%;
cursor:pointer;
border:none;
box-shadow:0 2px 6px rgba(255,20,147,.4);
}
.slider-row .slider-val{
min-width:52px;text-align:right;
font-family:'JetBrains Mono',monospace;
font-size:12px;
font-weight:500;
padding:3px 8px;
background:#09090b;
border:1px solid #27272a;
border-radius:6px;
color:#a1a1aa;
flex-shrink:0;
}
.checkbox-row{
display:flex;align-items:center;gap:8px;
font-size:13px;cursor:default;
color:#a1a1aa;
}
.checkbox-row input[type="checkbox"]{
accent-color:#FF1493;
width:16px;height:16px;
cursor:pointer;
}
.checkbox-row label{
color:#a1a1aa;font-size:13px;cursor:pointer;
}
/* ── Status Bar ── */
.app-statusbar{
background:#18181b;
border-top:1px solid #27272a;
padding:6px 20px;
display:flex;
gap:12px;
height:34px;
align-items:center;
font-size:12px;
}
.app-statusbar .sb-section{
padding:0 12px;
flex:1;
display:flex;align-items:center;
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:#52525b;
overflow:hidden;
white-space:nowrap;
}
.app-statusbar .sb-section.sb-fixed{
flex:0 0 auto;
min-width:90px;
text-align:center;
justify-content:center;
padding:3px 12px;
background:rgba(255,20,147,.08);
border-radius:6px;
color:#FF69B4;
font-weight:500;
}
.app-statusbar .sb-mode{
flex:0 0 auto;
min-width:120px;
text-align:center;
justify-content:center;
padding:3px 12px;
background:rgba(255,20,147,.08);
border-radius:6px;
color:#FF69B4;
font-weight:500;
font-family:'JetBrains Mono',monospace;
font-size:12px;
}
#gradio-run-btn{
position:absolute;
left:-9999px;
top:-9999px;
width:1px;
height:1px;
opacity:0.01;
pointer-events:none;
overflow:hidden;
}
#bbox-debug-count{
font-family:'JetBrains Mono',monospace;
font-size:12px;
color:#52525b;
}
/* ── Global scrollbar ── */
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:#09090b}
::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
::-webkit-scrollbar-thumb:hover{background:#3f3f46}
/* ══════════════════════════════════════════════════════════════
FORCE WHITE on Run Button + Mode Buttons in EVERY theme
══════════════════════════════════════════════════════════════ */
body:not(.dark) .btn-run,
body:not(.dark) .btn-run *,
body:not(.dark) .btn-run span,
body:not(.dark) .btn-run svg,
body:not(.dark) .btn-run svg path,
body:not(.dark) #custom-run-btn,
body:not(.dark) #custom-run-btn *,
body:not(.dark) #custom-run-btn span,
body:not(.dark) #custom-run-btn svg,
body:not(.dark) #custom-run-btn svg path,
body:not(.dark) #run-btn-label{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
body:not(.dark) .mode-btn.active,
body:not(.dark) .mode-btn.active *,
body:not(.dark) .mode-btn.active span,
body:not(.dark) .mode-btn.active .mode-icon,
body:not(.dark) #mode-remover.active,
body:not(.dark) #mode-remover.active *,
body:not(.dark) #mode-designer.active,
body:not(.dark) #mode-designer.active *,
body:not(.dark) #mode-mover.active,
body:not(.dark) #mode-mover.active *{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.dark .btn-run,
.dark .btn-run *,
.dark .btn-run span,
.dark .btn-run svg,
.dark .btn-run svg path,
.dark #custom-run-btn,
.dark #custom-run-btn *,
.dark #custom-run-btn span,
.dark #custom-run-btn svg,
.dark #custom-run-btn svg path,
.dark #run-btn-label{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
.dark .mode-btn.active,
.dark .mode-btn.active *,
.dark .mode-btn.active span,
.dark .mode-btn.active .mode-icon,
.dark #mode-remover.active,
.dark #mode-remover.active *,
.dark #mode-designer.active,
.dark #mode-designer.active *,
.dark #mode-mover.active,
.dark #mode-mover.active *{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.gradio-container .btn-run,
.gradio-container .btn-run *,
.gradio-container .btn-run span,
.gradio-container .btn-run svg,
.gradio-container .btn-run svg path,
.gradio-container #custom-run-btn,
.gradio-container #custom-run-btn *,
.gradio-container #run-btn-label{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
fill:#ffffff!important;
}
.gradio-container .mode-btn.active,
.gradio-container .mode-btn.active *,
.gradio-container .mode-btn.active span,
.gradio-container .mode-btn.active .mode-icon{
color:#ffffff!important;
-webkit-text-fill-color:#ffffff!important;
}
.dark .app-shell{background:#18181b}
.dark .upload-prompt-modern{background:transparent}
.dark .panel-card{background:#18181b}
.dark .settings-group{background:#18181b}
.dark .modern-tb-btn{color:#ffffff!important}
.dark .modern-tb-btn .tb-icon{color:#ffffff!important}
.dark .modern-tb-btn .tb-label{color:#ffffff!important}
.dark .modern-tb-btn:hover{color:#ffffff!important}
.dark .modern-tb-btn:active,.dark .modern-tb-btn.active{color:#ffffff!important}
.dark .output-frame .out-title{color:#ffffff!important}
.dark .output-frame .out-title span{color:#ffffff!important}
.dark .out-download-btn{color:#FFB6C1!important}
.dark .out-download-btn:hover{color:#ffffff!important}
@media(max-width:840px){
.app-main-row{flex-direction:column}
.app-main-right{width:100%}
.app-main-left{border-right:none;border-bottom:1px solid #27272a}
.mode-switcher{flex-wrap:wrap}
}
"""
bbox_drawer_js = r"""
() => {
function initCanvasBbox() {
if (window.__bboxInitDone) return;
const canvas = document.getElementById('bbox-draw-canvas');
const wrap = document.getElementById('bbox-draw-wrap');
const status = document.getElementById('bbox-status');
const badge = document.getElementById('bbox-count');
const debugCount = document.getElementById('bbox-debug-count');
const jsonDisplay = document.getElementById('bbox-json-content');
const moverHint = document.getElementById('mover-box-hint');
const btnDraw = document.getElementById('tb-draw');
const btnSelect = document.getElementById('tb-select');
const btnReset = document.getElementById('tb-reset');
const btnDel = document.getElementById('tb-del');
const btnUndo = document.getElementById('tb-undo');
const btnClear = document.getElementById('tb-clear');
const btnChange = document.getElementById('tb-change-img');
const uploadPrompt = document.getElementById('upload-prompt');
const uploadClickArea = document.getElementById('upload-click-area');
const fileInput = document.getElementById('custom-file-input');
const modeRemover = document.getElementById('mode-remover');
const modeDesigner = document.getElementById('mode-designer');
const modeMover = document.getElementById('mode-mover');
const runBtnEl = document.getElementById('custom-run-btn');
const runBtnLabel = document.getElementById('run-btn-label');
const statusMode = document.getElementById('sb-mode-label');
const hintBar = document.getElementById('hint-bar-content');
if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
setTimeout(initCanvasBbox, 250);
return;
}
window.__bboxInitDone = true;
const ctx = canvas.getContext('2d');
let boxes = [];
window.__bboxBoxes = boxes;
window.__currentTaskMode = 'Object-Remover';
let baseImg = null;
let dispW = 512, dispH = 400;
let selectedIdx = -1;
let mode = 'draw';
let dragging = false;
let dragType = null;
let dragStart = {x:0, y:0};
let dragOrig = null;
const HANDLE = 6;
const RED_STROKE = 'rgba(255,0,0,0.95)';
const RED_STROKE_WIDTH = 2;
const SEL_STROKE = 'rgba(255,0,0,0.65)';
const DEFAULT_PROMPTS = {
'Object-Remover': 'Remove the red highlighted object from the scene',
'Design-Adder': 'Add the design pattern inside the red highlighted bounding box area',
'Object-Mover': 'Move the object highlighted in the red box to the location indicated by the other red box in the scene'
};
const HINT_TEXTS = {
'Object-Remover': '<b>Draw:</b> Click &amp; drag to create selection boxes \u00b7 <b>Select:</b> Click a box to move or resize \u00b7 <kbd>Delete</kbd> removes selected \u00b7 <kbd>Clear</kbd> removes all \u00b7 <kbd>Reset</kbd> removes image',
'Design-Adder': '<b>Draw:</b> Click &amp; drag to create selection boxes \u00b7 <b>Select:</b> Click a box to move or resize \u00b7 <kbd>Delete</kbd> removes selected \u00b7 <kbd>Clear</kbd> removes all \u00b7 <kbd>Reset</kbd> removes image',
'Object-Mover': '<span class="hint-mover-tag">MOVER</span> Draw <b>exactly 2</b> boxes: <b>Box 1 (SRC)</b> = the object to move \u00b7 <b>Box 2 (DST)</b> = the target location \u00b7 <kbd>Clear</kbd> to redraw \u00b7 Only 2 boxes allowed'
};
/* ── Toast System ── */
window.showToast = function(message, type, title) {
type = type || 'warning';
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
container.className = 'toast-container';
document.body.appendChild(container);
}
const icons = { warning: '\u26a0\ufe0f', error: '\u274c', success: '\u2705', info: '\u2139\ufe0f' };
const titles = { warning: 'Warning', error: 'Error', success: 'Success', info: 'Info' };
const toast = document.createElement('div');
toast.className = 'toast toast-' + type;
toast.innerHTML =
'<span class="toast-icon">' + (icons[type] || '\u26a0\ufe0f') + '</span>' +
'<div class="toast-body">' +
'<div class="toast-title">' + (title || titles[type] || 'Notice') + '</div>' +
'<div class="toast-msg">' + message + '</div>' +
'</div>' +
'<button class="toast-close" onclick="this.parentElement.classList.add(\'toast-exit\');setTimeout(()=>this.parentElement.remove(),300)">\u00d7</button>';
container.appendChild(toast);
setTimeout(() => {
if (toast.parentElement) {
toast.classList.add('toast-exit');
setTimeout(() => { if (toast.parentElement) toast.remove(); }, 300);
}
}, 4000);
};
function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
function px2n(x1,y1,x2,y2) {
return {
x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
x2: Math.max(x1,x2)/dispW, y2: Math.max(y1,y2)/dispH
};
}
function clamp01(v){return Math.max(0,Math.min(1,v));}
function fitSize(nw, nh) {
const mw = wrap.clientWidth || 512, mh = 500;
const r = Math.min(mw/nw, mh/nh, 1);
dispW = Math.round(nw*r); dispH = Math.round(nh*r);
canvas.width = dispW; canvas.height = dispH;
canvas.style.width = dispW+'px';
canvas.style.height = dispH+'px';
}
function canvasXY(e) {
const r = canvas.getBoundingClientRect();
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
return {x: Math.max(0,Math.min(dispW, cx-r.left)),
y: Math.max(0,Math.min(dispH, cy-r.top))};
}
function setGradioValue(containerId, value) {
const container = document.getElementById(containerId);
if (!container) return;
const allInputs = container.querySelectorAll('input, textarea');
allInputs.forEach(el => {
if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const ns = Object.getOwnPropertyDescriptor(proto, 'value');
if (ns && ns.set) {
ns.set.call(el, value);
el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
}
});
}
function formatJsonPretty(boxes) {
if (!boxes || boxes.length === 0) return '[\n // No bounding boxes defined\n]';
const isMover = window.__currentTaskMode === 'Object-Mover';
let lines = '[\n';
boxes.forEach((b, i) => {
const roleComment = isMover ? (' // ' + (i === 0 ? 'SOURCE object' : 'TARGET location') + '\n') : '';
lines += roleComment;
lines += ' {\n';
lines += ' "x1": ' + b.x1.toFixed(4) + ',\n';
lines += ' "y1": ' + b.y1.toFixed(4) + ',\n';
lines += ' "x2": ' + b.x2.toFixed(4) + ',\n';
lines += ' "y2": ' + b.y2.toFixed(4) + '\n';
lines += ' }';
if (i < boxes.length - 1) lines += ',';
lines += '\n';
});
lines += ']';
return lines;
}
function syncToGradio() {
window.__bboxBoxes = boxes;
const jsonStr = JSON.stringify(boxes);
const isMover = window.__currentTaskMode === 'Object-Mover';
if (debugCount) {
if (isMover) {
if (boxes.length === 0) debugCount.textContent = 'Mover: Draw source box';
else if (boxes.length === 1) debugCount.textContent = 'Mover: Draw target box';
else debugCount.textContent = 'Mover: SRC + DST ready';
} else {
debugCount.textContent = boxes.length > 0
? boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') + ' drawn'
: 'No boxes drawn';
}
}
if (jsonDisplay) {
jsonDisplay.textContent = formatJsonPretty(boxes);
jsonDisplay.scrollTop = jsonDisplay.scrollHeight;
}
setGradioValue('boxes-json-input', jsonStr);
updateMoverHint();
}
function updateMoverHint() {
if (!moverHint) return;
const isMover = window.__currentTaskMode === 'Object-Mover';
if (!isMover || !baseImg) {
moverHint.style.display = 'none';
return;
}
moverHint.style.display = 'block';
if (boxes.length === 0) {
moverHint.textContent = '\u25a0 Draw Box 1: Select the SOURCE object';
moverHint.style.color = '#FF69B4';
} else if (boxes.length === 1) {
moverHint.textContent = '\u25a1 Draw Box 2: Mark the TARGET location';
moverHint.style.color = '#FFB6C1';
} else {
moverHint.textContent = '\u2714 Both boxes ready \u2014 Source + Target defined';
moverHint.style.color = '#FF69B4';
}
}
function syncImageToGradio(dataUrl) {
setGradioValue('hidden-image-b64', dataUrl);
}
function syncPromptToGradio() {
const promptInput = document.getElementById('custom-prompt-input');
if (promptInput) {
setGradioValue('prompt-gradio-input', promptInput.value);
}
}
function syncAdapterToGradio() {
setGradioValue('adapter-choice-input', window.__currentTaskMode);
}
function switchTaskMode(taskMode) {
window.__currentTaskMode = taskMode;
const promptInput = document.getElementById('custom-prompt-input');
const allModeBtns = [modeRemover, modeDesigner, modeMover];
allModeBtns.forEach(btn => { if(btn) btn.classList.remove('active'); });
if (taskMode === 'Object-Remover') {
if (modeRemover) modeRemover.classList.add('active');
if (promptInput) promptInput.value = DEFAULT_PROMPTS['Object-Remover'];
if (runBtnEl) { runBtnEl.classList.remove('design-mode','mover-mode'); }
if (runBtnLabel) runBtnLabel.textContent = 'Remove Object';
if (statusMode) statusMode.textContent = 'Object Remover';
} else if (taskMode === 'Design-Adder') {
if (modeDesigner) modeDesigner.classList.add('active');
if (promptInput) promptInput.value = DEFAULT_PROMPTS['Design-Adder'];
if (runBtnEl) { runBtnEl.classList.remove('mover-mode'); runBtnEl.classList.add('design-mode'); }
if (runBtnLabel) runBtnLabel.textContent = 'Add Design';
if (statusMode) statusMode.textContent = 'Design Adder';
} else if (taskMode === 'Object-Mover') {
if (modeMover) modeMover.classList.add('active');
if (promptInput) promptInput.value = DEFAULT_PROMPTS['Object-Mover'];
if (runBtnEl) { runBtnEl.classList.remove('design-mode'); runBtnEl.classList.add('mover-mode'); }
if (runBtnLabel) runBtnLabel.textContent = 'Move Object';
if (statusMode) statusMode.textContent = 'Object Mover';
if (boxes.length > 2) {
window.showToast(
'Object Mover requires exactly 2 boxes (Source + Target). You have ' + boxes.length + ' boxes. Please clear extra boxes before running.',
'warning', 'Too Many Boxes'
);
}
}
if (hintBar) {
hintBar.innerHTML = HINT_TEXTS[taskMode] || HINT_TEXTS['Object-Remover'];
}
syncPromptToGradio();
syncAdapterToGradio();
updateMoverHint();
redraw();
}
if (modeRemover) {
modeRemover.addEventListener('click', () => switchTaskMode('Object-Remover'));
}
if (modeDesigner) {
modeDesigner.addEventListener('click', () => switchTaskMode('Design-Adder'));
}
if (modeMover) {
modeMover.addEventListener('click', () => switchTaskMode('Object-Mover'));
}
function getBoxLabel(index) {
if (window.__currentTaskMode === 'Object-Mover') {
return index === 0 ? 'SRC' : 'DST';
}
return '#'+(index+1);
}
function redraw(tempRect) {
ctx.clearRect(0,0,dispW,dispH);
if (!baseImg) {
ctx.fillStyle='#09090b'; ctx.fillRect(0,0,dispW,dispH);
updateBadge(); return;
}
ctx.drawImage(baseImg, 0, 0, dispW, dispH);
boxes.forEach((b,i) => {
const p = n2px(b);
const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
if (i === selectedIdx) {
ctx.strokeStyle = SEL_STROKE;
ctx.lineWidth = RED_STROKE_WIDTH + 1;
ctx.setLineDash([4,3]);
} else {
ctx.strokeStyle = RED_STROKE;
ctx.lineWidth = RED_STROKE_WIDTH;
ctx.setLineDash([]);
}
ctx.strokeRect(lx, ty, w, h);
ctx.setLineDash([]);
const label = getBoxLabel(i);
ctx.fillStyle = '#FF0000';
ctx.font = 'bold 11px Inter,system-ui,sans-serif';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
const tw = ctx.measureText(label).width;
const rx = lx, ry = ty - 18;
const rw = tw + 10, rh = 18;
ctx.beginPath();
if (ctx.roundRect) {
ctx.roundRect(rx, ry, rw, rh, 3);
} else {
ctx.rect(rx, ry, rw, rh);
}
ctx.fill();
ctx.fillStyle = '#fff';
ctx.fillText(label, lx+5, ty-15);
if (i === selectedIdx) drawHandles(p);
});
if (tempRect) {
const rx = Math.min(tempRect.x1,tempRect.x2);
const ry = Math.min(tempRect.y1,tempRect.y2);
const rw = Math.abs(tempRect.x2-tempRect.x1);
const rh = Math.abs(tempRect.y2-tempRect.y1);
ctx.strokeStyle = RED_STROKE;
ctx.lineWidth = RED_STROKE_WIDTH;
ctx.setLineDash([4,3]);
ctx.strokeRect(rx, ry, rw, rh);
ctx.setLineDash([]);
}
updateBadge();
}
function drawHandles(p) {
const pts = handlePoints(p);
for (const k in pts) {
const h = pts[k];
ctx.fillStyle = '#FF0000';
ctx.beginPath();
ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2);
ctx.stroke();
}
}
function handlePoints(p) {
const mx = (p.x1+p.x2)/2, my = (p.y1+p.y2)/2;
return {
tl:{x:p.x1,y:p.y1}, tc:{x:mx,y:p.y1}, tr:{x:p.x2,y:p.y1},
ml:{x:p.x1,y:my}, mr:{x:p.x2,y:my},
bl:{x:p.x1,y:p.y2}, bc:{x:mx,y:p.y2}, br:{x:p.x2,y:p.y2}
};
}
function hitHandle(px, py, boxIdx) {
if (boxIdx < 0) return null;
const p = n2px(boxes[boxIdx]);
const pts = handlePoints(p);
for (const k in pts) {
if (Math.abs(px-pts[k].x) <= HANDLE+2 && Math.abs(py-pts[k].y) <= HANDLE+2) return k;
}
return null;
}
function hitBox(px, py) {
for (let i = boxes.length-1; i >= 0; i--) {
const p = n2px(boxes[i]);
if (px >= p.x1 && px <= p.x2 && py >= p.y1 && py <= p.y2) return i;
}
return -1;
}
function updateBadge() {
if (boxes.length > 0) {
badge.style.display = 'block';
const isMover = window.__currentTaskMode === 'Object-Mover';
if (isMover) {
badge.textContent = boxes.length + '/2 box' + (boxes.length>1?'es':'');
} else {
badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':'');
}
badge.style.color = '#FF1493';
badge.style.borderColor = 'rgba(255,20,147,.3)';
} else {
badge.style.display = 'none';
}
}
function setMode(m) {
mode = m;
btnDraw.classList.toggle('active', m==='draw');
btnSelect.classList.toggle('active', m==='select');
canvas.style.cursor = m==='draw' ? 'crosshair' : 'default';
if (m==='draw') selectedIdx = -1;
redraw();
}
function showStatus(txt) {
status.textContent = txt; status.style.display = 'block';
}
function hideStatus() { status.style.display = 'none'; }
function resetCanvas() {
baseImg = null;
boxes.length = 0;
window.__bboxBoxes = boxes;
selectedIdx = -1;
dragging = false;
dragType = null;
dragOrig = null;
fitSize(512, 400);
syncToGradio();
syncImageToGradio('');
redraw();
hideStatus();
uploadPrompt.style.display = '';
showStatus('Image removed');
setTimeout(hideStatus, 1500);
}
function onDown(e) {
if (!baseImg) return;
e.preventDefault();
const {x, y} = canvasXY(e);
if (mode === 'draw') {
if (window.__currentTaskMode === 'Object-Mover' && boxes.length >= 2) {
window.showToast(
'Object Mover allows exactly <b>2 boxes</b> (Source + Target). Clear existing boxes to redraw.',
'warning', 'Box Limit Reached'
);
return;
}
dragging = true; dragType = 'new';
dragStart = {x, y};
selectedIdx = -1;
} else {
if (selectedIdx >= 0) {
const h = hitHandle(x, y, selectedIdx);
if (h) {
dragging = true; dragType = h;
dragStart = {x, y};
dragOrig = {...boxes[selectedIdx]};
showStatus('Resizing ' + getBoxLabel(selectedIdx));
return;
}
}
const hi = hitBox(x, y);
if (hi >= 0) {
selectedIdx = hi;
const h2 = hitHandle(x, y, selectedIdx);
if (h2) {
dragging = true; dragType = h2;
dragStart = {x, y};
dragOrig = {...boxes[selectedIdx]};
showStatus('Resizing ' + getBoxLabel(selectedIdx));
redraw(); return;
}
dragging = true; dragType = 'move';
dragStart = {x, y};
dragOrig = {...boxes[selectedIdx]};
showStatus('Moving ' + getBoxLabel(selectedIdx));
} else {
selectedIdx = -1;
hideStatus();
}
redraw();
}
}
function onMove(e) {
if (!baseImg) return;
e.preventDefault();
const {x, y} = canvasXY(e);
if (!dragging) {
if (mode === 'select') {
if (selectedIdx >= 0 && hitHandle(x,y,selectedIdx)) {
const h = hitHandle(x,y,selectedIdx);
const curs = {tl:'nwse-resize',tr:'nesw-resize',bl:'nesw-resize',br:'nwse-resize',
tc:'ns-resize',bc:'ns-resize',ml:'ew-resize',mr:'ew-resize'};
canvas.style.cursor = curs[h] || 'move';
} else if (hitBox(x,y) >= 0) {
canvas.style.cursor = 'move';
} else {
canvas.style.cursor = 'default';
}
}
return;
}
if (dragType === 'new') {
redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y});
showStatus(Math.abs(x-dragStart.x).toFixed(0)+'\u00d7'+Math.abs(y-dragStart.y).toFixed(0)+' px');
return;
}
const dx = (x - dragStart.x) / dispW;
const dy = (y - dragStart.y) / dispH;
const b = boxes[selectedIdx];
const o = dragOrig;
if (dragType === 'move') {
const bw = o.x2-o.x1, bh = o.y2-o.y1;
let nx1 = o.x1+dx, ny1 = o.y1+dy;
nx1 = clamp01(nx1); ny1 = clamp01(ny1);
if (nx1+bw > 1) nx1 = 1-bw;
if (ny1+bh > 1) ny1 = 1-bh;
b.x1=nx1; b.y1=ny1; b.x2=nx1+bw; b.y2=ny1+bh;
} else {
const t = dragType;
if (t.includes('l')) b.x1 = clamp01(o.x1 + dx);
if (t.includes('r')) b.x2 = clamp01(o.x2 + dx);
if (t.includes('t')) b.y1 = clamp01(o.y1 + dy);
if (t.includes('b')) b.y2 = clamp01(o.y2 + dy);
if (Math.abs(b.x2-b.x1) < 0.01) { b.x1=o.x1; b.x2=o.x2; }
if (Math.abs(b.y2-b.y1) < 0.01) { b.y1=o.y1; b.y2=o.y2; }
if (b.x1 > b.x2) { const t2=b.x1; b.x1=b.x2; b.x2=t2; }
if (b.y1 > b.y2) { const t2=b.y1; b.y1=b.y2; b.y2=t2; }
}
redraw();
}
function onUp(e) {
if (!dragging) return;
if (e) e.preventDefault();
dragging = false;
if (dragType === 'new') {
const pt = e ? canvasXY(e) : {x:dragStart.x, y:dragStart.y};
if (Math.abs(pt.x-dragStart.x) > 4 && Math.abs(pt.y-dragStart.y) > 4) {
const nb = px2n(dragStart.x, dragStart.y, pt.x, pt.y);
boxes.push(nb);
window.__bboxBoxes = boxes;
selectedIdx = boxes.length - 1;
if (window.__currentTaskMode === 'Object-Mover') {
const lbl = boxes.length === 1 ? 'Source object (SRC)' : 'Target location (DST)';
showStatus(lbl + ' marked');
} else {
showStatus('Box #'+boxes.length+' created');
}
} else { hideStatus(); }
} else {
showStatus(getBoxLabel(selectedIdx) + ' updated');
}
dragType = null; dragOrig = null;
syncToGradio();
redraw();
}
canvas.addEventListener('mousedown', onDown);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onUp);
canvas.addEventListener('mouseleave', (e)=>{if(dragging)onUp(e);});
canvas.addEventListener('touchstart', onDown, {passive:false});
canvas.addEventListener('touchmove', onMove, {passive:false});
canvas.addEventListener('touchend', onUp, {passive:false});
canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
function processFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (event) => {
const dataUrl = event.target.result;
const img = new window.Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
baseImg = img;
boxes.length = 0;
window.__bboxBoxes = boxes;
selectedIdx = -1;
fitSize(img.naturalWidth, img.naturalHeight);
syncToGradio(); redraw(); hideStatus();
uploadPrompt.style.display = 'none';
syncImageToGradio(dataUrl);
};
img.src = dataUrl;
};
reader.readAsDataURL(file);
}
function openFilePicker() {
fileInput.click();
}
uploadClickArea.addEventListener('click', openFilePicker);
btnChange.addEventListener('click', openFilePicker);
fileInput.addEventListener('change', (e) => {
processFile(e.target.files[0]);
e.target.value = '';
});
wrap.addEventListener('dragover', (e) => {
e.preventDefault();
wrap.style.outline = '2px solid #FF1493';
wrap.style.outlineOffset = '-2px';
});
wrap.addEventListener('dragleave', (e) => {
e.preventDefault();
wrap.style.outline = '';
});
wrap.addEventListener('drop', (e) => {
e.preventDefault();
wrap.style.outline = '';
if (e.dataTransfer.files.length) {
processFile(e.dataTransfer.files[0]);
}
});
btnDraw.addEventListener('click', ()=>setMode('draw'));
btnSelect.addEventListener('click', ()=>setMode('select'));
btnReset.addEventListener('click', () => {
resetCanvas();
});
btnDel.addEventListener('click', () => {
if (selectedIdx >= 0 && selectedIdx < boxes.length) {
const removed = getBoxLabel(selectedIdx);
boxes.splice(selectedIdx, 1);
window.__bboxBoxes = boxes;
selectedIdx = -1;
syncToGradio(); redraw();
showStatus(removed + ' deleted');
} else {
showStatus('Select a box first');
}
});
btnUndo.addEventListener('click', () => {
if (boxes.length > 0) {
boxes.pop();
window.__bboxBoxes = boxes;
selectedIdx = -1;
syncToGradio(); redraw();
showStatus('Last box removed');
}
});
btnClear.addEventListener('click', () => {
boxes.length = 0;
window.__bboxBoxes = boxes;
selectedIdx = -1;
syncToGradio(); redraw(); hideStatus();
});
const promptInput = document.getElementById('custom-prompt-input');
if (promptInput) {
promptInput.addEventListener('input', () => {
promptInput.classList.remove('prompt-error');
syncPromptToGradio();
});
}
function syncSlider(customId, gradioId) {
const slider = document.getElementById(customId);
const valSpan = document.getElementById(customId + '-val');
if (!slider) return;
slider.addEventListener('input', () => {
if (valSpan) valSpan.textContent = slider.value;
const container = document.getElementById(gradioId);
if (!container) return;
const targets = [
...container.querySelectorAll('input[type="range"]'),
...container.querySelectorAll('input[type="number"]')
];
targets.forEach(el => {
const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (ns && ns.set) {
ns.set.call(el, slider.value);
el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
}
});
});
}
syncSlider('custom-seed', 'gradio-seed');
syncSlider('custom-guidance', 'gradio-guidance');
syncSlider('custom-steps', 'gradio-steps');
syncSlider('custom-height', 'gradio-height');
syncSlider('custom-width', 'gradio-width');
const randCheck = document.getElementById('custom-randomize');
if (randCheck) {
randCheck.addEventListener('change', () => {
const container = document.getElementById('gradio-randomize');
if (!container) return;
const cb = container.querySelector('input[type="checkbox"]');
if (cb && cb.checked !== randCheck.checked) {
cb.click();
}
});
}
function showLoaders() {
const l1 = document.getElementById('output-loader');
const l2 = document.getElementById('preview-loader');
if (l1) l1.classList.add('active');
if (l2) l2.classList.add('active');
const sb = document.querySelector('.app-statusbar .sb-fixed');
if (sb) sb.textContent = 'Processing...';
if (window.__loaderSafetyTimeout) clearTimeout(window.__loaderSafetyTimeout);
window.__loaderSafetyTimeout = setTimeout(hideLoaders, 120000);
}
function hideLoaders() {
const l1 = document.getElementById('output-loader');
const l2 = document.getElementById('preview-loader');
if (l1) l1.classList.remove('active');
if (l2) l2.classList.remove('active');
const sb = document.querySelector('.app-statusbar .sb-fixed');
if (sb) sb.textContent = 'Done';
if (window.__loaderSafetyTimeout) { clearTimeout(window.__loaderSafetyTimeout); window.__loaderSafetyTimeout = null; }
}
window.__showLoaders = showLoaders;
window.__hideLoaders = hideLoaders;
window.__clickGradioRunBtn = function() {
const pInput = document.getElementById('custom-prompt-input');
const promptVal = pInput ? pInput.value.trim() : '';
if (!baseImg) {
window.showToast('Please upload an image first before processing.', 'error', 'No Image');
return;
}
if (!promptVal) {
window.showToast('Prompt cannot be empty. Please describe the edit you want to perform.', 'warning', 'Empty Prompt');
if (pInput) {
pInput.classList.add('prompt-error');
pInput.focus();
setTimeout(() => pInput.classList.remove('prompt-error'), 2000);
}
return;
}
if (boxes.length === 0) {
window.showToast('Please draw at least one bounding box on the image.', 'warning', 'No Boxes');
return;
}
if (window.__currentTaskMode === 'Object-Mover') {
if (boxes.length < 2) {
window.showToast(
'Object Mover requires <b>exactly 2 boxes</b>. Draw a second box to mark the <b>target location</b>.',
'warning', 'Need Target Box'
);
return;
}
if (boxes.length > 2) {
window.showToast(
'Object Mover requires <b>exactly 2 boxes</b> (Source + Target). You have ' + boxes.length + '. Please clear and redraw.',
'warning', 'Too Many Boxes'
);
return;
}
}
syncPromptToGradio();
syncAdapterToGradio();
syncToGradio();
showLoaders();
setTimeout(() => {
const gradioBtn = document.getElementById('gradio-run-btn');
if (!gradioBtn) return;
const btn = gradioBtn.querySelector('button');
if (btn) {
btn.click();
} else {
gradioBtn.click();
}
}, 200);
};
const customRunBtn = document.getElementById('custom-run-btn');
if (customRunBtn) {
customRunBtn.addEventListener('click', () => {
window.__clickGradioRunBtn();
});
}
new ResizeObserver(() => {
if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
}).observe(wrap);
setMode('draw');
fitSize(512,400); redraw();
syncToGradio();
syncAdapterToGradio();
updateMoverHint();
}
initCanvasBbox();
}
"""
wire_outputs_js = r"""
() => {
function downloadImage(imgSrc, filename) {
const a = document.createElement('a');
a.href = imgSrc;
a.download = filename || 'image.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function watchOutputs() {
const resultContainer = document.getElementById('gradio-result');
const previewContainer = document.getElementById('gradio-preview');
const outBody = document.getElementById('output-image-container');
const prevBody = document.getElementById('preview-image-container');
const outPh = document.getElementById('output-placeholder');
const prevPh = document.getElementById('preview-placeholder');
const dlBtnOut = document.getElementById('dl-btn-output');
const dlBtnPrev = document.getElementById('dl-btn-preview');
if (!resultContainer || !previewContainer || !outBody || !prevBody) {
setTimeout(watchOutputs, 500);
return;
}
if (dlBtnOut) {
dlBtnOut.addEventListener('click', (e) => {
e.stopPropagation();
const img = outBody.querySelector('img.modern-out-img');
if (img && img.src) downloadImage(img.src, 'output_result.png');
});
}
if (dlBtnPrev) {
dlBtnPrev.addEventListener('click', (e) => {
e.stopPropagation();
const img = prevBody.querySelector('img.modern-out-img');
if (img && img.src) downloadImage(img.src, 'input_preview.png');
});
}
function syncImages() {
const resultImg = resultContainer.querySelector('img');
if (resultImg && resultImg.src) {
if (outPh) outPh.style.display = 'none';
let existing = outBody.querySelector('img.modern-out-img');
if (!existing) {
existing = document.createElement('img');
existing.className = 'modern-out-img';
outBody.appendChild(existing);
}
if (existing.src !== resultImg.src) {
existing.src = resultImg.src;
if (dlBtnOut) dlBtnOut.classList.add('visible');
if (window.__hideLoaders) window.__hideLoaders();
}
}
const previewImg = previewContainer.querySelector('img');
if (previewImg && previewImg.src) {
if (prevPh) prevPh.style.display = 'none';
let existing2 = prevBody.querySelector('img.modern-out-img');
if (!existing2) {
existing2 = document.createElement('img');
existing2.className = 'modern-out-img';
prevBody.appendChild(existing2);
}
if (existing2.src !== previewImg.src) {
existing2.src = previewImg.src;
if (dlBtnPrev) dlBtnPrev.classList.add('visible');
}
}
}
const observer = new MutationObserver(syncImages);
observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
observer.observe(previewContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
setInterval(syncImages, 800);
}
watchOutputs();
function watchDimensions() {
const wContainer = document.getElementById('gradio-width');
const hContainer = document.getElementById('gradio-height');
const wSlider = document.getElementById('custom-width');
const hSlider = document.getElementById('custom-height');
const wVal = document.getElementById('custom-width-val');
const hVal = document.getElementById('custom-height-val');
if (!wContainer || !hContainer || !wSlider || !hSlider) {
setTimeout(watchDimensions, 500);
return;
}
function syncDims() {
const wInput = wContainer.querySelector('input[type="range"],input[type="number"]');
const hInput = hContainer.querySelector('input[type="range"],input[type="number"]');
if (wInput && wInput.value) { wSlider.value = wInput.value; if(wVal) wVal.textContent = wInput.value; }
if (hInput && hInput.value) { hSlider.value = hInput.value; if(hVal) hVal.textContent = hInput.value; }
}
const obs = new MutationObserver(syncDims);
obs.observe(wContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
obs.observe(hContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
setInterval(syncDims, 1000);
}
watchDimensions();
}
"""
DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z"/><path d="M20 18H4v2h16v-2z"/></svg>'
with gr.Blocks() as demo:
hidden_image_b64 = gr.Textbox(
elem_id="hidden-image-b64",
elem_classes="hidden-input",
container=False
)
boxes_json = gr.Textbox(
value="[]",
elem_id="boxes-json-input",
elem_classes="hidden-input",
container=False
)
prompt = gr.Textbox(
value=DEFAULT_PROMPTS["Object-Remover"],
elem_id="prompt-gradio-input",
elem_classes="hidden-input",
container=False
)
adapter_choice = gr.Textbox(
value="Object-Remover",
elem_id="adapter-choice-input",
elem_classes="hidden-input",
container=False
)
seed = gr.Slider(
minimum=0, maximum=MAX_SEED, step=1, value=0,
elem_id="gradio-seed",
elem_classes="hidden-input",
container=False
)
randomize_seed = gr.Checkbox(
value=True,
elem_id="gradio-randomize",
elem_classes="hidden-input",
container=False
)
guidance_scale = gr.Slider(
minimum=1.0, maximum=10.0, step=0.1, value=1.0,
elem_id="gradio-guidance",
elem_classes="hidden-input",
container=False
)
num_inference_steps = gr.Slider(
minimum=1, maximum=20, step=1, value=4,
elem_id="gradio-steps",
elem_classes="hidden-input",
container=False
)
height_slider = gr.Slider(
minimum=256, maximum=2048, step=8, value=1024,
elem_id="gradio-height",
elem_classes="hidden-input",
container=False
)
width_slider = gr.Slider(
minimum=256, maximum=2048, step=8, value=1024,
elem_id="gradio-width",
elem_classes="hidden-input",
container=False
)
result = gr.Image(
elem_id="gradio-result",
elem_classes="hidden-input",
container=False,
format="png"
)
preview = gr.Image(
elem_id="gradio-preview",
elem_classes="hidden-input",
container=False
)
gr.HTML(f"""
<div id="toast-container" class="toast-container"></div>
<div class="app-shell">
<!-- Header -->
<div class="app-header">
<div class="app-header-left">
<div class="app-logo">\u2b1a</div>
<span class="app-title">QIE-Bbox-Studio</span>
<span class="app-badge">Bbox</span>
</div>
<div class="mode-switcher">
<button id="mode-remover" class="mode-btn active" title="Object Removal Mode">
<span class="mode-icon">\u2326</span> Object Remover
</button>
<button id="mode-designer" class="mode-btn" title="Design Adder Mode">
<span class="mode-icon">\u2726</span> Design Adder
</button>
<button id="mode-mover" class="mode-btn" title="Object Mover Mode">
<span class="mode-icon">\u21c4</span> Object Mover
</button>
</div>
</div>
<!-- Toolbar -->
<div class="app-toolbar">
<button id="tb-draw" class="modern-tb-btn active" title="Draw bounding boxes">
<span class="tb-icon">\u25ac</span><span class="tb-label">Draw</span>
</button>
<button id="tb-select" class="modern-tb-btn" title="Select, move, resize boxes">
<span class="tb-icon">\u21c9</span><span class="tb-label">Select</span>
</button>
<button id="tb-reset" class="modern-tb-btn" title="Reset canvas and remove image">
<span class="tb-icon">\u27f2</span><span class="tb-label">Reset</span>
</button>
<div class="tb-sep"></div>
<button id="tb-del" class="modern-tb-btn" title="Delete selected box">
<span class="tb-icon">\u2715</span><span class="tb-label">Delete</span>
</button>
<button id="tb-undo" class="modern-tb-btn" title="Undo last box">
<span class="tb-icon">\u21a9</span><span class="tb-label">Undo</span>
</button>
<button id="tb-clear" class="modern-tb-btn" title="Clear all boxes">
<span class="tb-icon">\u2716</span><span class="tb-label">Clear</span>
</button>
<div class="tb-sep"></div>
<button id="tb-change-img" class="modern-tb-btn" title="Upload a different image">
<span class="tb-label">Upload\u2026</span>
</button>
</div>
<!-- Main Content -->
<div class="app-main-row">
<!-- Left: Canvas -->
<div class="app-main-left">
<div id="bbox-draw-wrap">
<div id="upload-prompt" class="upload-prompt-modern">
<div id="upload-click-area" class="upload-click-area">
<svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#FF1493" stroke-width="2" stroke-dasharray="4 3"/>
<polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,20,147,0.15)" stroke="#FF1493" stroke-width="1.5"/>
<circle cx="28" cy="30" r="6" fill="rgba(255,20,147,0.2)" stroke="#FF1493" stroke-width="1.5"/>
</svg>
</div>
</div>
<input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
<canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
<div id="bbox-status"></div>
<div id="bbox-count"></div>
<div id="mover-box-hint"></div>
</div>
<div class="hint-bar" id="hint-bar-content">
<b>Draw:</b> Click &amp; drag to create selection boxes &nbsp;&middot;&nbsp;
<b>Select:</b> Click a box to move or resize &nbsp;&middot;&nbsp;
<kbd>Delete</kbd> removes selected &nbsp;&middot;&nbsp;
<kbd>Clear</kbd> removes all &nbsp;&middot;&nbsp;
<kbd>Reset</kbd> removes image
</div>
<div class="json-panel">
<div class="json-panel-title">Bounding Boxes</div>
<div class="json-panel-content" id="bbox-json-content">[
// No bounding boxes defined
]</div>
</div>
</div>
<!-- Right: Controls & Output -->
<div class="app-main-right">
<div class="panel-card">
<div class="panel-card-title">Edit Instruction</div>
<div class="panel-card-body">
<label class="modern-label" for="custom-prompt-input">Prompt</label>
<textarea id="custom-prompt-input" class="modern-textarea" rows="2" placeholder="Describe the edit...">Remove the red highlighted object from the scene</textarea>
</div>
</div>
<div style="padding:12px 20px;">
<button id="custom-run-btn" class="btn-run">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 7l-7 5-7-5V5l7 5 7-5v2zm0 6l-7 5-7-5v-2l7 5 7-5v2z"/></svg>
<span id="run-btn-label">Remove Object</span>
</button>
</div>
<div class="output-frame" style="flex:1">
<div class="out-title">
<span>Output</span>
<span id="dl-btn-output" class="out-download-btn" title="Download">
{DOWNLOAD_SVG} Save
</span>
</div>
<div class="out-body" id="output-image-container">
<div class="modern-loader" id="output-loader">
<div class="loader-spinner"></div>
<div class="loader-text">Processing image\u2026</div>
<div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
</div>
<div class="out-placeholder" id="output-placeholder">Result will appear here</div>
</div>
</div>
<div class="output-frame">
<div class="out-title">
<span>Input Preview</span>
<span id="dl-btn-preview" class="out-download-btn" title="Download">
{DOWNLOAD_SVG} Save
</span>
</div>
<div class="out-body" id="preview-image-container">
<div class="modern-loader" id="preview-loader">
<div class="loader-spinner"></div>
<div class="loader-text">Preparing input\u2026</div>
<div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
</div>
<div class="out-placeholder" id="preview-placeholder">Preview will appear here</div>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">Advanced Settings</div>
<div class="settings-group-body">
<div class="slider-row">
<label>Seed</label>
<input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
<span class="slider-val" id="custom-seed-val">0</span>
</div>
<div class="checkbox-row">
<input type="checkbox" id="custom-randomize" checked>
<label for="custom-randomize">Randomize seed</label>
</div>
<div class="slider-row">
<label>Guidance</label>
<input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
<span class="slider-val" id="custom-guidance-val">1.0</span>
</div>
<div class="slider-row">
<label>Steps</label>
<input type="range" id="custom-steps" min="1" max="20" step="1" value="4">
<span class="slider-val" id="custom-steps-val">4</span>
</div>
<div class="slider-row">
<span class="dim-label">Width</span>
<input type="range" id="custom-width" min="256" max="2048" step="8" value="1024">
<span class="slider-val" id="custom-width-val">1024</span>
</div>
<div class="slider-row">
<span class="dim-label">Height</span>
<input type="range" id="custom-height" min="256" max="2048" step="8" value="1024">
<span class="slider-val" id="custom-height-val">1024</span>
</div>
</div>
</div>
</div>
</div>
<!-- Status Bar -->
<div class="app-statusbar">
<div class="sb-section" id="bbox-debug-count">No boxes drawn</div>
<div class="sb-section sb-mode" id="sb-mode-label">Object Remover</div>
<div class="sb-section sb-fixed">Ready</div>
</div>
</div>
""")
run_btn = gr.Button("Run", elem_id="gradio-run-btn")
demo.load(fn=None, js=bbox_drawer_js)
demo.load(fn=None, js=wire_outputs_js)
run_btn.click(
fn=infer_bbox_task,
inputs=[hidden_image_b64, boxes_json, prompt, adapter_choice, seed, randomize_seed,
guidance_scale, num_inference_steps, height_slider, width_slider],
outputs=[result, seed, preview],
js="""(b64, bj, p, ac, s, rs, gs, nis, h, w) => {
const boxes = window.__bboxBoxes || [];
const json = JSON.stringify(boxes);
const taskMode = window.__currentTaskMode || 'Object-Remover';
return [b64, json, p, taskMode, s, rs, gs, nis, h, w];
}""",
)
hidden_image_b64.change(
fn=update_dimensions_on_upload,
inputs=[hidden_image_b64],
outputs=[width_slider, height_slider],
)
if __name__ == "__main__":
demo.launch(
css=css,
mcp_server=True, ssr_mode=False, show_error=True
)