Gemini899 commited on
Commit
73761ae
·
verified ·
1 Parent(s): 71f98c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -447
app.py CHANGED
@@ -1,465 +1,206 @@
 
 
 
1
  import gradio as gr
2
  import spaces
3
- import torch
4
- from diffusers import AutoencoderKL, TCDScheduler
5
- from diffusers.models.model_loading_utils import load_state_dict
6
- from gradio_imageslider import ImageSlider
7
- from huggingface_hub import hf_hub_download
8
-
9
- from controlnet_union import ControlNetModel_Union
10
- from pipeline_fill_sd_xl import StableDiffusionXLFillPipeline
11
-
12
  from PIL import Image, ImageDraw
13
- import numpy as np
14
-
15
- config_file = hf_hub_download(
16
- "xinsir/controlnet-union-sdxl-1.0",
17
- filename="config_promax.json",
18
- )
19
 
20
- config = ControlNetModel_Union.load_config(config_file)
21
- controlnet_model = ControlNetModel_Union.from_config(config)
22
 
23
- # Load the state dictionary
24
- model_file = hf_hub_download(
25
- "xinsir/controlnet-union-sdxl-1.0",
26
- filename="diffusion_pytorch_model_promax.safetensors",
27
- )
28
- state_dict = load_state_dict(model_file)
29
 
30
- # Extract the keys from the state_dict
31
- loaded_keys = list(state_dict.keys())
32
 
33
- # Call the method and store all returns in a variable
34
- result = ControlNetModel_Union._load_pretrained_model(
35
- controlnet_model, state_dict, model_file, "xinsir/controlnet-union-sdxl-1.0", loaded_keys
 
36
  )
37
-
38
- # Use the first element from the result
39
- model = result[0]
40
- model = model.to(device="cuda", dtype=torch.float16)
41
-
42
-
43
- vae = AutoencoderKL.from_pretrained(
44
- "madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16
45
- ).to("cuda")
46
-
47
- pipe = StableDiffusionXLFillPipeline.from_pretrained(
48
- "SG161222/RealVisXL_V5.0_Lightning",
49
- torch_dtype=torch.float16,
50
- vae=vae,
51
- controlnet=model,
52
- variant="fp16",
53
- ).to("cuda")
54
-
55
- pipe.scheduler = TCDScheduler.from_config(pipe.scheduler.config)
56
-
57
-
58
- def can_expand(source_width, source_height, target_width, target_height, alignment):
59
- """Checks if the image can be expanded based on the alignment."""
60
- if alignment in ("Left", "Right") and source_width >= target_width:
61
- return False
62
- if alignment in ("Top", "Bottom") and source_height >= target_height:
63
- return False
64
- return True
65
-
66
- def prepare_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
67
- target_size = (width, height)
68
-
69
- # Calculate the scaling factor to fit the image within the target size
70
- scale_factor = min(target_size[0] / image.width, target_size[1] / image.height)
71
- new_width = int(image.width * scale_factor)
72
- new_height = int(image.height * scale_factor)
73
-
74
- # Resize the source image to fit within target size
75
- source = image.resize((new_width, new_height), Image.LANCZOS)
76
-
77
- # Apply resize option using percentages
78
- if resize_option == "Full":
79
- resize_percentage = 100
80
- elif resize_option == "50%":
81
- resize_percentage = 50
82
- elif resize_option == "33%":
83
- resize_percentage = 33
84
- elif resize_option == "25%":
85
- resize_percentage = 25
86
- else: # Custom
87
- resize_percentage = custom_resize_percentage
88
-
89
- # Calculate new dimensions based on percentage
90
- resize_factor = resize_percentage / 100
91
- new_width = int(source.width * resize_factor)
92
- new_height = int(source.height * resize_factor)
93
-
94
- # Ensure minimum size of 64 pixels
95
- new_width = max(new_width, 64)
96
- new_height = max(new_height, 64)
97
-
98
- # Resize the image
99
- source = source.resize((new_width, new_height), Image.LANCZOS)
100
-
101
- # Calculate the overlap in pixels based on the percentage
102
- overlap_x = int(new_width * (overlap_percentage / 100))
103
- overlap_y = int(new_height * (overlap_percentage / 100))
104
-
105
- # Ensure minimum overlap of 1 pixel
106
- overlap_x = max(overlap_x, 1)
107
- overlap_y = max(overlap_y, 1)
108
-
109
- # Calculate margins based on alignment
110
- if alignment == "Middle":
111
- margin_x = (target_size[0] - new_width) // 2
112
- margin_y = (target_size[1] - new_height) // 2
113
- elif alignment == "Left":
114
- margin_x = 0
115
- margin_y = (target_size[1] - new_height) // 2
116
- elif alignment == "Right":
117
- margin_x = target_size[0] - new_width
118
- margin_y = (target_size[1] - new_height) // 2
119
- elif alignment == "Top":
120
- margin_x = (target_size[0] - new_width) // 2
121
- margin_y = 0
122
- elif alignment == "Bottom":
123
- margin_x = (target_size[0] - new_width) // 2
124
- margin_y = target_size[1] - new_height
125
-
126
- # Adjust margins to eliminate gaps
127
- margin_x = max(0, min(margin_x, target_size[0] - new_width))
128
- margin_y = max(0, min(margin_y, target_size[1] - new_height))
129
-
130
- # Create a new background image and paste the resized source image
131
- background = Image.new('RGB', target_size, (255, 255, 255))
132
- background.paste(source, (margin_x, margin_y))
133
-
134
- # Create the mask
135
- mask = Image.new('L', target_size, 255)
136
- mask_draw = ImageDraw.Draw(mask)
137
-
138
- # Calculate overlap areas
139
- white_gaps_patch = 2
140
-
141
- left_overlap = margin_x + overlap_x if overlap_left else margin_x + white_gaps_patch
142
- right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width - white_gaps_patch
143
- top_overlap = margin_y + overlap_y if overlap_top else margin_y + white_gaps_patch
144
- bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height - white_gaps_patch
145
-
146
  if alignment == "Left":
147
- left_overlap = margin_x + overlap_x if overlap_left else margin_x
 
148
  elif alignment == "Right":
149
- right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width
 
150
  elif alignment == "Top":
151
- top_overlap = margin_y + overlap_y if overlap_top else margin_y
 
152
  elif alignment == "Bottom":
153
- bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height
154
-
155
-
156
- # Draw the mask
157
- mask_draw.rectangle([
158
- (left_overlap, top_overlap),
159
- (right_overlap, bottom_overlap)
160
- ], fill=0)
161
-
162
- return background, mask
163
-
164
- def preview_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
165
- background, mask = prepare_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom)
166
-
167
- # Create a preview image showing the mask
168
- preview = background.copy().convert('RGBA')
169
-
170
- # Create a semi-transparent red overlay
171
- red_overlay = Image.new('RGBA', background.size, (255, 0, 0, 64)) # Reduced alpha to 64 (25% opacity)
172
-
173
- # Convert black pixels in the mask to semi-transparent red
174
- red_mask = Image.new('RGBA', background.size, (0, 0, 0, 0))
175
- red_mask.paste(red_overlay, (0, 0), mask)
176
-
177
- # Overlay the red mask on the background
178
- preview = Image.alpha_composite(preview, red_mask)
179
-
180
- return preview
181
-
182
- @spaces.GPU(duration=24)
183
- def infer(image, width, height, overlap_percentage, num_inference_steps, resize_option, custom_resize_percentage, prompt_input, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
184
- background, mask = prepare_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom)
185
-
186
- if not can_expand(background.width, background.height, width, height, alignment):
187
- alignment = "Middle"
188
-
189
- cnet_image = background.copy()
190
- cnet_image.paste(0, (0, 0), mask)
191
-
192
- final_prompt = f"{prompt_input} , high quality, 4k" if prompt_input else "high quality, 4k"
193
-
194
- # Use with torch.autocast to ensure consistent dtype
195
- with torch.autocast(device_type="cuda", dtype=torch.float16):
196
- (
197
- prompt_embeds,
198
- negative_prompt_embeds,
199
- pooled_prompt_embeds,
200
- negative_pooled_prompt_embeds,
201
- ) = pipe.encode_prompt(final_prompt, "cuda", True)
202
-
203
- for image in pipe(
204
- prompt_embeds=prompt_embeds,
205
- negative_prompt_embeds=negative_prompt_embeds,
206
- pooled_prompt_embeds=pooled_prompt_embeds,
207
- negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
208
- image=cnet_image,
209
- num_inference_steps=num_inference_steps
210
- ):
211
- yield cnet_image, image
212
-
213
- image = image.convert("RGBA")
214
- cnet_image.paste(image, (0, 0), mask)
215
-
216
- yield background, cnet_image
217
-
218
- def clear_result():
219
- """Clears the result ImageSlider."""
220
- return gr.update(value=None)
221
-
222
- def preload_presets(target_ratio, ui_width, ui_height):
223
- """Updates the width and height sliders based on the selected aspect ratio."""
224
- if target_ratio == "9:16":
225
- changed_width = 720
226
- changed_height = 1280
227
- return changed_width, changed_height, gr.update()
228
- elif target_ratio == "16:9":
229
- changed_width = 1280
230
- changed_height = 720
231
- return changed_width, changed_height, gr.update()
232
- elif target_ratio == "1:1":
233
- changed_width = 1024
234
- changed_height = 1024
235
- return changed_width, changed_height, gr.update()
236
- elif target_ratio == "Custom":
237
- return ui_width, ui_height, gr.update(open=True)
238
-
239
- def select_the_right_preset(user_width, user_height):
240
- if user_width == 720 and user_height == 1280:
241
- return "9:16"
242
- elif user_width == 1280 and user_height == 720:
243
- return "16:9"
244
- elif user_width == 1024 and user_height == 1024:
245
- return "1:1"
246
- else:
247
- return "Custom"
248
-
249
- def toggle_custom_resize_slider(resize_option):
250
- return gr.update(visible=(resize_option == "Custom"))
251
-
252
- def update_history(new_image, history):
253
- """Updates the history gallery with the new image."""
254
- if history is None:
255
- history = []
256
- history.insert(0, new_image)
257
- return history
258
-
259
- css = """
260
- .gradio-container {
261
- width: 1200px !important;
262
- }
263
- """
264
-
265
- # Define the title HTML string
266
- title = """<h1 align="center">Re-Size Image Outpaint</h1>
267
- """
268
-
269
- with gr.Blocks(theme="soft", css=css) as demo:
270
- with gr.Column():
271
- gr.HTML(title)
272
-
273
- with gr.Row():
274
- with gr.Column():
275
- input_image = gr.Image(
276
- type="pil",
277
- label="Input Image"
278
- )
279
-
280
- with gr.Row():
281
- with gr.Column(scale=2):
282
- prompt_input = gr.Textbox(label="Prompt (Optional)")
283
- with gr.Column(scale=1):
284
- run_button = gr.Button("Generate")
285
-
286
- with gr.Row():
287
- target_ratio = gr.Radio(
288
- label="Expected Ratio",
289
- choices=["9:16", "16:9", "1:1", "Custom"],
290
- value="9:16",
291
- scale=2
292
- )
293
-
294
- alignment_dropdown = gr.Dropdown(
295
- choices=["Middle", "Left", "Right", "Top", "Bottom"],
296
- value="Middle",
297
- label="Alignment"
298
- )
299
-
300
- with gr.Accordion(label="Advanced settings", open=False) as settings_panel:
301
- with gr.Column():
302
- with gr.Row():
303
- width_slider = gr.Slider(
304
- label="Target Width",
305
- minimum=720,
306
- maximum=1536,
307
- step=8,
308
- value=720,
309
- )
310
- height_slider = gr.Slider(
311
- label="Target Height",
312
- minimum=720,
313
- maximum=1536,
314
- step=8,
315
- value=1280,
316
- )
317
-
318
- num_inference_steps = gr.Slider(label="Steps", minimum=4, maximum=12, step=1, value=8)
319
- with gr.Group():
320
- overlap_percentage = gr.Slider(
321
- label="Mask overlap (%)",
322
- minimum=1,
323
- maximum=50,
324
- value=10,
325
- step=1
326
- )
327
- with gr.Row():
328
- overlap_top = gr.Checkbox(label="Overlap Top", value=True)
329
- overlap_right = gr.Checkbox(label="Overlap Right", value=True)
330
- with gr.Row():
331
- overlap_left = gr.Checkbox(label="Overlap Left", value=True)
332
- overlap_bottom = gr.Checkbox(label="Overlap Bottom", value=True)
333
- with gr.Row():
334
- resize_option = gr.Radio(
335
- label="Resize input image",
336
- choices=["Full", "50%", "33%", "25%", "Custom"],
337
- value="Full"
338
- )
339
- custom_resize_percentage = gr.Slider(
340
- label="Custom resize (%)",
341
- minimum=1,
342
- maximum=100,
343
- step=1,
344
- value=50,
345
- visible=False
346
- )
347
-
348
- with gr.Column():
349
- preview_button = gr.Button("Preview alignment and mask")
350
-
351
-
352
- gr.Examples(
353
- examples=[
354
- ["./examples/example_2.jpg", 1440, 810, "Left"],
355
- ["./examples/example_3.jpg", 1024, 1024, "Top"],
356
- ["./examples/example_3.jpg", 1024, 1024, "Bottom"],
357
- ],
358
- inputs=[input_image, width_slider, height_slider, alignment_dropdown],
359
- )
360
-
361
-
362
-
363
- with gr.Column():
364
- result = ImageSlider(
365
- interactive=False,
366
- label="Generated Image",
367
- )
368
- use_as_input_button = gr.Button("Use as Input Image", visible=False)
369
-
370
- history_gallery = gr.Gallery(label="History", columns=6, object_fit="contain", interactive=False)
371
- preview_image = gr.Image(label="Preview")
372
-
373
-
374
-
375
- def use_output_as_input(output_image):
376
- """Sets the generated output as the new input image."""
377
- return gr.update(value=output_image[1])
378
-
379
- use_as_input_button.click(
380
- fn=use_output_as_input,
381
- inputs=[result],
382
- outputs=[input_image]
383
- )
384
-
385
- target_ratio.change(
386
- fn=preload_presets,
387
- inputs=[target_ratio, width_slider, height_slider],
388
- outputs=[width_slider, height_slider, settings_panel],
389
- queue=False
390
- )
391
-
392
- width_slider.change(
393
- fn=select_the_right_preset,
394
- inputs=[width_slider, height_slider],
395
- outputs=[target_ratio],
396
- queue=False
397
- )
398
-
399
- height_slider.change(
400
- fn=select_the_right_preset,
401
- inputs=[width_slider, height_slider],
402
- outputs=[target_ratio],
403
- queue=False
404
- )
405
-
406
- resize_option.change(
407
- fn=toggle_custom_resize_slider,
408
- inputs=[resize_option],
409
- outputs=[custom_resize_percentage],
410
- queue=False
411
- )
412
-
413
- run_button.click(
414
- fn=clear_result,
415
- inputs=None,
416
- outputs=result,
417
- ).then(
418
- fn=infer,
419
- inputs=[input_image, width_slider, height_slider, overlap_percentage, num_inference_steps,
420
- resize_option, custom_resize_percentage, prompt_input, alignment_dropdown,
421
- overlap_left, overlap_right, overlap_top, overlap_bottom],
422
- outputs=result,
423
- ).then(
424
- # --- FIX APPLIED HERE ---
425
- # Safely update history only if the result (x) is not None.
426
- fn=lambda x, history: update_history(x[1], history) if x else history,
427
- inputs=[result, history_gallery],
428
- outputs=history_gallery,
429
- ).then(
430
- fn=lambda: gr.update(visible=True),
431
- inputs=None,
432
- outputs=use_as_input_button,
433
  )
434
 
435
- prompt_input.submit(
436
- fn=clear_result,
437
- inputs=None,
438
- outputs=result,
439
- ).then(
440
- fn=infer,
441
- inputs=[input_image, width_slider, height_slider, overlap_percentage, num_inference_steps,
442
- resize_option, custom_resize_percentage, prompt_input, alignment_dropdown,
443
- overlap_left, overlap_right, overlap_top, overlap_bottom],
444
- outputs=result,
445
- ).then(
446
- # --- FIX APPLIED HERE ---
447
- # Safely update history only if the result (x) is not None.
448
- fn=lambda x, history: update_history(x[1], history) if x else history,
449
- inputs=[result, history_gallery],
450
- outputs=history_gallery,
451
- ).then(
452
- fn=lambda: gr.update(visible=True),
453
- inputs=None,
454
- outputs=use_as_input_button,
455
  )
456
 
457
- preview_button.click(
458
- fn=preview_image_and_mask,
459
- inputs=[input_image, width_slider, height_slider, overlap_percentage, resize_option, custom_resize_percentage, alignment_dropdown,
460
- overlap_left, overlap_right, overlap_top, overlap_bottom],
461
- outputs=preview_image,
462
- queue=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  )
464
 
465
- demo.queue(max_size=12).launch(share=False)
 
 
1
+ import os
2
+ import io
3
+ import math
4
  import gradio as gr
5
  import spaces
 
 
 
 
 
 
 
 
 
6
  from PIL import Image, ImageDraw
7
+ import torch
 
 
 
 
 
8
 
9
+ from diffusers import StableDiffusionXLInpaintPipeline
 
10
 
11
+ # ====== Runtime / device config ======
12
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
13
+ DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
 
 
 
14
 
15
+ # (You can swap the model to your preferred SDXL inpainting one)
16
+ INPAINT_MODEL_ID = "diffusers/stable-diffusion-xl-1.0-inpainting-0.1"
17
 
18
+ # Load once at startup
19
+ pipe = StableDiffusionXLInpaintPipeline.from_pretrained(
20
+ INPAINT_MODEL_ID,
21
+ torch_dtype=DTYPE,
22
  )
23
+ if DEVICE == "cuda":
24
+ pipe = pipe.to("cuda")
25
+ else:
26
+ pipe = pipe.to("cpu")
27
+
28
+ # Try to enable xformers if available
29
+ try:
30
+ pipe.enable_xformers_memory_efficient_attention()
31
+ except Exception:
32
+ pass
33
+
34
+ # ====== Helpers ======
35
+
36
+ def _place_rect(canvas_w, canvas_h, img_w, img_h, alignment: str):
37
+ """
38
+ Return (x, y) top-left for placing the original image onto the new canvas
39
+ based on alignment: Middle | Left | Right | Top | Bottom
40
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  if alignment == "Left":
42
+ x = 0
43
+ y = (canvas_h - img_h) // 2
44
  elif alignment == "Right":
45
+ x = canvas_w - img_w
46
+ y = (canvas_h - img_h) // 2
47
  elif alignment == "Top":
48
+ x = (canvas_w - img_w) // 2
49
+ y = 0
50
  elif alignment == "Bottom":
51
+ x = (canvas_w - img_w) // 2
52
+ y = canvas_h - img_h
53
+ else: # "Middle"
54
+ x = (canvas_w - img_w) // 2
55
+ y = (canvas_h - img_h) // 2
56
+ return x, y
57
+
58
+ def _make_outpaint_canvas_and_mask(
59
+ image: Image.Image,
60
+ target_w: int,
61
+ target_h: int,
62
+ alignment: str = "Middle",
63
+ overlap_pct: float = 10.0,
64
+ ) -> tuple[Image.Image, Image.Image]:
65
+ """
66
+ Create a new canvas (RGB) with the original image placed according to alignment,
67
+ and a mask (L) where WHITE = areas to generate, BLACK = keep original.
68
+
69
+ overlap_pct controls how much of the original border we ALSO inpaint (for blending).
70
+ """
71
+ if image.mode != "RGB":
72
+ image = image.convert("RGB")
73
+
74
+ ow, oh = image.size
75
+ cw, ch = int(target_w), int(target_h)
76
+ if cw < ow or ch < oh:
77
+ # If target is smaller than input, pad to at least input size
78
+ cw = max(cw, ow)
79
+ ch = max(ch, oh)
80
+
81
+ # Create base canvas (start from the original pasted on a flat color)
82
+ base = Image.new("RGB", (cw, ch), (0, 0, 0))
83
+ x, y = _place_rect(cw, ch, ow, oh, alignment)
84
+ base.paste(image, (x, y))
85
+
86
+ # Create mask = white everywhere EXCEPT a slightly smaller black rectangle where we keep the original
87
+ mask = Image.new("L", (cw, ch), 255) # WHITE
88
+ draw = ImageDraw.Draw(mask)
89
+
90
+ # overlap amount (pixels) how much we want to overlap into the original for blending
91
+ margin = int(min(ow, oh) * (float(overlap_pct) / 100.0))
92
+ # Keep a safe bound
93
+ margin = max(0, min(margin, min(ow, oh) // 3))
94
+
95
+ left = x + margin
96
+ top = y + margin
97
+ right = x + ow - margin
98
+ bottom = y + oh - margin
99
+
100
+ # BLACK = "keep" region
101
+ draw.rectangle([left, top, right, bottom], fill=0)
102
+
103
+ return base, mask
104
+
105
+ def _adjust_to_multiple_of_8(w: int, h: int):
106
+ # SDXL can handle many sizes, but multiples of 8 are a safe bet.
107
+ return (w - (w % 8), h - (h % 8))
108
+
109
+ # ====== Core API function (SINGLE IMAGE OUT) ======
110
+ @spaces.GPU(duration=60)
111
+ def process_images(
112
+ image: Image.Image,
113
+ prompt: str = "",
114
+ strength: float = 0.75, # kept for client parity; not used by SDXL-inpaint
115
+ seed: int = 0,
116
+ inference_step: int = 8,
117
+ width: int = 720,
118
+ height: int = 1280,
119
+ overlap_percentage: float = 10.0,
120
+ alignment: str = "Middle", # Literal['Middle','Left','Right','Top','Bottom']
121
+ ):
122
+ """
123
+ This exactly mirrors your img2img "process_images" style:
124
+ - Accepts a single 'image' file input via handle_file(...)
125
+ - Returns a single PIL image (gradio will convert to a temp file on disk)
126
+ - Named parameters closely match your working client, plus expansion params.
127
+ """
128
+ if image is None:
129
+ return None
130
+
131
+ width, height = _adjust_to_multiple_of_8(int(width), int(height))
132
+ base, mask = _make_outpaint_canvas_and_mask(
133
+ image=image,
134
+ target_w=width,
135
+ target_h=height,
136
+ alignment=alignment,
137
+ overlap_pct=float(overlap_percentage),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  )
139
 
140
+ # Seed handling
141
+ if seed is None:
142
+ seed = 0
143
+ generator = torch.Generator(device=DEVICE)
144
+ if seed != 0:
145
+ generator = generator.manual_seed(int(seed))
146
+
147
+ # Reasonable defaults for SDXL inpainting
148
+ guidance_scale = 3.5
149
+
150
+ # Run the model
151
+ result = pipe(
152
+ prompt=prompt or "",
153
+ image=base,
154
+ mask_image=mask,
155
+ guidance_scale=guidance_scale,
156
+ num_inference_steps=int(inference_step),
157
+ generator=generator,
 
 
158
  )
159
 
160
+ out = result.images[0]
161
+ return out
162
+
163
+ # ====== UI (kept simple but fully functional) ======
164
+ with gr.Blocks(css="""
165
+ #col { max-width: 900px; margin: 0 auto; }
166
+ """) as demo:
167
+ gr.Markdown("## ReSize Image Outpainting — Unified `/process_images` API")
168
+
169
+ with gr.Row(elem_id="col"):
170
+ with gr.Column():
171
+ image = gr.Image(label="Input Image", type="pil", sources=["upload","clipboard"], height=430)
172
+ prompt = gr.Textbox(label="Prompt (optional)", placeholder="e.g., extend the background with soft bokeh")
173
+
174
+ with gr.Row():
175
+ width = gr.Number(value=720, label="Target Width", precision=0)
176
+ height = gr.Number(value=1280, label="Target Height", precision=0)
177
+
178
+ with gr.Row():
179
+ inference_step = gr.Number(value=8, label="Steps", precision=0)
180
+ overlap_percentage = gr.Slider(0, 30, value=10, step=1, label="Overlap (%) for blending")
181
+
182
+ alignment = gr.Dropdown(
183
+ choices=["Middle", "Left", "Right", "Top", "Bottom"],
184
+ value="Middle",
185
+ label="Alignment"
186
+ )
187
+
188
+ # kept only for client parity with your existing code
189
+ strength = gr.Slider(0.0, 1.0, value=0.75, step=0.01, label="(Compat) Strength (unused)")
190
+ seed = gr.Number(value=0, label="Seed (0 = random)", precision=0)
191
+
192
+ btn = gr.Button("Outpaint", variant="primary")
193
+
194
+ with gr.Column():
195
+ output = gr.Image(label="Output", height=430)
196
+
197
+ # Hook UI
198
+ btn.click(
199
+ fn=process_images,
200
+ inputs=[image, prompt, strength, seed, inference_step, width, height, overlap_percentage, alignment],
201
+ outputs=[output],
202
+ api_name="/process_images" # <-- IMPORTANT: unified API name
203
  )
204
 
205
+ if __name__ == "__main__":
206
+ demo.launch(show_error=True)