prithivMLmods commited on
Commit
6930028
Β·
verified Β·
1 Parent(s): 835bf14

upload app

Browse files
Files changed (1) hide show
  1. app.py +934 -0
app.py ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import random
4
+ import torch
5
+ import spaces
6
+ from typing import Iterable
7
+ from PIL import Image, ImageDraw
8
+ from diffusers import FlowMatchEulerDiscreteScheduler
9
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
10
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
11
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
12
+
13
+ from gradio.themes import Soft
14
+ from gradio.themes.utils import colors, fonts, sizes
15
+
16
+ colors.purple = colors.Color(
17
+ name="purple",
18
+ c50="#FAF5FF",
19
+ c100="#F3E8FF",
20
+ c200="#E9D5FF",
21
+ c300="#DAB2FF",
22
+ c400="#C084FC",
23
+ c500="#A855F7",
24
+ c600="#9333EA",
25
+ c700="#7E22CE",
26
+ c800="#6B21A8",
27
+ c900="#581C87",
28
+ c950="#3B0764",
29
+ )
30
+
31
+
32
+ class PurpleTheme(Soft):
33
+ def __init__(
34
+ self,
35
+ *,
36
+ primary_hue: colors.Color | str = colors.gray,
37
+ secondary_hue: colors.Color | str = colors.purple,
38
+ neutral_hue: colors.Color | str = colors.slate,
39
+ text_size: sizes.Size | str = sizes.text_lg,
40
+ font: fonts.Font | str | Iterable[fonts.Font | str] = (
41
+ fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
42
+ ),
43
+ font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
44
+ fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
45
+ ),
46
+ ):
47
+ super().__init__(
48
+ primary_hue=primary_hue,
49
+ secondary_hue=secondary_hue,
50
+ neutral_hue=neutral_hue,
51
+ text_size=text_size,
52
+ font=font,
53
+ font_mono=font_mono,
54
+ )
55
+ super().set(
56
+ background_fill_primary="*primary_50",
57
+ background_fill_primary_dark="*primary_900",
58
+ body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
59
+ body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
60
+ button_primary_text_color="white",
61
+ button_primary_text_color_hover="white",
62
+ button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
63
+ button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
64
+ button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
65
+ button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
66
+ button_secondary_text_color="black",
67
+ button_secondary_text_color_hover="white",
68
+ button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
69
+ button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
70
+ button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
71
+ button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
72
+ slider_color="*secondary_500",
73
+ slider_color_dark="*secondary_600",
74
+ block_title_text_weight="600",
75
+ block_border_width="3px",
76
+ block_shadow="*shadow_drop_lg",
77
+ button_primary_shadow="*shadow_drop_lg",
78
+ button_large_padding="11px",
79
+ color_accent_soft="*primary_100",
80
+ block_label_background_fill="*primary_200",
81
+ )
82
+
83
+
84
+ purple_theme = PurpleTheme()
85
+
86
+ MAX_SEED = np.iinfo(np.int32).max
87
+
88
+ dtype = torch.bfloat16
89
+ device = "cuda" if torch.cuda.is_available() else "cpu"
90
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
91
+ "Qwen/Qwen-Image-Edit-2509",
92
+ transformer=QwenImageTransformer2DModel.from_pretrained(
93
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V4",
94
+ torch_dtype=dtype,
95
+ device_map="cuda",
96
+ ),
97
+ torch_dtype=dtype,
98
+ ).to(device)
99
+ try:
100
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
101
+ print("Flash Attention 3 Processor set successfully.")
102
+ except Exception as e:
103
+ print(f"Warning: Could not set FA3 processor: {e}")
104
+
105
+ ADAPTER_SPECS = {
106
+ "Object-Remover": {
107
+ "repo": "prithivMLmods/QIE-2511-Object-Remover-v2",
108
+ "weights": "Qwen-Image-Edit-2511-Object-Remover-v2-9200.safetensors",
109
+ "adapter_name": "object-remover",
110
+ },
111
+ }
112
+ loaded = False
113
+
114
+ DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
115
+
116
+
117
+ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image:
118
+ import json
119
+ if not pil_image:
120
+ return pil_image
121
+ try:
122
+ boxes = json.loads(boxes_json_str) if boxes_json_str and boxes_json_str.strip() else []
123
+ except Exception:
124
+ boxes = []
125
+ if not boxes:
126
+ return pil_image
127
+
128
+ img = pil_image.copy().convert("RGBA")
129
+ w, h = img.size
130
+ overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
131
+ draw = ImageDraw.Draw(overlay)
132
+ bw = max(3, w // 250)
133
+
134
+ for b in boxes:
135
+ x1 = int(b["x1"] * w)
136
+ y1 = int(b["y1"] * h)
137
+ x2 = int(b["x2"] * w)
138
+ y2 = int(b["y2"] * h)
139
+ lx, rx = min(x1, x2), max(x1, x2)
140
+ ty, by_ = min(y1, y2), max(y1, y2)
141
+ draw.rectangle([lx, ty, rx, by_], fill=(255, 0, 0, 90))
142
+ draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0, 255), width=bw)
143
+
144
+ return Image.alpha_composite(img, overlay).convert("RGB")
145
+
146
+
147
+ @spaces.GPU
148
+ def infer_object_removal(
149
+ source_image: Image.Image,
150
+ boxes_json: str,
151
+ prompt: str,
152
+ seed: int = 0,
153
+ randomize_seed: bool = True,
154
+ guidance_scale: float = 1.0,
155
+ num_inference_steps: int = 4,
156
+ height: int = 1024,
157
+ width: int = 1024,
158
+ ):
159
+ global loaded
160
+ progress = gr.Progress(track_tqdm=True)
161
+
162
+ if not loaded:
163
+ pipe.load_lora_weights(
164
+ ADAPTER_SPECS["Object-Remover"]["repo"],
165
+ weight_name=ADAPTER_SPECS["Object-Remover"]["weights"],
166
+ adapter_name=ADAPTER_SPECS["Object-Remover"]["adapter_name"],
167
+ )
168
+ pipe.set_adapters(
169
+ [ADAPTER_SPECS["Object-Remover"]["adapter_name"]], adapter_weights=[1.0]
170
+ )
171
+ loaded = True
172
+
173
+ if not prompt or prompt.strip() == "":
174
+ prompt = DEFAULT_PROMPT
175
+ print(f"Prompt: {prompt}")
176
+ print(f"Boxes JSON received: '{boxes_json}'")
177
+
178
+ if source_image is None:
179
+ raise gr.Error("Please upload an image first.")
180
+
181
+ import json
182
+ try:
183
+ boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else []
184
+ except Exception as e:
185
+ print(f"JSON parse error: {e}")
186
+ boxes = []
187
+
188
+ if not boxes:
189
+ raise gr.Error("Please draw at least one bounding box on the image.")
190
+
191
+ progress(0.3, desc="Burning red boxes onto image...")
192
+ marked = burn_boxes_onto_image(source_image, boxes_json)
193
+
194
+ progress(0.5, desc="Running object removal inference...")
195
+
196
+ if randomize_seed:
197
+ seed = random.randint(0, MAX_SEED)
198
+ generator = torch.Generator(device=device).manual_seed(seed)
199
+
200
+ result = pipe(
201
+ image=[marked],
202
+ prompt=prompt,
203
+ height=height if height != 0 else None,
204
+ width=width if width != 0 else None,
205
+ num_inference_steps=num_inference_steps,
206
+ generator=generator,
207
+ guidance_scale=guidance_scale,
208
+ num_images_per_prompt=1,
209
+ ).images[0]
210
+
211
+ return result, seed, marked
212
+
213
+
214
+ def update_dimensions_on_upload(image):
215
+ if image is None:
216
+ return 1024, 1024
217
+ original_width, original_height = image.size
218
+ if original_width > original_height:
219
+ new_width = 1024
220
+ aspect_ratio = original_height / original_width
221
+ new_height = int(new_width * aspect_ratio)
222
+ else:
223
+ new_height = 1024
224
+ aspect_ratio = original_width / original_height
225
+ new_width = int(new_height * aspect_ratio)
226
+ new_width = (new_width // 8) * 8
227
+ new_height = (new_height // 8) * 8
228
+ return new_width, new_height
229
+
230
+
231
+ css = r"""
232
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
233
+ body,.gradio-container{background-color:#FAF5FF!important;background-image:linear-gradient(#E9D5FF 1px,transparent 1px),linear-gradient(90deg,#E9D5FF 1px,transparent 1px)!important;background-size:40px 40px!important;font-family:'Outfit',sans-serif!important}
234
+ .dark body,.dark .gradio-container{background-color:#1a1a1a!important;background-image:linear-gradient(rgba(168,85,247,.1) 1px,transparent 1px),linear-gradient(90deg,rgba(168,85,247,.1) 1px,transparent 1px)!important;background-size:40px 40px!important}
235
+ #col-container{margin:0 auto;max-width:1200px}
236
+ #main-title{text-align:center!important;padding:1rem 0 .5rem 0}
237
+ #main-title h1{font-size:2.4em!important;font-weight:700!important;background:linear-gradient(135deg,#A855F7 0%,#C084FC 50%,#9333EA 100%);background-size:200% 200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:gradient-shift 4s ease infinite;letter-spacing:-.02em}
238
+ @keyframes gradient-shift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
239
+ #subtitle{text-align:center!important;margin-bottom:1.5rem}
240
+ #subtitle p{margin:0 auto;color:#666;font-size:1rem;text-align:center!important}
241
+ #subtitle a{color:#A855F7!important;text-decoration:none;font-weight:500}
242
+ #subtitle a:hover{text-decoration:underline}
243
+ .gradio-group{background:rgba(255,255,255,.9)!important;border:2px solid #E9D5FF!important;border-radius:12px!important;box-shadow:0 4px 24px rgba(168,85,247,.08)!important;backdrop-filter:blur(10px);transition:all .3s ease}
244
+ .gradio-group:hover{box-shadow:0 8px 32px rgba(168,85,247,.12)!important;border-color:#C084FC!important}
245
+ .dark .gradio-group{background:rgba(30,30,30,.9)!important;border-color:rgba(168,85,247,.3)!important}
246
+ .primary{border-radius:8px!important;font-weight:600!important;letter-spacing:.02em!important;transition:all .3s ease!important}
247
+ .primary:hover{transform:translateY(-2px)!important}
248
+ .gradio-textbox textarea{font-family:'IBM Plex Mono',monospace!important;font-size:.95rem!important;line-height:1.7!important;background:rgba(255,255,255,.95)!important;border:1px solid #E9D5FF!important;border-radius:8px!important}
249
+ .gradio-accordion{border-radius:10px!important;border:1px solid #E9D5FF!important}
250
+ .gradio-accordion>.label-wrap{background:rgba(168,85,247,.03)!important;border-radius:10px!important}
251
+ footer{display:none!important}
252
+ @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
253
+ .gradio-row{animation:fadeIn .4s ease-out}
254
+ label{font-weight:600!important;color:#333!important}
255
+ .dark label{color:#eee!important}
256
+ .gradio-slider input[type="range"]{accent-color:#A855F7!important}
257
+ ::-webkit-scrollbar{width:8px;height:8px}
258
+ ::-webkit-scrollbar-track{background:rgba(168,85,247,.05);border-radius:4px}
259
+ ::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
260
+ ::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
261
+
262
+ #bbox-draw-wrap{position:relative;border:2px solid #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px}
263
+ #bbox-draw-wrap:hover{border-color:#A855F7}
264
+ #bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
265
+ .bbox-hint{background:rgba(168,85,247,.08);border:1px solid #E9D5FF;border-radius:8px;padding:10px 16px;margin:8px 0;font-size:.9rem;color:#6B21A8}
266
+ .dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
267
+
268
+ .bbox-toolbar-section{
269
+ display:flex;
270
+ gap:8px;
271
+ flex-wrap:wrap;
272
+ justify-content:center;
273
+ align-items:center;
274
+ padding:12px 16px;
275
+ margin-top:10px;
276
+ background:rgba(255,255,255,.92);
277
+ border:2px solid #E9D5FF;
278
+ border-radius:10px;
279
+ box-shadow:0 2px 12px rgba(168,85,247,.08);
280
+ }
281
+ .dark .bbox-toolbar-section{
282
+ background:rgba(30,30,30,.9);
283
+ border-color:rgba(168,85,247,.3);
284
+ }
285
+ .bbox-toolbar-section .toolbar-label{
286
+ font-family:'Outfit',sans-serif;
287
+ font-weight:600;
288
+ font-size:13px;
289
+ color:#6B21A8;
290
+ margin-right:6px;
291
+ user-select:none;
292
+ }
293
+ .dark .bbox-toolbar-section .toolbar-label{color:#C084FC}
294
+ .bbox-toolbar-section .toolbar-divider{
295
+ width:1px;
296
+ height:28px;
297
+ background:#E9D5FF;
298
+ margin:0 4px;
299
+ }
300
+ .dark .bbox-toolbar-section .toolbar-divider{background:rgba(168,85,247,.3)}
301
+ .bbox-toolbar-section button{
302
+ color:#fff;
303
+ border:none;
304
+ padding:7px 15px;
305
+ border-radius:7px;
306
+ cursor:pointer;
307
+ font-family:'Outfit',sans-serif;
308
+ font-weight:600;
309
+ font-size:13px;
310
+ box-shadow:0 2px 5px rgba(0,0,0,.15);
311
+ transition:background .2s,transform .15s,box-shadow .2s;
312
+ }
313
+ .bbox-toolbar-section button:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(0,0,0,.2)}
314
+ .bbox-toolbar-section button:active{transform:translateY(0)}
315
+ .bbox-tb-draw{background:#9333EA}
316
+ .bbox-tb-draw:hover{background:#A855F7}
317
+ .bbox-tb-draw.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
318
+ .bbox-tb-select{background:#6366f1}
319
+ .bbox-tb-select:hover{background:#818cf8}
320
+ .bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
321
+ .bbox-tb-del{background:#dc2626}
322
+ .bbox-tb-del:hover{background:#ef4444}
323
+ .bbox-tb-undo{background:#7E22CE}
324
+ .bbox-tb-undo:hover{background:#9333EA}
325
+ .bbox-tb-clear{background:#be123c}
326
+ .bbox-tb-clear:hover{background:#e11d48}
327
+
328
+ #bbox-status{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.75);color:#00ff88;padding:5px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none;pointer-events:none}
329
+ #bbox-count{position:absolute;top:10px;right:10px;background:rgba(147,51,234,.85);color:#fff;padding:4px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none}
330
+
331
+ #bbox-debug-count{
332
+ text-align:center;
333
+ padding:6px 12px;
334
+ margin-top:6px;
335
+ font-family:'IBM Plex Mono',monospace;
336
+ font-size:12px;
337
+ color:#6B21A8;
338
+ background:rgba(168,85,247,.06);
339
+ border:1px dashed #C084FC;
340
+ border-radius:6px;
341
+ }
342
+ .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
343
+
344
+ /* ===== FIX: hide the sync textbox visually but keep it in the DOM ===== */
345
+ #boxes-json-input{
346
+ max-height:0!important;
347
+ overflow:hidden!important;
348
+ margin:0!important;
349
+ padding:0!important;
350
+ opacity:0!important;
351
+ pointer-events:none!important;
352
+ position:absolute!important;
353
+ z-index:-1!important;
354
+ }
355
+ """
356
+
357
+ bbox_drawer_js = r"""
358
+ () => {
359
+ function initCanvasBbox() {
360
+ if (window.__bboxInitDone) return;
361
+
362
+ const canvas = document.getElementById('bbox-draw-canvas');
363
+ const wrap = document.getElementById('bbox-draw-wrap');
364
+ const status = document.getElementById('bbox-status');
365
+ const badge = document.getElementById('bbox-count');
366
+ const debugCount = document.getElementById('bbox-debug-count');
367
+
368
+ const btnDraw = document.getElementById('tb-draw');
369
+ const btnSelect = document.getElementById('tb-select');
370
+ const btnDel = document.getElementById('tb-del');
371
+ const btnUndo = document.getElementById('tb-undo');
372
+ const btnClear = document.getElementById('tb-clear');
373
+
374
+ if (!canvas || !wrap || !debugCount || !btnDraw) {
375
+ console.log('[BBox] waiting for DOM …');
376
+ setTimeout(initCanvasBbox, 250);
377
+ return;
378
+ }
379
+
380
+ window.__bboxInitDone = true;
381
+ console.log('[BBox] canvas init OK');
382
+ const ctx = canvas.getContext('2d');
383
+
384
+ let boxes = [];
385
+ window.__bboxBoxes = boxes;
386
+
387
+ let baseImg = null;
388
+ let dispW = 512, dispH = 400;
389
+ let selectedIdx = -1;
390
+ let mode = 'draw';
391
+
392
+ let dragging = false;
393
+ let dragType = null;
394
+ let dragStart = {x:0, y:0};
395
+ let dragOrig = null;
396
+ const HANDLE = 7;
397
+
398
+ /* ── helpers ── */
399
+ function n2px(b) {
400
+ return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH};
401
+ }
402
+ function px2n(x1,y1,x2,y2) {
403
+ return {
404
+ x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
405
+ x2: Math.max(x1,x2)/dispW, y2: Math.max(y1,y2)/dispH
406
+ };
407
+ }
408
+ function clamp01(v){return Math.max(0,Math.min(1,v));}
409
+ function fitSize(nw, nh) {
410
+ const mw = wrap.clientWidth || 512, mh = 500;
411
+ const r = Math.min(mw/nw, mh/nh, 1);
412
+ dispW = Math.round(nw*r); dispH = Math.round(nh*r);
413
+ canvas.width = dispW; canvas.height = dispH;
414
+ canvas.style.width = dispW+'px';
415
+ canvas.style.height = dispH+'px';
416
+ }
417
+ function canvasXY(e) {
418
+ const r = canvas.getBoundingClientRect();
419
+ const cx = e.touches ? e.touches[0].clientX : e.clientX;
420
+ const cy = e.touches ? e.touches[0].clientY : e.clientY;
421
+ return {x: Math.max(0,Math.min(dispW, cx-r.left)),
422
+ y: Math.max(0,Math.min(dispH, cy-r.top))};
423
+ }
424
+
425
+ /* ── FIX: always update debug + badge, then try textbox ── */
426
+ function syncToGradio() {
427
+ window.__bboxBoxes = boxes;
428
+ const jsonStr = JSON.stringify(boxes);
429
+
430
+ /* 1) update on-screen debug no matter what */
431
+ if (debugCount) {
432
+ debugCount.textContent = boxes.length > 0
433
+ ? '\u2705 ' + boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') +
434
+ ' ready | JSON: ' + jsonStr.substring(0,80) +
435
+ (jsonStr.length > 80 ? '\u2026' : '')
436
+ : '\u2B1C No boxes drawn yet';
437
+ }
438
+
439
+ /* 2) try to push into the Gradio Textbox (best-effort) */
440
+ const container = document.getElementById('boxes-json-input');
441
+ if (!container) {
442
+ console.warn('[BBox] #boxes-json-input not in DOM (hidden?)');
443
+ return;
444
+ }
445
+ const targets = [
446
+ ...container.querySelectorAll('textarea'),
447
+ ...container.querySelectorAll('input[type="text"]'),
448
+ ...container.querySelectorAll('input:not([type])')
449
+ ];
450
+ targets.forEach(el => {
451
+ const proto = el.tagName === 'TEXTAREA'
452
+ ? HTMLTextAreaElement.prototype
453
+ : HTMLInputElement.prototype;
454
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
455
+ if (ns && ns.set) {
456
+ ns.set.call(el, jsonStr);
457
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
458
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
459
+ el.dispatchEvent(new Event('blur', {bubbles:true, composed:true}));
460
+ }
461
+ });
462
+ }
463
+
464
+ /* ── draw ── */
465
+ function placeholder() {
466
+ ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,dispW,dispH);
467
+ ctx.strokeStyle='#444'; ctx.lineWidth=2; ctx.setLineDash([8,4]);
468
+ ctx.strokeRect(20,20,dispW-40,dispH-40); ctx.setLineDash([]);
469
+ ctx.fillStyle='#888'; ctx.font='16px Outfit,sans-serif';
470
+ ctx.textAlign='center'; ctx.textBaseline='middle';
471
+ ctx.fillText('Upload an image above first',dispW/2,dispH/2-10);
472
+ ctx.font='13px Outfit'; ctx.fillStyle='#666';
473
+ ctx.fillText('Then draw red boxes on objects to remove',dispW/2,dispH/2+14);
474
+ }
475
+
476
+ function redraw(tempRect) {
477
+ ctx.clearRect(0,0,dispW,dispH);
478
+ if (!baseImg) { placeholder(); updateBadge(); return; }
479
+ ctx.drawImage(baseImg, 0, 0, dispW, dispH);
480
+
481
+ boxes.forEach((b,i) => {
482
+ const p = n2px(b);
483
+ const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
484
+
485
+ ctx.fillStyle = 'rgba(255,0,0,0.25)';
486
+ ctx.fillRect(lx,ty,w,h);
487
+
488
+ if (i === selectedIdx) {
489
+ ctx.strokeStyle = 'rgba(0,120,255,0.95)';
490
+ ctx.lineWidth = 2.5; ctx.setLineDash([6,3]);
491
+ } else {
492
+ ctx.strokeStyle = 'rgba(255,0,0,0.9)';
493
+ ctx.lineWidth = 2.5; ctx.setLineDash([]);
494
+ }
495
+ ctx.strokeRect(lx,ty,w,h);
496
+ ctx.setLineDash([]);
497
+
498
+ ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
499
+ ctx.font = 'bold 11px IBM Plex Mono,monospace';
500
+ ctx.textAlign = 'left'; ctx.textBaseline = 'top';
501
+ const label = '#'+(i+1);
502
+ const tw = ctx.measureText(label).width;
503
+ ctx.fillRect(lx, ty-16, tw+6, 16);
504
+ ctx.fillStyle = '#fff';
505
+ ctx.fillText(label, lx+3, ty-14);
506
+
507
+ if (i === selectedIdx) drawHandles(p);
508
+ });
509
+
510
+ if (tempRect) {
511
+ const rx = Math.min(tempRect.x1,tempRect.x2);
512
+ const ry = Math.min(tempRect.y1,tempRect.y2);
513
+ const rw = Math.abs(tempRect.x2-tempRect.x1);
514
+ const rh = Math.abs(tempRect.y2-tempRect.y1);
515
+ ctx.fillStyle='rgba(255,0,0,0.12)';
516
+ ctx.fillRect(rx,ry,rw,rh);
517
+ ctx.strokeStyle='rgba(255,0,0,0.7)';
518
+ ctx.lineWidth=2; ctx.setLineDash([6,3]);
519
+ ctx.strokeRect(rx,ry,rw,rh); ctx.setLineDash([]);
520
+ }
521
+ updateBadge();
522
+ }
523
+
524
+ function drawHandles(p) {
525
+ const pts = handlePoints(p);
526
+ ctx.fillStyle = 'rgba(0,120,255,0.9)';
527
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
528
+ for (const k in pts) {
529
+ const h = pts[k];
530
+ ctx.fillRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2);
531
+ ctx.strokeRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2);
532
+ }
533
+ }
534
+
535
+ function handlePoints(p) {
536
+ const mx = (p.x1+p.x2)/2, my = (p.y1+p.y2)/2;
537
+ return {
538
+ tl:{x:p.x1,y:p.y1}, tc:{x:mx,y:p.y1}, tr:{x:p.x2,y:p.y1},
539
+ ml:{x:p.x1,y:my}, mr:{x:p.x2,y:my},
540
+ bl:{x:p.x1,y:p.y2}, bc:{x:mx,y:p.y2}, br:{x:p.x2,y:p.y2}
541
+ };
542
+ }
543
+
544
+ function hitHandle(px, py, boxIdx) {
545
+ if (boxIdx < 0) return null;
546
+ const p = n2px(boxes[boxIdx]);
547
+ const pts = handlePoints(p);
548
+ for (const k in pts) {
549
+ if (Math.abs(px-pts[k].x) <= HANDLE+2 && Math.abs(py-pts[k].y) <= HANDLE+2) return k;
550
+ }
551
+ return null;
552
+ }
553
+
554
+ function hitBox(px, py) {
555
+ for (let i = boxes.length-1; i >= 0; i--) {
556
+ const p = n2px(boxes[i]);
557
+ if (px >= p.x1 && px <= p.x2 && py >= p.y1 && py <= p.y2) return i;
558
+ }
559
+ return -1;
560
+ }
561
+
562
+ function updateBadge() {
563
+ if (boxes.length > 0) {
564
+ badge.style.display = 'block';
565
+ badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':'');
566
+ } else {
567
+ badge.style.display = 'none';
568
+ }
569
+ }
570
+
571
+ function setMode(m) {
572
+ mode = m;
573
+ btnDraw.classList.toggle('active', m==='draw');
574
+ btnSelect.classList.toggle('active', m==='select');
575
+ canvas.style.cursor = m==='draw' ? 'crosshair' : 'default';
576
+ if (m==='draw') selectedIdx = -1;
577
+ redraw();
578
+ }
579
+
580
+ function showStatus(txt) {
581
+ status.textContent = txt; status.style.display = 'block';
582
+ }
583
+ function hideStatus() { status.style.display = 'none'; }
584
+
585
+ /* ── pointer events ── */
586
+ function onDown(e) {
587
+ if (!baseImg) return;
588
+ e.preventDefault();
589
+ const {x, y} = canvasXY(e);
590
+
591
+ if (mode === 'draw') {
592
+ dragging = true; dragType = 'new';
593
+ dragStart = {x, y};
594
+ selectedIdx = -1;
595
+ } else {
596
+ if (selectedIdx >= 0) {
597
+ const h = hitHandle(x, y, selectedIdx);
598
+ if (h) {
599
+ dragging = true; dragType = h;
600
+ dragStart = {x, y};
601
+ dragOrig = {...boxes[selectedIdx]};
602
+ showStatus('Resizing box #'+(selectedIdx+1));
603
+ return;
604
+ }
605
+ }
606
+ const hi = hitBox(x, y);
607
+ if (hi >= 0) {
608
+ selectedIdx = hi;
609
+ const h2 = hitHandle(x, y, selectedIdx);
610
+ if (h2) {
611
+ dragging = true; dragType = h2;
612
+ dragStart = {x, y};
613
+ dragOrig = {...boxes[selectedIdx]};
614
+ showStatus('Resizing box #'+(selectedIdx+1));
615
+ redraw(); return;
616
+ }
617
+ dragging = true; dragType = 'move';
618
+ dragStart = {x, y};
619
+ dragOrig = {...boxes[selectedIdx]};
620
+ showStatus('Moving box #'+(selectedIdx+1));
621
+ } else {
622
+ selectedIdx = -1;
623
+ hideStatus();
624
+ }
625
+ redraw();
626
+ }
627
+ }
628
+
629
+ function onMove(e) {
630
+ if (!baseImg) return;
631
+ e.preventDefault();
632
+ const {x, y} = canvasXY(e);
633
+
634
+ if (!dragging) {
635
+ if (mode === 'select') {
636
+ if (selectedIdx >= 0 && hitHandle(x,y,selectedIdx)) {
637
+ const h = hitHandle(x,y,selectedIdx);
638
+ const curs = {tl:'nwse-resize',tr:'nesw-resize',bl:'nesw-resize',br:'nwse-resize',
639
+ tc:'ns-resize',bc:'ns-resize',ml:'ew-resize',mr:'ew-resize'};
640
+ canvas.style.cursor = curs[h] || 'move';
641
+ } else if (hitBox(x,y) >= 0) {
642
+ canvas.style.cursor = 'move';
643
+ } else {
644
+ canvas.style.cursor = 'default';
645
+ }
646
+ }
647
+ return;
648
+ }
649
+
650
+ if (dragType === 'new') {
651
+ redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y});
652
+ showStatus(Math.abs(x-dragStart.x).toFixed(0)+'\u00d7'+Math.abs(y-dragStart.y).toFixed(0)+' px');
653
+ return;
654
+ }
655
+
656
+ const dx = (x - dragStart.x) / dispW;
657
+ const dy = (y - dragStart.y) / dispH;
658
+ const b = boxes[selectedIdx];
659
+ const o = dragOrig;
660
+
661
+ if (dragType === 'move') {
662
+ const bw = o.x2-o.x1, bh = o.y2-o.y1;
663
+ let nx1 = o.x1+dx, ny1 = o.y1+dy;
664
+ nx1 = clamp01(nx1); ny1 = clamp01(ny1);
665
+ if (nx1+bw > 1) nx1 = 1-bw;
666
+ if (ny1+bh > 1) ny1 = 1-bh;
667
+ b.x1=nx1; b.y1=ny1; b.x2=nx1+bw; b.y2=ny1+bh;
668
+ } else {
669
+ const t = dragType;
670
+ if (t.includes('l')) b.x1 = clamp01(o.x1 + dx);
671
+ if (t.includes('r')) b.x2 = clamp01(o.x2 + dx);
672
+ if (t.includes('t')) b.y1 = clamp01(o.y1 + dy);
673
+ if (t.includes('b')) b.y2 = clamp01(o.y2 + dy);
674
+ if (Math.abs(b.x2-b.x1) < 0.01) { b.x1=o.x1; b.x2=o.x2; }
675
+ if (Math.abs(b.y2-b.y1) < 0.01) { b.y1=o.y1; b.y2=o.y2; }
676
+ if (b.x1 > b.x2) { const t2=b.x1; b.x1=b.x2; b.x2=t2; }
677
+ if (b.y1 > b.y2) { const t2=b.y1; b.y1=b.y2; b.y2=t2; }
678
+ }
679
+ redraw();
680
+ }
681
+
682
+ function onUp(e) {
683
+ if (!dragging) return;
684
+ if (e) e.preventDefault();
685
+ dragging = false;
686
+
687
+ if (dragType === 'new') {
688
+ const pt = e ? canvasXY(e) : {x:dragStart.x, y:dragStart.y};
689
+ if (Math.abs(pt.x-dragStart.x) > 4 && Math.abs(pt.y-dragStart.y) > 4) {
690
+ const nb = px2n(dragStart.x, dragStart.y, pt.x, pt.y);
691
+ boxes.push(nb);
692
+ window.__bboxBoxes = boxes;
693
+ selectedIdx = boxes.length - 1;
694
+ console.log('[BBox] created box #'+boxes.length, nb);
695
+ showStatus('Box #'+boxes.length+' created');
696
+ } else { hideStatus(); }
697
+ } else {
698
+ showStatus('Box #'+(selectedIdx+1)+' updated');
699
+ }
700
+ dragType = null; dragOrig = null;
701
+ syncToGradio();
702
+ redraw();
703
+ }
704
+
705
+ canvas.addEventListener('mousedown', onDown);
706
+ canvas.addEventListener('mousemove', onMove);
707
+ canvas.addEventListener('mouseup', onUp);
708
+ canvas.addEventListener('mouseleave', (e)=>{if(dragging)onUp(e);});
709
+ canvas.addEventListener('touchstart', onDown, {passive:false});
710
+ canvas.addEventListener('touchmove', onMove, {passive:false});
711
+ canvas.addEventListener('touchend', onUp, {passive:false});
712
+ canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
713
+
714
+ /* ── toolbar ── */
715
+ btnDraw.addEventListener('click', ()=>setMode('draw'));
716
+ btnSelect.addEventListener('click', ()=>setMode('select'));
717
+
718
+ btnDel.addEventListener('click', () => {
719
+ if (selectedIdx >= 0 && selectedIdx < boxes.length) {
720
+ const removed = selectedIdx + 1;
721
+ boxes.splice(selectedIdx, 1);
722
+ window.__bboxBoxes = boxes;
723
+ selectedIdx = -1;
724
+ syncToGradio(); redraw();
725
+ showStatus('Box #'+removed+' deleted');
726
+ } else {
727
+ showStatus('No box selected');
728
+ }
729
+ });
730
+
731
+ btnUndo.addEventListener('click', () => {
732
+ if (boxes.length > 0) {
733
+ boxes.pop();
734
+ window.__bboxBoxes = boxes;
735
+ selectedIdx = -1;
736
+ syncToGradio(); redraw();
737
+ showStatus('Last box removed');
738
+ }
739
+ });
740
+
741
+ btnClear.addEventListener('click', () => {
742
+ boxes.length = 0; /* clear in-place */
743
+ window.__bboxBoxes = boxes;
744
+ selectedIdx = -1;
745
+ syncToGradio(); redraw(); hideStatus();
746
+ });
747
+
748
+ /* ── FIX: robust image polling with multiple selectors ── */
749
+ let lastSrc = null;
750
+ setInterval(() => {
751
+ /* try several selectors Gradio might use */
752
+ const imgs = document.querySelectorAll('#source-image-component img');
753
+ let el = null;
754
+ for (const img of imgs) {
755
+ if (img.src && img.src.length > 30 &&
756
+ (img.src.startsWith('data:') ||
757
+ img.src.startsWith('blob:') ||
758
+ img.src.includes('/file=') ||
759
+ img.src.includes('/upload') ||
760
+ img.src.includes('localhost') ||
761
+ img.src.includes('127.0.0.1') ||
762
+ img.src.startsWith('http'))) {
763
+ el = img;
764
+ break;
765
+ }
766
+ }
767
+
768
+ if (el && el.src && el.src !== lastSrc) {
769
+ lastSrc = el.src;
770
+ const img = new window.Image();
771
+ img.crossOrigin = 'anonymous';
772
+ img.onload = () => {
773
+ baseImg = img;
774
+ boxes.length = 0;
775
+ window.__bboxBoxes = boxes;
776
+ selectedIdx = -1;
777
+ fitSize(img.naturalWidth, img.naturalHeight);
778
+ syncToGradio(); redraw(); hideStatus();
779
+ console.log('[BBox] loaded image', img.naturalWidth, 'x', img.naturalHeight);
780
+ };
781
+ img.onerror = () => {
782
+ console.warn('[BBox] image load failed for', el.src.substring(0,60));
783
+ };
784
+ img.src = el.src;
785
+ } else if (!el || !el.src) {
786
+ if (baseImg) {
787
+ baseImg = null;
788
+ boxes.length = 0;
789
+ window.__bboxBoxes = boxes;
790
+ selectedIdx = -1;
791
+ fitSize(512,400); syncToGradio(); redraw(); hideStatus();
792
+ }
793
+ lastSrc = null;
794
+ }
795
+ }, 300);
796
+
797
+ /* periodic re-sync every 500 ms */
798
+ setInterval(() => { syncToGradio(); }, 500);
799
+
800
+ new ResizeObserver(() => {
801
+ if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
802
+ }).observe(wrap);
803
+
804
+ setMode('draw');
805
+ fitSize(512,400); redraw();
806
+ syncToGradio();
807
+ }
808
+
809
+ initCanvasBbox();
810
+ }
811
+ """
812
+
813
+
814
+ with gr.Blocks() as demo:
815
+ gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
816
+ gr.Markdown(
817
+ "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). Upload an image, draw red bounding boxes over the objects you want to remove, and click Remove Object."
818
+ "Multiple boxes supported. Select, move, resize or delete individual boxes.",
819
+ elem_id="subtitle",
820
+ )
821
+
822
+ with gr.Row():
823
+ with gr.Column(scale=1):
824
+ source_image = gr.Image(
825
+ label="Upload Image",
826
+ type="pil",
827
+ height=350,
828
+ elem_id="source-image-component",
829
+ )
830
+
831
+ gr.Markdown("# **Bbox Edit Controller**")
832
+ gr.HTML(
833
+ '<div class="bbox-hint">'
834
+ "<b>Draw mode:</b> Click & drag to create red rectangles. "
835
+ "<b>Select mode:</b> Click a box to select it β†’ drag to <b>move</b>, "
836
+ "drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
837
+ "</div>"
838
+ )
839
+
840
+ gr.HTML(
841
+ """
842
+ <div id="bbox-draw-wrap">
843
+ <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
844
+ <div id="bbox-status"></div>
845
+ <div id="bbox-count"></div>
846
+ </div>
847
+ """
848
+ )
849
+
850
+ gr.HTML(
851
+ """
852
+ <div class="bbox-toolbar-section">
853
+ <span class="toolbar-label">πŸ›  Tools:</span>
854
+ <button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">✏️ Draw</button>
855
+ <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">πŸ”² Select</button>
856
+ <div class="toolbar-divider"></div>
857
+ <span class="toolbar-label">Actions:</span>
858
+ <button id="tb-del" class="bbox-tb-del" title="Delete selected box">βœ• Delete Selected</button>
859
+ <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
860
+ <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">πŸ—‘ Clear All</button>
861
+ </div>
862
+ """
863
+ )
864
+
865
+ gr.HTML('<div id="bbox-debug-count">⬜ No boxes drawn yet</div>')
866
+
867
+ boxes_json = gr.Textbox(
868
+ value="[]",
869
+ visible=True,
870
+ interactive=True,
871
+ elem_id="boxes-json-input",
872
+ label="boxes-json",
873
+ )
874
+
875
+ prompt = gr.Textbox(
876
+ label="Prompt",
877
+ value=DEFAULT_PROMPT,
878
+ lines=2,
879
+ info="Edit the prompt if needed",
880
+ )
881
+
882
+ run_btn = gr.Button("πŸ—‘οΈ Remove Object", variant="primary", size="lg")
883
+
884
+ with gr.Column(scale=1):
885
+ result = gr.Image(label="Output Image", height=449)
886
+ preview = gr.Image(label="Input Sent to Model (with red boxes)", height=393)
887
+
888
+ with gr.Accordion("Advanced Settings", open=True):
889
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
890
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
891
+ with gr.Row():
892
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
893
+ num_inference_steps = gr.Slider(label="Inference Steps", minimum=1, maximum=20, step=1, value=4)
894
+ with gr.Row():
895
+ height_slider = gr.Slider(label="Height", minimum=256, maximum=2048, step=8, value=1024)
896
+ width_slider = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
897
+
898
+ with gr.Accordion("About", open=False):
899
+ gr.Markdown(
900
+ "*QIE-Object-Remover-Bbox* by "
901
+ "[prithivMLmods](https://huggingface.co/prithivMLmods). "
902
+ "Adapter: [QIE-2511-Object-Remover-v2]"
903
+ "(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
904
+ "More adapters β†’ [Qwen-Image-Edit-LoRAs]"
905
+ "(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
906
+ )
907
+
908
+ demo.load(fn=None, js=bbox_drawer_js)
909
+
910
+ run_btn.click(
911
+ fn=infer_object_removal,
912
+ inputs=[source_image, boxes_json, prompt, seed, randomize_seed,
913
+ guidance_scale, num_inference_steps, height_slider, width_slider],
914
+ outputs=[result, seed, preview],
915
+ js="""(src, bj, p, s, rs, gs, nis, h, w) => {
916
+ const boxes = window.__bboxBoxes || [];
917
+ const json = JSON.stringify(boxes);
918
+ console.log('[BBox] submitting', boxes.length, 'boxes:', json);
919
+ return [src, json, p, s, rs, gs, nis, h, w];
920
+ }""",
921
+ )
922
+
923
+ source_image.upload(
924
+ fn=update_dimensions_on_upload,
925
+ inputs=[source_image],
926
+ outputs=[width_slider, height_slider],
927
+ )
928
+
929
+
930
+ if __name__ == "__main__":
931
+ demo.launch(
932
+ css=css, theme=purple_theme,
933
+ mcp_server=True, ssr_mode=False, show_error=True
934
+ )