linoyts HF Staff commited on
Commit
0dfd065
Β·
verified Β·
1 Parent(s): adf4046

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +652 -485
app.py CHANGED
@@ -1,19 +1,28 @@
1
- import os
2
- import io
3
  import gradio as gr
4
  import numpy as np
5
  import random
6
- import spaces
7
  import torch
8
- from diffusers import Flux2KleinPipeline
9
- from PIL import Image, ImageDraw
10
- import json
11
  import base64
 
 
 
 
 
 
12
 
13
  dtype = torch.bfloat16
14
  device = "cuda" if torch.cuda.is_available() else "cpu"
15
 
16
- MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
 
 
17
 
18
  DEFAULT_PROMPT = (
19
  "Move and resize the object inside the red bounding box to the position "
@@ -22,548 +31,706 @@ DEFAULT_PROMPT = (
22
  "and all other objects exactly as they are."
23
  )
24
 
25
- REPO_ID_REGULAR = "black-forest-labs/FLUX.2-klein-base-9B"
26
- REPO_ID_DISTILLED = "black-forest-labs/FLUX.2-klein-9B"
27
 
28
- print("Loading 9B Regular model...")
29
- pipe_regular = Flux2KleinPipeline.from_pretrained(REPO_ID_REGULAR, torch_dtype=dtype)
30
- pipe_regular.to("cuda")
31
- pipe_regular.load_lora_weights(
32
- "linoyts/flux_klein_9b_bbox_lora",
33
- weight_name="flux_klein_9b_bbox_lora.safetensors",
34
- )
35
- print("Loading 9B Distilled model...")
36
- pipe_distilled = Flux2KleinPipeline.from_pretrained(REPO_ID_DISTILLED, torch_dtype=dtype)
37
- pipe_distilled.to("cuda")
38
- pipe_distilled.load_lora_weights(
39
- "linoyts/flux_klein_9b_bbox_lora",
40
- weight_name="flux_klein_9b_bbox_lora.safetensors",
41
- )
42
-
43
- pipes = {
44
- "Distilled (4 steps)": pipe_distilled,
45
- "Base (50 steps)": pipe_regular,
46
- }
47
- DEFAULT_STEPS = {"Distilled (4 steps)": 4, "Base (50 steps)": 50}
48
- DEFAULT_CFG = {"Distilled (4 steps)": 1.0, "Base (50 steps)": 4.0}
49
-
50
-
51
- def update_steps_from_mode(mode_choice):
52
- return DEFAULT_STEPS[mode_choice], DEFAULT_CFG[mode_choice]
53
 
54
 
55
- def on_image_upload(pil_img):
56
- """When gr.Image uploads, encode a display-sized JPEG b64 for the canvas
57
- and store the original PIL for inference."""
58
- if pil_img is None:
59
- return None, ""
60
- # Resize for canvas display (keeps b64 small & fast)
61
- display = pil_img.copy()
62
- display.thumbnail((1280, 1280), Image.LANCZOS)
63
- buf = io.BytesIO()
64
- display.save(buf, format="JPEG", quality=90)
65
- b64 = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
66
- return pil_img, b64
67
 
 
 
 
 
68
 
69
- def burn_boxes_on_image(pil_image, src, dst):
70
  img = pil_image.copy().convert("RGB")
71
  w, h = img.size
72
  draw = ImageDraw.Draw(img)
73
- lw = max(4, w // 150)
74
- draw.rectangle(
75
- [int(src["x1"]*w), int(src["y1"]*h), int(src["x2"]*w), int(src["y2"]*h)],
76
- outline=(255, 0, 0), width=lw,
77
- )
78
- draw.rectangle(
79
- [int(dst["x1"]*w), int(dst["y1"]*h), int(dst["x2"]*w), int(dst["y2"]*h)],
80
- outline=(0, 255, 0), width=lw,
81
- )
82
  return img
83
 
84
 
85
- @spaces.GPU(duration=85)
86
- def infer(
87
- original_pil, boxes_json, prompt, mode_choice,
88
- seed, randomize_seed, num_inference_steps, guidance_scale,
89
- progress=gr.Progress(track_tqdm=True),
90
- ):
91
- if original_pil is None:
 
 
 
92
  raise gr.Error("Please upload an image first.")
 
93
  try:
94
- boxes = json.loads(boxes_json) if boxes_json else {}
95
  except Exception:
96
  boxes = {}
97
- src, dst = boxes.get("src"), boxes.get("dst")
98
- if not src or not dst:
99
- raise gr.Error("Draw a box around the subject, then move it to the target location.")
100
- if randomize_seed:
101
- seed = random.randint(0, MAX_SEED)
102
 
103
- progress(0.2, desc="Burning bounding boxes...")
104
- marked = burn_boxes_on_image(original_pil, src, dst)
105
 
106
- img_w, img_h = original_pil.size
107
- asp = img_w / img_h
108
- if asp >= 1:
109
- out_w, out_h = 1024, round(1024 / asp / 8) * 8
110
- else:
111
- out_h, out_w = 1024, round(1024 * asp / 8) * 8
112
- out_w = max(256, min(1024, out_w))
113
- out_h = max(256, min(1024, out_h))
114
 
115
- progress(0.4, desc=f"Running {mode_choice}...")
116
- pipe = pipes[mode_choice]
117
  generator = torch.Generator(device=device).manual_seed(seed)
 
 
118
  result = pipe(
119
- prompt=prompt, image=[marked],
120
- height=out_h, width=out_w,
 
 
121
  num_inference_steps=num_inference_steps,
122
- guidance_scale=guidance_scale,
123
  generator=generator,
 
124
  ).images[0]
125
- return result, marked, seed
126
-
127
-
128
- # ── Interactive Canvas ────────────────────────────────────────────────────────
129
- # Upload handled by gr.Image (native Gradio β€” always works).
130
- # Canvas JS polls the hidden #image-b64 textbox for new image data.
131
-
132
- CANVAS_HTML = """
133
- <div id="bbox-app">
134
- <style>
135
- @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
136
- #bbox-app {
137
- font-family: 'DM Sans', sans-serif;
138
- --c-red: #ef4444; --c-green: #22c55e;
139
- --c-s1: #f8fafc; --c-s2: #e2e8f0; --c-s3: #cbd5e1;
140
- --c-s4: #94a3b8; --c-s5: #64748b; --c-s6: #475569;
141
- --c-s7: #334155; --c-s8: #1e293b; --c-s9: #0f172a;
142
- --rad: 10px;
143
- }
144
- .dark #bbox-app {
145
- --c-s1: #1e293b; --c-s2: #334155; --c-s3: #475569;
146
- --c-s4: #64748b; --c-s5: #94a3b8; --c-s6: #cbd5e1;
147
- --c-s7: #e2e8f0; --c-s8: #f1f5f9; --c-s9: #f8fafc;
148
- }
149
- #bbox-app * { box-sizing: border-box; margin: 0; padding: 0; }
150
 
151
- #bbox-wrap {
152
- position: relative; border: 2px solid var(--c-s2); border-radius: var(--rad);
153
- overflow: hidden; background: var(--c-s1); min-height: 380px;
154
- display: flex; align-items: center; justify-content: center;
155
- }
156
- #bbox-wrap.has-img { background: var(--c-s9); cursor: crosshair; }
157
- #bbox-canvas { display: block; max-width: 100%; max-height: 520px; }
158
 
159
- #bbox-empty {
160
- color: var(--c-s4); font-size: 14px; font-weight: 500;
161
- text-align: center; padding: 40px; line-height: 1.6;
162
- }
163
- #bbox-empty.hidden { display: none; }
164
 
165
- #bbox-tb {
166
- display: flex; gap: 6px; margin-top: 8px; align-items: center; flex-wrap: wrap;
167
- }
168
- #bbox-tb button {
169
- display: inline-flex; align-items: center; gap: 5px;
170
- padding: 6px 14px; border: 1px solid var(--c-s2); border-radius: 8px;
171
- background: var(--c-s1); font-size: 13px; font-weight: 600; color: var(--c-s7);
172
- cursor: pointer; font-family: 'DM Sans', sans-serif; transition: all 0.12s;
173
- user-select: none;
174
- }
175
- #bbox-tb button:hover { background: var(--c-s2); }
176
- #bbox-tb button.on {
177
- background: var(--c-s8); color: var(--c-s1); border-color: var(--c-s8);
178
- }
179
- .dark #bbox-tb button.on { background: var(--c-s5); color: #fff; border-color: var(--c-s5); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- #bbox-bar {
182
- display: flex; align-items: center; gap: 8px; padding: 10px 14px;
183
- border: 1px solid var(--c-s2); border-radius: var(--rad); margin-top: 8px;
184
- background: var(--c-s1); min-height: 42px; flex-wrap: wrap;
185
- }
186
- .sc {
187
- display: inline-flex; align-items: center; gap: 5px;
188
- padding: 3px 10px; border-radius: 6px; font-size: 12px; font-weight: 600;
189
- }
190
- .sc.s { background: rgba(239,68,68,0.08); color: var(--c-red); }
191
- .sc.d { background: rgba(34,197,94,0.08); color: var(--c-green); }
192
- .sc .dt { width: 8px; height: 8px; border-radius: 50%; }
193
- .sc.s .dt { background: var(--c-red); }
194
- .sc.d .dt { background: var(--c-green); }
195
- #bbox-hint { font-size: 13px; color: var(--c-s5); margin-left: auto; }
196
- </style>
197
-
198
- <div id="bbox-wrap">
199
- <canvas id="bbox-canvas" width="512" height="380"></canvas>
200
- <div id="bbox-empty">⬆ Upload an image above to start</div>
201
- </div>
202
- <div id="bbox-tb">
203
- <button id="tb-d" class="on">✏️ Draw</button>
204
- <button id="tb-m">βœ‹ Move</button>
205
- <button id="tb-c">πŸ—‘οΈ Clear</button>
206
- </div>
207
- <div id="bbox-bar">
208
- <span id="bbox-hint">Upload an image above to get started</span>
209
- </div>
210
- </div>
211
 
212
- <script>
213
- (function(){
214
- if(window.__bbxInit) return;
215
- window.__bbxInit = true;
216
-
217
- const cv = document.getElementById('bbox-canvas');
218
- const cx = cv.getContext('2d');
219
- const wp = document.getElementById('bbox-wrap');
220
- const emp = document.getElementById('bbox-empty');
221
- const bar = document.getElementById('bbox-bar');
222
- const bD = document.getElementById('tb-d');
223
- const bM = document.getElementById('tb-m');
224
- const bC = document.getElementById('tb-c');
225
-
226
- let img = null, dW = 512, dH = 380, mode = 'draw';
227
- let srcB = null, dstB = null;
228
- let drawing = false, dStart = null;
229
- let dragging = false, dOff = null;
230
- let resizing = false, rH = null;
231
- const HS = 7;
232
-
233
- /* ── Poll the hidden textbox for image data ──
234
- gr.Image upload β†’ Python encodes b64 β†’ hidden textbox β†’ JS picks it up */
235
- let lastB64 = '';
236
- setInterval(function(){
237
- const el = document.querySelector('#image-b64 textarea, #image-b64 input');
238
- if (!el) return;
239
- const val = el.value;
240
- if (val && val !== lastB64 && val.startsWith('data:image')) {
241
- lastB64 = val;
242
- loadImg(val);
243
  }
244
- // Detect clear (user removed image from gr.Image)
245
- if ((!val || val === '') && lastB64 !== '') {
246
- lastB64 = '';
247
- img = null; srcB = null; dstB = null;
248
- wp.classList.remove('has-img');
249
- emp.classList.remove('hidden');
250
- redraw();
251
  }
252
- }, 250);
253
-
254
- /* ── Gradio hidden textbox write ── */
255
- function gSet(id, v) {
256
- const c = document.getElementById(id);
257
- if (!c) return;
258
- const el = c.querySelector('textarea') || c.querySelector('input');
259
- if (!el) return;
260
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
261
- const desc = Object.getOwnPropertyDescriptor(proto, 'value');
262
- if (desc && desc.set) {
263
- desc.set.call(el, v);
264
- el.dispatchEvent(new Event('input', {bubbles: true}));
265
- el.dispatchEvent(new Event('change', {bubbles: true}));
266
  }
267
- }
268
 
269
- function syncBoxes() {
270
- const d = {};
271
- if (srcB) d.src = {
272
- x1: Math.min(srcB.x1,srcB.x2)/dW, y1: Math.min(srcB.y1,srcB.y2)/dH,
273
- x2: Math.max(srcB.x1,srcB.x2)/dW, y2: Math.max(srcB.y1,srcB.y2)/dH
274
- };
275
- if (dstB) d.dst = {
276
- x1: Math.min(dstB.x1,dstB.x2)/dW, y1: Math.min(dstB.y1,dstB.y2)/dH,
277
- x2: Math.max(dstB.x1,dstB.x2)/dW, y2: Math.max(dstB.y1,dstB.y2)/dH
278
- };
279
- gSet('boxes-json', JSON.stringify(d));
280
- }
 
 
281
 
282
- /* ── Image loading ── */
283
- function loadImg(dataUrl) {
284
- const i = new window.Image();
285
- i.onload = function() {
286
- img = i; srcB = null; dstB = null;
287
- const mw = wp.clientWidth || 640, mh = 520;
288
- const r = Math.min(mw / i.naturalWidth, mh / i.naturalHeight, 1);
289
- dW = Math.round(i.naturalWidth * r);
290
- dH = Math.round(i.naturalHeight * r);
291
- cv.width = dW; cv.height = dH;
292
- cv.style.width = dW + 'px'; cv.style.height = dH + 'px';
293
- wp.classList.add('has-img');
294
- emp.classList.add('hidden');
295
- syncBoxes();
296
- setMode('draw');
297
- redraw();
298
- };
299
- i.onerror = function(){ console.error('Canvas: failed to load image'); };
300
- i.src = dataUrl;
301
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
- /* ── Status hint ── */
304
- function updHint() {
305
- let chips = '';
306
- if (srcB) chips += '<span class="sc s"><span class="dt"></span>Source</span>';
307
- if (dstB) chips += '<span class="sc d"><span class="dt"></span>Target</span>';
308
- let h = '';
309
- if (!img) h = 'Upload an image above to get started';
310
- else if (!srcB) h = 'Draw a box around the subject';
311
- else if (mode === 'draw') h = 'Switch to <b>Move</b> to reposition';
312
- else if (srcB && dstB) h = 'Drag or resize the green box, then click <b>Run</b>';
313
- else h = 'Click on the canvas to place the target';
314
- bar.innerHTML = chips + '<span id="bbox-hint">' + h + '</span>';
315
- }
 
 
 
 
 
316
 
317
- /* ── Geometry helpers ── */
318
- function hPts(b) {
319
- const lx=Math.min(b.x1,b.x2), rx=Math.max(b.x1,b.x2),
320
- ty=Math.min(b.y1,b.y2), by=Math.max(b.y1,b.y2),
321
- mx=(lx+rx)/2, my=(ty+by)/2;
322
- return {tl:{x:lx,y:ty}, tr:{x:rx,y:ty}, bl:{x:lx,y:by}, br:{x:rx,y:by},
323
- tc:{x:mx,y:ty}, bc:{x:mx,y:by}, ml:{x:lx,y:my}, mr:{x:rx,y:my}};
324
- }
325
- function hitHdl(px,py,b) {
326
- if(!b) return null;
327
- const ps = hPts(b);
328
- for (const k in ps) if (Math.abs(px-ps[k].x)<=HS+3 && Math.abs(py-ps[k].y)<=HS+3) return k;
329
- return null;
330
- }
331
- function hitBox(px,py,b) {
332
- if(!b) return false;
333
- return px>=Math.min(b.x1,b.x2) && px<=Math.max(b.x1,b.x2) &&
334
- py>=Math.min(b.y1,b.y2) && py<=Math.max(b.y1,b.y2);
335
- }
336
- function evtPt(e) {
337
- const r = cv.getBoundingClientRect();
338
- const ex = e.clientX !== undefined ? e.clientX : 0;
339
- const ey = e.clientY !== undefined ? e.clientY : 0;
340
- return {x: Math.max(0, Math.min(dW, ex - r.left)), y: Math.max(0, Math.min(dH, ey - r.top))};
341
- }
342
 
343
- /* ── Canvas rendering ── */
344
- function redraw(tmp) {
345
- cx.clearRect(0, 0, dW, dH);
346
- if (!img) { cx.fillStyle = '#f1f5f9'; cx.fillRect(0, 0, dW, dH); updHint(); return; }
347
- cx.drawImage(img, 0, 0, dW, dH);
348
- const lw = Math.max(2, dW / 250);
349
-
350
- // Source = red dashed
351
- if (srcB) {
352
- const lx=Math.min(srcB.x1,srcB.x2), ty=Math.min(srcB.y1,srcB.y2),
353
- w=Math.abs(srcB.x2-srcB.x1), h=Math.abs(srcB.y2-srcB.y1);
354
- cx.save(); cx.strokeStyle='#ef4444'; cx.lineWidth=lw+1;
355
- cx.setLineDash([8,5]); cx.strokeRect(lx,ty,w,h); cx.setLineDash([]); cx.restore();
356
- cx.save(); cx.font='bold 12px "DM Sans",sans-serif';
357
- const tw = cx.measureText('Source').width;
358
- const ly = ty > 22 ? ty - 22 : ty + 4;
359
- cx.fillStyle='#ef4444'; cx.fillRect(lx, ly, tw+12, 20);
360
- cx.fillStyle='#fff'; cx.fillText('Source', lx+6, ly+14); cx.restore();
361
  }
362
 
363
- // Destination = green solid + handles
364
- if (dstB) {
365
- const lx=Math.min(dstB.x1,dstB.x2), ty=Math.min(dstB.y1,dstB.y2),
366
- w=Math.abs(dstB.x2-dstB.x1), h=Math.abs(dstB.y2-dstB.y1);
367
- cx.save(); cx.strokeStyle='#22c55e'; cx.lineWidth=lw+1;
368
- cx.strokeRect(lx,ty,w,h);
369
- cx.fillStyle='rgba(34,197,94,0.06)'; cx.fillRect(lx,ty,w,h); cx.restore();
370
- const ps = hPts(dstB); cx.save();
371
- for (const k in ps) {
372
- cx.fillStyle='#fff'; cx.strokeStyle='#22c55e'; cx.lineWidth=2;
373
- cx.beginPath(); cx.arc(ps[k].x, ps[k].y, HS, 0, Math.PI*2); cx.fill(); cx.stroke();
374
- }
375
- cx.restore();
376
- cx.save(); cx.font='bold 12px "DM Sans",sans-serif';
377
- const tw2 = cx.measureText('Target').width;
378
- const ly2 = ty > 22 ? ty - 22 : ty + 4;
379
- cx.fillStyle='#22c55e'; cx.fillRect(lx, ly2, tw2+12, 20);
380
- cx.fillStyle='#fff'; cx.fillText('Target', lx+6, ly2+14); cx.restore();
381
  }
382
 
383
- // Temp drawing rect
384
- if (tmp) {
385
- const rx=Math.min(tmp.x1,tmp.x2), ry=Math.min(tmp.y1,tmp.y2),
386
- rw=Math.abs(tmp.x2-tmp.x1), rh=Math.abs(tmp.y2-tmp.y1);
387
- cx.save(); cx.strokeStyle='#ef4444'; cx.lineWidth=lw;
388
- cx.setLineDash([6,4]); cx.strokeRect(rx,ry,rw,rh); cx.setLineDash([]); cx.restore();
389
  }
390
- updHint();
391
- }
392
 
393
- /* ── Mode switching ── */
394
- function setMode(m) {
395
- mode = m;
396
- bD.classList.toggle('on', m==='draw');
397
- bM.classList.toggle('on', m==='move');
398
- wp.style.cursor = m === 'draw' ? 'crosshair' : 'grab';
399
- redraw();
400
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
- bD.addEventListener('click', function(){ setMode('draw'); });
403
- bM.addEventListener('click', function(){ if (srcB) setMode('move'); });
404
- bC.addEventListener('click', function(){ srcB=null; dstB=null; syncBoxes(); setMode('draw'); redraw(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
- /* ── Pointer events ── */
407
- cv.addEventListener('pointerdown', onDown);
408
- cv.addEventListener('pointermove', onMove);
409
- cv.addEventListener('pointerup', onUp);
410
- cv.addEventListener('touchstart', function(e){ e.preventDefault(); }, {passive: false});
 
 
 
 
 
 
 
 
411
 
412
- function onDown(e) {
413
- if (!img) return;
414
- e.preventDefault();
415
- cv.setPointerCapture(e.pointerId);
416
- const p = evtPt(e);
417
 
418
- if (mode === 'draw') {
419
- drawing = true; dStart = p; srcB = null; dstB = null;
420
- return;
421
- }
422
- if (mode === 'move' && dstB) {
423
- const h = hitHdl(p.x, p.y, dstB);
424
- if (h) { resizing = true; rH = h; return; }
425
- if (hitBox(p.x, p.y, dstB)) {
426
- dragging = true;
427
- dOff = {x: p.x - Math.min(dstB.x1,dstB.x2), y: p.y - Math.min(dstB.y1,dstB.y2)};
428
- wp.style.cursor = 'grabbing'; return;
 
 
 
 
 
 
 
 
 
 
429
  }
 
430
  }
431
- if (mode === 'move' && srcB && !dstB) {
432
- dstB = {...srcB}; dragging = true;
433
- dOff = {x: p.x - Math.min(dstB.x1,dstB.x2), y: p.y - Math.min(dstB.y1,dstB.y2)};
434
- wp.style.cursor = 'grabbing';
435
- }
436
- }
437
 
438
- function onMove(e) {
439
- if (!img) return;
440
- e.preventDefault();
441
- const p = evtPt(e);
 
 
 
 
 
 
 
 
 
 
 
442
 
443
- if (mode === 'draw' && drawing && dStart) {
444
- redraw({x1:dStart.x, y1:dStart.y, x2:p.x, y2:p.y}); return;
445
- }
446
- if (mode === 'move' && resizing && dstB && rH) {
447
- const lx=Math.min(dstB.x1,dstB.x2), rx=Math.max(dstB.x1,dstB.x2),
448
- ty=Math.min(dstB.y1,dstB.y2), by=Math.max(dstB.y1,dstB.y2);
449
- if (rH==='tl'){dstB.x1=p.x;dstB.y1=p.y;dstB.x2=rx;dstB.y2=by}
450
- else if (rH==='tr'){dstB.x1=lx;dstB.y1=p.y;dstB.x2=p.x;dstB.y2=by}
451
- else if (rH==='bl'){dstB.x1=p.x;dstB.y1=ty;dstB.x2=rx;dstB.y2=p.y}
452
- else if (rH==='br'){dstB.x1=lx;dstB.y1=ty;dstB.x2=p.x;dstB.y2=p.y}
453
- else if (rH==='tc'){dstB.x1=lx;dstB.y1=p.y;dstB.x2=rx;dstB.y2=by}
454
- else if (rH==='bc'){dstB.x1=lx;dstB.y1=ty;dstB.x2=rx;dstB.y2=p.y}
455
- else if (rH==='ml'){dstB.x1=p.x;dstB.y1=ty;dstB.x2=rx;dstB.y2=by}
456
- else if (rH==='mr'){dstB.x1=lx;dstB.y1=ty;dstB.x2=p.x;dstB.y2=by}
457
- redraw(); return;
458
  }
459
- if (mode === 'move' && dragging && dstB && dOff) {
460
- const w = Math.abs(dstB.x2-dstB.x1), h = Math.abs(dstB.y2-dstB.y1);
461
- dstB.x1 = p.x - dOff.x; dstB.y1 = p.y - dOff.y;
462
- dstB.x2 = dstB.x1 + w; dstB.y2 = dstB.y1 + h;
463
- redraw(); return;
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
- // Hover cursors
466
- if (mode === 'move' && dstB) {
467
- if (hitHdl(p.x, p.y, dstB)) wp.style.cursor = 'nwse-resize';
468
- else if (hitBox(p.x, p.y, dstB)) wp.style.cursor = 'grab';
469
- else wp.style.cursor = 'default';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  }
471
- }
472
-
473
- function onUp(e) {
474
- if (mode === 'draw' && drawing && dStart) {
475
- const p = evtPt(e);
476
- if (Math.abs(p.x - dStart.x) > 10 && Math.abs(p.y - dStart.y) > 10) {
477
- srcB = {
478
- x1: Math.min(dStart.x, p.x), y1: Math.min(dStart.y, p.y),
479
- x2: Math.max(dStart.x, p.x), y2: Math.max(dStart.y, p.y)
480
- };
481
- dstB = {...srcB};
482
- syncBoxes();
483
- setMode('move');
484
  }
 
 
 
485
  }
486
- if (resizing || dragging) syncBoxes();
487
- drawing = false; dStart = null; dragging = false; dOff = null; resizing = false; rH = null;
488
- if (mode === 'move') wp.style.cursor = 'grab';
489
- redraw();
490
- }
491
-
492
- redraw();
493
- })();
494
- </script>
495
- """
496
 
 
497
 
498
- # ── Gradio Layout ─────────────────────────────────────────────────────────────
499
-
500
- css = """
501
- #col-container { margin: 0 auto; max-width: 1200px; }
502
- /* Hide the b64 and boxes textboxes from view */
503
- #image-b64, #boxes-json { display: none !important; height: 0 !important; overflow: hidden !important; }
504
  """
505
 
506
- with gr.Blocks(css=css, theme=gr.themes.Default()) as demo:
507
- # gr.State holds the original PIL image for inference (not displayed)
508
- original_pil = gr.State(value=None)
509
 
510
- with gr.Column(elem_id="col-container"):
511
- gr.Markdown("""# Bbox Move & Resize – FLUX.2 Klein 9B + LoRA
512
- **1.** Upload an image &nbsp;β†’&nbsp; **2.** Draw a box around the subject &nbsp;β†’&nbsp; **3.** Drag / resize the green box &nbsp;β†’&nbsp; **4.** Run
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  """)
514
 
515
- with gr.Row():
516
- with gr.Column(scale=3):
517
- # Native Gradio upload β€” this always works
518
- upload_image = gr.Image(
519
- label="Upload image",
520
- type="pil",
521
- sources=["upload", "clipboard"],
522
- height=100,
523
- )
524
- # Interactive canvas (no upload logic β€” just box drawing/moving)
525
- gr.HTML(CANVAS_HTML)
526
- # Hidden bridge textboxes
527
- image_b64 = gr.Textbox(elem_id="image-b64", visible=False)
528
- boxes_json = gr.Textbox(elem_id="boxes-json", visible=False)
529
-
530
- with gr.Row():
531
- prompt = gr.Text(label="Prompt", value=DEFAULT_PROMPT, lines=2, scale=3)
532
- run_button = gr.Button("Run", variant="primary", scale=1, min_width=100)
533
-
534
- mode_choice = gr.Radio(
535
- label="Mode",
536
- choices=["Distilled (4 steps)", "Base (50 steps)"],
537
- value="Distilled (4 steps)",
538
- )
539
- with gr.Accordion("Advanced Settings", open=False):
540
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
541
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
542
- with gr.Row():
543
- num_inference_steps = gr.Slider(label="Inference steps", minimum=1, maximum=100, step=1, value=4)
544
- guidance_scale = gr.Slider(label="Guidance scale", minimum=0.0, maximum=10.0, step=0.1, value=1.0)
545
-
546
- with gr.Column(scale=2):
547
- result = gr.Image(label="Result")
548
- marked_preview = gr.Image(label="Input sent to model (with boxes)")
549
-
550
- # ── Wiring ────────────────────────────────────────────────────────────────
551
-
552
- # Upload β†’ Python encodes b64 for canvas + stores PIL in State
553
- upload_image.change(
554
- fn=on_image_upload,
555
- inputs=[upload_image],
556
- outputs=[original_pil, image_b64],
557
- )
558
 
559
- mode_choice.change(fn=update_steps_from_mode, inputs=[mode_choice], outputs=[num_inference_steps, guidance_scale])
 
560
 
561
- # Run uses the State (original PIL), not the b64
562
- gr.on(
563
- triggers=[run_button.click],
564
  fn=infer,
565
- inputs=[original_pil, boxes_json, prompt, mode_choice, seed, randomize_seed, num_inference_steps, guidance_scale],
566
- outputs=[result, marked_preview, seed],
 
 
 
 
 
 
 
567
  )
568
 
569
- demo.launch()
 
 
 
 
1
  import gradio as gr
2
  import numpy as np
3
  import random
 
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 Flux2KleinPipeline
11
+
12
+ MAX_SEED = np.iinfo(np.int32).max
13
 
14
  dtype = torch.bfloat16
15
  device = "cuda" if torch.cuda.is_available() else "cpu"
16
 
17
+ REPO_ID_DISTILLED = "black-forest-labs/FLUX.2-klein-9B"
18
+ print("Loading 9B Distilled model...")
19
+ pipe = Flux2KleinPipeline.from_pretrained(REPO_ID_DISTILLED, torch_dtype=dtype)
20
+ pipe.to("cuda")
21
+ pipe.load_lora_weights(
22
+ "linoyts/flux_klein_9b_bbox_lora",
23
+ weight_name="flux_klein_9b_bbox_lora.safetensors",
24
+ )
25
+ print("Pipeline ready.")
26
 
27
  DEFAULT_PROMPT = (
28
  "Move and resize the object inside the red bounding box to the position "
 
31
  "and all other objects exactly as they are."
32
  )
33
 
 
 
34
 
35
+ def b64_to_pil(b64_str):
36
+ if not b64_str or not b64_str.startswith("data:image"):
37
+ return None
38
+ try:
39
+ _, data = b64_str.split(',', 1)
40
+ return Image.open(BytesIO(base64.b64decode(data))).convert("RGB")
41
+ except Exception as e:
42
+ print(f"Error decoding image: {e}")
43
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
+ def burn_boxes_onto_image(pil_image, boxes_json_str):
47
+ """Draw red (source) and green (destination) boxes onto the image."""
48
+ if not pil_image:
49
+ return pil_image
50
+ try:
51
+ boxes = json.loads(boxes_json_str) if boxes_json_str and boxes_json_str.strip() else {}
52
+ except Exception:
53
+ boxes = {}
 
 
 
 
54
 
55
+ src = boxes.get("src")
56
+ dst = boxes.get("dst")
57
+ if not src or not dst:
58
+ return pil_image
59
 
 
60
  img = pil_image.copy().convert("RGB")
61
  w, h = img.size
62
  draw = ImageDraw.Draw(img)
63
+ bw = max(4, w // 150)
64
+
65
+ # Red = source
66
+ draw.rectangle([int(src["x1"]*w), int(src["y1"]*h), int(src["x2"]*w), int(src["y2"]*h)],
67
+ outline=(255, 0, 0), width=bw)
68
+ # Green = destination
69
+ draw.rectangle([int(dst["x1"]*w), int(dst["y1"]*h), int(dst["x2"]*w), int(dst["y2"]*h)],
70
+ outline=(0, 255, 0), width=bw)
 
71
  return img
72
 
73
 
74
+ @spaces.GPU
75
+ def infer(b64_str, boxes_json, prompt, seed, randomize_seed,
76
+ guidance_scale, num_inference_steps, height, width):
77
+ progress = gr.Progress(track_tqdm=True)
78
+
79
+ if not prompt or prompt.strip() == "":
80
+ raise gr.Error("Please enter a prompt.")
81
+
82
+ source_image = b64_to_pil(b64_str)
83
+ if source_image is None:
84
  raise gr.Error("Please upload an image first.")
85
+
86
  try:
87
+ boxes = json.loads(boxes_json) if boxes_json and boxes_json.strip() else {}
88
  except Exception:
89
  boxes = {}
 
 
 
 
 
90
 
91
+ if not boxes.get("src") or not boxes.get("dst"):
92
+ raise gr.Error("Draw a box around the subject, then move it to the target location.")
93
 
94
+ progress(0.3, desc="Burning bounding boxes...")
95
+ marked = burn_boxes_onto_image(source_image, boxes_json)
 
 
 
 
 
 
96
 
97
+ if randomize_seed:
98
+ seed = random.randint(0, MAX_SEED)
99
  generator = torch.Generator(device=device).manual_seed(seed)
100
+
101
+ progress(0.5, desc="Running inference...")
102
  result = pipe(
103
+ image=[marked],
104
+ prompt=prompt,
105
+ height=height if height != 0 else None,
106
+ width=width if width != 0 else None,
107
  num_inference_steps=num_inference_steps,
 
108
  generator=generator,
109
+ guidance_scale=guidance_scale,
110
  ).images[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ return result, seed, marked
 
 
 
 
 
 
113
 
 
 
 
 
 
114
 
115
+ def update_dimensions_on_upload(b64_str):
116
+ image = b64_to_pil(b64_str)
117
+ if image is None:
118
+ return 1024, 1024
119
+ ow, oh = image.size
120
+ if ow > oh:
121
+ nw = 1024
122
+ nh = int(nw * oh / ow)
123
+ else:
124
+ nh = 1024
125
+ nw = int(nh * ow / oh)
126
+ return (nw // 8) * 8, (nh // 8) * 8
127
+
128
+
129
+ # ── CSS ───────────────────────────────────────────────────────────────────────
130
+ css = r"""
131
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
132
+ *{box-sizing:border-box;margin:0;padding:0}
133
+ body,.gradio-container{background:#0f0f13!important;font-family:'DM Sans',system-ui,sans-serif!important;font-size:14px!important;color:#e4e4e7!important;min-height:100vh}
134
+ footer{display:none!important}
135
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
136
+
137
+ .app-shell{background:#18181b;border:1px solid #27272a;border-radius:16px;margin:12px auto;max-width:1400px;overflow:hidden;box-shadow:0 25px 50px -12px rgba(0,0,0,.6)}
138
+ .app-header{background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;padding:14px 24px;display:flex;align-items:center;gap:12px}
139
+ .app-logo{width:36px;height:36px;background:linear-gradient(135deg,#22c55e,#16a34a);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:800;color:#fff;box-shadow:0 4px 12px rgba(34,197,94,.35)}
140
+ .app-title{font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
141
+ .app-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(34,197,94,.15);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
142
+
143
+ .app-toolbar{background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;display:flex;gap:4px;align-items:center;flex-wrap:wrap}
144
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
145
+ .modern-tb-btn{display:inline-flex;align-items:center;gap:6px;min-width:32px;height:34px;background:transparent;border:1px solid transparent;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;font-family:'DM Sans',sans-serif;color:#e4e4e7!important;transition:all .15s}
146
+ .modern-tb-btn:hover{background:rgba(34,197,94,.15);border-color:rgba(34,197,94,.3)}
147
+ .modern-tb-btn.active{background:rgba(34,197,94,.25);border-color:rgba(34,197,94,.45)}
148
+
149
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
150
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
151
+ .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
152
+
153
+ #bbox-draw-wrap{position:relative;background:#09090b;min-height:440px;overflow:hidden;cursor:crosshair}
154
+ #bbox-draw-canvas{display:block;margin:0 auto}
155
+ #bbox-status{position:absolute;top:12px;left:12px;background:rgba(34,197,94,.9);color:#fff;padding:4px 12px;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500;border-radius:6px;z-index:10;display:none;pointer-events:none}
156
+ #bbox-count{position:absolute;top:12px;right:12px;background:rgba(24,24,27,.9);color:#4ade80;padding:4px 12px;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:600;border-radius:6px;border:1px solid rgba(34,197,94,.3);z-index:10;display:none}
157
+
158
+ .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
159
+ .upload-click-area{display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;padding:36px 44px;border:2px dashed #3f3f46;border-radius:16px;background:rgba(34,197,94,.03);transition:all .2s;gap:8px}
160
+ .upload-click-area:hover{background:rgba(34,197,94,.08);border-color:#22c55e;transform:scale(1.03)}
161
+ .upload-click-area svg{width:80px;height:80px}
162
+ .upload-main-text{color:#71717a;font-size:14px;font-weight:500}
163
+ .upload-sub-text{color:#52525b;font-size:12px}
164
+
165
+ .hint-bar{background:rgba(34,197,94,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7}
166
+ .hint-bar b{color:#86efac;font-weight:600}
167
+
168
+ .panel-card{border-bottom:1px solid #27272a}
169
+ .panel-card-title{padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6)}
170
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
171
+ .modern-textarea{width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:10px 14px;font-family:'DM Sans',sans-serif;font-size:14px;color:#e4e4e7;resize:vertical;outline:none;min-height:42px;transition:border-color .2s}
172
+ .modern-textarea:focus{border-color:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.15)}
173
+ .modern-textarea::placeholder{color:#3f3f46}
174
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
175
+
176
+ .btn-run{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:linear-gradient(135deg,#22c55e,#16a34a);border:none;border-radius:10px;padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'DM Sans',sans-serif;color:#fff;transition:all .2s;box-shadow:0 4px 16px rgba(34,197,94,.3),inset 0 1px 0 rgba(255,255,255,.1)}
177
+ .btn-run:hover{background:linear-gradient(135deg,#4ade80,#22c55e);box-shadow:0 6px 24px rgba(34,197,94,.45);transform:translateY(-1px)}
178
+
179
+ .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column}
180
+ .output-frame .out-title{padding:10px 20px;font-size:13px;font-weight:700;color:#e4e4e7;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between}
181
+ .output-frame .out-body{flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:180px;position:relative}
182
+ .output-frame .out-body img{max-width:100%;max-height:460px}
183
+ .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
184
+
185
+ .modern-loader{display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px}
186
+ .modern-loader.active{display:flex}
187
+ .modern-loader .loader-spinner{width:36px;height:36px;border:3px solid #27272a;border-top-color:#22c55e;border-radius:50%;animation:spin .8s linear infinite}
188
+ @keyframes spin{to{transform:rotate(360deg)}}
189
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa}
190
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
191
+ .loader-bar-fill{height:100%;background:linear-gradient(90deg,#22c55e,#4ade80,#22c55e);background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px}
192
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
193
+
194
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;overflow:hidden}
195
+ .settings-group-title{font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5)}
196
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
197
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
198
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
199
+ .slider-row input[type="range"]{flex:1;-webkit-appearance:none;height:6px;background:#27272a;border-radius:3px;outline:none}
200
+ .slider-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#22c55e,#16a34a);border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(34,197,94,.4)}
201
+ .slider-row .slider-val{min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;border-radius:6px;color:#a1a1aa}
202
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
203
+ .checkbox-row input[type="checkbox"]{accent-color:#22c55e;width:16px;height:16px;cursor:pointer}
204
+
205
+ .app-statusbar{background:#18181b;border-top:1px solid #27272a;padding:6px 20px;display:flex;gap:12px;height:34px;align-items:center}
206
+ .app-statusbar .sb-section{flex:1;font-family:'JetBrains Mono',monospace;font-size:12px;color:#52525b}
207
+ .app-statusbar .sb-fixed{flex:0 0 auto;padding:3px 12px;background:rgba(34,197,94,.08);border-radius:6px;color:#4ade80;font-weight:500;font-family:'JetBrains Mono',monospace;font-size:12px}
208
+
209
+ #gradio-run-btn{position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0.01;pointer-events:none}
210
+ @media(max-width:840px){.app-main-row{flex-direction:column}.app-main-right{width:100%}.app-main-left{border-right:none;border-bottom:1px solid #27272a}}
211
+ """
212
 
213
+ # ── JS for canvas interaction ─────────────────────────────────────────────────
214
+ # Loaded via demo.load() so it runs in MAIN PAGE context (not sandboxed).
215
+ # This is why fileInput.click() works.
216
+
217
+ canvas_js = r"""
218
+ () => {
219
+ function init() {
220
+ if (window.__bboxInitDone) return;
221
+
222
+ const canvas = document.getElementById('bbox-draw-canvas');
223
+ const wrap = document.getElementById('bbox-draw-wrap');
224
+ const status = document.getElementById('bbox-status');
225
+ const badge = document.getElementById('bbox-count');
226
+ const debugCount = document.getElementById('bbox-debug-count');
227
+ const uploadPrompt = document.getElementById('upload-prompt');
228
+ const uploadClickArea = document.getElementById('upload-click-area');
229
+ const fileInput = document.getElementById('custom-file-input');
230
+ const promptInput = document.getElementById('custom-prompt-input');
231
+
232
+ const btnDraw = document.getElementById('tb-draw');
233
+ const btnMove = document.getElementById('tb-move');
234
+ const btnClear = document.getElementById('tb-clear');
235
+ const btnChange = document.getElementById('tb-change-img');
236
+
237
+ if (!canvas || !wrap || !fileInput || !btnDraw) {
238
+ setTimeout(init, 250);
239
+ return;
240
+ }
 
 
241
 
242
+ window.__bboxInitDone = true;
243
+ const ctx = canvas.getContext('2d');
244
+
245
+ let baseImg = null;
246
+ let dispW = 512, dispH = 400;
247
+ let mode = 'draw'; // 'draw' | 'move'
248
+ let srcBox = null; // {x1,y1,x2,y2} normalized [0,1]
249
+ let dstBox = null;
250
+ let dragging = false;
251
+ let dragType = null; // 'new' | 'moveBox' | handle name
252
+ let dragStart = {x:0, y:0};
253
+ let dragOrig = null;
254
+ const HANDLE = 6;
255
+
256
+ function n2px(b) { return {x1:b.x1*dispW, y1:b.y1*dispH, x2:b.x2*dispW, y2:b.y2*dispH}; }
257
+ function px2n(x1,y1,x2,y2) {
258
+ return {x1:Math.min(x1,x2)/dispW, y1:Math.min(y1,y2)/dispH,
259
+ x2:Math.max(x1,x2)/dispW, y2:Math.max(y1,y2)/dispH};
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  }
261
+ function clamp01(v){return Math.max(0,Math.min(1,v));}
262
+ function fitSize(nw,nh) {
263
+ const mw = wrap.clientWidth || 512, mh = 500;
264
+ const r = Math.min(mw/nw, mh/nh, 1);
265
+ dispW = Math.round(nw*r); dispH = Math.round(nh*r);
266
+ canvas.width=dispW; canvas.height=dispH;
267
+ canvas.style.width=dispW+'px'; canvas.style.height=dispH+'px';
268
  }
269
+ function canvasXY(e) {
270
+ const r = canvas.getBoundingClientRect();
271
+ const cx = e.touches ? e.touches[0].clientX : e.clientX;
272
+ const cy = e.touches ? e.touches[0].clientY : e.clientY;
273
+ return {x:Math.max(0,Math.min(dispW,cx-r.left)), y:Math.max(0,Math.min(dispH,cy-r.top))};
 
 
 
 
 
 
 
 
 
274
  }
 
275
 
276
+ function setGradioValue(containerId, value) {
277
+ const container = document.getElementById(containerId);
278
+ if (!container) return;
279
+ container.querySelectorAll('input, textarea').forEach(el => {
280
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
281
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
282
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
283
+ if (ns && ns.set) {
284
+ ns.set.call(el, value);
285
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
286
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
287
+ }
288
+ });
289
+ }
290
 
291
+ function syncToGradio() {
292
+ const d = {};
293
+ if (srcBox) d.src = srcBox;
294
+ if (dstBox) d.dst = dstBox;
295
+ setGradioValue('boxes-json-input', JSON.stringify(d));
296
+ if (debugCount) {
297
+ if (srcBox && dstBox) debugCount.textContent = 'Source + Target defined';
298
+ else if (srcBox) debugCount.textContent = 'Source defined β€” move target';
299
+ else debugCount.textContent = 'No boxes drawn';
300
+ }
301
+ }
302
+ function syncImageToGradio(dataUrl) { setGradioValue('hidden-image-b64', dataUrl); }
303
+ function syncPromptToGradio() { if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value); }
304
+
305
+ function showStatus(t) { status.textContent=t; status.style.display='block'; }
306
+ function hideStatus() { status.style.display='none'; }
307
+
308
+ function handlePoints(b) {
309
+ const p = n2px(b);
310
+ const mx=(p.x1+p.x2)/2, my=(p.y1+p.y2)/2;
311
+ return {tl:{x:p.x1,y:p.y1},tc:{x:mx,y:p.y1},tr:{x:p.x2,y:p.y1},
312
+ ml:{x:p.x1,y:my},mr:{x:p.x2,y:my},
313
+ bl:{x:p.x1,y:p.y2},bc:{x:mx,y:p.y2},br:{x:p.x2,y:p.y2}};
314
+ }
315
+ function hitHandle(px,py,box) {
316
+ if (!box) return null;
317
+ const pts = handlePoints(box);
318
+ for (const k in pts) if (Math.abs(px-pts[k].x)<=HANDLE+2 && Math.abs(py-pts[k].y)<=HANDLE+2) return k;
319
+ return null;
320
+ }
321
+ function hitBox(px,py,box) {
322
+ if (!box) return false;
323
+ const p = n2px(box);
324
+ return px>=p.x1 && px<=p.x2 && py>=p.y1 && py<=p.y2;
325
+ }
326
 
327
+ function redraw(tempRect) {
328
+ ctx.clearRect(0,0,dispW,dispH);
329
+ if (!baseImg) { ctx.fillStyle='#09090b'; ctx.fillRect(0,0,dispW,dispH); updateBadge(); return; }
330
+ ctx.drawImage(baseImg, 0, 0, dispW, dispH);
331
+ const lw = Math.max(2, dispW/250);
332
+
333
+ // Source box = red dashed
334
+ if (srcBox) {
335
+ const p = n2px(srcBox);
336
+ ctx.save(); ctx.strokeStyle='rgba(239,68,68,0.95)'; ctx.lineWidth=lw+1;
337
+ ctx.setLineDash([8,5]); ctx.strokeRect(p.x1,p.y1,p.x2-p.x1,p.y2-p.y1); ctx.setLineDash([]); ctx.restore();
338
+ // Label
339
+ ctx.save(); ctx.font='bold 11px "DM Sans",sans-serif';
340
+ const tw=ctx.measureText('Source').width;
341
+ const ly=p.y1>20?p.y1-20:p.y1+4;
342
+ ctx.fillStyle='#ef4444'; ctx.fillRect(p.x1,ly,tw+10,18);
343
+ ctx.fillStyle='#fff'; ctx.fillText('Source',p.x1+5,ly+13); ctx.restore();
344
+ }
345
 
346
+ // Destination box = green solid + handles
347
+ if (dstBox) {
348
+ const p = n2px(dstBox);
349
+ const w=p.x2-p.x1, h=p.y2-p.y1;
350
+ ctx.save(); ctx.strokeStyle='rgba(34,197,94,0.95)'; ctx.lineWidth=lw+1;
351
+ ctx.strokeRect(p.x1,p.y1,w,h);
352
+ ctx.fillStyle='rgba(34,197,94,0.06)'; ctx.fillRect(p.x1,p.y1,w,h); ctx.restore();
353
+ // Handles
354
+ const pts = handlePoints(dstBox); ctx.save();
355
+ for (const k in pts) {
356
+ ctx.fillStyle='#22c55e'; ctx.beginPath(); ctx.arc(pts[k].x,pts[k].y,HANDLE,0,Math.PI*2); ctx.fill();
357
+ ctx.strokeStyle='#fff'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(pts[k].x,pts[k].y,HANDLE,0,Math.PI*2); ctx.stroke();
358
+ } ctx.restore();
359
+ // Label
360
+ ctx.save(); ctx.font='bold 11px "DM Sans",sans-serif';
361
+ const tw2=ctx.measureText('Target').width;
362
+ const ly2=p.y1>20?p.y1-20:p.y1+4;
363
+ ctx.fillStyle='#22c55e'; ctx.fillRect(p.x1,ly2,tw2+10,18);
364
+ ctx.fillStyle='#fff'; ctx.fillText('Target',p.x1+5,ly2+13); ctx.restore();
365
+ }
 
 
 
 
 
366
 
367
+ if (tempRect) {
368
+ ctx.save(); ctx.strokeStyle='rgba(239,68,68,0.95)'; ctx.lineWidth=lw;
369
+ ctx.setLineDash([4,3]); ctx.strokeRect(
370
+ Math.min(tempRect.x1,tempRect.x2), Math.min(tempRect.y1,tempRect.y2),
371
+ Math.abs(tempRect.x2-tempRect.x1), Math.abs(tempRect.y2-tempRect.y1));
372
+ ctx.setLineDash([]); ctx.restore();
373
+ }
374
+ updateBadge();
 
 
 
 
 
 
 
 
 
 
375
  }
376
 
377
+ function updateBadge() {
378
+ if (srcBox || dstBox) {
379
+ badge.style.display='block';
380
+ badge.textContent = (srcBox && dstBox) ? 'SRC + DST' : 'SRC only';
381
+ } else { badge.style.display='none'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
+ function setMode(m) {
385
+ mode = m;
386
+ btnDraw.classList.toggle('active', m==='draw');
387
+ btnMove.classList.toggle('active', m==='move');
388
+ canvas.style.cursor = m==='draw' ? 'crosshair' : 'default';
389
+ redraw();
390
  }
 
 
391
 
392
+ // ── File upload ──
393
+ uploadClickArea.addEventListener('click', () => fileInput.click());
394
+ btnChange.addEventListener('click', () => fileInput.click());
395
+ fileInput.addEventListener('change', e => {
396
+ processFile(e.target.files[0]);
397
+ e.target.value = '';
398
+ });
399
+ wrap.addEventListener('dragover', e => { e.preventDefault(); wrap.style.outline='2px solid #22c55e'; });
400
+ wrap.addEventListener('dragleave', e => { e.preventDefault(); wrap.style.outline=''; });
401
+ wrap.addEventListener('drop', e => { e.preventDefault(); wrap.style.outline=''; processFile(e.dataTransfer.files[0]); });
402
+
403
+ function processFile(file) {
404
+ if (!file || !file.type.startsWith('image/')) return;
405
+ const reader = new FileReader();
406
+ reader.onload = ev => {
407
+ const dataUrl = ev.target.result;
408
+ const img = new window.Image();
409
+ img.crossOrigin = 'anonymous';
410
+ img.onload = () => {
411
+ baseImg = img; srcBox = null; dstBox = null;
412
+ fitSize(img.naturalWidth, img.naturalHeight);
413
+ syncToGradio(); redraw(); hideStatus();
414
+ uploadPrompt.style.display = 'none';
415
+ syncImageToGradio(dataUrl);
416
+ };
417
+ img.src = dataUrl;
418
+ };
419
+ reader.readAsDataURL(file);
420
+ }
421
 
422
+ // ── Toolbar ──
423
+ btnDraw.addEventListener('click', () => setMode('draw'));
424
+ btnMove.addEventListener('click', () => { if (srcBox) setMode('move'); });
425
+ btnClear.addEventListener('click', () => {
426
+ srcBox=null; dstBox=null; syncToGradio(); setMode('draw'); redraw();
427
+ });
428
+
429
+ // ── Canvas events ──
430
+ canvas.addEventListener('mousedown', onDown);
431
+ canvas.addEventListener('mousemove', onMove);
432
+ canvas.addEventListener('mouseup', onUp);
433
+ canvas.addEventListener('mouseleave', e => { if(dragging) onUp(e); });
434
+ canvas.addEventListener('touchstart', onDown, {passive:false});
435
+ canvas.addEventListener('touchmove', onMove, {passive:false});
436
+ canvas.addEventListener('touchend', onUp, {passive:false});
437
+
438
+ function onDown(e) {
439
+ if (!baseImg) return;
440
+ e.preventDefault();
441
+ const {x,y} = canvasXY(e);
442
+
443
+ if (mode === 'draw') {
444
+ dragging=true; dragType='new'; dragStart={x,y}; srcBox=null; dstBox=null;
445
+ return;
446
+ }
447
+ // Move mode
448
+ if (dstBox) {
449
+ const h = hitHandle(x,y,dstBox);
450
+ if (h) { dragging=true; dragType=h; dragStart={x,y}; dragOrig={...dstBox}; showStatus('Resizing'); return; }
451
+ if (hitBox(x,y,dstBox)) { dragging=true; dragType='moveBox'; dragStart={x,y}; dragOrig={...dstBox}; showStatus('Moving'); return; }
452
+ }
453
+ }
454
 
455
+ function onMove(e) {
456
+ if (!baseImg) return;
457
+ e.preventDefault();
458
+ const {x,y} = canvasXY(e);
459
+
460
+ if (!dragging) {
461
+ if (mode==='move' && dstBox) {
462
+ if (hitHandle(x,y,dstBox)) { canvas.style.cursor='nwse-resize'; }
463
+ else if (hitBox(x,y,dstBox)) { canvas.style.cursor='grab'; }
464
+ else { canvas.style.cursor='default'; }
465
+ }
466
+ return;
467
+ }
468
 
469
+ if (dragType === 'new') {
470
+ redraw({x1:dragStart.x, y1:dragStart.y, x2:x, y2:y});
471
+ return;
472
+ }
 
473
 
474
+ const dx = (x - dragStart.x) / dispW;
475
+ const dy = (y - dragStart.y) / dispH;
476
+ const o = dragOrig;
477
+
478
+ if (dragType === 'moveBox') {
479
+ const bw=o.x2-o.x1, bh=o.y2-o.y1;
480
+ let nx=clamp01(o.x1+dx), ny=clamp01(o.y1+dy);
481
+ if (nx+bw>1) nx=1-bw; if (ny+bh>1) ny=1-bh;
482
+ dstBox = {x1:nx, y1:ny, x2:nx+bw, y2:ny+bh};
483
+ } else {
484
+ // Resize handles
485
+ let {x1,y1,x2,y2} = {...o};
486
+ const t = dragType;
487
+ if (t.includes('l')) x1 = clamp01(o.x1+dx);
488
+ if (t.includes('r')) x2 = clamp01(o.x2+dx);
489
+ if (t.includes('t')) y1 = clamp01(o.y1+dy);
490
+ if (t.includes('b')) y2 = clamp01(o.y2+dy);
491
+ if (x1>x2) {const tmp=x1;x1=x2;x2=tmp;}
492
+ if (y1>y2) {const tmp=y1;y1=y2;y2=tmp;}
493
+ if (Math.abs(x2-x1)<0.01||Math.abs(y2-y1)<0.01) return;
494
+ dstBox = {x1,y1,x2,y2};
495
  }
496
+ redraw();
497
  }
 
 
 
 
 
 
498
 
499
+ function onUp(e) {
500
+ if (!dragging) return;
501
+ if (e) e.preventDefault();
502
+
503
+ if (dragType === 'new') {
504
+ const pt = e ? canvasXY(e) : dragStart;
505
+ if (Math.abs(pt.x-dragStart.x)>8 && Math.abs(pt.y-dragStart.y)>8) {
506
+ srcBox = px2n(dragStart.x, dragStart.y, pt.x, pt.y);
507
+ dstBox = {...srcBox};
508
+ showStatus('Box drawn β€” now move/resize the target');
509
+ setMode('move');
510
+ } else { hideStatus(); }
511
+ } else {
512
+ showStatus('Target updated');
513
+ }
514
 
515
+ dragging=false; dragType=null; dragOrig=null;
516
+ syncToGradio(); redraw();
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  }
518
+
519
+ // ── Prompt sync ──
520
+ if (promptInput) { promptInput.addEventListener('input', syncPromptToGradio); }
521
+
522
+ // ── Slider sync ──
523
+ function syncSlider(customId, gradioId) {
524
+ const slider = document.getElementById(customId);
525
+ const valSpan = document.getElementById(customId + '-val');
526
+ if (!slider) return;
527
+ slider.addEventListener('input', () => {
528
+ if (valSpan) valSpan.textContent = slider.value;
529
+ const container = document.getElementById(gradioId);
530
+ if (!container) return;
531
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
532
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
533
+ if (ns && ns.set) { ns.set.call(el, slider.value); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); }
534
+ });
535
+ });
536
  }
537
+ syncSlider('custom-seed','gradio-seed');
538
+ syncSlider('custom-guidance','gradio-guidance');
539
+ syncSlider('custom-steps','gradio-steps');
540
+ syncSlider('custom-width','gradio-width');
541
+ syncSlider('custom-height','gradio-height');
542
+
543
+ const randCheck = document.getElementById('custom-randomize');
544
+ if (randCheck) { randCheck.addEventListener('change', () => {
545
+ const cb = document.querySelector('#gradio-randomize input[type="checkbox"]');
546
+ if (cb && cb.checked !== randCheck.checked) cb.click();
547
+ }); }
548
+
549
+ // ── Run button ──
550
+ function showLoaders() { document.querySelectorAll('.modern-loader').forEach(l=>l.classList.add('active')); }
551
+ function hideLoaders() { document.querySelectorAll('.modern-loader').forEach(l=>l.classList.remove('active')); }
552
+ window.__hideLoaders = hideLoaders;
553
+
554
+ const customRunBtn = document.getElementById('custom-run-btn');
555
+ if (customRunBtn) customRunBtn.addEventListener('click', () => {
556
+ syncPromptToGradio(); syncToGradio(); showLoaders();
557
+ setTimeout(() => {
558
+ const gb = document.getElementById('gradio-run-btn');
559
+ if (gb) { const b=gb.querySelector('button'); if(b)b.click(); else gb.click(); }
560
+ }, 200);
561
+ });
562
+
563
+ // ── Watch outputs ──
564
+ function watchOutputs() {
565
+ const rc=document.getElementById('gradio-result'), pc=document.getElementById('gradio-preview');
566
+ const ob=document.getElementById('output-image-container'), pb=document.getElementById('preview-image-container');
567
+ if(!rc||!pc||!ob||!pb){setTimeout(watchOutputs,500);return;}
568
+ function sync() {
569
+ const ri=rc.querySelector('img'); if(ri&&ri.src){
570
+ let e=ob.querySelector('img.oimg'); if(!e){e=document.createElement('img');e.className='oimg';ob.appendChild(e);}
571
+ if(e.src!==ri.src){e.src=ri.src;const ph=ob.querySelector('.out-placeholder');if(ph)ph.style.display='none';hideLoaders();}
572
+ }
573
+ const pi=pc.querySelector('img'); if(pi&&pi.src){
574
+ let e2=pb.querySelector('img.oimg'); if(!e2){e2=document.createElement('img');e2.className='oimg';pb.appendChild(e2);}
575
+ if(e2.src!==pi.src){e2.src=pi.src;const ph2=pb.querySelector('.out-placeholder');if(ph2)ph2.style.display='none';}
576
+ }
577
+ }
578
+ new MutationObserver(sync).observe(rc,{childList:true,subtree:true,attributes:true,attributeFilter:['src']});
579
+ new MutationObserver(sync).observe(pc,{childList:true,subtree:true,attributes:true,attributeFilter:['src']});
580
+ setInterval(sync,800);
581
  }
582
+ watchOutputs();
583
+
584
+ // ── Dimension sync from Gradio ──
585
+ function watchDims() {
586
+ const wc=document.getElementById('gradio-width'),hc=document.getElementById('gradio-height');
587
+ const ws=document.getElementById('custom-width'),hs=document.getElementById('custom-height');
588
+ if(!wc||!hc||!ws||!hs){setTimeout(watchDims,500);return;}
589
+ function s(){
590
+ const wi=wc.querySelector('input[type="range"],input[type="number"]');
591
+ const hi=hc.querySelector('input[type="range"],input[type="number"]');
592
+ if(wi){ws.value=wi.value;const v=document.getElementById('custom-width-val');if(v)v.textContent=wi.value;}
593
+ if(hi){hs.value=hi.value;const v=document.getElementById('custom-height-val');if(v)v.textContent=hi.value;}
 
594
  }
595
+ new MutationObserver(s).observe(wc,{childList:true,subtree:true,attributes:true});
596
+ new MutationObserver(s).observe(hc,{childList:true,subtree:true,attributes:true});
597
+ setInterval(s,1000);
598
  }
599
+ watchDims();
 
 
 
 
 
 
 
 
 
600
 
601
+ new ResizeObserver(() => { if(baseImg){fitSize(baseImg.naturalWidth,baseImg.naturalHeight);redraw();} }).observe(wrap);
602
 
603
+ setMode('draw'); fitSize(512,400); redraw(); syncToGradio();
604
+ }
605
+ init();
606
+ }
 
 
607
  """
608
 
 
 
 
609
 
610
+ # ── Gradio Blocks ─────────────────────────────────────────────────────────────
611
+
612
+ with gr.Blocks(css=css) as demo:
613
+
614
+ # Hidden Gradio inputs
615
+ hidden_image_b64 = gr.Textbox(elem_id="hidden-image-b64", elem_classes="hidden-input", container=False)
616
+ boxes_json = gr.Textbox(value="{}", elem_id="boxes-json-input", elem_classes="hidden-input", container=False)
617
+ prompt = gr.Textbox(value=DEFAULT_PROMPT, elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
618
+ seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
619
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
620
+ guidance_scale = gr.Slider(minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
621
+ num_inference_steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
622
+ height_slider = gr.Slider(minimum=256, maximum=2048, step=8, value=1024, elem_id="gradio-height", elem_classes="hidden-input", container=False)
623
+ width_slider = gr.Slider(minimum=256, maximum=2048, step=8, value=1024, elem_id="gradio-width", elem_classes="hidden-input", container=False)
624
+ result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
625
+ preview = gr.Image(elem_id="gradio-preview", elem_classes="hidden-input", container=False)
626
+
627
+ # The entire visible UI
628
+ gr.HTML(f"""
629
+ <div class="app-shell">
630
+ <div class="app-header">
631
+ <div class="app-logo">β†—</div>
632
+ <span class="app-title">Bbox Move &amp; Resize</span>
633
+ <span class="app-badge">FLUX.2 Klein 9B + LoRA</span>
634
+ </div>
635
+ <div class="app-toolbar">
636
+ <button id="tb-draw" class="modern-tb-btn active"><span>β–¬</span> Draw</button>
637
+ <button id="tb-move" class="modern-tb-btn"><span>⇉</span> Move</button>
638
+ <div class="tb-sep"></div>
639
+ <button id="tb-clear" class="modern-tb-btn"><span>βœ–</span> Clear</button>
640
+ <button id="tb-change-img" class="modern-tb-btn">Upload…</button>
641
+ </div>
642
+
643
+ <div class="app-main-row">
644
+ <div class="app-main-left">
645
+ <div id="bbox-draw-wrap">
646
+ <div id="upload-prompt" class="upload-prompt-modern">
647
+ <div id="upload-click-area" class="upload-click-area">
648
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
649
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#22c55e" stroke-width="2" stroke-dasharray="4 3"/>
650
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(34,197,94,0.15)" stroke="#22c55e" stroke-width="1.5"/>
651
+ <circle cx="28" cy="30" r="6" fill="rgba(34,197,94,0.2)" stroke="#22c55e" stroke-width="1.5"/>
652
+ </svg>
653
+ <span class="upload-main-text">Click to upload an image</span>
654
+ <span class="upload-sub-text">or drag &amp; drop</span>
655
+ </div>
656
+ </div>
657
+ <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
658
+ <canvas id="bbox-draw-canvas" width="512" height="400"></canvas>
659
+ <div id="bbox-status"></div>
660
+ <div id="bbox-count"></div>
661
+ </div>
662
+ <div class="hint-bar">
663
+ <b>Draw:</b> Click &amp; drag to select the subject &nbsp;Β·&nbsp;
664
+ <b>Move:</b> Drag the green box to reposition &nbsp;Β·&nbsp;
665
+ Resize via corner handles
666
+ </div>
667
+ </div>
668
+
669
+ <div class="app-main-right">
670
+ <div class="panel-card">
671
+ <div class="panel-card-title">Edit Instruction</div>
672
+ <div class="panel-card-body">
673
+ <label class="modern-label">Prompt</label>
674
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3">{DEFAULT_PROMPT}</textarea>
675
+ </div>
676
+ </div>
677
+ <div style="padding:12px 20px;">
678
+ <button id="custom-run-btn" class="btn-run">β–Ά Run</button>
679
+ </div>
680
+
681
+ <div class="output-frame" style="flex:1">
682
+ <div class="out-title"><span>Output</span></div>
683
+ <div class="out-body" id="output-image-container">
684
+ <div class="modern-loader" id="output-loader"><div class="loader-spinner"></div><div class="loader-text">Processing…</div><div class="loader-bar-track"><div class="loader-bar-fill"></div></div></div>
685
+ <div class="out-placeholder">Result will appear here</div>
686
+ </div>
687
+ </div>
688
+ <div class="output-frame">
689
+ <div class="out-title"><span>Input Preview (with boxes)</span></div>
690
+ <div class="out-body" id="preview-image-container">
691
+ <div class="modern-loader" id="preview-loader"><div class="loader-spinner"></div><div class="loader-text">Preparing…</div><div class="loader-bar-track"><div class="loader-bar-fill"></div></div></div>
692
+ <div class="out-placeholder">Preview will appear here</div>
693
+ </div>
694
+ </div>
695
+
696
+ <div class="settings-group">
697
+ <div class="settings-group-title">Settings</div>
698
+ <div class="settings-group-body">
699
+ <div class="slider-row"><label>Seed</label><input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0"><span class="slider-val" id="custom-seed-val">0</span></div>
700
+ <div class="checkbox-row"><input type="checkbox" id="custom-randomize" checked><label for="custom-randomize">Randomize seed</label></div>
701
+ <div class="slider-row"><label>Guidance</label><input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0"><span class="slider-val" id="custom-guidance-val">1.0</span></div>
702
+ <div class="slider-row"><label>Steps</label><input type="range" id="custom-steps" min="1" max="50" step="1" value="4"><span class="slider-val" id="custom-steps-val">4</span></div>
703
+ <div class="slider-row"><label>Width</label><input type="range" id="custom-width" min="256" max="2048" step="8" value="1024"><span class="slider-val" id="custom-width-val">1024</span></div>
704
+ <div class="slider-row"><label>Height</label><input type="range" id="custom-height" min="256" max="2048" step="8" value="1024"><span class="slider-val" id="custom-height-val">1024</span></div>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </div>
709
+
710
+ <div class="app-statusbar">
711
+ <div class="sb-section" id="bbox-debug-count">No boxes drawn</div>
712
+ <div class="sb-section sb-fixed">Ready</div>
713
+ </div>
714
+ </div>
715
  """)
716
 
717
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
 
719
+ # JS loaded via demo.load β€” runs in MAIN page context, NOT sandboxed
720
+ demo.load(fn=None, js=canvas_js)
721
 
722
+ run_btn.click(
 
 
723
  fn=infer,
724
+ inputs=[hidden_image_b64, boxes_json, prompt, seed, randomize_seed,
725
+ guidance_scale, num_inference_steps, height_slider, width_slider],
726
+ outputs=[result, seed, preview],
727
+ )
728
+
729
+ hidden_image_b64.change(
730
+ fn=update_dimensions_on_upload,
731
+ inputs=[hidden_image_b64],
732
+ outputs=[width_slider, height_slider],
733
  )
734
 
735
+ if __name__ == "__main__":
736
+ demo.launch(ssr_mode=False, show_error=True)