LiXiY commited on
Commit
37ac762
·
verified ·
1 Parent(s): c725362

Upload 18 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/background_image/1.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/background_image/2.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/background_image/3.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/background_image/4.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/ref_image/1.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/ref_image/2.png filter=lfs diff=lfs merge=lfs -text
42
+ examples/ref_image/3.png filter=lfs diff=lfs merge=lfs -text
43
+ examples/ref_image/4.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,561 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ from io import BytesIO
3
+
4
+ import gradio as gr
5
+ import torch
6
+ from PIL import Image, ImageChops
7
+ import spaces
8
+ from diffsynth.pipelines.flux2_image import Flux2ImagePipeline, ModelConfig
9
+
10
+ from utils import process_source, process_reference, paste_back, binarize_mask
11
+
12
+ pipe = Flux2ImagePipeline.from_pretrained(
13
+ torch_dtype=torch.bfloat16,
14
+ device="cuda",
15
+ model_configs=[
16
+ ModelConfig(model_id="black-forest-labs/FLUX.2-klein-base-4B", origin_file_pattern="text_encoder/*.safetensors"),
17
+ ModelConfig(model_id="black-forest-labs/FLUX.2-klein-base-4B", origin_file_pattern="transformer/*.safetensors"),
18
+ ModelConfig(model_id="black-forest-labs/FLUX.2-klein-base-4B", origin_file_pattern="vae/diffusion_pytorch_model.safetensors"),
19
+ ],
20
+ tokenizer_config=ModelConfig(model_id="black-forest-labs/FLUX.2-klein-base-4B", origin_file_pattern="tokenizer/"),
21
+ )
22
+
23
+ pipe.load_lora(pipe.dit, ModelConfig(model_id="HuanJue/Easy-Insert", origin_file_pattern="easy-insert.safetensors"))
24
+
25
+ PROMPT = "Replace the white mask of image1 with the content in image2. Preserving the background, lighting, and surrounding elements, maintain a seamless and natural result."
26
+
27
+
28
+ # ===================== Example Data =====================
29
+
30
+ CANVAS_W, CANVAS_H = 1024, 1024
31
+
32
+ EXAMPLES = [
33
+ ("examples/background_image/1.png", "examples/insert_mask/1.png", "examples/ref_image/1.png", "examples/ref_mask/1.png"),
34
+ ("examples/background_image/2.png", "examples/insert_mask/2.png", "examples/ref_image/2.png", "examples/ref_mask/2.png"),
35
+ ("examples/background_image/3.png", "examples/insert_mask/3.png", "examples/ref_image/3.png", "examples/ref_mask/3.png"),
36
+ ("examples/background_image/4.png", "examples/insert_mask/4.png", "examples/ref_image/4.png", "examples/ref_mask/4.png"),
37
+ ]
38
+
39
+
40
+ # ===================== Thumbnail HTML Generation =====================
41
+
42
+ def img_to_b64(path, size=(240, 240)):
43
+ img = Image.open(path).convert("RGB")
44
+ # Preserve the original aspect ratio; fit inside `size`. 2x the display
45
+ # width (110 CSS px) so thumbnails stay sharp on HiDPI screens.
46
+ img.thumbnail(size, Image.LANCZOS)
47
+ buf = BytesIO()
48
+ img.save(buf, format="PNG")
49
+ return base64.b64encode(buf.getvalue()).decode()
50
+
51
+
52
+ def build_examples_html():
53
+ row_pairs = [EXAMPLES[0:2], EXAMPLES[2:4]]
54
+
55
+ html = '<div class="ex-grid">'
56
+ for row_idx, row_examples in enumerate(row_pairs):
57
+ html += '<div class="ex-grid-row">'
58
+ for col_idx, (bg, mask, ref, ref_mask) in enumerate(row_examples):
59
+ i = row_idx * 2 + col_idx
60
+ bg_b64 = img_to_b64(bg)
61
+ mask_b64 = img_to_b64(mask)
62
+ ref_b64 = img_to_b64(ref)
63
+ ref_mask_b64 = img_to_b64(ref_mask)
64
+ html += f'''
65
+ <div class="ex-row" onclick="(function(){{var el=document.getElementById('ex_btn_{i}');if(!el)return;var btn=el.querySelector('button')||el;btn.dispatchEvent(new MouseEvent('click',{{bubbles:true,cancelable:true}}));}})()">
66
+ <div class="ex-label">Example {i + 1}</div>
67
+ <div class="ex-thumbs">
68
+ <div class="ex-thumb-wrap">
69
+ <img src="data:image/png;base64,{bg_b64}" class="ex-thumb" draggable="false"/>
70
+ <span class="ex-thumb-sublabel">Background</span>
71
+ </div>
72
+ <div class="ex-thumb-wrap">
73
+ <img src="data:image/png;base64,{mask_b64}" class="ex-thumb" draggable="false"/>
74
+ <span class="ex-thumb-sublabel">Mask</span>
75
+ </div>
76
+ <div class="ex-thumb-wrap">
77
+ <img src="data:image/png;base64,{ref_b64}" class="ex-thumb" draggable="false"/>
78
+ <span class="ex-thumb-sublabel">Reference</span>
79
+ </div>
80
+ <div class="ex-thumb-wrap">
81
+ <img src="data:image/png;base64,{ref_mask_b64}" class="ex-thumb" draggable="false"/>
82
+ <span class="ex-thumb-sublabel">Ref Mask</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ '''
87
+ html += '</div>'
88
+ html += '</div>'
89
+ return html
90
+
91
+
92
+ # ===================== Utility Functions =====================
93
+
94
+ def extract_mask_from_layers(layers, target_size):
95
+ mask = Image.new("L", target_size, 0)
96
+ for layer in layers:
97
+ if layer is not None:
98
+ layer_rgba = layer.convert("RGBA").resize(target_size)
99
+ alpha = layer_rgba.split()[3]
100
+ alpha_binary = alpha.point(lambda x: 255 if x > 0 else 0)
101
+ mask = Image.composite(Image.new("L", target_size, 255), mask, alpha_binary)
102
+ return mask
103
+
104
+
105
+ EDITOR_SIZE = 800
106
+ # Matches Gradio light-mode image editor background so padded examples blend in.
107
+ EDITOR_BG_COLOR = (243, 244, 246)
108
+
109
+
110
+ def fit_transform(img, editor_size=EDITOR_SIZE):
111
+ """Display transform the fixed-canvas editor applies to `img`: scale to fit
112
+ inside the square canvas, then center-pad. Returns (disp_size, pad)."""
113
+ scale = min(editor_size / img.width, editor_size / img.height, 1.0)
114
+ disp_size = (
115
+ max(1, round(img.width * scale)),
116
+ max(1, round(img.height * scale)),
117
+ )
118
+ pad = (
119
+ (editor_size - disp_size[0]) // 2,
120
+ (editor_size - disp_size[1]) // 2,
121
+ )
122
+ return disp_size, pad
123
+
124
+
125
+ def build_display_bg(bg, editor_size=EDITOR_SIZE):
126
+ """Rebuild the padded square background the editor shows for `bg`."""
127
+ disp_size, pad = fit_transform(bg, editor_size)
128
+ canvas = Image.new("RGB", (editor_size, editor_size), EDITOR_BG_COLOR)
129
+ canvas.paste(bg.convert("RGB").resize(disp_size, Image.LANCZOS), pad)
130
+ return canvas
131
+
132
+
133
+ def images_close(a, b, tol_frac=0.001):
134
+ """True if two RGB images are (nearly) pixel-identical."""
135
+ if a.size != b.size:
136
+ return False
137
+ diff = ImageChops.difference(a.convert("RGB"), b.convert("RGB"))
138
+ changed = sum(diff.histogram()[1:])
139
+ return changed <= tol_frac * a.size[0] * a.size[1] * 3
140
+
141
+
142
+ def make_editor_value(bg_pil, mask_pil=None, editor_size=EDITOR_SIZE):
143
+ """Build an ImageEditor value on a fixed square canvas.
144
+
145
+ The background is downscaled to fit inside `editor_size` and padded to a
146
+ square; the mask is padded the same way and sent as a real layer. This
147
+ keeps the mask and background perfectly aligned in the frontend (both are
148
+ 800x800 and the padding is identical), while the full-resolution image and
149
+ exact mask are kept in gr.State for inference.
150
+
151
+ Returns (editor_value, sent_mask, transform).
152
+ """
153
+ bg = bg_pil.convert("RGB")
154
+ disp_size, pad = fit_transform(bg, editor_size)
155
+ bg_sq = build_display_bg(bg, editor_size)
156
+
157
+ transform = {
158
+ "editor_size": editor_size,
159
+ "disp_size": disp_size,
160
+ "pad": pad,
161
+ }
162
+
163
+ if mask_pil is None:
164
+ # composite=None avoids saving a second, redundant image file. The editor
165
+ # can render the background directly; if it needs a composite it will build
166
+ # it from background + (empty) layers.
167
+ return {"background": bg_sq, "layers": [], "composite": None}, None, transform
168
+
169
+ mask = mask_pil.convert("L").resize(disp_size, Image.NEAREST)
170
+ mask_sq = Image.new("L", (editor_size, editor_size), 0)
171
+ mask_sq.paste(mask, pad)
172
+
173
+ transparent = Image.new("RGBA", (editor_size, editor_size), (0, 0, 0, 0))
174
+ # Semi-transparent white (alpha 153 ≈ 60%) so the background shows through
175
+ # the loaded mask. Mask extraction binarizes alpha > 0, so this still
176
+ # resolves to a solid mask at inference time.
177
+ white_solid = Image.new("RGBA", (editor_size, editor_size), (255, 255, 255, 153))
178
+ mask_layer = Image.composite(white_solid, transparent, mask_sq)
179
+
180
+ return {"background": bg_sq, "layers": [mask_layer], "composite": None}, mask_sq, transform
181
+
182
+
183
+ def masks_close(a, b, tol_frac=0.0):
184
+ """True if two binarized masks are pixel-identical (within tol_frac).
185
+ The fixed-canvas PNG round-trip is exact, so we treat any non-zero
186
+ difference as a real user edit."""
187
+ if a.size != b.size:
188
+ b = b.resize(a.size, Image.NEAREST)
189
+ diff = ImageChops.difference(binarize_mask(a), binarize_mask(b))
190
+ changed = sum(diff.histogram()[1:])
191
+ return changed <= tol_frac * a.size[0] * a.size[1]
192
+
193
+
194
+ def load_example(idx):
195
+ bg_path, mask_path, ref_path, ref_mask_path = EXAMPLES[idx]
196
+ bg_img = Image.open(bg_path).convert("RGB")
197
+ ref_img = Image.open(ref_path).convert("RGB")
198
+ base_val, base_sent, base_transform = make_editor_value(
199
+ bg_img, Image.open(mask_path)
200
+ )
201
+ ref_val, ref_sent, ref_transform = make_editor_value(
202
+ ref_img, Image.open(ref_mask_path)
203
+ )
204
+ # Keep the pristine full-res image + exact mask in State. The editor value
205
+ # is only for display; its small size avoids slow frontend re-uploads.
206
+ base_state = (bg_img, Image.open(mask_path).convert("L"), base_sent, base_transform)
207
+ ref_state = (ref_img, Image.open(ref_mask_path).convert("L"), ref_sent, ref_transform)
208
+ return base_val, ref_val, base_state, ref_state
209
+
210
+
211
+ def load_ex1():
212
+ return load_example(0)
213
+
214
+ def load_ex2():
215
+ return load_example(1)
216
+
217
+ def load_ex3():
218
+ return load_example(2)
219
+
220
+ def load_ex4():
221
+ return load_example(3)
222
+
223
+
224
+ # ===================== Generation Function =====================
225
+
226
+ def resolve_source(editor_value, state):
227
+ """Return (full_res_image, full_res_mask) for one editor.
228
+
229
+ `state` is (image, mask, sent_mask, transform) captured when an example was
230
+ loaded. The editor value's background is display-only, so we don't trust it
231
+ blindly: rebuild the padded display image from the state's pristine image
232
+ and compare it with what the editor currently shows. If they match, the
233
+ example is still loaded and the state's full-res image (+ exact mask) is
234
+ used. If they differ, the user uploaded a different image - the editor
235
+ background is then the full-res original, and the brush strokes are mapped
236
+ back through the fixed-canvas transform.
237
+ """
238
+ bg_ed = editor_value.get("background")
239
+ if bg_ed is None:
240
+ return None, None
241
+ img_ed = bg_ed.convert("RGB")
242
+
243
+ ed_mask = extract_mask_from_layers(
244
+ editor_value.get("layers", []), (EDITOR_SIZE, EDITOR_SIZE)
245
+ )
246
+
247
+ if state is not None:
248
+ img, full_mask, sent_mask, transform = state
249
+ img = img.convert("RGB")
250
+ if images_close(img_ed, build_display_bg(img)):
251
+ if full_mask is not None:
252
+ # Example flow: exact full-res mask is in state. If the editor's
253
+ # layer mask still matches what we sent (user hasn't brushed or
254
+ # erased), use the state mask directly - bit-exact with inference.py.
255
+ if sent_mask is not None and masks_close(ed_mask, sent_mask):
256
+ return img, binarize_mask(full_mask)
257
+ # User edited the example's mask (erased and/or brushed): crop the
258
+ # padding and map the edited mask back to the original resolution.
259
+ if transform:
260
+ ed_mask_bin = binarize_mask(ed_mask)
261
+ px, py = transform["pad"]
262
+ dw, dh = transform["disp_size"]
263
+ cropped = ed_mask_bin.crop((px, py, px + dw, py + dh))
264
+ return img, binarize_mask(cropped.resize(img.size, Image.NEAREST))
265
+
266
+ # Upload flow: the editor background is the user's full-res original.
267
+ disp_size, pad = fit_transform(img_ed)
268
+ ed_mask_bin = binarize_mask(ed_mask)
269
+ cropped = ed_mask_bin.crop((pad[0], pad[1], pad[0] + disp_size[0], pad[1] + disp_size[1]))
270
+ return img_ed, binarize_mask(cropped.resize(img_ed.size, Image.NEAREST))
271
+
272
+ @spaces.GPU
273
+ def run_local(base, ref, base_state, ref_state, seed, num_inference_steps, cfg_scale):
274
+ if base is None or ref is None or not isinstance(base, dict) or not isinstance(ref, dict):
275
+ return None, gr.update(visible=False)
276
+
277
+ pil_bg, pil_mask = resolve_source(base, base_state)
278
+ pil_ref, pil_ref_mask = resolve_source(ref, ref_state)
279
+ if pil_bg is None or pil_ref is None:
280
+ return None, gr.update(visible=False)
281
+
282
+ if pil_mask.getextrema() == (0, 0) or pil_ref_mask.getextrema() == (0, 0):
283
+ error_html = """
284
+ <div class="error-overlay" style="
285
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
286
+ background: rgba(0,0,0,0.5); display: flex; justify-content: center;
287
+ align-items: center; z-index: 9999;
288
+ ">
289
+ <div style="
290
+ background: white; padding: 30px; border-radius: 10px;
291
+ text-align: center; font-size: 18px; box-shadow: 0 0 15px rgba(0,0,0,0.3);
292
+ ">
293
+ <p style="color: red; margin-bottom: 20px;">
294
+ ⚠️ Please paint a mask on both the background image and the reference image first, or click an example!
295
+ </p>
296
+ <button onclick="this.closest('.error-overlay').remove()"
297
+ style="padding: 8px 20px; cursor: pointer; border: none;
298
+ background: #eee; border-radius: 5px;">
299
+ OK
300
+ </button>
301
+ </div>
302
+ </div>
303
+ """
304
+ return None, gr.update(value=error_html, visible=True)
305
+
306
+ background_image, _, crop_box, source_mask_cropped = process_source(pil_bg, pil_mask, CANVAS_W)
307
+ ref_image = process_reference(pil_ref, pil_ref_mask, CANVAS_W)
308
+
309
+ generated_image = pipe(
310
+ PROMPT,
311
+ edit_image=[background_image, ref_image],
312
+ seed=int(seed),
313
+ rand_device="cuda",
314
+ num_inference_steps=int(num_inference_steps),
315
+ cfg_scale=float(cfg_scale),
316
+ height=CANVAS_W,
317
+ width=CANVAS_H,
318
+ )
319
+ result_img = paste_back(generated_image, pil_bg, crop_box, source_mask_cropped, feather=0)
320
+
321
+ return result_img, gr.update(visible=False)
322
+
323
+
324
+ # ===================== Gradio UI =====================
325
+
326
+ with gr.Blocks(css="""
327
+ .input-row {
328
+ overflow: visible !important;
329
+ }
330
+
331
+ .input-row .gr-image-editor {
332
+ overflow: hidden !important;
333
+ }
334
+ .input-row .gr-image-editor .image-container,
335
+ .input-row .gr-image-editor .canvas-container,
336
+ .input-row .gr-image-editor canvas {
337
+ max-width: 100% !important;
338
+ max-height: 100% !important;
339
+ object-fit: contain !important;
340
+ }
341
+
342
+ .ex-section-header {
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 10px;
346
+ margin: 28px 0 14px 0;
347
+ justify-content: center;
348
+ }
349
+ .ex-section-header::before {
350
+ content: '';
351
+ flex: 1;
352
+ height: 1px;
353
+ max-width: 180px;
354
+ background: #e5e7eb;
355
+ }
356
+ .ex-section-header::after {
357
+ content: '';
358
+ flex: 1;
359
+ height: 1px;
360
+ max-width: 180px;
361
+ background: #e5e7eb;
362
+ }
363
+
364
+ .ex-container {
365
+ display: flex;
366
+ flex-direction: column;
367
+ align-items: center;
368
+ padding-bottom: 20px;
369
+ }
370
+
371
+ .ex-grid {
372
+ display: flex;
373
+ flex-direction: column;
374
+ align-items: center;
375
+ gap: 12px;
376
+ padding-bottom: 20px;
377
+ }
378
+ .ex-grid-row {
379
+ display: flex;
380
+ gap: 20px;
381
+ justify-content: center;
382
+ flex-wrap: wrap;
383
+ }
384
+
385
+ .ex-row {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 20px;
389
+ padding: 14px 28px;
390
+ border: 2px solid #e5e7eb;
391
+ border-radius: 12px;
392
+ cursor: pointer;
393
+ transition: all 0.25s ease;
394
+ background: #ffffff;
395
+ user-select: none;
396
+ width: fit-content;
397
+ }
398
+ .ex-row:hover {
399
+ border-color: #3b82f6;
400
+ background: #f0f7ff;
401
+ box-shadow: 0 4px 18px rgba(59, 130, 246, 0.15);
402
+ transform: translateY(-2px);
403
+ }
404
+ .ex-row:active {
405
+ transform: translateY(0);
406
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
407
+ }
408
+
409
+ .ex-label {
410
+ font-weight: 700;
411
+ font-size: 15px;
412
+ min-width: 62px;
413
+ color: #1e40af;
414
+ letter-spacing: 0.02em;
415
+ }
416
+
417
+ .ex-thumbs {
418
+ display: flex;
419
+ gap: 14px;
420
+ }
421
+
422
+ .ex-thumb-wrap {
423
+ display: flex;
424
+ flex-direction: column;
425
+ align-items: center;
426
+ gap: 6px;
427
+ }
428
+
429
+ .ex-thumb {
430
+ width: 110px;
431
+ height: auto;
432
+ object-fit: contain;
433
+ border-radius: 8px;
434
+ border: 2px solid #e5e7eb;
435
+ transition: all 0.25s ease;
436
+ pointer-events: none;
437
+ }
438
+ .ex-row:hover .ex-thumb {
439
+ border-color: #93c5fd;
440
+ }
441
+
442
+ .ex-thumb-sublabel {
443
+ font-size: 12px;
444
+ color: #6b7280;
445
+ font-weight: 500;
446
+ }
447
+
448
+ /* Gradio >=5 does not mount components with visible=False into the DOM,
449
+ so the example-thumbnail JS cannot find them. Hide via CSS instead:
450
+ the buttons stay mounted (click handlers still fire) but invisible. */
451
+ .ex-hidden-btn {
452
+ display: none !important;
453
+ }
454
+ """) as demo:
455
+
456
+ gr.Markdown(
457
+ "<h1 style='text-align: center;'>Reference-Based Object Insertion & Clothing Replacement</h1>"
458
+ "<h3 style='text-align: center;'>Insert an object from the reference image into the masked region of the background image, or replace the clothing in the masked region with the reference</h3>"
459
+ "<p style='text-align: center;'>"
460
+ "<a href='https://github.com/huan-yin/Easy-Insert' target='_blank' "
461
+ "style='color: #3b82f6; text-decoration: none; font-weight: 500;'>"
462
+ "GitHub Repo: huan-yin/Easy-Insert</a></p>"
463
+ )
464
+ gr.Markdown(
465
+ """
466
+ **Instructions:**
467
+ 1. Upload a background image and use the brush/eraser tools below it to mark or modify the insertion region (mask); upload a reference image and paint over the object you want to insert (reference mask).
468
+ 2. Or click any thumbnail row in the "Examples" section below to automatically load the background image + mask + reference image + reference mask.
469
+ 3. Click the "Generate" button; the result will be shown below.
470
+ """
471
+ )
472
+
473
+ with gr.Row(elem_classes="input-row"):
474
+ base = gr.ImageEditor(
475
+ label="Background Image (paint the insertion region)",
476
+ type="pil",
477
+ format="png",
478
+ width=420,
479
+ height=450,
480
+ sources=["upload"],
481
+ canvas_size=(EDITOR_SIZE, EDITOR_SIZE),
482
+ fixed_canvas=True,
483
+ brush=gr.Brush(
484
+ default_size=30,
485
+ default_color="rgba(255, 255, 255, 0.6)",
486
+ color_mode="fixed",
487
+ colors=["rgba(255, 255, 255, 0.6)"],
488
+ ),
489
+ eraser=gr.Eraser(default_size=30),
490
+ )
491
+ ref = gr.ImageEditor(
492
+ label="Reference Image (paint the object to insert)",
493
+ type="pil",
494
+ format="png",
495
+ width=420,
496
+ height=450,
497
+ sources=["upload"],
498
+ canvas_size=(EDITOR_SIZE, EDITOR_SIZE),
499
+ fixed_canvas=True,
500
+ brush=gr.Brush(
501
+ default_size=30,
502
+ default_color="rgba(255, 255, 255, 0.6)",
503
+ color_mode="fixed",
504
+ colors=["rgba(255, 255, 255, 0.6)"],
505
+ ),
506
+ eraser=gr.Eraser(default_size=30),
507
+ )
508
+
509
+ with gr.Row():
510
+ seed = gr.Number(label="Random Seed", value=1, precision=0)
511
+ num_inference_steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=15)
512
+ cfg_scale = gr.Slider(label="CFG Scale", minimum=1, maximum=10, step=0.1, value=4)
513
+
514
+ with gr.Row():
515
+ gen_btn = gr.Button("Generate", variant="primary")
516
+
517
+ # ==================== Generation Result ====================
518
+ with gr.Row():
519
+ output_image = gr.Image(
520
+ label="Generated Result",
521
+ interactive=False,
522
+ width=420,
523
+ height=450,
524
+ )
525
+
526
+ with gr.Row():
527
+ error_dialog = gr.HTML(visible=False)
528
+
529
+ gr.HTML('<div class="ex-section-header"><span style="font-weight:700;font-size:16px;color:#374151;">Examples (click to load background image + mask + reference image + reference mask)</span></div>')
530
+
531
+ base_state = gr.State(None)
532
+ ref_state = gr.State(None)
533
+
534
+ ex_btn0 = gr.Button("Example 1", elem_id="ex_btn_0", elem_classes=["ex-hidden-btn"])
535
+ ex_btn1 = gr.Button("Example 2", elem_id="ex_btn_1", elem_classes=["ex-hidden-btn"])
536
+ ex_btn2 = gr.Button("Example 3", elem_id="ex_btn_2", elem_classes=["ex-hidden-btn"])
537
+ ex_btn3 = gr.Button("Example 4", elem_id="ex_btn_3", elem_classes=["ex-hidden-btn"])
538
+
539
+ gr.HTML('<div class="ex-container">' + build_examples_html() + '</div>')
540
+
541
+ # ==================== Event Bindings ====================
542
+
543
+ # Single-step load: a clear-then-load chain sends the frontend two rapid
544
+ # updates, and the upload round-trip in between references temp PNGs that
545
+ # get overwritten mid-read (truncated/broken PNG errors on preprocess).
546
+ ex_btn0.click(fn=load_ex1, outputs=[base, ref, base_state, ref_state])
547
+ ex_btn1.click(fn=load_ex2, outputs=[base, ref, base_state, ref_state])
548
+ ex_btn2.click(fn=load_ex3, outputs=[base, ref, base_state, ref_state])
549
+ ex_btn3.click(fn=load_ex4, outputs=[base, ref, base_state, ref_state])
550
+
551
+ gen_btn.click(
552
+ fn=run_local,
553
+ inputs=[base, ref, base_state, ref_state, seed, num_inference_steps, cfg_scale],
554
+ outputs=[output_image, error_dialog],
555
+ )
556
+
557
+ demo.launch(server_name="0.0.0.0", server_port=7860)
558
+
559
+
560
+
561
+
examples/background_image/1.png ADDED

Git LFS Details

  • SHA256: 21df8ce91664f6bb0e22187a79e0b131b4f12ea858b8bd4293396a05b3194850
  • Pointer size: 132 Bytes
  • Size of remote file: 1.35 MB
examples/background_image/2.png ADDED

Git LFS Details

  • SHA256: e839affc11c9855f35a5523382334b90b3e36f86e85156c215d233eccbc45199
  • Pointer size: 132 Bytes
  • Size of remote file: 3.16 MB
examples/background_image/3.png ADDED

Git LFS Details

  • SHA256: bf7377a8a1e7318cbdf50600a366233fe6fc01acc9cf1d4aedf681fa71bcc9b7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.8 MB
examples/background_image/4.png ADDED

Git LFS Details

  • SHA256: 19c8068d08e91bd636ce3c1713ec6dabaaf9e049c0065ebdedbab26f010f2cd8
  • Pointer size: 132 Bytes
  • Size of remote file: 2.32 MB
examples/insert_mask/1.png ADDED
examples/insert_mask/2.png ADDED
examples/insert_mask/3.png ADDED
examples/insert_mask/4.png ADDED
examples/ref_image/1.png ADDED

Git LFS Details

  • SHA256: 0e7cfebf8faa713c56659fd9863cf1ced78f2328c860338251096d92bb32f5cb
  • Pointer size: 132 Bytes
  • Size of remote file: 1.27 MB
examples/ref_image/2.png ADDED

Git LFS Details

  • SHA256: cae7ddf4316a0ef71fcdcc1c042b151ba68e95ee0d8283c21532cca895098380
  • Pointer size: 132 Bytes
  • Size of remote file: 1.16 MB
examples/ref_image/3.png ADDED

Git LFS Details

  • SHA256: 2f0413ed395e1c88e1fcd80e3f0fe749a2a0addaa8486448649750a853d26634
  • Pointer size: 131 Bytes
  • Size of remote file: 392 kB
examples/ref_image/4.png ADDED

Git LFS Details

  • SHA256: 4585a034a19fe2dc9afb3b44acfd74dfb4ccc1044efbbce26ac33fb4985dcadb
  • Pointer size: 132 Bytes
  • Size of remote file: 2.24 MB
examples/ref_mask/1.png ADDED
examples/ref_mask/2.png ADDED
examples/ref_mask/3.png ADDED
examples/ref_mask/4.png ADDED
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ diffsynth==2.1.2
2
+ transformers
3
+ accelerate
4
+ peft
5
+ safetensors
6
+ sentencepiece
7
+ torchvision
8
+ Pillow
9
+ numpy
10
+ gradio==6.24.0