prithivMLmods commited on
Commit
d0dfb05
Β·
verified Β·
1 Parent(s): e026e01

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +294 -210
app.py CHANGED
@@ -3,8 +3,6 @@ import numpy as np
3
  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
@@ -116,19 +114,6 @@ 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:
124
- _, data = b64_str.split(',', 1)
125
- image_data = base64.b64decode(data)
126
- return Image.open(BytesIO(image_data)).convert("RGB")
127
- except Exception as e:
128
- print(f"Error decoding image: {e}")
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
@@ -153,7 +138,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,7 +145,7 @@ 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,
@@ -190,9 +174,8 @@ def infer_object_removal(
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:
@@ -227,8 +210,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
234
  original_width, original_height = image.size
@@ -276,65 +258,12 @@ label{font-weight:600!important;color:#333!important}
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 Upload Prompt overlay */
286
- #upload-prompt {
287
- position: absolute;
288
- top: 50%; left: 50%;
289
- transform: translate(-50%, -50%);
290
- text-align: center;
291
- z-index: 20;
292
- background: rgba(255, 255, 255, 0.95);
293
- padding: 40px;
294
- border-radius: 12px;
295
- box-shadow: 0 8px 32px rgba(168,85,247,0.15);
296
- backdrop-filter: blur(8px);
297
- width: 80%;
298
- max-width: 400px;
299
- cursor: pointer;
300
- border: 2px dashed #C084FC;
301
- transition: all 0.2s ease;
302
- }
303
- #upload-prompt:hover {
304
- border-color: #A855F7;
305
- background: rgba(250, 245, 255, 0.95);
306
- transform: translate(-50%, -52%);
307
- box-shadow: 0 12px 36px rgba(168,85,247,0.25);
308
- }
309
- .dark #upload-prompt {
310
- background: rgba(30, 30, 30, 0.95);
311
- border-color: rgba(168,85,247,0.5);
312
- }
313
- .dark #upload-prompt:hover {
314
- background: rgba(40, 40, 40, 0.95);
315
- border-color: #C084FC;
316
- }
317
- .upload-icon {
318
- width: 54px;
319
- height: 54px;
320
- color: #A855F7;
321
- margin-bottom: 12px;
322
- }
323
- #upload-prompt p {
324
- margin: 0 0 6px 0;
325
- font-family: 'Outfit', sans-serif;
326
- color: #6B21A8;
327
- font-weight: 700;
328
- font-size: 1.5rem;
329
- }
330
- .dark #upload-prompt p { color: #DAB2FF; }
331
- .upload-subtext {
332
- font-size: 0.95rem;
333
- color: #888;
334
- font-family: 'Outfit', sans-serif;
335
- }
336
- .dark .upload-subtext { color: #aaa; }
337
-
338
  .bbox-toolbar-section{
339
  display:flex;
340
  gap:8px;
@@ -388,14 +317,16 @@ label{font-weight:600!important;color:#333!important}
388
  .bbox-tb-select{background:#6366f1}
389
  .bbox-tb-select:hover{background:#818cf8}
390
  .bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
 
 
391
  .bbox-tb-del{background:#dc2626}
392
  .bbox-tb-del:hover{background:#ef4444}
393
  .bbox-tb-undo{background:#7E22CE}
394
  .bbox-tb-undo:hover{background:#9333EA}
395
  .bbox-tb-clear{background:#be123c}
396
  .bbox-tb-clear:hover{background:#e11d48}
397
- .bbox-tb-change{background:#4b5563}
398
- .bbox-tb-change:hover{background:#6b7280}
399
 
400
  #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}
401
  #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}
@@ -413,9 +344,70 @@ label{font-weight:600!important;color:#333!important}
413
  }
414
  .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
415
 
416
- .hidden-input {
417
- display: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  """
420
 
421
  bbox_drawer_js = r"""
@@ -428,18 +420,19 @@ bbox_drawer_js = r"""
428
  const status = document.getElementById('bbox-status');
429
  const badge = document.getElementById('bbox-count');
430
  const debugCount = document.getElementById('bbox-debug-count');
431
-
432
- const btnDraw = document.getElementById('tb-draw');
433
- const btnSelect = document.getElementById('tb-select');
434
- const btnDel = document.getElementById('tb-del');
435
- const btnUndo = document.getElementById('tb-undo');
436
- const btnClear = document.getElementById('tb-clear');
437
- const btnChange = document.getElementById('tb-change-img');
438
-
439
- const uploadPrompt = document.getElementById('upload-prompt');
440
- const fileInput = document.getElementById('custom-file-input');
441
-
442
- if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
 
443
  console.log('[BBox] waiting for DOM...');
444
  setTimeout(initCanvasBbox, 250);
445
  return;
@@ -453,7 +446,8 @@ bbox_drawer_js = r"""
453
  window.__bboxBoxes = boxes;
454
 
455
  let baseImg = null;
456
- let dispW = 512, dispH = 400;
 
457
  let selectedIdx = -1;
458
  let mode = 'draw';
459
 
@@ -466,7 +460,9 @@ bbox_drawer_js = r"""
466
  const RED_STROKE_WIDTH = 3;
467
  const SEL_STROKE = 'rgba(0,120,255,0.95)';
468
 
469
- function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
 
 
470
  function px2n(x1,y1,x2,y2) {
471
  return {
472
  x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
@@ -475,7 +471,7 @@ bbox_drawer_js = r"""
475
  }
476
  function clamp01(v){return Math.max(0,Math.min(1,v));}
477
  function fitSize(nw, nh) {
478
- const mw = wrap.clientWidth || 512, mh = 500;
479
  const r = Math.min(mw/nw, mh/nh, 1);
480
  dispW = Math.round(nw*r); dispH = Math.round(nh*r);
481
  canvas.width = dispW; canvas.height = dispH;
@@ -503,53 +499,79 @@ bbox_drawer_js = r"""
503
  }
504
 
505
  const container = document.getElementById('boxes-json-input');
506
- if (!container) return;
 
 
 
507
  const targets = [
508
  ...container.querySelectorAll('textarea'),
509
- ...container.querySelectorAll('input:not([type="file"])')
 
510
  ];
511
  targets.forEach(el => {
512
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
 
 
513
  const ns = Object.getOwnPropertyDescriptor(proto, 'value');
514
  if (ns && ns.set) {
515
  ns.set.call(el, jsonStr);
516
  el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
517
  el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
 
518
  }
519
  });
520
  }
521
 
522
- function syncImageToGradio(dataUrl) {
523
- const container = document.getElementById('hidden-image-b64');
 
524
  if (!container) return;
525
- const targets = [
526
- ...container.querySelectorAll('textarea'),
527
- ...container.querySelectorAll('input')
528
- ];
529
- targets.forEach(el => {
530
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
531
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
532
- if (ns && ns.set) {
533
- ns.set.call(el, dataUrl);
534
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
535
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
536
- }
537
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  }
539
 
540
  function redraw(tempRect) {
541
  ctx.clearRect(0,0,dispW,dispH);
542
- if (!baseImg) {
543
- ctx.fillStyle='#1a1a1a'; ctx.fillRect(0,0,dispW,dispH);
544
- updateBadge(); return;
545
- }
546
  ctx.drawImage(baseImg, 0, 0, dispW, dispH);
547
 
548
  boxes.forEach((b,i) => {
549
  const p = n2px(b);
550
  const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
551
 
552
- /* RED OUTLINE ONLY β€” no fill */
553
  if (i === selectedIdx) {
554
  ctx.strokeStyle = SEL_STROKE;
555
  ctx.lineWidth = RED_STROKE_WIDTH + 1;
@@ -562,7 +584,6 @@ bbox_drawer_js = r"""
562
  ctx.strokeRect(lx, ty, w, h);
563
  ctx.setLineDash([]);
564
 
565
- /* label tag */
566
  ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
567
  ctx.font = 'bold 11px IBM Plex Mono,monospace';
568
  ctx.textAlign = 'left'; ctx.textBaseline = 'top';
@@ -575,7 +596,6 @@ bbox_drawer_js = r"""
575
  if (i === selectedIdx) drawHandles(p);
576
  });
577
 
578
- /* temp drawing rect β€” outline only */
579
  if (tempRect) {
580
  const rx = Math.min(tempRect.x1,tempRect.x2);
581
  const ry = Math.min(tempRect.y1,tempRect.y2);
@@ -651,6 +671,121 @@ bbox_drawer_js = r"""
651
  }
652
  function hideStatus() { status.style.display = 'none'; }
653
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  function onDown(e) {
655
  if (!baseImg) return;
656
  e.preventDefault();
@@ -779,59 +914,6 @@ bbox_drawer_js = r"""
779
  canvas.addEventListener('touchend', onUp, {passive:false});
780
  canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
781
 
782
- // --- File Upload Logic ---
783
- function processFile(file) {
784
- if (!file || !file.type.startsWith('image/')) return;
785
- const reader = new FileReader();
786
- reader.onload = (event) => {
787
- const dataUrl = event.target.result;
788
- const img = new window.Image();
789
- img.crossOrigin = 'anonymous';
790
- img.onload = () => {
791
- baseImg = img;
792
- boxes.length = 0;
793
- window.__bboxBoxes = boxes;
794
- selectedIdx = -1;
795
- fitSize(img.naturalWidth, img.naturalHeight);
796
- syncToGradio(); redraw(); hideStatus();
797
- uploadPrompt.style.display = 'none';
798
- syncImageToGradio(dataUrl);
799
- };
800
- img.src = dataUrl;
801
- };
802
- reader.readAsDataURL(file);
803
- }
804
-
805
- uploadPrompt.addEventListener('click', (e) => {
806
- if(e.target !== fileInput) fileInput.click();
807
- });
808
- btnChange.addEventListener('click', () => fileInput.click());
809
-
810
- fileInput.addEventListener('change', (e) => {
811
- processFile(e.target.files[0]);
812
- e.target.value = ''; // Reset input to allow re-upload of same file
813
- });
814
-
815
- wrap.addEventListener('dragover', (e) => {
816
- e.preventDefault();
817
- wrap.style.borderColor = '#A855F7';
818
- wrap.style.boxShadow = '0 0 15px rgba(168,85,247,0.3)';
819
- });
820
- wrap.addEventListener('dragleave', (e) => {
821
- e.preventDefault();
822
- wrap.style.borderColor = '';
823
- wrap.style.boxShadow = '';
824
- });
825
- wrap.addEventListener('drop', (e) => {
826
- e.preventDefault();
827
- wrap.style.borderColor = '';
828
- wrap.style.boxShadow = '';
829
- if (e.dataTransfer.files.length) {
830
- processFile(e.dataTransfer.files[0]);
831
- }
832
- });
833
-
834
- // --- Toolbar Logic ---
835
  btnDraw.addEventListener('click', ()=>setMode('draw'));
836
  btnSelect.addEventListener('click', ()=>setMode('select'));
837
 
@@ -865,12 +947,15 @@ bbox_drawer_js = r"""
865
  syncToGradio(); redraw(); hideStatus();
866
  });
867
 
 
 
 
868
  new ResizeObserver(() => {
869
  if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
870
  }).observe(wrap);
871
 
872
  setMode('draw');
873
- fitSize(512,400); redraw();
874
  syncToGradio();
875
  }
876
 
@@ -883,74 +968,72 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
883
  gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
884
  gr.Markdown(
885
  "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
886
- "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. "
887
  "Multiple boxes supported. Select, move, resize or delete individual boxes.",
888
  elem_id="subtitle",
889
  )
890
 
891
  with gr.Row():
892
  with gr.Column(scale=1):
893
-
894
- hidden_image_b64 = gr.Textbox(
895
- elem_id="hidden-image-b64",
896
- elem_classes="hidden-input",
897
- container=False
 
 
898
  )
899
 
900
  gr.Markdown("# **Bbox Edit Controller**")
 
 
 
 
 
 
 
 
901
 
902
  gr.HTML(
903
  """
904
  <div id="bbox-draw-wrap">
905
- <div id="upload-prompt">
906
- <svg class="upload-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
907
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
908
- <polyline points="17 8 12 3 7 8"></polyline>
909
- <line x1="12" y1="3" x2="12" y2="15"></line>
910
- </svg>
911
- <p>Upload Image</p>
912
- <span class="upload-subtext">Drag and drop or click to browse</span>
913
- <input type="file" id="custom-file-input" accept="image/*" style="display:none;" />
914
- </div>
915
- <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
916
  <div id="bbox-status"></div>
917
  <div id="bbox-count"></div>
 
918
  </div>
 
919
  """
920
  )
921
 
922
  gr.HTML(
923
  """
924
  <div class="bbox-toolbar-section">
 
 
 
 
925
  <span class="toolbar-label">πŸ›  Tools:</span>
926
  <button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">✏️ Draw</button>
927
  <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">πŸ”² Select</button>
928
  <div class="toolbar-divider"></div>
929
  <span class="toolbar-label">Actions:</span>
930
- <button id="tb-del" class="bbox-tb-del" title="Delete selected box">βœ• Delete</button>
931
  <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
932
  <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">πŸ—‘ Clear All</button>
933
- <div class="toolbar-divider"></div>
934
- <button id="tb-change-img" class="bbox-tb-change" title="Upload a different image">πŸ“Έ Change Image</button>
935
  </div>
936
  """
937
  )
938
-
939
- gr.HTML(
940
- '<div class="bbox-hint">'
941
- "<b>Draw mode:</b> Click & drag to create red rectangles. "
942
- "<b>Select mode:</b> Click a box to select it \u2192 drag to <b>move</b>, "
943
- "drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
944
- "</div>"
945
- )
946
-
947
  gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
948
 
949
  boxes_json = gr.Textbox(
950
  value="[]",
 
 
951
  elem_id="boxes-json-input",
952
- elem_classes="hidden-input",
953
- container=False
954
  )
955
 
956
  prompt = gr.Textbox(
@@ -982,7 +1065,7 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
982
  "[prithivMLmods](https://huggingface.co/prithivMLmods). "
983
  "Adapter: [QIE-2511-Object-Remover-v2]"
984
  "(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
985
- "More adapters \u2192 [Qwen-Image-Edit-LoRAs]"
986
  "(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
987
  )
988
 
@@ -990,23 +1073,24 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
990
 
991
  run_btn.click(
992
  fn=infer_object_removal,
993
- inputs=[hidden_image_b64, boxes_json, prompt, seed, randomize_seed,
994
  guidance_scale, num_inference_steps, height_slider, width_slider],
995
  outputs=[result, seed, preview],
996
- js="""(b64, bj, p, s, rs, gs, nis, h, w) => {
997
  const boxes = window.__bboxBoxes || [];
998
  const json = JSON.stringify(boxes);
999
  console.log('[BBox] submitting', boxes.length, 'boxes:', json);
1000
- return [b64, json, p, s, rs, gs, nis, h, w];
1001
  }""",
1002
  )
1003
 
1004
- hidden_image_b64.change(
1005
  fn=update_dimensions_on_upload,
1006
- inputs=[hidden_image_b64],
1007
  outputs=[width_slider, height_slider],
1008
  )
1009
 
 
1010
  if __name__ == "__main__":
1011
  demo.launch(
1012
  css=css, theme=purple_theme,
 
3
  import random
4
  import torch
5
  import spaces
 
 
6
  from typing import Iterable
7
  from PIL import Image, ImageDraw
8
  from diffusers import FlowMatchEulerDiscreteScheduler
 
114
  DEFAULT_PROMPT = "Remove the red highlighted object from the scene"
115
 
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.Image:
118
  """Burn red outline-only rectangles onto the image (no fill)."""
119
  import json
 
138
  y2 = int(b["y2"] * h)
139
  lx, rx = min(x1, x2), max(x1, x2)
140
  ty, by_ = min(y1, y2), max(y1, y2)
 
141
  draw.rectangle([lx, ty, rx, by_], outline=(255, 0, 0), width=bw)
142
 
143
  return img
 
145
 
146
  @spaces.GPU
147
  def infer_object_removal(
148
+ source_image: Image.Image,
149
  boxes_json: str,
150
  prompt: str,
151
  seed: int = 0,
 
174
  print(f"Prompt: {prompt}")
175
  print(f"Boxes JSON received: '{boxes_json}'")
176
 
 
177
  if source_image is None:
178
+ raise gr.Error("Please upload an image first.")
179
 
180
  import json
181
  try:
 
210
  return result, seed, marked
211
 
212
 
213
+ def update_dimensions_on_upload(image):
 
214
  if image is None:
215
  return 1024, 1024
216
  original_width, original_height = image.size
 
258
  ::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
259
  ::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
260
 
261
+ #bbox-draw-wrap{position:relative;border:2px solid #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px}
262
  #bbox-draw-wrap:hover{border-color:#A855F7}
263
  #bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
264
  .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}
265
  .dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  .bbox-toolbar-section{
268
  display:flex;
269
  gap:8px;
 
317
  .bbox-tb-select{background:#6366f1}
318
  .bbox-tb-select:hover{background:#818cf8}
319
  .bbox-tb-select.active{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
320
+ .bbox-tb-upload{background:#0891b2}
321
+ .bbox-tb-upload:hover{background:#06b6d4}
322
  .bbox-tb-del{background:#dc2626}
323
  .bbox-tb-del:hover{background:#ef4444}
324
  .bbox-tb-undo{background:#7E22CE}
325
  .bbox-tb-undo:hover{background:#9333EA}
326
  .bbox-tb-clear{background:#be123c}
327
  .bbox-tb-clear:hover{background:#e11d48}
328
+ .bbox-tb-remove-img{background:#64748b}
329
+ .bbox-tb-remove-img:hover{background:#94a3b8}
330
 
331
  #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}
332
  #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}
 
344
  }
345
  .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
346
 
347
+ #boxes-json-input{
348
+ max-height:0!important;
349
+ overflow:hidden!important;
350
+ margin:0!important;
351
+ padding:0!important;
352
+ opacity:0!important;
353
+ pointer-events:none!important;
354
+ position:absolute!important;
355
+ z-index:-1!important;
356
+ }
357
+
358
+ #source-image-component{
359
+ max-height:0!important;
360
+ overflow:hidden!important;
361
+ margin:0!important;
362
+ padding:0!important;
363
+ opacity:0!important;
364
+ pointer-events:none!important;
365
+ position:absolute!important;
366
+ z-index:-1!important;
367
+ }
368
+
369
+ #bbox-file-input{
370
+ display:none;
371
  }
372
+
373
+ /* Drop zone overlay */
374
+ #bbox-drop-overlay{
375
+ position:absolute;
376
+ top:0;left:0;right:0;bottom:0;
377
+ background:rgba(147,51,234,.3);
378
+ border:3px dashed #A855F7;
379
+ border-radius:12px;
380
+ display:none;
381
+ align-items:center;
382
+ justify-content:center;
383
+ z-index:20;
384
+ pointer-events:none;
385
+ }
386
+ #bbox-drop-overlay.active{display:flex}
387
+ #bbox-drop-overlay span{
388
+ font-family:'Outfit',sans-serif;
389
+ font-size:18px;
390
+ font-weight:700;
391
+ color:#fff;
392
+ background:rgba(147,51,234,.8);
393
+ padding:12px 24px;
394
+ border-radius:10px;
395
+ }
396
+
397
+ /* Image info bar */
398
+ #bbox-image-info{
399
+ text-align:center;
400
+ padding:4px 12px;
401
+ margin-top:4px;
402
+ font-family:'IBM Plex Mono',monospace;
403
+ font-size:11px;
404
+ color:#9333EA;
405
+ background:rgba(168,85,247,.06);
406
+ border:1px solid #E9D5FF;
407
+ border-radius:6px;
408
+ display:none;
409
+ }
410
+ .dark #bbox-image-info{color:#C084FC;background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.3)}
411
  """
412
 
413
  bbox_drawer_js = r"""
 
420
  const status = document.getElementById('bbox-status');
421
  const badge = document.getElementById('bbox-count');
422
  const debugCount = document.getElementById('bbox-debug-count');
423
+ const dropOverlay= document.getElementById('bbox-drop-overlay');
424
+ const imageInfo = document.getElementById('bbox-image-info');
425
+
426
+ const btnDraw = document.getElementById('tb-draw');
427
+ const btnSelect = document.getElementById('tb-select');
428
+ const btnUpload = document.getElementById('tb-upload');
429
+ const btnDel = document.getElementById('tb-del');
430
+ const btnUndo = document.getElementById('tb-undo');
431
+ const btnClear = document.getElementById('tb-clear');
432
+ const btnRemoveImg= document.getElementById('tb-remove-img');
433
+ const fileInput = document.getElementById('bbox-file-input');
434
+
435
+ if (!canvas || !wrap || !debugCount || !btnDraw) {
436
  console.log('[BBox] waiting for DOM...');
437
  setTimeout(initCanvasBbox, 250);
438
  return;
 
446
  window.__bboxBoxes = boxes;
447
 
448
  let baseImg = null;
449
+ let originalFile = null;
450
+ let dispW = 512, dispH = 450;
451
  let selectedIdx = -1;
452
  let mode = 'draw';
453
 
 
460
  const RED_STROKE_WIDTH = 3;
461
  const SEL_STROKE = 'rgba(0,120,255,0.95)';
462
 
463
+ function n2px(b) {
464
+ return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH};
465
+ }
466
  function px2n(x1,y1,x2,y2) {
467
  return {
468
  x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
 
471
  }
472
  function clamp01(v){return Math.max(0,Math.min(1,v));}
473
  function fitSize(nw, nh) {
474
+ const mw = wrap.clientWidth || 512, mh = 550;
475
  const r = Math.min(mw/nw, mh/nh, 1);
476
  dispW = Math.round(nw*r); dispH = Math.round(nh*r);
477
  canvas.width = dispW; canvas.height = dispH;
 
499
  }
500
 
501
  const container = document.getElementById('boxes-json-input');
502
+ if (!container) {
503
+ console.warn('[BBox] #boxes-json-input not in DOM');
504
+ return;
505
+ }
506
  const targets = [
507
  ...container.querySelectorAll('textarea'),
508
+ ...container.querySelectorAll('input[type="text"]'),
509
+ ...container.querySelectorAll('input:not([type])')
510
  ];
511
  targets.forEach(el => {
512
+ const proto = el.tagName === 'TEXTAREA'
513
+ ? HTMLTextAreaElement.prototype
514
+ : HTMLInputElement.prototype;
515
  const ns = Object.getOwnPropertyDescriptor(proto, 'value');
516
  if (ns && ns.set) {
517
  ns.set.call(el, jsonStr);
518
  el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
519
  el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
520
+ el.dispatchEvent(new Event('blur', {bubbles:true, composed:true}));
521
  }
522
  });
523
  }
524
 
525
+ /* Push image data to hidden Gradio Image component */
526
+ function pushImageToGradio(dataURL) {
527
+ const container = document.getElementById('source-image-component');
528
  if (!container) return;
529
+
530
+ /* Convert data URL to File and use Gradio's upload mechanism */
531
+ fetch(dataURL)
532
+ .then(res => res.blob())
533
+ .then(blob => {
534
+ const file = new File([blob], 'canvas-image.png', {type: 'image/png'});
535
+ const dt = new DataTransfer();
536
+ dt.items.add(file);
537
+
538
+ const inp = container.querySelector('input[type="file"]');
539
+ if (inp) {
540
+ inp.files = dt.files;
541
+ inp.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
542
+ console.log('[BBox] pushed image to Gradio component');
543
+ } else {
544
+ /* Fallback: try drop event */
545
+ const dropTarget = container.querySelector('.upload-container, .image-container, [data-testid]') || container;
546
+ const dropEvt = new DragEvent('drop', {bubbles:true, composed:true, dataTransfer: dt});
547
+ dropTarget.dispatchEvent(dropEvt);
548
+ console.log('[BBox] pushed image via drop event');
549
+ }
550
+ });
551
+ }
552
+
553
+ function placeholder() {
554
+ ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,dispW,dispH);
555
+ ctx.strokeStyle='#7E22CE'; ctx.lineWidth=2; ctx.setLineDash([8,4]);
556
+ ctx.strokeRect(20,20,dispW-40,dispH-40); ctx.setLineDash([]);
557
+ ctx.fillStyle='#A855F7'; ctx.font='bold 18px Outfit,sans-serif';
558
+ ctx.textAlign='center'; ctx.textBaseline='middle';
559
+ ctx.fillText('πŸ“· Drop image here or click Upload',dispW/2,dispH/2-20);
560
+ ctx.font='14px Outfit'; ctx.fillStyle='#888';
561
+ ctx.fillText('Supports PNG, JPG, WEBP',dispW/2,dispH/2+10);
562
+ ctx.font='12px Outfit'; ctx.fillStyle='#666';
563
+ ctx.fillText('Then draw red boxes on objects to remove',dispW/2,dispH/2+32);
564
  }
565
 
566
  function redraw(tempRect) {
567
  ctx.clearRect(0,0,dispW,dispH);
568
+ if (!baseImg) { placeholder(); updateBadge(); return; }
 
 
 
569
  ctx.drawImage(baseImg, 0, 0, dispW, dispH);
570
 
571
  boxes.forEach((b,i) => {
572
  const p = n2px(b);
573
  const lx=p.x1, ty=p.y1, w=p.x2-p.x1, h=p.y2-p.y1;
574
 
 
575
  if (i === selectedIdx) {
576
  ctx.strokeStyle = SEL_STROKE;
577
  ctx.lineWidth = RED_STROKE_WIDTH + 1;
 
584
  ctx.strokeRect(lx, ty, w, h);
585
  ctx.setLineDash([]);
586
 
 
587
  ctx.fillStyle = i===selectedIdx ? 'rgba(0,120,255,0.85)' : 'rgba(255,0,0,0.85)';
588
  ctx.font = 'bold 11px IBM Plex Mono,monospace';
589
  ctx.textAlign = 'left'; ctx.textBaseline = 'top';
 
596
  if (i === selectedIdx) drawHandles(p);
597
  });
598
 
 
599
  if (tempRect) {
600
  const rx = Math.min(tempRect.x1,tempRect.x2);
601
  const ry = Math.min(tempRect.y1,tempRect.y2);
 
671
  }
672
  function hideStatus() { status.style.display = 'none'; }
673
 
674
+ function updateImageInfo() {
675
+ if (baseImg && imageInfo) {
676
+ imageInfo.style.display = 'block';
677
+ imageInfo.textContent = 'πŸ–Ό Image: ' + baseImg.naturalWidth + 'Γ—' + baseImg.naturalHeight + ' px';
678
+ } else if (imageInfo) {
679
+ imageInfo.style.display = 'none';
680
+ }
681
+ }
682
+
683
+ /* ---- Image loading ---- */
684
+ function loadImageFromSource(src) {
685
+ const img = new window.Image();
686
+ img.crossOrigin = 'anonymous';
687
+ img.onload = () => {
688
+ baseImg = img;
689
+ boxes.length = 0;
690
+ window.__bboxBoxes = boxes;
691
+ selectedIdx = -1;
692
+ fitSize(img.naturalWidth, img.naturalHeight);
693
+ syncToGradio(); redraw(); hideStatus();
694
+ updateImageInfo();
695
+ showStatus('Image loaded – draw boxes now!');
696
+ setTimeout(hideStatus, 2000);
697
+ console.log('[BBox] loaded image', img.naturalWidth, 'x', img.naturalHeight);
698
+
699
+ /* Push to hidden Gradio component */
700
+ const tmpCanvas = document.createElement('canvas');
701
+ tmpCanvas.width = img.naturalWidth;
702
+ tmpCanvas.height = img.naturalHeight;
703
+ const tmpCtx = tmpCanvas.getContext('2d');
704
+ tmpCtx.drawImage(img, 0, 0);
705
+ const dataURL = tmpCanvas.toDataURL('image/png');
706
+ pushImageToGradio(dataURL);
707
+ };
708
+ img.onerror = () => {
709
+ console.warn('[BBox] image load failed');
710
+ showStatus('Failed to load image');
711
+ };
712
+ img.src = src;
713
+ }
714
+
715
+ function loadImageFromFile(file) {
716
+ if (!file || !file.type.startsWith('image/')) return;
717
+ originalFile = file;
718
+ const reader = new FileReader();
719
+ reader.onload = (ev) => {
720
+ loadImageFromSource(ev.target.result);
721
+ };
722
+ reader.readAsDataURL(file);
723
+ }
724
+
725
+ function removeImage() {
726
+ baseImg = null;
727
+ originalFile = null;
728
+ boxes.length = 0;
729
+ window.__bboxBoxes = boxes;
730
+ selectedIdx = -1;
731
+ fitSize(512, 450);
732
+ syncToGradio(); redraw(); hideStatus();
733
+ updateImageInfo();
734
+
735
+ /* Clear hidden Gradio component */
736
+ const container = document.getElementById('source-image-component');
737
+ if (container) {
738
+ const clearBtn = container.querySelector('button[aria-label="Clear"], button.clear-btn, [data-testid="clear-btn"]');
739
+ if (clearBtn) clearBtn.click();
740
+ }
741
+ }
742
+
743
+ /* ---- File input button ---- */
744
+ btnUpload.addEventListener('click', () => {
745
+ fileInput.click();
746
+ });
747
+ fileInput.addEventListener('change', (e) => {
748
+ if (e.target.files && e.target.files[0]) {
749
+ loadImageFromFile(e.target.files[0]);
750
+ }
751
+ e.target.value = '';
752
+ });
753
+
754
+ /* ---- Remove image button ---- */
755
+ btnRemoveImg.addEventListener('click', removeImage);
756
+
757
+ /* ---- Drag & Drop on canvas ---- */
758
+ wrap.addEventListener('dragenter', (e) => {
759
+ e.preventDefault(); e.stopPropagation();
760
+ dropOverlay.classList.add('active');
761
+ });
762
+ wrap.addEventListener('dragover', (e) => {
763
+ e.preventDefault(); e.stopPropagation();
764
+ dropOverlay.classList.add('active');
765
+ });
766
+ wrap.addEventListener('dragleave', (e) => {
767
+ e.preventDefault(); e.stopPropagation();
768
+ if (!wrap.contains(e.relatedTarget)) {
769
+ dropOverlay.classList.remove('active');
770
+ }
771
+ });
772
+ wrap.addEventListener('drop', (e) => {
773
+ e.preventDefault(); e.stopPropagation();
774
+ dropOverlay.classList.remove('active');
775
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
776
+ loadImageFromFile(e.dataTransfer.files[0]);
777
+ }
778
+ });
779
+
780
+ /* ---- Also allow click on canvas to upload when no image ---- */
781
+ canvas.addEventListener('dblclick', (e) => {
782
+ if (!baseImg) {
783
+ e.preventDefault();
784
+ fileInput.click();
785
+ }
786
+ });
787
+
788
+ /* ---- Drawing handlers ---- */
789
  function onDown(e) {
790
  if (!baseImg) return;
791
  e.preventDefault();
 
914
  canvas.addEventListener('touchend', onUp, {passive:false});
915
  canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
916
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917
  btnDraw.addEventListener('click', ()=>setMode('draw'));
918
  btnSelect.addEventListener('click', ()=>setMode('select'));
919
 
 
947
  syncToGradio(); redraw(); hideStatus();
948
  });
949
 
950
+ /* Periodic sync */
951
+ setInterval(() => { syncToGradio(); }, 500);
952
+
953
  new ResizeObserver(() => {
954
  if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
955
  }).observe(wrap);
956
 
957
  setMode('draw');
958
+ fitSize(512,450); redraw();
959
  syncToGradio();
960
  }
961
 
 
968
  gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
969
  gr.Markdown(
970
  "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
971
+ "Upload an image directly into the canvas below, draw red bounding boxes over the objects you want to remove, and click Remove Object. "
972
  "Multiple boxes supported. Select, move, resize or delete individual boxes.",
973
  elem_id="subtitle",
974
  )
975
 
976
  with gr.Row():
977
  with gr.Column(scale=1):
978
+ # Hidden source image component (receives image data from canvas JS)
979
+ source_image = gr.Image(
980
+ label="Upload Image",
981
+ type="pil",
982
+ height=350,
983
+ elem_id="source-image-component",
984
+ visible=False,
985
  )
986
 
987
  gr.Markdown("# **Bbox Edit Controller**")
988
+ gr.HTML(
989
+ '<div class="bbox-hint">'
990
+ "πŸ“· <b>Upload:</b> Click <b>Upload</b> button, drag & drop onto the canvas, or <b>double-click</b> the canvas. "
991
+ "<b>Draw mode:</b> Click & drag to create red rectangles. "
992
+ "<b>Select mode:</b> Click a box to select it β†’ drag to <b>move</b>, "
993
+ "drag handles to <b>resize</b>. Use <b>Delete Selected</b> to remove one box."
994
+ "</div>"
995
+ )
996
 
997
  gr.HTML(
998
  """
999
  <div id="bbox-draw-wrap">
1000
+ <canvas id="bbox-draw-canvas" width="512" height="450"></canvas>
 
 
 
 
 
 
 
 
 
 
1001
  <div id="bbox-status"></div>
1002
  <div id="bbox-count"></div>
1003
+ <div id="bbox-drop-overlay"><span>πŸ“· Drop image here</span></div>
1004
  </div>
1005
+ <input type="file" id="bbox-file-input" accept="image/*">
1006
  """
1007
  )
1008
 
1009
  gr.HTML(
1010
  """
1011
  <div class="bbox-toolbar-section">
1012
+ <span class="toolbar-label">πŸ“· Image:</span>
1013
+ <button id="tb-upload" class="bbox-tb-upload" title="Upload an image">πŸ“ Upload</button>
1014
+ <button id="tb-remove-img" class="bbox-tb-remove-img" title="Remove current image">πŸ—‘ Remove Image</button>
1015
+ <div class="toolbar-divider"></div>
1016
  <span class="toolbar-label">πŸ›  Tools:</span>
1017
  <button id="tb-draw" class="bbox-tb-draw active" title="Draw new boxes">✏️ Draw</button>
1018
  <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">πŸ”² Select</button>
1019
  <div class="toolbar-divider"></div>
1020
  <span class="toolbar-label">Actions:</span>
1021
+ <button id="tb-del" class="bbox-tb-del" title="Delete selected box">βœ• Delete Selected</button>
1022
  <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
1023
  <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">πŸ—‘ Clear All</button>
 
 
1024
  </div>
1025
  """
1026
  )
1027
+
1028
+ gr.HTML('<div id="bbox-image-info"></div>')
 
 
 
 
 
 
 
1029
  gr.HTML('<div id="bbox-debug-count">\u2B1C No boxes drawn yet</div>')
1030
 
1031
  boxes_json = gr.Textbox(
1032
  value="[]",
1033
+ visible=True,
1034
+ interactive=True,
1035
  elem_id="boxes-json-input",
1036
+ label="boxes-json",
 
1037
  )
1038
 
1039
  prompt = gr.Textbox(
 
1065
  "[prithivMLmods](https://huggingface.co/prithivMLmods). "
1066
  "Adapter: [QIE-2511-Object-Remover-v2]"
1067
  "(https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
1068
+ "More adapters β†’ [Qwen-Image-Edit-LoRAs]"
1069
  "(https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2509)."
1070
  )
1071
 
 
1073
 
1074
  run_btn.click(
1075
  fn=infer_object_removal,
1076
+ inputs=[source_image, boxes_json, prompt, seed, randomize_seed,
1077
  guidance_scale, num_inference_steps, height_slider, width_slider],
1078
  outputs=[result, seed, preview],
1079
+ js="""(src, bj, p, s, rs, gs, nis, h, w) => {
1080
  const boxes = window.__bboxBoxes || [];
1081
  const json = JSON.stringify(boxes);
1082
  console.log('[BBox] submitting', boxes.length, 'boxes:', json);
1083
+ return [src, json, p, s, rs, gs, nis, h, w];
1084
  }""",
1085
  )
1086
 
1087
+ source_image.upload(
1088
  fn=update_dimensions_on_upload,
1089
+ inputs=[source_image],
1090
  outputs=[width_slider, height_slider],
1091
  )
1092
 
1093
+
1094
  if __name__ == "__main__":
1095
  demo.launch(
1096
  css=css, theme=purple_theme,