prithivMLmods's picture
update app
16ba541 verified
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-2509",
transformer=QwenImageTransformer2DModel.from_pretrained(
"prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V4",
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-2509-Object-Remover-Bbox-v3",
"weights": "QIE-2509-Object-Remover-Bbox-v3-10000.safetensors",
"adapter_name": "object-remover",
},
}
loaded = False
DEFAULT_PROMPT = "Remove the red highlighted object from 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_object_removal(
b64_str,
boxes_json,
prompt,
seed=0,
randomize_seed=True,
guidance_scale=1.0,
num_inference_steps=4,
height=1024,
width=1024,
):
global loaded
progress = gr.Progress(track_tqdm=True)
if not loaded:
pipe.load_lora_weights(
ADAPTER_SPECS["Object-Remover"]["repo"],
weight_name=ADAPTER_SPECS["Object-Remover"]["weights"],
adapter_name=ADAPTER_SPECS["Object-Remover"]["adapter_name"],
)
pipe.set_adapters(
[ADAPTER_SPECS["Object-Remover"]["adapter_name"]], adapter_weights=[1.0]
)
loaded = True
if not prompt or prompt.strip() == "":
raise gr.Error("Please enter an edit prompt.")
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.")
progress(0.3, desc="Burning red boxes onto image...")
marked = burn_boxes_onto_image(source_image, boxes_json)
progress(0.5, desc="Running object removal 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;
}
/* ── 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,#6366f1,#8b5cf6,#a78bfa);
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(99,102,241,.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(99,102,241,.15);
color:#818cf8;
border:1px solid rgba(99,102,241,.25);
letter-spacing:.3px;
}
/* ── 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(99,102,241,.15);
color:#ffffff!important;
border-color:rgba(99,102,241,.3);
}
.modern-tb-btn:active,.modern-tb-btn.active{
background:rgba(99,102,241,.25);
color:#ffffff!important;
border-color:rgba(99,102,241,.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(99,102,241,.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:#a78bfa;
padding:4px 12px;
font-family:'JetBrains Mono',monospace;
font-size:12px;
font-weight:600;
border-radius:6px;
border:1px solid rgba(99,102,241,.3);
z-index:10;display:none;
backdrop-filter:blur(8px);
}
/* ── 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(99,102,241,.03);
transition:all .2s ease;
}
.upload-click-area:hover{
background:rgba(99,102,241,.08);
border-color:#6366f1;
transform:scale(1.03);
}
.upload-click-area:active{
background:rgba(99,102,241,.12);
transform:scale(.98);
}
.upload-click-area svg{
width:80px;height:80px;
}
/* ── Hint Bar ── */
.hint-bar{
background:rgba(99,102,241,.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:#c7d2fe;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;
}
/* ── 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:#6366f1;
background:rgba(99,102,241,.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:#6366f1;
box-shadow:0 0 0 3px rgba(99,102,241,.15);
}
.modern-textarea::placeholder{color:#3f3f46}
.modern-textarea.error-flash{
border-color:#ef4444!important;
box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;
animation:shake .4s ease;
}
@keyframes shake{
0%,100%{transform:translateX(0)}
20%,60%{transform:translateX(-4px)}
40%,80%{transform:translateX(4px)}
}
/* ── Toast Notification ── */
.toast-notification{
position:fixed;
top:24px;
left:50%;
transform:translateX(-50%) translateY(-120%);
z-index:9999;
padding:10px 24px;
border-radius:10px;
font-family:'Inter',sans-serif;
font-size:14px;
font-weight:600;
display:flex;
align-items:center;
gap:8px;
box-shadow:0 8px 24px rgba(0,0,0,.5);
transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;
opacity:0;
pointer-events:none;
}
.toast-notification.visible{
transform:translateX(-50%) translateY(0);
opacity:1;
pointer-events:auto;
}
.toast-notification.error{
background:linear-gradient(135deg,#dc2626,#b91c1c);
color:#fff;
border:1px solid rgba(255,255,255,.15);
}
.toast-notification.warning{
background:linear-gradient(135deg,#d97706,#b45309);
color:#fff;
border:1px solid rgba(255,255,255,.15);
}
.toast-notification.info{
background:linear-gradient(135deg,#2563eb,#1d4ed8);
color:#fff;
border:1px solid rgba(255,255,255,.15);
}
.toast-notification .toast-icon{font-size:16px;line-height:1}
.toast-notification .toast-text{line-height:1.3}
/* ── Primary Button ── */
.btn-run{
display:flex;align-items:center;justify-content:center;gap:8px;
width:100%;
background:linear-gradient(135deg,#6366f1,#7c3aed);
border:none;
border-radius:10px;
padding:12px 24px;
cursor:pointer;
font-size:15px;
font-weight:600;
font-family:'Inter',sans-serif;
color:#fff;
transition:all .2s ease;
box-shadow:0 4px 16px rgba(99,102,241,.3),
inset 0 1px 0 rgba(255,255,255,.1);
letter-spacing:-.2px;
}
.btn-run:hover{
background:linear-gradient(135deg,#7c7cf5,#8b5cf6);
box-shadow:0 6px 24px rgba(99,102,241,.45),
inset 0 1px 0 rgba(255,255,255,.15);
transform:translateY(-1px);
}
.btn-run:active{
transform:translateY(0);
box-shadow:0 2px 8px rgba(99,102,241,.3);
}
.btn-run svg{width:18px;height:18px;fill:#fff}
/* ── 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;
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;
}
.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(99,102,241,.1);
border:1px solid rgba(99,102,241,.2);
border-radius:6px;
cursor:pointer;
padding:3px 10px;
font-size:11px;
font-weight:500;
color:#c7d2fe!important;
gap:4px;
height:24px;
transition:all .15s;
}
.out-download-btn:hover{
background:rgba(99,102,241,.2);
border-color:rgba(99,102,241,.35);
color:#ffffff!important;
}
.out-download-btn.visible{display:inline-flex}
.out-download-btn svg{width:12px;height:12px;fill:#c7d2fe}
/* ── 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:#6366f1;
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,#6366f1,#8b5cf6,#6366f1);
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,#6366f1,#7c3aed);
border-radius:50%;
cursor:pointer;
box-shadow:0 2px 6px rgba(99,102,241,.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,#6366f1,#7c3aed);
border-radius:50%;
cursor:pointer;
border:none;
box-shadow:0 2px 6px rgba(99,102,241,.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:#6366f1;
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(99,102,241,.08);
border-radius:6px;
color:#818cf8;
font-weight:500;
}
#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}
/* ── Dark mode force-overrides ── */
.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:#c7d2fe!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}
}
"""
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 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 promptInput = document.getElementById('custom-prompt-input');
if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
setTimeout(initCanvasBbox, 250);
return;
}
window.__bboxInitDone = true;
const ctx = canvas.getContext('2d');
let boxes = [];
window.__bboxBoxes = boxes;
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(239,68,68,0.95)';
const RED_STROKE_WIDTH = 2;
const SEL_STROKE = 'rgba(99,102,241,0.95)';
let toastTimer = null;
function showToast(message, type) {
let toast = document.getElementById('app-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'app-toast';
toast.className = 'toast-notification';
toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
document.body.appendChild(toast);
}
const icon = toast.querySelector('.toast-icon');
const text = toast.querySelector('.toast-text');
toast.className = 'toast-notification ' + (type || 'error');
if (type === 'warning') icon.textContent = '\u26A0';
else if (type === 'info') icon.textContent = '\u2139';
else icon.textContent = '\u2717';
text.textContent = message;
if (toastTimer) clearTimeout(toastTimer);
void toast.offsetWidth;
toast.classList.add('visible');
toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
}
window.__showToast = showToast;
function flashPromptError() {
if (!promptInput) return;
promptInput.classList.add('error-flash');
promptInput.focus();
setTimeout(() => promptInput.classList.remove('error-flash'), 800);
}
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]';
let lines = '[\n';
boxes.forEach((b, i) => {
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);
if (debugCount) {
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);
}
function syncImageToGradio(dataUrl) {
setGradioValue('hidden-image-b64', dataUrl);
}
function syncPromptToGradio() {
if (promptInput) {
setGradioValue('prompt-gradio-input', promptInput.value);
}
}
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 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([]);
ctx.fillStyle = i===selectedIdx ? '#6366f1' : '#ef4444';
ctx.font = 'bold 11px Inter,system-ui,sans-serif';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
const label = '#'+(i+1);
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 = '#6366f1';
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';
badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':'');
} 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 onDown(e) {
if (!baseImg) return;
e.preventDefault();
const {x, y} = canvasXY(e);
if (mode === 'draw') {
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 #'+(selectedIdx+1));
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 #'+(selectedIdx+1));
redraw(); return;
}
dragging = true; dragType = 'move';
dragStart = {x, y};
dragOrig = {...boxes[selectedIdx]};
showStatus('Moving #'+(selectedIdx+1));
} 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;
showStatus('Box #'+boxes.length+' created');
} else { hideStatus(); }
} else {
showStatus('Box #'+(selectedIdx+1)+' 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 #6366f1';
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 = selectedIdx + 1;
boxes.splice(selectedIdx, 1);
window.__bboxBoxes = boxes;
selectedIdx = -1;
syncToGradio(); redraw();
showStatus('Box #'+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();
});
if (promptInput) {
promptInput.addEventListener('input', () => {
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...';
}
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';
}
window.__showLoaders = showLoaders;
window.__hideLoaders = hideLoaders;
function validateBeforeRun() {
const promptVal = promptInput ? promptInput.value.trim() : '';
const hasImage = !!baseImg;
const hasBoxes = boxes.length > 0;
if (!hasImage && !promptVal && !hasBoxes) {
showToast('Please upload an image, draw boxes, and enter a prompt', 'error');
flashPromptError();
return false;
}
if (!hasImage) {
showToast('Please upload an image first', 'error');
return false;
}
if (!hasBoxes) {
showToast('Please draw at least one bounding box on the image', 'error');
return false;
}
if (!promptVal) {
showToast('Please enter an edit prompt', 'warning');
flashPromptError();
return false;
}
return true;
}
window.__clickGradioRunBtn = function() {
if (!validateBeforeRun()) return;
syncPromptToGradio();
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();
}
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(css=css) 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_PROMPT,
elem_id="prompt-gradio-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 class="app-shell">
<!-- Header -->
<div class="app-header">
<div class="app-header-left">
<div class="app-logo">⌦</div>
<span class="app-title">QIE Object Remover</span>
<span class="app-badge">Bbox</span>
</div>
</div>
<!-- Toolbar -->
<div class="app-toolbar">
<button id="tb-draw" class="modern-tb-btn active" title="Draw bounding boxes">
<span class="tb-icon">▬</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">⇉</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">⟲</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">✕</span><span class="tb-label">Delete</span>
</button>
<button id="tb-undo" class="modern-tb-btn" title="Undo last box">
<span class="tb-icon">↩</span><span class="tb-label">Undo</span>
</button>
<button id="tb-clear" class="modern-tb-btn" title="Clear all boxes">
<span class="tb-icon">✖</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…</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="#6366f1" stroke-width="2" stroke-dasharray="4 3"/>
<polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(99,102,241,0.15)" stroke="#6366f1" stroke-width="1.5"/>
<circle cx="28" cy="30" r="6" fill="rgba(99,102,241,0.2)" stroke="#6366f1" 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>
<div class="hint-bar">
<b>Draw:</b> Click &amp; drag to create selection boxes &nbsp;·&nbsp;
<b>Select:</b> Click a box to move or resize &nbsp;·&nbsp;
<kbd>Delete</kbd> removes selected &nbsp;·&nbsp;
<kbd>Clear</kbd> removes all &nbsp;·&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...">{DEFAULT_PROMPT}</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>
Remove Object
</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…</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…</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-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_object_removal,
inputs=[hidden_image_b64, boxes_json, prompt, seed, randomize_seed,
guidance_scale, num_inference_steps, height_slider, width_slider],
outputs=[result, seed, preview],
js="""(b64, bj, p, s, rs, gs, nis, h, w) => {
const boxes = window.__bboxBoxes || [];
const json = JSON.stringify(boxes);
return [b64, json, p, 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(
mcp_server=True, ssr_mode=False, show_error=True
)