prithivMLmods commited on
Commit
844ec2d
·
verified ·
1 Parent(s): 53891b4

update app

Browse files
Files changed (1) hide show
  1. app.py +181 -109
app.py CHANGED
@@ -3,6 +3,8 @@ import numpy as np
3
  import random
4
  import torch
5
  import spaces
 
 
6
  from typing import Iterable
7
  from PIL import Image, ImageDraw
8
  from diffusers import FlowMatchEulerDiscreteScheduler
@@ -114,6 +116,19 @@ loaded = False
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
@@ -146,7 +161,7 @@ def burn_boxes_onto_image(pil_image: Image.Image, boxes_json_str: str) -> Image.
146
 
147
  @spaces.GPU
148
  def infer_object_removal(
149
- source_image: Image.Image,
150
  boxes_json: str,
151
  prompt: str,
152
  seed: int = 0,
@@ -175,8 +190,9 @@ def infer_object_removal(
175
  print(f"Prompt: {prompt}")
176
  print(f"Boxes JSON received: '{boxes_json}'")
177
 
 
178
  if source_image is None:
179
- raise gr.Error("Please upload an image first.")
180
 
181
  import json
182
  try:
@@ -211,7 +227,8 @@ def infer_object_removal(
211
  return result, seed, marked
212
 
213
 
214
- def update_dimensions_on_upload(image):
 
215
  if image is None:
216
  return 1024, 1024
217
  original_width, original_height = image.size
@@ -259,12 +276,57 @@ label{font-weight:600!important;color:#333!important}
259
  ::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#A855F7,#C084FC);border-radius:4px}
260
  ::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#9333EA,#A855F7)}
261
 
262
- #bbox-draw-wrap{position:relative;border:2px solid #C084FC;border-radius:12px;overflow:hidden;background:#1a1a1a;min-height:420px}
263
  #bbox-draw-wrap:hover{border-color:#A855F7}
264
  #bbox-draw-canvas{cursor:crosshair;display:block;margin:0 auto}
265
  .bbox-hint{background:rgba(168,85,247,.08);border:1px solid #E9D5FF;border-radius:8px;padding:10px 16px;margin:8px 0;font-size:.9rem;color:#6B21A8}
266
  .dark .bbox-hint{background:rgba(168,85,247,.15);border-color:rgba(168,85,247,.3);color:#C084FC}
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  .bbox-toolbar-section{
269
  display:flex;
270
  gap:8px;
@@ -324,6 +386,8 @@ label{font-weight:600!important;color:#333!important}
324
  .bbox-tb-undo:hover{background:#9333EA}
325
  .bbox-tb-clear{background:#be123c}
326
  .bbox-tb-clear:hover{background:#e11d48}
 
 
327
 
328
  #bbox-status{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.75);color:#00ff88;padding:5px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none;pointer-events:none}
329
  #bbox-count{position:absolute;top:10px;right:10px;background:rgba(147,51,234,.85);color:#fff;padding:4px 10px;border-radius:6px;font-family:'IBM Plex Mono',monospace;font-size:11px;z-index:10;display:none}
@@ -341,15 +405,8 @@ label{font-weight:600!important;color:#333!important}
341
  }
342
  .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
343
 
344
- #boxes-json-input{
345
- max-height:0!important;
346
- overflow:hidden!important;
347
- margin:0!important;
348
- padding:0!important;
349
- opacity:0!important;
350
- pointer-events:none!important;
351
- position:absolute!important;
352
- z-index:-1!important;
353
  }
354
  """
355
 
@@ -364,13 +421,18 @@ bbox_drawer_js = r"""
364
  const badge = document.getElementById('bbox-count');
365
  const debugCount = document.getElementById('bbox-debug-count');
366
 
367
- const btnDraw = document.getElementById('tb-draw');
368
- const btnSelect = document.getElementById('tb-select');
369
- const btnDel = document.getElementById('tb-del');
370
- const btnUndo = document.getElementById('tb-undo');
371
- const btnClear = document.getElementById('tb-clear');
372
-
373
- if (!canvas || !wrap || !debugCount || !btnDraw) {
 
 
 
 
 
374
  console.log('[BBox] waiting for DOM...');
375
  setTimeout(initCanvasBbox, 250);
376
  return;
@@ -397,9 +459,7 @@ bbox_drawer_js = r"""
397
  const RED_STROKE_WIDTH = 3;
398
  const SEL_STROKE = 'rgba(0,120,255,0.95)';
399
 
400
- function n2px(b) {
401
- return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH};
402
- }
403
  function px2n(x1,y1,x2,y2) {
404
  return {
405
  x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
@@ -436,43 +496,46 @@ bbox_drawer_js = r"""
436
  }
437
 
438
  const container = document.getElementById('boxes-json-input');
439
- if (!container) {
440
- console.warn('[BBox] #boxes-json-input not in DOM');
441
- return;
442
- }
443
  const targets = [
444
  ...container.querySelectorAll('textarea'),
445
- ...container.querySelectorAll('input[type="text"]'),
446
- ...container.querySelectorAll('input:not([type])')
447
  ];
448
  targets.forEach(el => {
449
- const proto = el.tagName === 'TEXTAREA'
450
- ? HTMLTextAreaElement.prototype
451
- : HTMLInputElement.prototype;
452
  const ns = Object.getOwnPropertyDescriptor(proto, 'value');
453
  if (ns && ns.set) {
454
  ns.set.call(el, jsonStr);
455
  el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
456
  el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
457
- el.dispatchEvent(new Event('blur', {bubbles:true, composed:true}));
458
  }
459
  });
460
  }
461
 
462
- function placeholder() {
463
- ctx.fillStyle='#2a2a2a'; ctx.fillRect(0,0,dispW,dispH);
464
- ctx.strokeStyle='#444'; ctx.lineWidth=2; ctx.setLineDash([8,4]);
465
- ctx.strokeRect(20,20,dispW-40,dispH-40); ctx.setLineDash([]);
466
- ctx.fillStyle='#888'; ctx.font='16px Outfit,sans-serif';
467
- ctx.textAlign='center'; ctx.textBaseline='middle';
468
- ctx.fillText('Upload an image above first',dispW/2,dispH/2-10);
469
- ctx.font='13px Outfit'; ctx.fillStyle='#666';
470
- ctx.fillText('Then draw red boxes on objects to remove',dispW/2,dispH/2+14);
 
 
 
 
 
 
 
471
  }
472
 
473
  function redraw(tempRect) {
474
  ctx.clearRect(0,0,dispW,dispH);
475
- if (!baseImg) { placeholder(); updateBadge(); return; }
 
 
 
476
  ctx.drawImage(baseImg, 0, 0, dispW, dispH);
477
 
478
  boxes.forEach((b,i) => {
@@ -709,6 +772,57 @@ bbox_drawer_js = r"""
709
  canvas.addEventListener('touchend', onUp, {passive:false});
710
  canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  btnDraw.addEventListener('click', ()=>setMode('draw'));
713
  btnSelect.addEventListener('click', ()=>setMode('select'));
714
 
@@ -742,55 +856,6 @@ bbox_drawer_js = r"""
742
  syncToGradio(); redraw(); hideStatus();
743
  });
744
 
745
- let lastSrc = null;
746
- setInterval(() => {
747
- const imgs = document.querySelectorAll('#source-image-component img');
748
- let el = null;
749
- for (const img of imgs) {
750
- if (img.src && img.src.length > 30 &&
751
- (img.src.startsWith('data:') ||
752
- img.src.startsWith('blob:') ||
753
- img.src.includes('/file=') ||
754
- img.src.includes('/upload') ||
755
- img.src.includes('localhost') ||
756
- img.src.includes('127.0.0.1') ||
757
- img.src.startsWith('http'))) {
758
- el = img;
759
- break;
760
- }
761
- }
762
-
763
- if (el && el.src && el.src !== lastSrc) {
764
- lastSrc = el.src;
765
- const img = new window.Image();
766
- img.crossOrigin = 'anonymous';
767
- img.onload = () => {
768
- baseImg = img;
769
- boxes.length = 0;
770
- window.__bboxBoxes = boxes;
771
- selectedIdx = -1;
772
- fitSize(img.naturalWidth, img.naturalHeight);
773
- syncToGradio(); redraw(); hideStatus();
774
- console.log('[BBox] loaded image', img.naturalWidth, 'x', img.naturalHeight);
775
- };
776
- img.onerror = () => {
777
- console.warn('[BBox] image load failed for', el.src.substring(0,60));
778
- };
779
- img.src = el.src;
780
- } else if (!el || !el.src) {
781
- if (baseImg) {
782
- baseImg = null;
783
- boxes.length = 0;
784
- window.__bboxBoxes = boxes;
785
- selectedIdx = -1;
786
- fitSize(512,400); syncToGradio(); redraw(); hideStatus();
787
- }
788
- lastSrc = null;
789
- }
790
- }, 300);
791
-
792
- setInterval(() => { syncToGradio(); }, 500);
793
-
794
  new ResizeObserver(() => {
795
  if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
796
  }).observe(wrap);
@@ -809,21 +874,22 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
809
  gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
810
  gr.Markdown(
811
  "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
812
- "Upload an image, draw red bounding boxes over the objects you want to remove, and click Remove Object. "
813
  "Multiple boxes supported. Select, move, resize or delete individual boxes.",
814
  elem_id="subtitle",
815
  )
816
 
817
  with gr.Row():
818
  with gr.Column(scale=1):
819
- source_image = gr.Image(
820
- label="Upload Image",
821
- type="pil",
822
- height=350,
823
- elem_id="source-image-component",
 
824
  )
825
 
826
- gr.Markdown("# **Bbox Edit Controller**")
827
  gr.HTML(
828
  '<div class="bbox-hint">'
829
  "<b>Draw mode:</b> Click & drag to create red rectangles. "
@@ -832,9 +898,15 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
832
  "</div>"
833
  )
834
 
 
835
  gr.HTML(
836
  """
837
  <div id="bbox-draw-wrap">
 
 
 
 
 
838
  <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
839
  <div id="bbox-status"></div>
840
  <div id="bbox-count"></div>
@@ -850,9 +922,11 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
850
  <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">🔲 Select</button>
851
  <div class="toolbar-divider"></div>
852
  <span class="toolbar-label">Actions:</span>
853
- <button id="tb-del" class="bbox-tb-del" title="Delete selected box">✕ Delete Selected</button>
854
  <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
855
  <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">🗑 Clear All</button>
 
 
856
  </div>
857
  """
858
  )
@@ -861,10 +935,9 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
861
 
862
  boxes_json = gr.Textbox(
863
  value="[]",
864
- visible=True,
865
- interactive=True,
866
  elem_id="boxes-json-input",
867
- label="boxes-json",
 
868
  )
869
 
870
  prompt = gr.Textbox(
@@ -904,24 +977,23 @@ with gr.Blocks(css=css, theme=purple_theme) as demo:
904
 
905
  run_btn.click(
906
  fn=infer_object_removal,
907
- inputs=[source_image, boxes_json, prompt, seed, randomize_seed,
908
  guidance_scale, num_inference_steps, height_slider, width_slider],
909
  outputs=[result, seed, preview],
910
- js="""(src, bj, p, s, rs, gs, nis, h, w) => {
911
  const boxes = window.__bboxBoxes || [];
912
  const json = JSON.stringify(boxes);
913
  console.log('[BBox] submitting', boxes.length, 'boxes:', json);
914
- return [src, json, p, s, rs, gs, nis, h, w];
915
  }""",
916
  )
917
 
918
- source_image.upload(
919
  fn=update_dimensions_on_upload,
920
- inputs=[source_image],
921
  outputs=[width_slider, height_slider],
922
  )
923
 
924
-
925
  if __name__ == "__main__":
926
  demo.launch(
927
  css=css, theme=purple_theme,
 
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
  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
 
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
  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
  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
  ::-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
+ }
300
+ .dark #upload-prompt {
301
+ background: rgba(30, 30, 30, 0.95);
302
+ }
303
+ #upload-prompt p {
304
+ margin: 0 0 20px 0;
305
+ font-family: 'Outfit', sans-serif;
306
+ color: #6B21A8;
307
+ font-weight: 500;
308
+ font-size: 1.1rem;
309
+ }
310
+ .dark #upload-prompt p { color: #DAB2FF; }
311
+
312
+ #custom-upload-btn {
313
+ background: linear-gradient(90deg, #A855F7, #9333EA);
314
+ color: white;
315
+ border: none;
316
+ padding: 12px 28px;
317
+ font-size: 16px;
318
+ font-weight: 600;
319
+ border-radius: 8px;
320
+ cursor: pointer;
321
+ box-shadow: 0 4px 12px rgba(168,85,247,0.3);
322
+ transition: transform 0.2s, box-shadow 0.2s;
323
+ font-family: 'Outfit', sans-serif;
324
+ }
325
+ #custom-upload-btn:hover {
326
+ transform: translateY(-2px);
327
+ box-shadow: 0 6px 16px rgba(168,85,247,0.4);
328
+ }
329
+
330
  .bbox-toolbar-section{
331
  display:flex;
332
  gap:8px;
 
386
  .bbox-tb-undo:hover{background:#9333EA}
387
  .bbox-tb-clear{background:#be123c}
388
  .bbox-tb-clear:hover{background:#e11d48}
389
+ .bbox-tb-change{background:#4b5563}
390
+ .bbox-tb-change:hover{background:#6b7280}
391
 
392
  #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}
393
  #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}
 
405
  }
406
  .dark #bbox-debug-count{color:#C084FC;background:rgba(168,85,247,.12)}
407
 
408
+ .hidden-input {
409
+ display: none !important;
 
 
 
 
 
 
 
410
  }
411
  """
412
 
 
421
  const badge = document.getElementById('bbox-count');
422
  const debugCount = document.getElementById('bbox-debug-count');
423
 
424
+ const btnDraw = document.getElementById('tb-draw');
425
+ const btnSelect = document.getElementById('tb-select');
426
+ const btnDel = document.getElementById('tb-del');
427
+ const btnUndo = document.getElementById('tb-undo');
428
+ const btnClear = document.getElementById('tb-clear');
429
+ const btnChange = document.getElementById('tb-change-img');
430
+
431
+ const uploadPrompt = document.getElementById('upload-prompt');
432
+ const fileInput = document.getElementById('custom-file-input');
433
+ const uploadBtn = document.getElementById('custom-upload-btn');
434
+
435
+ if (!canvas || !wrap || !debugCount || !btnDraw || !fileInput) {
436
  console.log('[BBox] waiting for DOM...');
437
  setTimeout(initCanvasBbox, 250);
438
  return;
 
459
  const RED_STROKE_WIDTH = 3;
460
  const SEL_STROKE = 'rgba(0,120,255,0.95)';
461
 
462
+ function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
 
 
463
  function px2n(x1,y1,x2,y2) {
464
  return {
465
  x1: Math.min(x1,x2)/dispW, y1: Math.min(y1,y2)/dispH,
 
496
  }
497
 
498
  const container = document.getElementById('boxes-json-input');
499
+ if (!container) return;
 
 
 
500
  const targets = [
501
  ...container.querySelectorAll('textarea'),
502
+ ...container.querySelectorAll('input:not([type="file"])')
 
503
  ];
504
  targets.forEach(el => {
505
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
 
 
506
  const ns = Object.getOwnPropertyDescriptor(proto, 'value');
507
  if (ns && ns.set) {
508
  ns.set.call(el, jsonStr);
509
  el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
510
  el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
 
511
  }
512
  });
513
  }
514
 
515
+ function syncImageToGradio(dataUrl) {
516
+ const container = document.getElementById('hidden-image-b64');
517
+ if (!container) return;
518
+ const targets = [
519
+ ...container.querySelectorAll('textarea'),
520
+ ...container.querySelectorAll('input')
521
+ ];
522
+ targets.forEach(el => {
523
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
524
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
525
+ if (ns && ns.set) {
526
+ ns.set.call(el, dataUrl);
527
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
528
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
529
+ }
530
+ });
531
  }
532
 
533
  function redraw(tempRect) {
534
  ctx.clearRect(0,0,dispW,dispH);
535
+ if (!baseImg) {
536
+ ctx.fillStyle='#1a1a1a'; ctx.fillRect(0,0,dispW,dispH);
537
+ updateBadge(); return;
538
+ }
539
  ctx.drawImage(baseImg, 0, 0, dispW, dispH);
540
 
541
  boxes.forEach((b,i) => {
 
772
  canvas.addEventListener('touchend', onUp, {passive:false});
773
  canvas.addEventListener('touchcancel',(e)=>{e.preventDefault();dragging=false;redraw();},{passive:false});
774
 
775
+ // --- File Upload Logic ---
776
+ function processFile(file) {
777
+ if (!file || !file.type.startsWith('image/')) return;
778
+ const reader = new FileReader();
779
+ reader.onload = (event) => {
780
+ const dataUrl = event.target.result;
781
+ const img = new window.Image();
782
+ img.crossOrigin = 'anonymous';
783
+ img.onload = () => {
784
+ baseImg = img;
785
+ boxes.length = 0;
786
+ window.__bboxBoxes = boxes;
787
+ selectedIdx = -1;
788
+ fitSize(img.naturalWidth, img.naturalHeight);
789
+ syncToGradio(); redraw(); hideStatus();
790
+ uploadPrompt.style.display = 'none';
791
+ syncImageToGradio(dataUrl);
792
+ };
793
+ img.src = dataUrl;
794
+ };
795
+ reader.readAsDataURL(file);
796
+ }
797
+
798
+ uploadBtn.addEventListener('click', () => fileInput.click());
799
+ btnChange.addEventListener('click', () => fileInput.click());
800
+
801
+ fileInput.addEventListener('change', (e) => {
802
+ processFile(e.target.files[0]);
803
+ e.target.value = ''; // Reset input to allow re-upload of same file
804
+ });
805
+
806
+ wrap.addEventListener('dragover', (e) => {
807
+ e.preventDefault();
808
+ wrap.style.borderColor = '#A855F7';
809
+ wrap.style.boxShadow = '0 0 15px rgba(168,85,247,0.3)';
810
+ });
811
+ wrap.addEventListener('dragleave', (e) => {
812
+ e.preventDefault();
813
+ wrap.style.borderColor = '';
814
+ wrap.style.boxShadow = '';
815
+ });
816
+ wrap.addEventListener('drop', (e) => {
817
+ e.preventDefault();
818
+ wrap.style.borderColor = '';
819
+ wrap.style.boxShadow = '';
820
+ if (e.dataTransfer.files.length) {
821
+ processFile(e.dataTransfer.files[0]);
822
+ }
823
+ });
824
+
825
+ // --- Toolbar Logic ---
826
  btnDraw.addEventListener('click', ()=>setMode('draw'));
827
  btnSelect.addEventListener('click', ()=>setMode('select'));
828
 
 
856
  syncToGradio(); redraw(); hideStatus();
857
  });
858
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  new ResizeObserver(() => {
860
  if (baseImg) { fitSize(baseImg.naturalWidth, baseImg.naturalHeight); redraw(); }
861
  }).observe(wrap);
 
874
  gr.Markdown("# **QIE-Object-Remover-Bbox**", elem_id="main-title")
875
  gr.Markdown(
876
  "Perform diverse image edits using a specialized [LoRA](https://huggingface.co/prithivMLmods/QIE-2511-Object-Remover-v2). "
877
+ "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. "
878
  "Multiple boxes supported. Select, move, resize or delete individual boxes.",
879
  elem_id="subtitle",
880
  )
881
 
882
  with gr.Row():
883
  with gr.Column(scale=1):
884
+
885
+ # --- We replace the visible gr.Image upload with a hidden textbox for the Base64 data ---
886
+ hidden_image_b64 = gr.Textbox(
887
+ elem_id="hidden-image-b64",
888
+ elem_classes="hidden-input",
889
+ container=False
890
  )
891
 
892
+ gr.Markdown("### **Bbox Edit Controller**")
893
  gr.HTML(
894
  '<div class="bbox-hint">'
895
  "<b>Draw mode:</b> Click & drag to create red rectangles. "
 
898
  "</div>"
899
  )
900
 
901
+ # Insert custom upload markup right over the canvas
902
  gr.HTML(
903
  """
904
  <div id="bbox-draw-wrap">
905
+ <div id="upload-prompt">
906
+ <p>Drag and drop an image here</p>
907
+ <button id="custom-upload-btn">Browse Files</button>
908
+ <input type="file" id="custom-file-input" accept="image/*" style="display:none;" />
909
+ </div>
910
  <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
911
  <div id="bbox-status"></div>
912
  <div id="bbox-count"></div>
 
922
  <button id="tb-select" class="bbox-tb-select" title="Select / move / resize">🔲 Select</button>
923
  <div class="toolbar-divider"></div>
924
  <span class="toolbar-label">Actions:</span>
925
+ <button id="tb-del" class="bbox-tb-del" title="Delete selected box">✕ Delete</button>
926
  <button id="tb-undo" class="bbox-tb-undo" title="Remove last box">↩ Undo</button>
927
  <button id="tb-clear" class="bbox-tb-clear" title="Remove all boxes">🗑 Clear All</button>
928
+ <div class="toolbar-divider"></div>
929
+ <button id="tb-change-img" class="bbox-tb-change" title="Upload a different image">📸 Change Image</button>
930
  </div>
931
  """
932
  )
 
935
 
936
  boxes_json = gr.Textbox(
937
  value="[]",
 
 
938
  elem_id="boxes-json-input",
939
+ elem_classes="hidden-input",
940
+ container=False
941
  )
942
 
943
  prompt = gr.Textbox(
 
977
 
978
  run_btn.click(
979
  fn=infer_object_removal,
980
+ inputs=[hidden_image_b64, boxes_json, prompt, seed, randomize_seed,
981
  guidance_scale, num_inference_steps, height_slider, width_slider],
982
  outputs=[result, seed, preview],
983
+ js="""(b64, bj, p, s, rs, gs, nis, h, w) => {
984
  const boxes = window.__bboxBoxes || [];
985
  const json = JSON.stringify(boxes);
986
  console.log('[BBox] submitting', boxes.length, 'boxes:', json);
987
+ return [b64, json, p, s, rs, gs, nis, h, w];
988
  }""",
989
  )
990
 
991
+ hidden_image_b64.change(
992
  fn=update_dimensions_on_upload,
993
+ inputs=[hidden_image_b64],
994
  outputs=[width_slider, height_slider],
995
  )
996
 
 
997
  if __name__ == "__main__":
998
  demo.launch(
999
  css=css, theme=purple_theme,