update bbox v3 [QIE-2509-Object-Remover-Bbox-v3] ✅

#4
Files changed (1) hide show
  1. app.py +1485 -790
app.py CHANGED
@@ -4,87 +4,14 @@ import random
4
  import torch
5
  import spaces
6
  import base64
 
7
  from io import BytesIO
8
- from typing import Iterable
9
  from PIL import Image, ImageDraw
10
  from diffusers import FlowMatchEulerDiscreteScheduler
11
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
12
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
13
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
14
 
15
- from gradio.themes import Soft
16
- from gradio.themes.utils import colors, fonts, sizes
17
-
18
- colors.purple = colors.Color(
19
- name="purple",
20
- c50="#FAF5FF",
21
- c100="#F3E8FF",
22
- c200="#E9D5FF",
23
- c300="#DAB2FF",
24
- c400="#C084FC",
25
- c500="#A855F7",
26
- c600="#9333EA",
27
- c700="#7E22CE",
28
- c800="#6B21A8",
29
- c900="#581C87",
30
- c950="#3B0764",
31
- )
32
-
33
-
34
- class PurpleTheme(Soft):
35
- def __init__(
36
- self,
37
- *,
38
- primary_hue: colors.Color | str = colors.gray,
39
- secondary_hue: colors.Color | str = colors.purple,
40
- neutral_hue: colors.Color | str = colors.slate,
41
- text_size: sizes.Size | str = sizes.text_lg,
42
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
43
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
44
- ),
45
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
46
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
47
- ),
48
- ):
49
- super().__init__(
50
- primary_hue=primary_hue,
51
- secondary_hue=secondary_hue,
52
- neutral_hue=neutral_hue,
53
- text_size=text_size,
54
- font=font,
55
- font_mono=font_mono,
56
- )
57
- super().set(
58
- background_fill_primary="*primary_50",
59
- background_fill_primary_dark="*primary_900",
60
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
61
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
62
- button_primary_text_color="white",
63
- button_primary_text_color_hover="white",
64
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
65
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
66
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
67
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
68
- button_secondary_text_color="black",
69
- button_secondary_text_color_hover="white",
70
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
71
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
72
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
73
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
74
- slider_color="*secondary_500",
75
- slider_color_dark="*secondary_600",
76
- block_title_text_weight="600",
77
- block_border_width="3px",
78
- block_shadow="*shadow_drop_lg",
79
- button_primary_shadow="*shadow_drop_lg",
80
- button_large_padding="11px",
81
- color_accent_soft="*primary_100",
82
- block_label_background_fill="*primary_200",
83
- )
84
-
85
-
86
- purple_theme = PurpleTheme()
87
-
88
  MAX_SEED = np.iinfo(np.int32).max
89
 
90
  dtype = torch.bfloat16
@@ -105,9 +32,9 @@ except Exception as e:
105
  print(f"Warning: Could not set FA3 processor: {e}")
106
 
107
  ADAPTER_SPECS = {
108
- "Object-Remover": {
109
- "repo": "prithivMLmods/QIE-2509-Object-Remover-Bbox",
110
- "weights": "QIE-2509-Object-Remover-Bbox-5000.safetensors",
111
  "adapter_name": "object-remover",
112
  },
113
  }
@@ -116,8 +43,7 @@ loaded = False
116
  DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
117
 
118
 
119
- def b64_to_pil(b64_str: str) -> Image.Image | None:
120
- """Helper to decode base64 string from JS into a PIL Image"""
121
  if not b64_str or not b64_str.startswith("data:image"):
122
  return None
123
  try:
@@ -129,9 +55,7 @@ def b64_to_pil(b64_str: str) -> Image.Image | None:
129
  return None
130
 
131
 
132
- def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image:
133
- """Burn red outline-only rectangles onto the image (no fill)."""
134
- import json
135
  if not pil_image:
136
  return pil_image
137
  try:
@@ -153,7 +77,6 @@ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.
153
  y2 = int(b["y2"] * h)
154
  lx, rx = min(x1, x2), max(x1, x2)
155
  ty, by_ = min(y1, y2), max(y1, y2)
156
- # Red outline only — no fill
157
  draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
158
 
159
  return img
@@ -161,15 +84,15 @@ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.
161
 
162
  @spaces.GPU
163
  def infer_object_removal(
164
- b64_str: str,
165
- boxes_json: str,
166
- prompt: str,
167
- seed: int = 0,
168
- randomize_seed: bool = True,
169
- guidance_scale: float = 1.0,
170
- num_inference_steps: int = 4,
171
- height: int = 1024,
172
- width: int = 1024,
173
  ):
174
  global loaded
175
  progress = gr.Progress(track_tqdm=True)
@@ -187,18 +110,14 @@ def infer_object_removal(
187
 
188
  if not prompt or prompt.strip() == "":
189
  prompt = DEFAULT_PROMPT
190
- print(f"Prompt: {prompt}")
191
- print(f"Boxes JSON received: '{boxes_json}'")
192
 
193
  source_image = b64_to_pil(b64_str)
194
  if source_image is None:
195
- raise gr.Error("Please upload an image first using the Bbox editor area.")
196
 
197
- import json
198
  try:
199
  boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else []
200
- except Exception as e:
201
- print(f"JSON parse error: {e}")
202
  boxes = []
203
 
204
  if not boxes:
@@ -227,7 +146,7 @@ def infer_object_removal(
227
  return result, seed, marked
228
 
229
 
230
- def update_dimensions_on_upload(b64_str: str):
231
  image = b64_to_pil(b64_str)
232
  if image is None:
233
  return 1024, 1024
@@ -246,797 +165,1574 @@ def update_dimensions_on_upload(b64_str: str):
246
 
247
 
248
  css = r"""
249
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
250
- 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}
251
- .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}
252
- #col-container{margin:0 auto;max-width:1200px}
253
- #main-title{text-align:center!important;padding:1rem 0 .5rem 0}
254
- #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}
255
- @keyframes gradient-shift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
256
- #subtitle{text-align:center!important;margin-bottom:1.5rem}
257
- #subtitle p{margin:0 auto;color:#666;font-size:1rem;text-align:center!important}
258
- #subtitle a{color:#A855F7!important;text-decoration:none;font-weight:500}
259
- #subtitle a:hover{text-decoration:underline}
260
- .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}
261
- .gradio-group:hover{box-shadow:0 8px 32px rgba(168,85,247,.12)!important;border-color:#C084FC!important}
262
- .dark .gradio-group{background:rgba(30,30,30,.9)!important;border-color:rgba(168,85,247,.3)!important}
263
- .primary{border-radius:8px!important;font-weight:600!important;letter-spacing:.02em!important;transition:all .3s ease!important}
264
- .primary:hover{transform:translateY(-2px)!important}
265
- .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}
266
- .gradio-accordion{border-radius:10px!important;border:1px solid #E9D5FF!important}
267
- .gradio-accordion>.label-wrap{background:rgba(168,85,247,.03)!important;border-radius:10px!important}
268
  footer{display:none!important}
269
- @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
270
- .gradio-row{animation:fadeIn .4s ease-out}
271
- label{font-weight:600!important;color:#333!important}
272
- .dark label{color:#eee!important}
273
- .gradio-slider input[type="range"]{accent-color:#A855F7!important}
274
- ::-webkit-scrollbar{width:8px;height:8px}
275
- ::-webkit-scrollbar-track{background:rgba(168,85,247,.05);border-radius:4px}
276
- ::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
277
- ::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
278
-
279
- #bbox-draw-wrap{position:relative;border:2px dashed #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px;transition: border-color 0.2s ease;}
280
- #bbox-draw-wrap:hover{border-color:#A855F7}
281
- #bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
282
- .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}
283
- .dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
284
-
285
- /* Custom Uiverse.io Upload Prompt Component styling */
286
- .upload-container {
287
- position: absolute;
288
- top: 50%; left: 50%;
289
- transform: translate(-50%, -50%);
290
- z-index: 20;
291
- height: 300px;
292
- width: 300px;
293
- border-radius: 10px;
294
- box-shadow: 4px 4px 30px rgba(168, 85, 247, 0.2);
295
- display: flex;
296
- flex-direction: column;
297
- align-items: center;
298
- justify-content: space-between;
299
- padding: 10px;
300
- gap: 5px;
301
- background-color: rgba(250, 245, 255, 0.95);
302
- backdrop-filter: blur(8px);
303
- }
304
- .dark .upload-container {
305
- background-color: rgba(30, 30, 30, 0.95);
306
- box-shadow: 4px 4px 30px rgba(0, 0, 0, 0.5);
307
- }
308
-
309
- .upload-header {
310
- flex: 1;
311
- width: 100%;
312
- border: 2px dashed #A855F7;
313
- border-radius: 10px;
314
- display: flex;
315
- align-items: center;
316
- justify-content: center;
317
- flex-direction: column;
318
- cursor: pointer;
319
- transition: background-color 0.2s;
320
- color: #A855F7;
321
- }
322
- .upload-header:hover {
323
- background-color: rgba(168, 85, 247, 0.05);
324
- }
325
- .dark .upload-header:hover {
326
- background-color: rgba(168, 85, 247, 0.15);
327
- }
328
-
329
- .upload-header svg {
330
- height: 100px;
331
- }
332
-
333
- .upload-header p {
334
- text-align: center;
335
- color: #6B21A8;
336
- font-family: 'Outfit', sans-serif;
337
- font-weight: 600;
338
- margin-top: 10px;
339
- }
340
- .dark .upload-header p {
341
- color: #DAB2FF;
342
- }
343
-
344
- .upload-footer {
345
- background-color: rgba(168, 85, 247, 0.08);
346
- width: 100%;
347
- height: 40px;
348
- padding: 8px;
349
- border-radius: 10px;
350
- display: flex;
351
- align-items: center;
352
- justify-content: flex-end;
353
- color: #6B21A8;
354
- border: none;
355
- box-sizing: border-box;
356
- }
357
- .dark .upload-footer {
358
- background-color: rgba(168, 85, 247, 0.15);
359
- color: #DAB2FF;
360
- }
361
-
362
- .upload-footer svg {
363
- height: 130%;
364
- fill: #A855F7;
365
- background-color: rgba(255, 255, 255, 0.5);
366
- border-radius: 50%;
367
- padding: 2px;
368
- cursor: pointer;
369
- box-shadow: 0 2px 10px rgba(168, 85, 247, 0.2);
370
- transition: transform 0.2s;
371
- }
372
- .dark .upload-footer svg {
373
- background-color: rgba(0, 0, 0, 0.3);
374
- }
375
- .upload-footer svg:hover {
376
- transform: scale(1.1);
377
- }
378
-
379
- .upload-footer p {
380
- flex: 1;
381
- text-align: center;
382
- font-family: 'Outfit', sans-serif;
383
- font-size: 0.9rem;
384
- margin: 0;
385
- }
386
-
387
- .bbox-toolbar-section{
388
  display:flex;
389
- gap:8px;
390
- flex-wrap:wrap;
391
- justify-content:center;
392
  align-items:center;
393
- padding:12px 16px;
394
- margin-top:10px;
395
- background:rgba(255,255,255,.92);
396
- border:2px solid #E9D5FF;
 
 
 
 
 
 
397
  border-radius:10px;
398
- box-shadow:0 2px 12px rgba(168,85,247,.08);
 
 
399
  }
400
- .dark .bbox-toolbar-section{
401
- background:rgba(30,30,30,.9);
402
- border-color:rgba(168,85,247,.3);
 
 
 
403
  }
404
- .bbox-toolbar-section .toolbar-label{
405
- font-family:'Outfit',sans-serif;
406
  font-weight:600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  font-size:13px;
408
- color:#6B21A8;
409
- margin-right:6px;
410
- user-select:none;
 
 
411
  }
412
- .dark .bbox-toolbar-section .toolbar-label{color:#C084FC}
413
- .bbox-toolbar-section .toolbar-divider{
414
- width:1px;
415
- height:28px;
416
- background:#E9D5FF;
417
- margin:0 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
- .dark .bbox-toolbar-section .toolbar-divider{background:rgba(168,85,247,.3)}
420
- .bbox-toolbar-section button{
 
 
421
  color:#fff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  border:none;
423
- padding:7px 15px;
424
- border-radius:7px;
425
  cursor:pointer;
426
- font-family:'Outfit',sans-serif;
427
  font-weight:600;
428
- font-size:13px;
429
- box-shadow:0 2px 5px rgba(0,0,0,.15);
430
- transition:background .2s,transform .15s,box-shadow .2s;
431
- }
432
- .bbox-toolbar-section button:hover{transform:translateY(-1px);box-shadow:0 4px 10px rgba(0,0,0,.2)}
433
- .bbox-toolbar-section button:active{transform:translateY(0)}
434
- .bbox-tb-draw{background:#9333EA}
435
- .bbox-tb-draw:hover{background:#A855F7}
436
- .bbox-tb-draw.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
437
- .bbox-tb-select{background:#6366f1}
438
- .bbox-tb-select:hover{background:#818cf8}
439
- .bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
440
- .bbox-tb-del{background:#dc2626}
441
- .bbox-tb-del:hover{background:#ef4444}
442
- .bbox-tb-undo{background:#7E22CE}
443
- .bbox-tb-undo:hover{background:#9333EA}
444
- .bbox-tb-clear{background:#be123c}
445
- .bbox-tb-clear:hover{background:#e11d48}
446
- .bbox-tb-change{background:#4b5563}
447
- .bbox-tb-change:hover{background:#6b7280}
448
-
449
- #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}
450
- #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}
451
 
452
- #bbox-debug-count{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  text-align:center;
454
- padding:6px 12px;
455
- margin-top:6px;
456
- font-family:'IBM Plex Mono',monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  font-size:12px;
458
- color:#6B21A8;
459
- background:rgba(168,85,247,.06);
460
- border:1px dashed #C084FC;
 
 
 
 
 
 
 
 
461
  border-radius:6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  }
463
- .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
464
 
465
- .hidden-input {
466
- display: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  }
468
  """
469
 
470
  bbox_drawer_js = r"""
471
  () => {
472
- function initCanvasBbox() {
473
- if (window.__bboxInitDone) return;
474
-
475
- const canvas = document.getElementById('bbox-draw-canvas');
476
- const wrap = document.getElementById('bbox-draw-wrap');
477
- const status = document.getElementById('bbox-status');
478
- const badge = document.getElementById('bbox-count');
479
- const debugCount = document.getElementById('bbox-debug-count');
480
-
481
- const btnDraw = document.getElementById('tb-draw');
482
- const btnSelect = document.getElementById('tb-select');
483
- const btnDel = document.getElementById('tb-del');
484
- const btnUndo = document.getElementById('tb-undo');
485
- const btnClear = document.getElementById('tb-clear');
486
- const btnChange = document.getElementById('tb-change-img');
487
-
488
- const uploadPrompt = document.getElementById('upload-prompt');
489
- const uploadHeader = document.getElementById('upload-header');
490
- const fileInput = document.getElementById('custom-file-input');
491
-
492
- if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
493
- console.log('[BBox] waiting for DOM...');
494
- setTimeout(initCanvasBbox, 250);
495
- return;
496
- }
497
 
498
- window.__bboxInitDone = true;
499
- console.log('[BBox] canvas init OK');
500
- const ctx = canvas.getContext('2d');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- let boxes = [];
503
- window.__bboxBoxes = boxes;
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
- let baseImg = null;
506
- let dispW = 512, dispH = 400;
507
- let selectedIdx = -1;
508
- let mode = 'draw';
509
-
510
- let dragging = false;
511
- let dragType = null;
512
- let dragStart = {x:0, y:0};
513
- let dragOrig = null;
514
- const HANDLE = 7;
515
- const RED_STROKE = 'rgba(255,0,0,0.95)';
516
- const RED_STROKE_WIDTH = 3;
517
- const SEL_STROKE = 'rgba(0,120,255,0.95)';
518
-
519
- function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
520
- function px2n(x1,y1,x2,y2) {
521
- return {
522
- x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
523
- x2: Math.max(x1,x2)/dispW, y2: Math.max(y1,y2)/dispH
524
- };
525
- }
526
- function clamp01(v){return Math.max(0,Math.min(1,v));}
527
- function fitSize(nw, nh) {
528
- const mw = wrap.clientWidth || 512, mh = 500;
529
- const r = Math.min(mw/nw, mh/nh, 1);
530
- dispW = Math.round(nw*r); dispH = Math.round(nh*r);
531
- canvas.width = dispW; canvas.height = dispH;
532
- canvas.style.width = dispW+'px';
533
- canvas.style.height = dispH+'px';
534
  }
535
- function canvasXY(e) {
536
- const r = canvas.getBoundingClientRect();
537
- const cx = e.touches ? e.touches[0].clientX : e.clientX;
538
- const cy = e.touches ? e.touches[0].clientY : e.clientY;
539
- return {x: Math.max(0,Math.min(dispW, cx-r.left)),
540
- y: Math.max(0,Math.min(dispH, cy-r.top))};
541
  }
 
 
542
 
543
- function syncToGradio() {
544
- window.__bboxBoxes = boxes;
545
- const jsonStr = JSON.stringify(boxes);
546
-
547
- if (debugCount) {
548
- debugCount.textContent = boxes.length > 0
549
- ? '\u2705 ' + boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') +
550
- ' ready | JSON: ' + jsonStr.substring(0,80) +
551
- (jsonStr.length > 80 ? '\u2026' : '')
552
- : '\u2B1C No boxes drawn yet';
553
- }
554
 
555
- const container = document.getElementById('boxes-json-input');
556
- if (!container) return;
557
- const targets = [
558
- ...container.querySelectorAll('textarea'),
559
- ...container.querySelectorAll('input:not([type="file"])')
560
- ];
561
- targets.forEach(el => {
562
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
563
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
564
- if (ns && ns.set) {
565
- ns.set.call(el, jsonStr);
566
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
567
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
568
- }
569
- });
570
  }
 
571
 
572
- function syncImageToGradio(dataUrl) {
573
- const container = document.getElementById('hidden-image-b64');
574
- if (!container) return;
575
- const targets = [
576
- ...container.querySelectorAll('textarea'),
577
- ...container.querySelectorAll('input')
578
- ];
579
- targets.forEach(el => {
580
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
581
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
582
- if (ns && ns.set) {
583
- ns.set.call(el, dataUrl);
584
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
585
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
586
- }
587
- });
588
  }
589
-
590
- function redraw(tempRect) {
591
- ctx.clearRect(0,0,dispW,dispH);
592
- if (!baseImg) {
593
- ctx.fillStyle='#1a1a1a'; ctx.fillRect(0,0,dispW,dispH);
594
- updateBadge(); return;
595
- }
596
- ctx.drawImage(baseImg, 0, 0, dispW, dispH);
597
-
598
- boxes.forEach((b,i) => {
599
- const p = n2px(b);
600
- const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
601
-
602
- /* RED OUTLINE ONLY — no fill */
603
- if (i === selectedIdx) {
604
- ctx.strokeStyle = SEL_STROKE;
605
- ctx.lineWidth = RED_STROKE_WIDTH + 1;
606
- ctx.setLineDash([6,3]);
607
- } else {
608
- ctx.strokeStyle = RED_STROKE;
609
- ctx.lineWidth = RED_STROKE_WIDTH;
610
- ctx.setLineDash([]);
611
- }
612
- ctx.strokeRect(lx, ty, w, h);
613
- ctx.setLineDash([]);
614
-
615
- /* label tag */
616
- ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
617
- ctx.font = 'bold 11px IBM Plex Mono,monospace';
618
- ctx.textAlign = 'left'; ctx.textBaseline = 'top';
619
- const label = '#'+(i+1);
620
- const tw = ctx.measureText(label).width;
621
- ctx.fillRect(lx, ty-16, tw+6, 16);
622
- ctx.fillStyle = '#fff';
623
- ctx.fillText(label, lx+3, ty-14);
624
-
625
- if (i === selectedIdx) drawHandles(p);
626
- });
627
-
628
- /* temp drawing rect — outline only */
629
- if (tempRect) {
630
- const rx = Math.min(tempRect.x1,tempRect.x2);
631
- const ry = Math.min(tempRect.y1,tempRect.y2);
632
- const rw = Math.abs(tempRect.x2-tempRect.x1);
633
- const rh = Math.abs(tempRect.y2-tempRect.y1);
634
  ctx.strokeStyle = RED_STROKE;
635
  ctx.lineWidth = RED_STROKE_WIDTH;
636
- ctx.setLineDash([6,3]);
637
- ctx.strokeRect(rx, ry, rw, rh);
638
  ctx.setLineDash([]);
639
  }
640
- updateBadge();
641
- }
642
-
643
- function drawHandles(p) {
644
- const pts = handlePoints(p);
645
- ctx.fillStyle = 'rgba(0,120,255,0.9)';
646
- ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
647
- for (const k in pts) {
648
- const h = pts[k];
649
- ctx.fillRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2);
650
- ctx.strokeRect(h.x-HANDLE, h.y-HANDLE, HANDLE*2, HANDLE*2);
 
 
 
 
651
  }
652
- }
 
 
653
 
654
- function handlePoints(p) {
655
- const mx = (p.x1+p.x2)/2, my = (p.y1+p.y2)/2;
656
- return {
657
- tl:{x:p.x1,y:p.y1}, tc:{x:mx,y:p.y1}, tr:{x:p.x2,y:p.y1},
658
- ml:{x:p.x1,y:my}, mr:{x:p.x2,y:my},
659
- bl:{x:p.x1,y:p.y2}, bc:{x:mx,y:p.y2}, br:{x:p.x2,y:p.y2}
660
- };
661
- }
662
 
663
- function hitHandle(px, py, boxIdx) {
664
- if (boxIdx < 0) return null;
665
- const p = n2px(boxes[boxIdx]);
666
- const pts = handlePoints(p);
667
- for (const k in pts) {
668
- if (Math.abs(px-pts[k].x) <= HANDLE+2 && Math.abs(py-pts[k].y) <= HANDLE+2) return k;
669
- }
670
- return null;
 
 
671
  }
 
 
672
 
673
- function hitBox(px, py) {
674
- for (let i = boxes.length-1; i >= 0; i--) {
675
- const p = n2px(boxes[i]);
676
- if (px >= p.x1 && px <= p.x2 && py >= p.y1 && py <= p.y2) return i;
677
- }
678
- return -1;
 
 
 
 
 
 
679
  }
 
680
 
681
- function updateBadge() {
682
- if (boxes.length > 0) {
683
- badge.style.display = 'block';
684
- badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':'');
685
- } else {
686
- badge.style.display = 'none';
687
- }
 
 
 
 
 
 
 
 
688
  }
 
 
689
 
690
- function setMode(m) {
691
- mode = m;
692
- btnDraw.classList.toggle('active', m==='draw');
693
- btnSelect.classList.toggle('active', m==='select');
694
- canvas.style.cursor = m==='draw' ? 'crosshair' : 'default';
695
- if (m==='draw') selectedIdx = -1;
696
- redraw();
697
  }
 
 
698
 
699
- function showStatus(txt) {
700
- status.textContent = txt; status.style.display = 'block';
 
 
 
 
701
  }
702
- function hideStatus() { status.style.display = 'none'; }
703
 
704
- function onDown(e) {
705
- if (!baseImg) return;
706
- e.preventDefault();
707
- const {x, y} = canvasXY(e);
 
 
 
 
708
 
709
- if (mode === 'draw') {
710
- dragging = true; dragType = 'new';
711
- dragStart = {x, y};
712
- selectedIdx = -1;
713
- } else {
714
- if (selectedIdx >= 0) {
715
- const h = hitHandle(x, y, selectedIdx);
716
- if (h) {
717
- dragging = true; dragType = h;
718
- dragStart = {x, y};
719
- dragOrig = {...boxes[selectedIdx]};
720
- showStatus('Resizing box #'+(selectedIdx+1));
721
- return;
722
- }
 
 
 
 
 
 
 
 
723
  }
724
- const hi = hitBox(x, y);
725
- if (hi >= 0) {
726
- selectedIdx = hi;
727
- const h2 = hitHandle(x, y, selectedIdx);
728
- if (h2) {
729
- dragging = true; dragType = h2;
730
- dragStart = {x, y};
731
- dragOrig = {...boxes[selectedIdx]};
732
- showStatus('Resizing box #'+(selectedIdx+1));
733
- redraw(); return;
734
- }
735
- dragging = true; dragType = 'move';
736
  dragStart = {x, y};
737
  dragOrig = {...boxes[selectedIdx]};
738
- showStatus('Moving box #'+(selectedIdx+1));
739
- } else {
740
- selectedIdx = -1;
741
- hideStatus();
742
  }
743
- redraw();
 
 
 
 
 
 
744
  }
 
745
  }
 
746
 
747
- function onMove(e) {
748
- if (!baseImg) return;
749
- e.preventDefault();
750
- const {x, y} = canvasXY(e);
751
-
752
- if (!dragging) {
753
- if (mode === 'select') {
754
- if (selectedIdx >= 0 && hitHandle(x,y,selectedIdx)) {
755
- const h = hitHandle(x,y,selectedIdx);
756
- const curs = {tl:'nwse-resize',tr:'nesw-resize',bl:'nesw-resize',br:'nwse-resize',
757
- tc:'ns-resize',bc:'ns-resize',ml:'ew-resize',mr:'ew-resize'};
758
- canvas.style.cursor = curs[h] || 'move';
759
- } else if (hitBox(x,y) >= 0) {
760
- canvas.style.cursor = 'move';
761
- } else {
762
- canvas.style.cursor = 'default';
763
- }
764
  }
765
- return;
766
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
- if (dragType === 'new') {
769
- redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y});
770
- showStatus(Math.abs(x-dragStart.x).toFixed(0)+'\u00d7'+Math.abs(y-dragStart.y).toFixed(0)+' px');
771
- return;
772
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
 
774
- const dx = (x - dragStart.x) / dispW;
775
- const dy = (y - dragStart.y) / dispH;
776
- const b = boxes[selectedIdx];
777
- const o = dragOrig;
778
-
779
- if (dragType === 'move') {
780
- const bw = o.x2-o.x1, bh = o.y2-o.y1;
781
- let nx1 = o.x1+dx, ny1 = o.y1+dy;
782
- nx1 = clamp01(nx1); ny1 = clamp01(ny1);
783
- if (nx1+bw > 1) nx1 = 1-bw;
784
- if (ny1+bh > 1) ny1 = 1-bh;
785
- b.x1=nx1; b.y1=ny1; b.x2=nx1+bw; b.y2=ny1+bh;
786
- } else {
787
- const t = dragType;
788
- if (t.includes('l')) b.x1 = clamp01(o.x1 + dx);
789
- if (t.includes('r')) b.x2 = clamp01(o.x2 + dx);
790
- if (t.includes('t')) b.y1 = clamp01(o.y1 + dy);
791
- if (t.includes('b')) b.y2 = clamp01(o.y2 + dy);
792
- if (Math.abs(b.x2-b.x1) < 0.01) { b.x1=o.x1; b.x2=o.x2; }
793
- if (Math.abs(b.y2-b.y1) < 0.01) { b.y1=o.y1; b.y2=o.y2; }
794
- if (b.x1 > b.x2) { const t2=b.x1; b.x1=b.x2; b.x2=t2; }
795
- if (b.y1 > b.y2) { const t2=b.y1; b.y1=b.y2; b.y2=t2; }
796
- }
797
- redraw();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  }
 
799
 
800
- function onUp(e) {
801
- if (!dragging) return;
802
- if (e) e.preventDefault();
803
- dragging = false;
804
-
805
- if (dragType === 'new') {
806
- const pt = e ? canvasXY(e) : {x:dragStart.x, y:dragStart.y};
807
- if (Math.abs(pt.x-dragStart.x) > 4 && Math.abs(pt.y-dragStart.y) > 4) {
808
- const nb = px2n(dragStart.x, dragStart.y, pt.x, pt.y);
809
- boxes.push(nb);
810
- window.__bboxBoxes = boxes;
811
- selectedIdx = boxes.length - 1;
812
- console.log('[BBox] created box #'+boxes.length, nb);
813
- showStatus('Box #'+boxes.length+' created');
814
- } else { hideStatus(); }
815
- } else {
816
- showStatus('Box #'+(selectedIdx+1)+' updated');
817
- }
818
- dragType = null; dragOrig = null;
819
- syncToGradio();
820
- redraw();
821
  }
 
822
 
823
- canvas.addEventListener('mousedown', onDown);
824
- canvas.addEventListener('mousemove', onMove);
825
- canvas.addEventListener('mouseup', onUp);
826
- canvas.addEventListener('mouseleave', (e)=>{if(dragging)onUp(e);});
827
- canvas.addEventListener('touchstart', onDown, {passive:false});
828
- canvas.addEventListener('touchmove', onMove, {passive:false});
829
- canvas.addEventListener('touchend', onUp, {passive:false});
830
- canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
831
-
832
- // --- File Upload Logic ---
833
- function processFile(file) {
834
- if (!file || !file.type.startsWith('image/')) return;
835
- const reader = new FileReader();
836
- reader.onload = (event) => {
837
- const dataUrl = event.target.result;
838
- const img = new window.Image();
839
- img.crossOrigin = 'anonymous';
840
- img.onload = () => {
841
- baseImg = img;
842
- boxes.length = 0;
843
- window.__bboxBoxes = boxes;
844
- selectedIdx = -1;
845
- fitSize(img.naturalWidth, img.naturalHeight);
846
- syncToGradio(); redraw(); hideStatus();
847
- uploadPrompt.style.display = 'none';
848
- syncImageToGradio(dataUrl);
849
- };
850
- img.src = dataUrl;
851
- };
852
- reader.readAsDataURL(file);
853
  }
 
854
 
855
- uploadHeader.addEventListener('click', () => fileInput.click());
856
- btnChange.addEventListener('click', () => fileInput.click());
857
-
858
- fileInput.addEventListener('change', (e) => {
859
- processFile(e.target.files[0]);
860
- e.target.value = ''; // Reset input to allow re-upload of same file
 
 
 
 
 
861
  });
 
862
 
863
- wrap.addEventListener('dragover', (e) => {
864
- e.preventDefault();
865
- wrap.style.borderColor = '#A855F7';
866
- wrap.style.boxShadow = '0 0 15px rgba(168,85,247,0.3)';
867
- });
868
- wrap.addEventListener('dragleave', (e) => {
869
- e.preventDefault();
870
- wrap.style.borderColor = '';
871
- wrap.style.boxShadow = '';
 
 
 
 
 
 
 
 
 
 
 
872
  });
873
- wrap.addEventListener('drop', (e) => {
874
- e.preventDefault();
875
- wrap.style.borderColor = '';
876
- wrap.style.boxShadow = '';
877
- if (e.dataTransfer.files.length) {
878
- processFile(e.dataTransfer.files[0]);
 
 
 
 
 
 
 
 
 
879
  }
880
  });
 
881
 
882
- // --- Toolbar Logic ---
883
- btnDraw.addEventListener('click', ()=>setMode('draw'));
884
- btnSelect.addEventListener('click', ()=>setMode('select'));
 
 
 
 
 
885
 
886
- btnDel.addEventListener('click', () => {
887
- if (selectedIdx >= 0 && selectedIdx < boxes.length) {
888
- const removed = selectedIdx + 1;
889
- boxes.splice(selectedIdx, 1);
890
- window.__bboxBoxes = boxes;
891
- selectedIdx = -1;
892
- syncToGradio(); redraw();
893
- showStatus('Box #'+removed+' deleted');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
  } else {
895
- showStatus('No box selected');
896
  }
897
- });
 
898
 
899
- btnUndo.addEventListener('click', () => {
900
- if (boxes.length > 0) {
901
- boxes.pop();
902
- window.__bboxBoxes = boxes;
903
- selectedIdx = -1;
904
- syncToGradio(); redraw();
905
- showStatus('Last box removed');
906
- }
907
  });
 
908
 
909
- btnClear.addEventListener('click', () => {
910
- boxes.length = 0;
911
- window.__bboxBoxes = boxes;
912
- selectedIdx = -1;
913
- syncToGradio(); redraw(); hideStatus();
914
- });
 
 
915
 
916
- new ResizeObserver(() => {
917
- if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
918
- }).observe(wrap);
919
 
920
- setMode('draw');
921
- fitSize(512,400); redraw();
922
- syncToGradio();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  }
924
 
925
- initCanvasBbox();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
926
  }
927
  """
928
 
 
929
 
930
  with gr.Blocks() as demo:
931
- gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
932
- gr.Markdown(
933
- "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2509-Object-Remover-Bbox). "
934
- "Upload an image directly into the bounding box editor area below, draw red bounding boxes over the objects you want to remove, and click Remove Object. "
935
- "Multiple boxes supported. Select, move, resize or delete individual boxes. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/QIE-Object-Remover-Bbox)",
936
- elem_id="subtitle",
937
- )
938
 
939
- with gr.Row():
940
- with gr.Column(scale=1):
941
-
942
- hidden_image_b64 = gr.Textbox(
943
- elem_id="hidden-image-b64",
944
- elem_classes="hidden-input",
945
- container=False
946
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
 
948
- #gr.Markdown("### **Bbox Edit Controller**")
 
 
 
 
 
 
 
 
 
 
949
 
950
- gr.HTML(
951
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
952
  <div id="bbox-draw-wrap">
953
- <div id="upload-prompt" class="upload-container">
954
- <div class="upload-header" id="upload-header">
955
- <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
956
- <path d="M7 10V9C7 6.23858 9.23858 4 12 4C14.7614 4 17 6.23858 17 9V10C19.2091 10 21 11.7909 21 14C21 15.4806 20.1956 16.8084 19 17.5M7 10C4.79086 10 3 11.7909 3 14C3 15.4806 3.8044 16.8084 5 17.5M7 10C7.43285 10 7.84965 10.0688 8.24006 10.1959M12 12V21M12 12L15 15M12 12L9 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
957
- </svg>
958
- <p>Browse File to upload!</p>
959
- </div>
960
- <div class="upload-footer">
961
- <svg fill="currentColor" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
962
- <path d="M15.331 6H8.5v20h15V14.154h-8.169z"></path>
963
- <path d="M18.153 6h-.009v5.342H23.5v-.002z"></path>
964
- </svg>
965
- <p>Not selected file</p>
966
- <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
967
- <path d="M5.16565 10.1534C5.07629 8.99181 5.99473 8 7.15975 8H16.8402C18.0053 8 18.9237 8.9918 18.8344 10.1534L18.142 19.1534C18.0619 20.1954 17.193 21 16.1479 21H7.85206C6.80699 21 5.93811 20.1954 5.85795 19.1534L5.16565 10.1534Z" stroke="currentColor" stroke-width="2"></path>
968
- <path d="M19.5 5H4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
969
- <path d="M10 3C10 2.44772 10.4477 2 11 2H13C13.5523 2 14 2.44772 14 3V5H10V3Z" stroke="currentColor" stroke-width="2"></path>
970
- </svg>
971
- </div>
972
- <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
973
  </div>
974
-
975
  <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
976
  <div id="bbox-status"></div>
977
  <div id="bbox-count"></div>
978
  </div>
979
- """
980
- )
981
-
982
- gr.HTML(
983
- """
984
- <div class="bbox-toolbar-section">
985
- <span class="toolbar-label">🛠 Tools:</span>
986
- <button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">✏️ Draw</button>
987
- <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">🔲 Select</button>
988
- <div class="toolbar-divider"></div>
989
- <span class="toolbar-label">Actions:</span>
990
- <button id="tb-del" class="bbox-tb-del" title="Delete selected box">✕ Delete</button>
991
- <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
992
- <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">🗑 Clear All</button>
993
- <div class="toolbar-divider"></div>
994
- <button id="tb-change-img" class="bbox-tb-change" title="Upload a different image">📸 Change Image</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995
  </div>
996
- """
997
- )
998
-
999
- gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
1000
-
1001
- boxes_json = gr.Textbox(
1002
- value="[]",
1003
- elem_id="boxes-json-input",
1004
- elem_classes="hidden-input",
1005
- container=False
1006
- )
1007
-
1008
- gr.HTML(
1009
- '<div class="bbox-hint">'
1010
- "<b>Draw mode:</b> Click & drag to create red rectangles. "
1011
- "<b>Select mode:</b> Click a box to select it \u2192 drag to <b>move</b>, "
1012
- "drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
1013
- "</div>"
1014
- )
1015
-
1016
- prompt = gr.Textbox(
1017
- label="Prompt",
1018
- value=DEFAULT_PROMPT,
1019
- lines=1,
1020
- info="Edit the prompt if needed",
1021
- )
1022
-
1023
- run_btn = gr.Button("\U0001F5D1\uFE0F Remove Object", variant="primary", size="lg")
1024
-
1025
- with gr.Column(scale=1):
1026
- result = gr.Image(label="Output Image", height=475, format="png")
1027
- preview = gr.Image(label="Input Sent to Model (with red boxes)", height=415)
1028
-
1029
- with gr.Accordion("Advanced Settings", open=False, visible=False):
1030
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
1031
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
1032
- with gr.Row():
1033
- guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
1034
- num_inference_steps = gr.Slider(label="Inference Steps", minimum=1, maximum=20, step=1, value=4)
1035
- with gr.Row():
1036
- height_slider = gr.Slider(label="Height", minimum=256, maximum=2048, step=8, value=1024)
1037
- width_slider = gr.Slider(label="Width", minimum=256, maximum=2048, step=8, value=1024)
1038
 
1039
  demo.load(fn=None, js=bbox_drawer_js)
 
1040
 
1041
  run_btn.click(
1042
  fn=infer_object_removal,
@@ -1045,8 +1741,7 @@ with gr.Blocks() as demo:
1045
  outputs=[result, seed, preview],
1046
  js="""(b64, bj, p, s, rs, gs, nis, h, w) => {
1047
  const boxes = window.__bboxBoxes || [];
1048
- const json = JSON.stringify(boxes);
1049
- console.log('[BBox] submitting', boxes.length, 'boxes:', json);
1050
  return [b64, json, p, s, rs, gs, nis, h, w];
1051
  }""",
1052
  )
@@ -1059,6 +1754,6 @@ with gr.Blocks() as demo:
1059
 
1060
  if __name__ == "__main__":
1061
  demo.launch(
1062
- css=css, theme=purple_theme,
1063
  mcp_server=True, ssr_mode=False, show_error=True
1064
  )
 
4
  import torch
5
  import spaces
6
  import base64
7
+ import json
8
  from io import BytesIO
 
9
  from PIL import Image, ImageDraw
10
  from diffusers import FlowMatchEulerDiscreteScheduler
11
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
12
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
13
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  MAX_SEED = np.iinfo(np.int32).max
16
 
17
  dtype = torch.bfloat16
 
32
  print(f"Warning: Could not set FA3 processor: {e}")
33
 
34
  ADAPTER_SPECS = {
35
+ "Object-Remover": {
36
+ "repo": "prithivMLmods/QIE-2509-Object-Remover-Bbox-v3",
37
+ "weights": "QIE-2509-Object-Remover-Bbox-v3-10000.safetensors",
38
  "adapter_name": "object-remover",
39
  },
40
  }
 
43
  DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
44
 
45
 
46
+ def b64_to_pil(b64_str):
 
47
  if not b64_str or not b64_str.startswith("data:image"):
48
  return None
49
  try:
 
55
  return None
56
 
57
 
58
+ def burn_boxes_onto_image(pil_image, boxes_json_str):
 
 
59
  if not pil_image:
60
  return pil_image
61
  try:
 
77
  y2 = int(b["y2"] * h)
78
  lx, rx = min(x1, x2), max(x1, x2)
79
  ty, by_ = min(y1, y2), max(y1, y2)
 
80
  draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
81
 
82
  return img
 
84
 
85
  @spaces.GPU
86
  def infer_object_removal(
87
+ b64_str,
88
+ boxes_json,
89
+ prompt,
90
+ seed=0,
91
+ randomize_seed=True,
92
+ guidance_scale=1.0,
93
+ num_inference_steps=4,
94
+ height=1024,
95
+ width=1024,
96
  ):
97
  global loaded
98
  progress = gr.Progress(track_tqdm=True)
 
110
 
111
  if not prompt or prompt.strip() == "":
112
  prompt = DEFAULT_PROMPT
 
 
113
 
114
  source_image = b64_to_pil(b64_str)
115
  if source_image is None:
116
+ raise gr.Error("Please upload an image first using the canvas area.")
117
 
 
118
  try:
119
  boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else []
120
+ except Exception:
 
121
  boxes = []
122
 
123
  if not boxes:
 
146
  return result, seed, marked
147
 
148
 
149
+ def update_dimensions_on_upload(b64_str):
150
  image = b64_to_pil(b64_str)
151
  if image is None:
152
  return 1024, 1024
 
165
 
166
 
167
  css = r"""
168
+ @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');
169
+
170
+ *{box-sizing:border-box;margin:0;padding:0}
171
+
172
+ body,.gradio-container{
173
+ background:#0f0f13!important;
174
+ font-family:'Inter',system-ui,-apple-system,sans-serif!important;
175
+ font-size:14px!important;
176
+ color:#e4e4e7!important;
177
+ min-height:100vh;
178
+ }
179
+ .dark body,.dark .gradio-container{
180
+ background:#0f0f13!important;
181
+ color:#e4e4e7!important;
182
+ }
 
 
 
 
183
  footer{display:none!important}
184
+
185
+ .hidden-input{
186
+ display:none!important;
187
+ height:0!important;
188
+ overflow:hidden!important;
189
+ margin:0!important;
190
+ padding:0!important;
191
+ }
192
+
193
+ /* ── Main Container ── */
194
+ .app-shell{
195
+ background:#18181b;
196
+ border:1px solid #27272a;
197
+ border-radius:16px;
198
+ margin:12px auto;
199
+ max-width:1400px;
200
+ overflow:hidden;
201
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),
202
+ 0 0 0 1px rgba(255,255,255,.03);
203
+ }
204
+
205
+ /* ── Header Bar ── */
206
+ .app-header{
207
+ background:linear-gradient(135deg,#18181b 0%,#1e1e24 100%);
208
+ border-bottom:1px solid #27272a;
209
+ padding:14px 24px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  display:flex;
 
 
 
211
  align-items:center;
212
+ justify-content:space-between;
213
+ }
214
+ .app-header-left{
215
+ display:flex;
216
+ align-items:center;
217
+ gap:12px;
218
+ }
219
+ .app-logo{
220
+ width:36px;height:36px;
221
+ background:linear-gradient(135deg,#6366f1,#8b5cf6,#a78bfa);
222
  border-radius:10px;
223
+ display:flex;align-items:center;justify-content:center;
224
+ font-size:18px;font-weight:800;color:#fff;
225
+ box-shadow:0 4px 12px rgba(99,102,241,.35);
226
  }
227
+ .app-title{
228
+ font-size:18px;font-weight:700;
229
+ background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
230
+ -webkit-background-clip:text;
231
+ -webkit-text-fill-color:transparent;
232
+ letter-spacing:-.3px;
233
  }
234
+ .app-badge{
235
+ font-size:11px;
236
  font-weight:600;
237
+ padding:3px 10px;
238
+ border-radius:20px;
239
+ background:rgba(99,102,241,.15);
240
+ color:#818cf8;
241
+ border:1px solid rgba(99,102,241,.25);
242
+ letter-spacing:.3px;
243
+ }
244
+
245
+ /* ── Toolbar ── */
246
+ .app-toolbar{
247
+ background:#18181b;
248
+ border-bottom:1px solid #27272a;
249
+ padding:8px 16px;
250
+ display:flex;
251
+ gap:4px;
252
+ align-items:center;
253
+ flex-wrap:wrap;
254
+ }
255
+ .tb-sep{
256
+ width:1px;height:28px;
257
+ background:#27272a;
258
+ margin:0 8px;
259
+ }
260
+ .modern-tb-btn{
261
+ display:inline-flex;align-items:center;justify-content:center;
262
+ gap:6px;
263
+ min-width:32px;height:34px;
264
+ background:transparent;
265
+ border:1px solid transparent;
266
+ border-radius:8px;
267
+ cursor:pointer;
268
  font-size:13px;
269
+ font-weight:600;
270
+ padding:0 12px;
271
+ font-family:'Inter',sans-serif;
272
+ color:#ffffff!important;
273
+ transition:all .15s ease;
274
  }
275
+ .modern-tb-btn:hover{
276
+ background:rgba(99,102,241,.15);
277
+ color:#ffffff!important;
278
+ border-color:rgba(99,102,241,.3);
279
+ }
280
+ .modern-tb-btn:active,.modern-tb-btn.active{
281
+ background:rgba(99,102,241,.25);
282
+ color:#ffffff!important;
283
+ border-color:rgba(99,102,241,.45);
284
+ }
285
+ .modern-tb-btn .tb-icon{
286
+ font-size:15px;
287
+ line-height:1;
288
+ color:#ffffff!important;
289
+ }
290
+ .modern-tb-btn .tb-label{
291
+ font-size:13px;
292
+ color:#ffffff!important;
293
+ font-weight:600;
294
+ }
295
+
296
+ /* ── Main Layout ── */
297
+ .app-main-row{
298
+ display:flex;
299
+ gap:0;
300
+ flex:1;
301
+ overflow:hidden;
302
+ }
303
+ .app-main-left{
304
+ flex:1;
305
+ display:flex;
306
+ flex-direction:column;
307
+ min-width:0;
308
+ border-right:1px solid #27272a;
309
+ }
310
+ .app-main-right{
311
+ width:420px;
312
+ display:flex;
313
+ flex-direction:column;
314
+ flex-shrink:0;
315
+ background:#18181b;
316
+ }
317
+
318
+ /* ── Canvas Area ── */
319
+ #bbox-draw-wrap{
320
+ position:relative;
321
+ background:#09090b;
322
+ margin:0;
323
+ min-height:440px;
324
+ overflow:hidden;
325
+ cursor:crosshair;
326
  }
327
+ #bbox-draw-canvas{display:block;margin:0 auto}
328
+ #bbox-status{
329
+ position:absolute;top:12px;left:12px;
330
+ background:rgba(99,102,241,.9);
331
  color:#fff;
332
+ padding:4px 12px;
333
+ font-family:'JetBrains Mono',monospace;
334
+ font-size:12px;
335
+ font-weight:500;
336
+ border-radius:6px;
337
+ z-index:10;display:none;
338
+ pointer-events:none;
339
+ backdrop-filter:blur(8px);
340
+ }
341
+ #bbox-count{
342
+ position:absolute;top:12px;right:12px;
343
+ background:rgba(24,24,27,.9);
344
+ color:#a78bfa;
345
+ padding:4px 12px;
346
+ font-family:'JetBrains Mono',monospace;
347
+ font-size:12px;
348
+ font-weight:600;
349
+ border-radius:6px;
350
+ border:1px solid rgba(99,102,241,.3);
351
+ z-index:10;display:none;
352
+ backdrop-filter:blur(8px);
353
+ }
354
+
355
+ /* ── Upload Prompt (icon only) ── */
356
+ .upload-prompt-modern{
357
+ position:absolute;
358
+ top:50%;left:50%;
359
+ transform:translate(-50%,-50%);
360
+ z-index:20;
361
+ }
362
+ .upload-click-area{
363
+ display:flex;
364
+ flex-direction:column;
365
+ align-items:center;
366
+ justify-content:center;
367
+ cursor:pointer;
368
+ padding:36px 44px;
369
+ border:2px dashed #3f3f46;
370
+ border-radius:16px;
371
+ background:rgba(99,102,241,.03);
372
+ transition:all .2s ease;
373
+ }
374
+ .upload-click-area:hover{
375
+ background:rgba(99,102,241,.08);
376
+ border-color:#6366f1;
377
+ transform:scale(1.03);
378
+ }
379
+ .upload-click-area:active{
380
+ background:rgba(99,102,241,.12);
381
+ transform:scale(.98);
382
+ }
383
+ .upload-click-area svg{
384
+ width:80px;height:80px;
385
+ }
386
+
387
+ /* ── Hint Bar ── */
388
+ .hint-bar{
389
+ background:rgba(99,102,241,.06);
390
+ border-top:1px solid #27272a;
391
+ border-bottom:1px solid #27272a;
392
+ padding:10px 20px;
393
+ font-size:13px;
394
+ color:#a1a1aa;
395
+ line-height:1.7;
396
+ }
397
+ .hint-bar b{color:#c7d2fe;font-weight:600}
398
+ .hint-bar kbd{
399
+ display:inline-block;
400
+ padding:1px 6px;
401
+ background:#27272a;
402
+ border:1px solid #3f3f46;
403
+ border-radius:4px;
404
+ font-family:'JetBrains Mono',monospace;
405
+ font-size:11px;
406
+ color:#a1a1aa;
407
+ }
408
+
409
+ /* ── JSON Panel ── */
410
+ .json-panel{
411
+ background:#18181b;
412
+ border-top:1px solid #27272a;
413
+ display:flex;
414
+ flex-direction:column;
415
+ height:160px;
416
+ max-height:160px;
417
+ min-height:160px;
418
+ }
419
+ .json-panel-title{
420
+ padding:8px 16px;
421
+ font-size:12px;
422
+ font-weight:600;
423
+ color:#71717a;
424
+ text-transform:uppercase;
425
+ letter-spacing:.8px;
426
+ border-bottom:1px solid #27272a;
427
+ display:flex;
428
+ align-items:center;
429
+ gap:8px;
430
+ flex-shrink:0;
431
+ }
432
+ .json-panel-title::before{
433
+ content:'{ }';
434
+ font-family:'JetBrains Mono',monospace;
435
+ font-size:11px;
436
+ color:#6366f1;
437
+ background:rgba(99,102,241,.12);
438
+ padding:2px 6px;
439
+ border-radius:4px;
440
+ }
441
+ .json-panel-content{
442
+ background:#09090b;
443
+ margin:0;
444
+ padding:12px 16px;
445
+ font-family:'JetBrains Mono',monospace;
446
+ font-size:12px;
447
+ color:#a1a1aa;
448
+ flex:1;
449
+ overflow-y:auto;
450
+ overflow-x:hidden;
451
+ word-break:break-all;
452
+ white-space:pre-wrap;
453
+ line-height:1.6;
454
+ }
455
+ .json-panel-content::-webkit-scrollbar{width:8px}
456
+ .json-panel-content::-webkit-scrollbar-track{background:#09090b}
457
+ .json-panel-content::-webkit-scrollbar-thumb{
458
+ background:#27272a;
459
+ border-radius:4px;
460
+ }
461
+ .json-panel-content::-webkit-scrollbar-thumb:hover{background:#3f3f46}
462
+
463
+ /* ── Right Panel Cards ── */
464
+ .panel-card{
465
+ border-bottom:1px solid #27272a;
466
+ }
467
+ .panel-card-title{
468
+ padding:12px 20px;
469
+ font-size:12px;
470
+ font-weight:600;
471
+ color:#71717a;
472
+ text-transform:uppercase;
473
+ letter-spacing:.8px;
474
+ border-bottom:1px solid rgba(39,39,42,.6);
475
+ }
476
+ .panel-card-body{
477
+ padding:16px 20px;
478
+ display:flex;
479
+ flex-direction:column;
480
+ gap:8px;
481
+ }
482
+
483
+ .modern-label{
484
+ font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block;
485
+ }
486
+ .modern-textarea{
487
+ width:100%;
488
+ background:#09090b;
489
+ border:1px solid #27272a;
490
+ border-radius:8px;
491
+ padding:10px 14px;
492
+ font-family:'Inter',sans-serif;
493
+ font-size:14px;
494
+ color:#e4e4e7;
495
+ resize:vertical;
496
+ outline:none;
497
+ min-height:42px;
498
+ transition:border-color .2s;
499
+ }
500
+ .modern-textarea:focus{
501
+ border-color:#6366f1;
502
+ box-shadow:0 0 0 3px rgba(99,102,241,.15);
503
+ }
504
+ .modern-textarea::placeholder{color:#3f3f46}
505
+
506
+ /* ── Primary Button ── */
507
+ .btn-run{
508
+ display:flex;align-items:center;justify-content:center;gap:8px;
509
+ width:100%;
510
+ background:linear-gradient(135deg,#6366f1,#7c3aed);
511
  border:none;
512
+ border-radius:10px;
513
+ padding:12px 24px;
514
  cursor:pointer;
515
+ font-size:15px;
516
  font-weight:600;
517
+ font-family:'Inter',sans-serif;
518
+ color:#fff;
519
+ transition:all .2s ease;
520
+ box-shadow:0 4px 16px rgba(99,102,241,.3),
521
+ inset 0 1px 0 rgba(255,255,255,.1);
522
+ letter-spacing:-.2px;
523
+ }
524
+ .btn-run:hover{
525
+ background:linear-gradient(135deg,#7c7cf5,#8b5cf6);
526
+ box-shadow:0 6px 24px rgba(99,102,241,.45),
527
+ inset 0 1px 0 rgba(255,255,255,.15);
528
+ transform:translateY(-1px);
529
+ }
530
+ .btn-run:active{
531
+ transform:translateY(0);
532
+ box-shadow:0 2px 8px rgba(99,102,241,.3);
533
+ }
534
+ .btn-run svg{width:18px;height:18px;fill:#fff}
 
 
 
 
 
535
 
536
+ /* ── Output Frames ── */
537
+ .output-frame{
538
+ border-bottom:1px solid #27272a;
539
+ display:flex;
540
+ flex-direction:column;
541
+ position:relative;
542
+ }
543
+ .output-frame .out-title{
544
+ padding:10px 20px;
545
+ font-size:13px;
546
+ font-weight:700;
547
+ color:#ffffff!important;
548
+ text-transform:uppercase;
549
+ letter-spacing:.8px;
550
+ border-bottom:1px solid rgba(39,39,42,.6);
551
+ display:flex;
552
+ align-items:center;
553
+ justify-content:space-between;
554
+ }
555
+ .output-frame .out-title span{
556
+ color:#ffffff!important;
557
+ }
558
+ .output-frame .out-body{
559
+ flex:1;
560
+ background:#09090b;
561
+ display:flex;
562
+ align-items:center;
563
+ justify-content:center;
564
+ overflow:hidden;
565
+ min-height:180px;
566
+ position:relative;
567
+ }
568
+ .output-frame .out-body img{
569
+ max-width:100%;max-height:460px;
570
+ image-rendering:auto;
571
+ }
572
+ .output-frame .out-placeholder{
573
+ color:#3f3f46;
574
+ font-size:13px;
575
  text-align:center;
576
+ padding:20px;
577
+ }
578
+
579
+ .out-download-btn{
580
+ display:none;
581
+ align-items:center;
582
+ justify-content:center;
583
+ background:rgba(99,102,241,.1);
584
+ border:1px solid rgba(99,102,241,.2);
585
+ border-radius:6px;
586
+ cursor:pointer;
587
+ padding:3px 10px;
588
+ font-size:11px;
589
+ font-weight:500;
590
+ color:#c7d2fe!important;
591
+ gap:4px;
592
+ height:24px;
593
+ transition:all .15s;
594
+ }
595
+ .out-download-btn:hover{
596
+ background:rgba(99,102,241,.2);
597
+ border-color:rgba(99,102,241,.35);
598
+ color:#ffffff!important;
599
+ }
600
+ .out-download-btn.visible{display:inline-flex}
601
+ .out-download-btn svg{width:12px;height:12px;fill:#c7d2fe}
602
+
603
+ /* ── Loader ── */
604
+ .modern-loader{
605
+ display:none;
606
+ position:absolute;
607
+ top:0;left:0;right:0;bottom:0;
608
+ background:rgba(9,9,11,.92);
609
+ z-index:15;
610
+ flex-direction:column;
611
+ align-items:center;
612
+ justify-content:center;
613
+ gap:16px;
614
+ backdrop-filter:blur(4px);
615
+ }
616
+ .modern-loader.active{display:flex}
617
+ .modern-loader .loader-spinner{
618
+ width:36px;height:36px;
619
+ border:3px solid #27272a;
620
+ border-top-color:#6366f1;
621
+ border-radius:50%;
622
+ animation:spin .8s linear infinite;
623
+ }
624
+ @keyframes spin{to{transform:rotate(360deg)}}
625
+ .modern-loader .loader-text{
626
+ font-size:13px;
627
+ color:#a1a1aa;
628
+ font-weight:500;
629
+ }
630
+ .loader-bar-track{
631
+ width:200px;height:4px;
632
+ background:#27272a;
633
+ border-radius:2px;
634
+ overflow:hidden;
635
+ }
636
+ .loader-bar-fill{
637
+ height:100%;
638
+ background:linear-gradient(90deg,#6366f1,#8b5cf6,#6366f1);
639
+ background-size:200% 100%;
640
+ animation:shimmer 1.5s ease-in-out infinite;
641
+ border-radius:2px;
642
+ }
643
+ @keyframes shimmer{
644
+ 0%{background-position:200% 0}
645
+ 100%{background-position:-200% 0}
646
+ }
647
+
648
+ /* ── Settings ── */
649
+ .settings-group{
650
+ border:1px solid #27272a;
651
+ border-radius:10px;
652
+ margin:12px 16px;
653
+ padding:0;
654
+ overflow:hidden;
655
+ }
656
+ .settings-group-title{
657
+ font-size:12px;
658
+ font-weight:600;
659
+ color:#71717a;
660
+ text-transform:uppercase;
661
+ letter-spacing:.8px;
662
+ padding:10px 16px;
663
+ border-bottom:1px solid #27272a;
664
+ background:rgba(24,24,27,.5);
665
+ }
666
+ .settings-group-body{
667
+ padding:14px 16px;
668
+ display:flex;
669
+ flex-direction:column;
670
+ gap:12px;
671
+ }
672
+ .slider-row{
673
+ display:flex;
674
+ align-items:center;
675
+ gap:10px;
676
+ min-height:28px;
677
+ }
678
+ .slider-row label,.slider-row .dim-label{
679
+ font-size:13px;
680
+ font-weight:500;
681
+ color:#a1a1aa;
682
+ min-width:72px;
683
+ flex-shrink:0;
684
+ }
685
+ .slider-row input[type="range"]{
686
+ flex:1;
687
+ -webkit-appearance:none;
688
+ appearance:none;
689
+ height:6px;
690
+ background:#27272a;
691
+ border-radius:3px;
692
+ outline:none;
693
+ min-width:0;
694
+ }
695
+ .slider-row input[type="range"]::-webkit-slider-thumb{
696
+ -webkit-appearance:none;
697
+ appearance:none;
698
+ width:16px;height:16px;
699
+ background:linear-gradient(135deg,#6366f1,#7c3aed);
700
+ border-radius:50%;
701
+ cursor:pointer;
702
+ box-shadow:0 2px 6px rgba(99,102,241,.4);
703
+ transition:transform .15s;
704
+ }
705
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{
706
+ transform:scale(1.2);
707
+ }
708
+ .slider-row input[type="range"]::-moz-range-thumb{
709
+ width:16px;height:16px;
710
+ background:linear-gradient(135deg,#6366f1,#7c3aed);
711
+ border-radius:50%;
712
+ cursor:pointer;
713
+ border:none;
714
+ box-shadow:0 2px 6px rgba(99,102,241,.4);
715
+ }
716
+ .slider-row .slider-val{
717
+ min-width:52px;text-align:right;
718
+ font-family:'JetBrains Mono',monospace;
719
+ font-size:12px;
720
+ font-weight:500;
721
+ padding:3px 8px;
722
+ background:#09090b;
723
+ border:1px solid #27272a;
724
+ border-radius:6px;
725
+ color:#a1a1aa;
726
+ flex-shrink:0;
727
+ }
728
+ .checkbox-row{
729
+ display:flex;align-items:center;gap:8px;
730
+ font-size:13px;cursor:default;
731
+ color:#a1a1aa;
732
+ }
733
+ .checkbox-row input[type="checkbox"]{
734
+ accent-color:#6366f1;
735
+ width:16px;height:16px;
736
+ cursor:pointer;
737
+ }
738
+ .checkbox-row label{
739
+ color:#a1a1aa;font-size:13px;cursor:pointer;
740
+ }
741
+
742
+ /* ── Status Bar ── */
743
+ .app-statusbar{
744
+ background:#18181b;
745
+ border-top:1px solid #27272a;
746
+ padding:6px 20px;
747
+ display:flex;
748
+ gap:12px;
749
+ height:34px;
750
+ align-items:center;
751
+ font-size:12px;
752
+ }
753
+ .app-statusbar .sb-section{
754
+ padding:0 12px;
755
+ flex:1;
756
+ display:flex;align-items:center;
757
+ font-family:'JetBrains Mono',monospace;
758
  font-size:12px;
759
+ color:#52525b;
760
+ overflow:hidden;
761
+ white-space:nowrap;
762
+ }
763
+ .app-statusbar .sb-section.sb-fixed{
764
+ flex:0 0 auto;
765
+ min-width:90px;
766
+ text-align:center;
767
+ justify-content:center;
768
+ padding:3px 12px;
769
+ background:rgba(99,102,241,.08);
770
  border-radius:6px;
771
+ color:#818cf8;
772
+ font-weight:500;
773
+ }
774
+
775
+ #gradio-run-btn{
776
+ position:absolute;
777
+ left:-9999px;
778
+ top:-9999px;
779
+ width:1px;
780
+ height:1px;
781
+ opacity:0.01;
782
+ pointer-events:none;
783
+ overflow:hidden;
784
+ }
785
+
786
+ #bbox-debug-count{
787
+ font-family:'JetBrains Mono',monospace;
788
+ font-size:12px;
789
+ color:#52525b;
790
  }
 
791
 
792
+ /* ── Global scrollbar ── */
793
+ ::-webkit-scrollbar{width:8px;height:8px}
794
+ ::-webkit-scrollbar-track{background:#09090b}
795
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
796
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
797
+
798
+ /* ── Dark mode force-overrides ── */
799
+ .dark .app-shell{background:#18181b}
800
+ .dark .upload-prompt-modern{background:transparent}
801
+ .dark .panel-card{background:#18181b}
802
+ .dark .settings-group{background:#18181b}
803
+ .dark .modern-tb-btn{color:#ffffff!important}
804
+ .dark .modern-tb-btn .tb-icon{color:#ffffff!important}
805
+ .dark .modern-tb-btn .tb-label{color:#ffffff!important}
806
+ .dark .modern-tb-btn:hover{color:#ffffff!important}
807
+ .dark .modern-tb-btn:active,.dark .modern-tb-btn.active{color:#ffffff!important}
808
+ .dark .output-frame .out-title{color:#ffffff!important}
809
+ .dark .output-frame .out-title span{color:#ffffff!important}
810
+ .dark .out-download-btn{color:#c7d2fe!important}
811
+ .dark .out-download-btn:hover{color:#ffffff!important}
812
+
813
+ @media(max-width:840px){
814
+ .app-main-row{flex-direction:column}
815
+ .app-main-right{width:100%}
816
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
817
  }
818
  """
819
 
820
  bbox_drawer_js = r"""
821
  () => {
822
+ function initCanvasBbox() {
823
+ if (window.__bboxInitDone) return;
824
+
825
+ const canvas = document.getElementById('bbox-draw-canvas');
826
+ const wrap = document.getElementById('bbox-draw-wrap');
827
+ const status = document.getElementById('bbox-status');
828
+ const badge = document.getElementById('bbox-count');
829
+ const debugCount = document.getElementById('bbox-debug-count');
830
+ const jsonDisplay = document.getElementById('bbox-json-content');
831
+
832
+ const btnDraw = document.getElementById('tb-draw');
833
+ const btnSelect = document.getElementById('tb-select');
834
+ const btnDel = document.getElementById('tb-del');
835
+ const btnUndo = document.getElementById('tb-undo');
836
+ const btnClear = document.getElementById('tb-clear');
837
+ const btnChange = document.getElementById('tb-change-img');
838
+
839
+ const uploadPrompt = document.getElementById('upload-prompt');
840
+ const uploadClickArea = document.getElementById('upload-click-area');
841
+ const fileInput = document.getElementById('custom-file-input');
842
+
843
+ if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
844
+ setTimeout(initCanvasBbox, 250);
845
+ return;
846
+ }
847
 
848
+ window.__bboxInitDone = true;
849
+ const ctx = canvas.getContext('2d');
850
+
851
+ let boxes = [];
852
+ window.__bboxBoxes = boxes;
853
+
854
+ let baseImg = null;
855
+ let dispW = 512, dispH = 400;
856
+ let selectedIdx = -1;
857
+ let mode = 'draw';
858
+
859
+ let dragging = false;
860
+ let dragType = null;
861
+ let dragStart = {x:0, y:0};
862
+ let dragOrig = null;
863
+ const HANDLE = 6;
864
+ const RED_STROKE = 'rgba(239,68,68,0.95)';
865
+ const RED_STROKE_WIDTH = 2;
866
+ const SEL_STROKE = 'rgba(99,102,241,0.95)';
867
+
868
+ function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
869
+ function px2n(x1,y1,x2,y2) {
870
+ return {
871
+ x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
872
+ x2: Math.max(x1,x2)/dispW, y2: Math.max(y1,y2)/dispH
873
+ };
874
+ }
875
+ function clamp01(v){return Math.max(0,Math.min(1,v));}
876
+ function fitSize(nw, nh) {
877
+ const mw = wrap.clientWidth || 512, mh = 500;
878
+ const r = Math.min(mw/nw, mh/nh, 1);
879
+ dispW = Math.round(nw*r); dispH = Math.round(nh*r);
880
+ canvas.width = dispW; canvas.height = dispH;
881
+ canvas.style.width = dispW+'px';
882
+ canvas.style.height = dispH+'px';
883
+ }
884
+ function canvasXY(e) {
885
+ const r = canvas.getBoundingClientRect();
886
+ const cx = e.touches ? e.touches[0].clientX : e.clientX;
887
+ const cy = e.touches ? e.touches[0].clientY : e.clientY;
888
+ return {x: Math.max(0,Math.min(dispW, cx-r.left)),
889
+ y: Math.max(0,Math.min(dispH, cy-r.top))};
890
+ }
891
 
892
+ function setGradioValue(containerId, value) {
893
+ const container = document.getElementById(containerId);
894
+ if (!container) return;
895
+ const allInputs = container.querySelectorAll('input, textarea');
896
+ allInputs.forEach(el => {
897
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
898
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
899
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
900
+ if (ns && ns.set) {
901
+ ns.set.call(el, value);
902
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
903
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
904
+ }
905
+ });
906
+ }
907
 
908
+ function formatJsonPretty(boxes) {
909
+ if (!boxes || boxes.length === 0) return '[\n // No bounding boxes defined\n]';
910
+ let lines = '[\n';
911
+ boxes.forEach((b, i) => {
912
+ lines += ' {\n';
913
+ lines += ' "x1": ' + b.x1.toFixed(4) + ',\n';
914
+ lines += ' "y1": ' + b.y1.toFixed(4) + ',\n';
915
+ lines += ' "x2": ' + b.x2.toFixed(4) + ',\n';
916
+ lines += ' "y2": ' + b.y2.toFixed(4) + '\n';
917
+ lines += ' }';
918
+ if (i < boxes.length - 1) lines += ',';
919
+ lines += '\n';
920
+ });
921
+ lines += ']';
922
+ return lines;
923
+ }
924
+
925
+ function syncToGradio() {
926
+ window.__bboxBoxes = boxes;
927
+ const jsonStr = JSON.stringify(boxes);
928
+ if (debugCount) {
929
+ debugCount.textContent = boxes.length > 0
930
+ ? boxes.length + ' box' + (boxes.length > 1 ? 'es' : '') + ' drawn'
931
+ : 'No boxes drawn';
 
 
 
 
 
932
  }
933
+ if (jsonDisplay) {
934
+ jsonDisplay.textContent = formatJsonPretty(boxes);
935
+ jsonDisplay.scrollTop = jsonDisplay.scrollHeight;
 
 
 
936
  }
937
+ setGradioValue('boxes-json-input', jsonStr);
938
+ }
939
 
940
+ function syncImageToGradio(dataUrl) {
941
+ setGradioValue('hidden-image-b64', dataUrl);
942
+ }
 
 
 
 
 
 
 
 
943
 
944
+ function syncPromptToGradio() {
945
+ const promptInput = document.getElementById('custom-prompt-input');
946
+ if (promptInput) {
947
+ setGradioValue('prompt-gradio-input', promptInput.value);
 
 
 
 
 
 
 
 
 
 
 
948
  }
949
+ }
950
 
951
+ function redraw(tempRect) {
952
+ ctx.clearRect(0,0,dispW,dispH);
953
+ if (!baseImg) {
954
+ ctx.fillStyle='#09090b'; ctx.fillRect(0,0,dispW,dispH);
955
+ updateBadge(); return;
 
 
 
 
 
 
 
 
 
 
 
956
  }
957
+ ctx.drawImage(baseImg, 0, 0, dispW, dispH);
958
+
959
+ boxes.forEach((b,i) => {
960
+ const p = n2px(b);
961
+ const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
962
+ if (i === selectedIdx) {
963
+ ctx.strokeStyle = SEL_STROKE;
964
+ ctx.lineWidth = RED_STROKE_WIDTH + 1;
965
+ ctx.setLineDash([4,3]);
966
+ } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  ctx.strokeStyle = RED_STROKE;
968
  ctx.lineWidth = RED_STROKE_WIDTH;
 
 
969
  ctx.setLineDash([]);
970
  }
971
+ ctx.strokeRect(lx, ty, w, h);
972
+ ctx.setLineDash([]);
973
+
974
+ ctx.fillStyle = i===selectedIdx ? '#6366f1' : '#ef4444';
975
+ ctx.font = 'bold 11px Inter,system-ui,sans-serif';
976
+ ctx.textAlign = 'left'; ctx.textBaseline = 'top';
977
+ const label = '#'+(i+1);
978
+ const tw = ctx.measureText(label).width;
979
+ const rx = lx, ry = ty - 18;
980
+ const rw = tw + 10, rh = 18;
981
+ ctx.beginPath();
982
+ if (ctx.roundRect) {
983
+ ctx.roundRect(rx, ry, rw, rh, 3);
984
+ } else {
985
+ ctx.rect(rx, ry, rw, rh);
986
  }
987
+ ctx.fill();
988
+ ctx.fillStyle = '#fff';
989
+ ctx.fillText(label, lx+5, ty-15);
990
 
991
+ if (i === selectedIdx) drawHandles(p);
992
+ });
 
 
 
 
 
 
993
 
994
+ if (tempRect) {
995
+ const rx = Math.min(tempRect.x1,tempRect.x2);
996
+ const ry = Math.min(tempRect.y1,tempRect.y2);
997
+ const rw = Math.abs(tempRect.x2-tempRect.x1);
998
+ const rh = Math.abs(tempRect.y2-tempRect.y1);
999
+ ctx.strokeStyle = RED_STROKE;
1000
+ ctx.lineWidth = RED_STROKE_WIDTH;
1001
+ ctx.setLineDash([4,3]);
1002
+ ctx.strokeRect(rx, ry, rw, rh);
1003
+ ctx.setLineDash([]);
1004
  }
1005
+ updateBadge();
1006
+ }
1007
 
1008
+ function drawHandles(p) {
1009
+ const pts = handlePoints(p);
1010
+ for (const k in pts) {
1011
+ const h = pts[k];
1012
+ ctx.fillStyle = '#6366f1';
1013
+ ctx.beginPath();
1014
+ ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2);
1015
+ ctx.fill();
1016
+ ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
1017
+ ctx.beginPath();
1018
+ ctx.arc(h.x, h.y, HANDLE, 0, Math.PI*2);
1019
+ ctx.stroke();
1020
  }
1021
+ }
1022
 
1023
+ function handlePoints(p) {
1024
+ const mx = (p.x1+p.x2)/2, my = (p.y1+p.y2)/2;
1025
+ return {
1026
+ tl:{x:p.x1,y:p.y1}, tc:{x:mx,y:p.y1}, tr:{x:p.x2,y:p.y1},
1027
+ ml:{x:p.x1,y:my}, mr:{x:p.x2,y:my},
1028
+ bl:{x:p.x1,y:p.y2}, bc:{x:mx,y:p.y2}, br:{x:p.x2,y:p.y2}
1029
+ };
1030
+ }
1031
+
1032
+ function hitHandle(px, py, boxIdx) {
1033
+ if (boxIdx < 0) return null;
1034
+ const p = n2px(boxes[boxIdx]);
1035
+ const pts = handlePoints(p);
1036
+ for (const k in pts) {
1037
+ if (Math.abs(px-pts[k].x) <= HANDLE+2 && Math.abs(py-pts[k].y) <= HANDLE+2) return k;
1038
  }
1039
+ return null;
1040
+ }
1041
 
1042
+ function hitBox(px, py) {
1043
+ for (let i = boxes.length-1; i >= 0; i--) {
1044
+ const p = n2px(boxes[i]);
1045
+ if (px >= p.x1 && px <= p.x2 && py >= p.y1 && py <= p.y2) return i;
 
 
 
1046
  }
1047
+ return -1;
1048
+ }
1049
 
1050
+ function updateBadge() {
1051
+ if (boxes.length > 0) {
1052
+ badge.style.display = 'block';
1053
+ badge.textContent = boxes.length + ' box' + (boxes.length>1?'es':'');
1054
+ } else {
1055
+ badge.style.display = 'none';
1056
  }
1057
+ }
1058
 
1059
+ function setMode(m) {
1060
+ mode = m;
1061
+ btnDraw.classList.toggle('active', m==='draw');
1062
+ btnSelect.classList.toggle('active', m==='select');
1063
+ canvas.style.cursor = m==='draw' ? 'crosshair' : 'default';
1064
+ if (m==='draw') selectedIdx = -1;
1065
+ redraw();
1066
+ }
1067
 
1068
+ function showStatus(txt) {
1069
+ status.textContent = txt; status.style.display = 'block';
1070
+ }
1071
+ function hideStatus() { status.style.display = 'none'; }
1072
+
1073
+ function onDown(e) {
1074
+ if (!baseImg) return;
1075
+ e.preventDefault();
1076
+ const {x, y} = canvasXY(e);
1077
+ if (mode === 'draw') {
1078
+ dragging = true; dragType = 'new';
1079
+ dragStart = {x, y};
1080
+ selectedIdx = -1;
1081
+ } else {
1082
+ if (selectedIdx >= 0) {
1083
+ const h = hitHandle(x, y, selectedIdx);
1084
+ if (h) {
1085
+ dragging = true; dragType = h;
1086
+ dragStart = {x, y};
1087
+ dragOrig = {...boxes[selectedIdx]};
1088
+ showStatus('Resizing #'+(selectedIdx+1));
1089
+ return;
1090
  }
1091
+ }
1092
+ const hi = hitBox(x, y);
1093
+ if (hi >= 0) {
1094
+ selectedIdx = hi;
1095
+ const h2 = hitHandle(x, y, selectedIdx);
1096
+ if (h2) {
1097
+ dragging = true; dragType = h2;
 
 
 
 
 
1098
  dragStart = {x, y};
1099
  dragOrig = {...boxes[selectedIdx]};
1100
+ showStatus('Resizing #'+(selectedIdx+1));
1101
+ redraw(); return;
 
 
1102
  }
1103
+ dragging = true; dragType = 'move';
1104
+ dragStart = {x, y};
1105
+ dragOrig = {...boxes[selectedIdx]};
1106
+ showStatus('Moving #'+(selectedIdx+1));
1107
+ } else {
1108
+ selectedIdx = -1;
1109
+ hideStatus();
1110
  }
1111
+ redraw();
1112
  }
1113
+ }
1114
 
1115
+ function onMove(e) {
1116
+ if (!baseImg) return;
1117
+ e.preventDefault();
1118
+ const {x, y} = canvasXY(e);
1119
+ if (!dragging) {
1120
+ if (mode === 'select') {
1121
+ if (selectedIdx >= 0 && hitHandle(x,y,selectedIdx)) {
1122
+ const h = hitHandle(x,y,selectedIdx);
1123
+ const curs = {tl:'nwse-resize',tr:'nesw-resize',bl:'nesw-resize',br:'nwse-resize',
1124
+ tc:'ns-resize',bc:'ns-resize',ml:'ew-resize',mr:'ew-resize'};
1125
+ canvas.style.cursor = curs[h] || 'move';
1126
+ } else if (hitBox(x,y) >= 0) {
1127
+ canvas.style.cursor = 'move';
1128
+ } else {
1129
+ canvas.style.cursor = 'default';
 
 
1130
  }
 
1131
  }
1132
+ return;
1133
+ }
1134
+ if (dragType === 'new') {
1135
+ redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y});
1136
+ showStatus(Math.abs(x-dragStart.x).toFixed(0)+'\u00d7'+Math.abs(y-dragStart.y).toFixed(0)+' px');
1137
+ return;
1138
+ }
1139
+ const dx = (x - dragStart.x) / dispW;
1140
+ const dy = (y - dragStart.y) / dispH;
1141
+ const b = boxes[selectedIdx];
1142
+ const o = dragOrig;
1143
+ if (dragType === 'move') {
1144
+ const bw = o.x2-o.x1, bh = o.y2-o.y1;
1145
+ let nx1 = o.x1+dx, ny1 = o.y1+dy;
1146
+ nx1 = clamp01(nx1); ny1 = clamp01(ny1);
1147
+ if (nx1+bw > 1) nx1 = 1-bw;
1148
+ if (ny1+bh > 1) ny1 = 1-bh;
1149
+ b.x1=nx1; b.y1=ny1; b.x2=nx1+bw; b.y2=ny1+bh;
1150
+ } else {
1151
+ const t = dragType;
1152
+ if (t.includes('l')) b.x1 = clamp01(o.x1 + dx);
1153
+ if (t.includes('r')) b.x2 = clamp01(o.x2 + dx);
1154
+ if (t.includes('t')) b.y1 = clamp01(o.y1 + dy);
1155
+ if (t.includes('b')) b.y2 = clamp01(o.y2 + dy);
1156
+ if (Math.abs(b.x2-b.x1) < 0.01) { b.x1=o.x1; b.x2=o.x2; }
1157
+ if (Math.abs(b.y2-b.y1) < 0.01) { b.y1=o.y1; b.y2=o.y2; }
1158
+ if (b.x1 > b.x2) { const t2=b.x1; b.x1=b.x2; b.x2=t2; }
1159
+ if (b.y1 > b.y2) { const t2=b.y1; b.y1=b.y2; b.y2=t2; }
1160
+ }
1161
+ redraw();
1162
+ }
1163
 
1164
+ function onUp(e) {
1165
+ if (!dragging) return;
1166
+ if (e) e.preventDefault();
1167
+ dragging = false;
1168
+ if (dragType === 'new') {
1169
+ const pt = e ? canvasXY(e) : {x:dragStart.x, y:dragStart.y};
1170
+ if (Math.abs(pt.x-dragStart.x) > 4 && Math.abs(pt.y-dragStart.y) > 4) {
1171
+ const nb = px2n(dragStart.x, dragStart.y, pt.x, pt.y);
1172
+ boxes.push(nb);
1173
+ window.__bboxBoxes = boxes;
1174
+ selectedIdx = boxes.length - 1;
1175
+ showStatus('Box #'+boxes.length+' created');
1176
+ } else { hideStatus(); }
1177
+ } else {
1178
+ showStatus('Box #'+(selectedIdx+1)+' updated');
1179
+ }
1180
+ dragType = null; dragOrig = null;
1181
+ syncToGradio();
1182
+ redraw();
1183
+ }
1184
 
1185
+ canvas.addEventListener('mousedown', onDown);
1186
+ canvas.addEventListener('mousemove', onMove);
1187
+ canvas.addEventListener('mouseup', onUp);
1188
+ canvas.addEventListener('mouseleave', (e)=>{if(dragging)onUp(e);});
1189
+ canvas.addEventListener('touchstart', onDown, {passive:false});
1190
+ canvas.addEventListener('touchmove', onMove, {passive:false});
1191
+ canvas.addEventListener('touchend', onUp, {passive:false});
1192
+ canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
1193
+
1194
+ function processFile(file) {
1195
+ if (!file || !file.type.startsWith('image/')) return;
1196
+ const reader = new FileReader();
1197
+ reader.onload = (event) => {
1198
+ const dataUrl = event.target.result;
1199
+ const img = new window.Image();
1200
+ img.crossOrigin = 'anonymous';
1201
+ img.onload = () => {
1202
+ baseImg = img;
1203
+ boxes.length = 0;
1204
+ window.__bboxBoxes = boxes;
1205
+ selectedIdx = -1;
1206
+ fitSize(img.naturalWidth, img.naturalHeight);
1207
+ syncToGradio(); redraw(); hideStatus();
1208
+ uploadPrompt.style.display = 'none';
1209
+ syncImageToGradio(dataUrl);
1210
+ };
1211
+ img.src = dataUrl;
1212
+ };
1213
+ reader.readAsDataURL(file);
1214
+ }
1215
+
1216
+ function openFilePicker() {
1217
+ fileInput.click();
1218
+ }
1219
+
1220
+ uploadClickArea.addEventListener('click', openFilePicker);
1221
+ btnChange.addEventListener('click', openFilePicker);
1222
+
1223
+ fileInput.addEventListener('change', (e) => {
1224
+ processFile(e.target.files[0]);
1225
+ e.target.value = '';
1226
+ });
1227
+
1228
+ wrap.addEventListener('dragover', (e) => {
1229
+ e.preventDefault();
1230
+ wrap.style.outline = '2px solid #6366f1';
1231
+ wrap.style.outlineOffset = '-2px';
1232
+ });
1233
+ wrap.addEventListener('dragleave', (e) => {
1234
+ e.preventDefault();
1235
+ wrap.style.outline = '';
1236
+ });
1237
+ wrap.addEventListener('drop', (e) => {
1238
+ e.preventDefault();
1239
+ wrap.style.outline = '';
1240
+ if (e.dataTransfer.files.length) {
1241
+ processFile(e.dataTransfer.files[0]);
1242
  }
1243
+ });
1244
 
1245
+ btnDraw.addEventListener('click', ()=>setMode('draw'));
1246
+ btnSelect.addEventListener('click', ()=>setMode('select'));
1247
+
1248
+ btnDel.addEventListener('click', () => {
1249
+ if (selectedIdx >= 0 && selectedIdx < boxes.length) {
1250
+ const removed = selectedIdx + 1;
1251
+ boxes.splice(selectedIdx, 1);
1252
+ window.__bboxBoxes = boxes;
1253
+ selectedIdx = -1;
1254
+ syncToGradio(); redraw();
1255
+ showStatus('Box #'+removed+' deleted');
1256
+ } else {
1257
+ showStatus('Select a box first');
 
 
 
 
 
 
 
 
1258
  }
1259
+ });
1260
 
1261
+ btnUndo.addEventListener('click', () => {
1262
+ if (boxes.length > 0) {
1263
+ boxes.pop();
1264
+ window.__bboxBoxes = boxes;
1265
+ selectedIdx = -1;
1266
+ syncToGradio(); redraw();
1267
+ showStatus('Last box removed');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1268
  }
1269
+ });
1270
 
1271
+ btnClear.addEventListener('click', () => {
1272
+ boxes.length = 0;
1273
+ window.__bboxBoxes = boxes;
1274
+ selectedIdx = -1;
1275
+ syncToGradio(); redraw(); hideStatus();
1276
+ });
1277
+
1278
+ const promptInput = document.getElementById('custom-prompt-input');
1279
+ if (promptInput) {
1280
+ promptInput.addEventListener('input', () => {
1281
+ syncPromptToGradio();
1282
  });
1283
+ }
1284
 
1285
+ function syncSlider(customId, gradioId) {
1286
+ const slider = document.getElementById(customId);
1287
+ const valSpan = document.getElementById(customId + '-val');
1288
+ if (!slider) return;
1289
+ slider.addEventListener('input', () => {
1290
+ if (valSpan) valSpan.textContent = slider.value;
1291
+ const container = document.getElementById(gradioId);
1292
+ if (!container) return;
1293
+ const targets = [
1294
+ ...container.querySelectorAll('input[type="range"]'),
1295
+ ...container.querySelectorAll('input[type="number"]')
1296
+ ];
1297
+ targets.forEach(el => {
1298
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
1299
+ if (ns && ns.set) {
1300
+ ns.set.call(el, slider.value);
1301
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
1302
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
1303
+ }
1304
+ });
1305
  });
1306
+ }
1307
+ syncSlider('custom-seed', 'gradio-seed');
1308
+ syncSlider('custom-guidance', 'gradio-guidance');
1309
+ syncSlider('custom-steps', 'gradio-steps');
1310
+ syncSlider('custom-height', 'gradio-height');
1311
+ syncSlider('custom-width', 'gradio-width');
1312
+
1313
+ const randCheck = document.getElementById('custom-randomize');
1314
+ if (randCheck) {
1315
+ randCheck.addEventListener('change', () => {
1316
+ const container = document.getElementById('gradio-randomize');
1317
+ if (!container) return;
1318
+ const cb = container.querySelector('input[type="checkbox"]');
1319
+ if (cb && cb.checked !== randCheck.checked) {
1320
+ cb.click();
1321
  }
1322
  });
1323
+ }
1324
 
1325
+ function showLoaders() {
1326
+ const l1 = document.getElementById('output-loader');
1327
+ const l2 = document.getElementById('preview-loader');
1328
+ if (l1) l1.classList.add('active');
1329
+ if (l2) l2.classList.add('active');
1330
+ const sb = document.querySelector('.app-statusbar .sb-fixed');
1331
+ if (sb) sb.textContent = 'Processing...';
1332
+ }
1333
 
1334
+ function hideLoaders() {
1335
+ const l1 = document.getElementById('output-loader');
1336
+ const l2 = document.getElementById('preview-loader');
1337
+ if (l1) l1.classList.remove('active');
1338
+ if (l2) l2.classList.remove('active');
1339
+ const sb = document.querySelector('.app-statusbar .sb-fixed');
1340
+ if (sb) sb.textContent = 'Done';
1341
+ }
1342
+
1343
+ window.__showLoaders = showLoaders;
1344
+ window.__hideLoaders = hideLoaders;
1345
+
1346
+ window.__clickGradioRunBtn = function() {
1347
+ syncPromptToGradio();
1348
+ syncToGradio();
1349
+ showLoaders();
1350
+
1351
+ setTimeout(() => {
1352
+ const gradioBtn = document.getElementById('gradio-run-btn');
1353
+ if (!gradioBtn) return;
1354
+ const btn = gradioBtn.querySelector('button');
1355
+ if (btn) {
1356
+ btn.click();
1357
  } else {
1358
+ gradioBtn.click();
1359
  }
1360
+ }, 200);
1361
+ };
1362
 
1363
+ const customRunBtn = document.getElementById('custom-run-btn');
1364
+ if (customRunBtn) {
1365
+ customRunBtn.addEventListener('click', () => {
1366
+ window.__clickGradioRunBtn();
 
 
 
 
1367
  });
1368
+ }
1369
 
1370
+ new ResizeObserver(() => {
1371
+ if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
1372
+ }).observe(wrap);
1373
+
1374
+ setMode('draw');
1375
+ fitSize(512,400); redraw();
1376
+ syncToGradio();
1377
+ }
1378
 
1379
+ initCanvasBbox();
1380
+ }
1381
+ """
1382
 
1383
+ wire_outputs_js = r"""
1384
+ () => {
1385
+ function downloadImage(imgSrc, filename) {
1386
+ const a = document.createElement('a');
1387
+ a.href = imgSrc;
1388
+ a.download = filename || 'image.png';
1389
+ document.body.appendChild(a);
1390
+ a.click();
1391
+ document.body.removeChild(a);
1392
+ }
1393
+
1394
+ function watchOutputs() {
1395
+ const resultContainer = document.getElementById('gradio-result');
1396
+ const previewContainer = document.getElementById('gradio-preview');
1397
+ const outBody = document.getElementById('output-image-container');
1398
+ const prevBody = document.getElementById('preview-image-container');
1399
+ const outPh = document.getElementById('output-placeholder');
1400
+ const prevPh = document.getElementById('preview-placeholder');
1401
+ const dlBtnOut = document.getElementById('dl-btn-output');
1402
+ const dlBtnPrev = document.getElementById('dl-btn-preview');
1403
+
1404
+ if (!resultContainer || !previewContainer || !outBody || !prevBody) {
1405
+ setTimeout(watchOutputs, 500);
1406
+ return;
1407
+ }
1408
+
1409
+ if (dlBtnOut) {
1410
+ dlBtnOut.addEventListener('click', (e) => {
1411
+ e.stopPropagation();
1412
+ const img = outBody.querySelector('img.modern-out-img');
1413
+ if (img && img.src) downloadImage(img.src, 'output_result.png');
1414
+ });
1415
+ }
1416
+ if (dlBtnPrev) {
1417
+ dlBtnPrev.addEventListener('click', (e) => {
1418
+ e.stopPropagation();
1419
+ const img = prevBody.querySelector('img.modern-out-img');
1420
+ if (img && img.src) downloadImage(img.src, 'input_preview.png');
1421
+ });
1422
  }
1423
 
1424
+ function syncImages() {
1425
+ const resultImg = resultContainer.querySelector('img');
1426
+ if (resultImg && resultImg.src) {
1427
+ if (outPh) outPh.style.display = 'none';
1428
+ let existing = outBody.querySelector('img.modern-out-img');
1429
+ if (!existing) {
1430
+ existing = document.createElement('img');
1431
+ existing.className = 'modern-out-img';
1432
+ outBody.appendChild(existing);
1433
+ }
1434
+ if (existing.src !== resultImg.src) {
1435
+ existing.src = resultImg.src;
1436
+ if (dlBtnOut) dlBtnOut.classList.add('visible');
1437
+ if (window.__hideLoaders) window.__hideLoaders();
1438
+ }
1439
+ }
1440
+ const previewImg = previewContainer.querySelector('img');
1441
+ if (previewImg && previewImg.src) {
1442
+ if (prevPh) prevPh.style.display = 'none';
1443
+ let existing2 = prevBody.querySelector('img.modern-out-img');
1444
+ if (!existing2) {
1445
+ existing2 = document.createElement('img');
1446
+ existing2.className = 'modern-out-img';
1447
+ prevBody.appendChild(existing2);
1448
+ }
1449
+ if (existing2.src !== previewImg.src) {
1450
+ existing2.src = previewImg.src;
1451
+ if (dlBtnPrev) dlBtnPrev.classList.add('visible');
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ const observer = new MutationObserver(syncImages);
1457
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
1458
+ observer.observe(previewContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
1459
+ setInterval(syncImages, 800);
1460
+ }
1461
+ watchOutputs();
1462
+
1463
+ function watchDimensions() {
1464
+ const wContainer = document.getElementById('gradio-width');
1465
+ const hContainer = document.getElementById('gradio-height');
1466
+ const wSlider = document.getElementById('custom-width');
1467
+ const hSlider = document.getElementById('custom-height');
1468
+ const wVal = document.getElementById('custom-width-val');
1469
+ const hVal = document.getElementById('custom-height-val');
1470
+ if (!wContainer || !hContainer || !wSlider || !hSlider) {
1471
+ setTimeout(watchDimensions, 500);
1472
+ return;
1473
+ }
1474
+ function syncDims() {
1475
+ const wInput = wContainer.querySelector('input[type="range"],input[type="number"]');
1476
+ const hInput = hContainer.querySelector('input[type="range"],input[type="number"]');
1477
+ if (wInput && wInput.value) { wSlider.value = wInput.value; if(wVal) wVal.textContent = wInput.value; }
1478
+ if (hInput && hInput.value) { hSlider.value = hInput.value; if(hVal) hVal.textContent = hInput.value; }
1479
+ }
1480
+ const obs = new MutationObserver(syncDims);
1481
+ obs.observe(wContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
1482
+ obs.observe(hContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
1483
+ setInterval(syncDims, 1000);
1484
+ }
1485
+ watchDimensions();
1486
  }
1487
  """
1488
 
1489
+ 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>'
1490
 
1491
  with gr.Blocks() as demo:
 
 
 
 
 
 
 
1492
 
1493
+ hidden_image_b64 = gr.Textbox(
1494
+ elem_id="hidden-image-b64",
1495
+ elem_classes="hidden-input",
1496
+ container=False
1497
+ )
1498
+ boxes_json = gr.Textbox(
1499
+ value="[]",
1500
+ elem_id="boxes-json-input",
1501
+ elem_classes="hidden-input",
1502
+ container=False
1503
+ )
1504
+ prompt = gr.Textbox(
1505
+ value=DEFAULT_PROMPT,
1506
+ elem_id="prompt-gradio-input",
1507
+ elem_classes="hidden-input",
1508
+ container=False
1509
+ )
1510
+ seed = gr.Slider(
1511
+ minimum=0, maximum=MAX_SEED, step=1, value=0,
1512
+ elem_id="gradio-seed",
1513
+ elem_classes="hidden-input",
1514
+ container=False
1515
+ )
1516
+ randomize_seed = gr.Checkbox(
1517
+ value=True,
1518
+ elem_id="gradio-randomize",
1519
+ elem_classes="hidden-input",
1520
+ container=False
1521
+ )
1522
+ guidance_scale = gr.Slider(
1523
+ minimum=1.0, maximum=10.0, step=0.1, value=1.0,
1524
+ elem_id="gradio-guidance",
1525
+ elem_classes="hidden-input",
1526
+ container=False
1527
+ )
1528
+ num_inference_steps = gr.Slider(
1529
+ minimum=1, maximum=20, step=1, value=4,
1530
+ elem_id="gradio-steps",
1531
+ elem_classes="hidden-input",
1532
+ container=False
1533
+ )
1534
+ height_slider = gr.Slider(
1535
+ minimum=256, maximum=2048, step=8, value=1024,
1536
+ elem_id="gradio-height",
1537
+ elem_classes="hidden-input",
1538
+ container=False
1539
+ )
1540
+ width_slider = gr.Slider(
1541
+ minimum=256, maximum=2048, step=8, value=1024,
1542
+ elem_id="gradio-width",
1543
+ elem_classes="hidden-input",
1544
+ container=False
1545
+ )
1546
 
1547
+ result = gr.Image(
1548
+ elem_id="gradio-result",
1549
+ elem_classes="hidden-input",
1550
+ container=False,
1551
+ format="png"
1552
+ )
1553
+ preview = gr.Image(
1554
+ elem_id="gradio-preview",
1555
+ elem_classes="hidden-input",
1556
+ container=False
1557
+ )
1558
 
1559
+ gr.HTML(f"""
1560
+ <div class="app-shell">
1561
+
1562
+ <!-- Header -->
1563
+ <div class="app-header">
1564
+ <div class="app-header-left">
1565
+ <div class="app-logo">⌦</div>
1566
+ <span class="app-title">QIE Object Remover</span>
1567
+ <span class="app-badge">Bbox</span>
1568
+ </div>
1569
+ </div>
1570
+
1571
+ <!-- Toolbar -->
1572
+ <div class="app-toolbar">
1573
+ <button id="tb-draw" class="modern-tb-btn active" title="Draw bounding boxes">
1574
+ <span class="tb-icon">▬</span><span class="tb-label">Draw</span>
1575
+ </button>
1576
+ <button id="tb-select" class="modern-tb-btn" title="Select, move, resize boxes">
1577
+ <span class="tb-icon">⇉</span><span class="tb-label">Select</span>
1578
+ </button>
1579
+ <div class="tb-sep"></div>
1580
+ <button id="tb-del" class="modern-tb-btn" title="Delete selected box">
1581
+ <span class="tb-icon">✕</span><span class="tb-label">Delete</span>
1582
+ </button>
1583
+ <button id="tb-undo" class="modern-tb-btn" title="Undo last box">
1584
+ <span class="tb-icon">↩</span><span class="tb-label">Undo</span>
1585
+ </button>
1586
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all boxes">
1587
+ <span class="tb-icon">✖</span><span class="tb-label">Clear</span>
1588
+ </button>
1589
+ <div class="tb-sep"></div>
1590
+ <button id="tb-change-img" class="modern-tb-btn" title="Upload a different image">
1591
+ <span class="tb-label">Upload…</span>
1592
+ </button>
1593
+ </div>
1594
+
1595
+ <!-- Main Content -->
1596
+ <div class="app-main-row">
1597
+
1598
+ <!-- Left: Canvas -->
1599
+ <div class="app-main-left">
1600
  <div id="bbox-draw-wrap">
1601
+ <div id="upload-prompt" class="upload-prompt-modern">
1602
+ <div id="upload-click-area" class="upload-click-area">
1603
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1604
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#6366f1" stroke-width="2" stroke-dasharray="4 3"/>
1605
+ <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"/>
1606
+ <circle cx="28" cy="30" r="6" fill="rgba(99,102,241,0.2)" stroke="#6366f1" stroke-width="1.5"/>
1607
+ <line x1="40" y1="4" x2="40" y2="16" stroke="#818cf8" stroke-width="2" stroke-linecap="round"/>
1608
+ <line x1="34" y1="10" x2="46" y2="10" stroke="#818cf8" stroke-width="2" stroke-linecap="round"/>
1609
+ </svg>
1610
+ </div>
 
 
 
 
 
 
 
 
 
 
1611
  </div>
1612
+ <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
1613
  <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
1614
  <div id="bbox-status"></div>
1615
  <div id="bbox-count"></div>
1616
  </div>
1617
+
1618
+ <div class="hint-bar">
1619
+ <b>Draw:</b> Click &amp; drag to create selection boxes &nbsp;·&nbsp;
1620
+ <b>Select:</b> Click a box to move or resize &nbsp;·&nbsp;
1621
+ <kbd>Delete</kbd> removes selected &nbsp;·&nbsp;
1622
+ <kbd>Clear</kbd> removes all
1623
+ </div>
1624
+
1625
+ <div class="json-panel">
1626
+ <div class="json-panel-title">Bounding Boxes</div>
1627
+ <div class="json-panel-content" id="bbox-json-content">[
1628
+ // No bounding boxes defined
1629
+ ]</div>
1630
+ </div>
1631
+ </div>
1632
+
1633
+ <!-- Right: Controls & Output -->
1634
+ <div class="app-main-right">
1635
+
1636
+ <div class="panel-card">
1637
+ <div class="panel-card-title">Edit Instruction</div>
1638
+ <div class="panel-card-body">
1639
+ <label class="modern-label" for="custom-prompt-input">Prompt</label>
1640
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="2" placeholder="Describe the edit...">Remove the red highlighted object from the scene</textarea>
1641
+ </div>
1642
+ </div>
1643
+
1644
+ <div style="padding:12px 20px;">
1645
+ <button id="custom-run-btn" class="btn-run">
1646
+ <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>
1647
+ Remove Object
1648
+ </button>
1649
+ </div>
1650
+
1651
+ <div class="output-frame" style="flex:1">
1652
+ <div class="out-title">
1653
+ <span>Output</span>
1654
+ <span id="dl-btn-output" class="out-download-btn" title="Download">
1655
+ {DOWNLOAD_SVG} Save
1656
+ </span>
1657
+ </div>
1658
+ <div class="out-body" id="output-image-container">
1659
+ <div class="modern-loader" id="output-loader">
1660
+ <div class="loader-spinner"></div>
1661
+ <div class="loader-text">Processing image…</div>
1662
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1663
+ </div>
1664
+ <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
1665
+ </div>
1666
+ </div>
1667
+
1668
+ <div class="output-frame">
1669
+ <div class="out-title">
1670
+ <span>Input Preview</span>
1671
+ <span id="dl-btn-preview" class="out-download-btn" title="Download">
1672
+ {DOWNLOAD_SVG} Save
1673
+ </span>
1674
+ </div>
1675
+ <div class="out-body" id="preview-image-container">
1676
+ <div class="modern-loader" id="preview-loader">
1677
+ <div class="loader-spinner"></div>
1678
+ <div class="loader-text">Preparing input…</div>
1679
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1680
+ </div>
1681
+ <div class="out-placeholder" id="preview-placeholder">Preview will appear here</div>
1682
+ </div>
1683
+ </div>
1684
+
1685
+ <div class="settings-group">
1686
+ <div class="settings-group-title">Advanced Settings</div>
1687
+ <div class="settings-group-body">
1688
+ <div class="slider-row">
1689
+ <label>Seed</label>
1690
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1691
+ <span class="slider-val" id="custom-seed-val">0</span>
1692
+ </div>
1693
+ <div class="checkbox-row">
1694
+ <input type="checkbox" id="custom-randomize" checked>
1695
+ <label for="custom-randomize">Randomize seed</label>
1696
+ </div>
1697
+ <div class="slider-row">
1698
+ <label>Guidance</label>
1699
+ <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
1700
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
1701
+ </div>
1702
+ <div class="slider-row">
1703
+ <label>Steps</label>
1704
+ <input type="range" id="custom-steps" min="1" max="20" step="1" value="4">
1705
+ <span class="slider-val" id="custom-steps-val">4</span>
1706
+ </div>
1707
+ <div class="slider-row">
1708
+ <span class="dim-label">Width</span>
1709
+ <input type="range" id="custom-width" min="256" max="2048" step="8" value="1024">
1710
+ <span class="slider-val" id="custom-width-val">1024</span>
1711
+ </div>
1712
+ <div class="slider-row">
1713
+ <span class="dim-label">Height</span>
1714
+ <input type="range" id="custom-height" min="256" max="2048" step="8" value="1024">
1715
+ <span class="slider-val" id="custom-height-val">1024</span>
1716
+ </div>
1717
+ </div>
1718
  </div>
1719
+
1720
+ </div>
1721
+ </div>
1722
+
1723
+ <!-- Status Bar -->
1724
+ <div class="app-statusbar">
1725
+ <div class="sb-section" id="bbox-debug-count">No boxes drawn</div>
1726
+ <div class="sb-section sb-fixed">Ready</div>
1727
+ </div>
1728
+
1729
+ </div>
1730
+ """)
1731
+
1732
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1733
 
1734
  demo.load(fn=None, js=bbox_drawer_js)
1735
+ demo.load(fn=None, js=wire_outputs_js)
1736
 
1737
  run_btn.click(
1738
  fn=infer_object_removal,
 
1741
  outputs=[result, seed, preview],
1742
  js="""(b64, bj, p, s, rs, gs, nis, h, w) => {
1743
  const boxes = window.__bboxBoxes || [];
1744
+ const json = JSON.stringify(boxes);
 
1745
  return [b64, json, p, s, rs, gs, nis, h, w];
1746
  }""",
1747
  )
 
1754
 
1755
  if __name__ == "__main__":
1756
  demo.launch(
1757
+ css=css,
1758
  mcp_server=True, ssr_mode=False, show_error=True
1759
  )