prithivMLmods commited on
Commit
03b41ea
·
verified ·
1 Parent(s): ce44242

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -193
app.py CHANGED
@@ -3,7 +3,8 @@ import spaces
3
  import torch
4
  from diffusers import AutoencoderKL, TCDScheduler
5
  from diffusers.models.model_loading_utils import load_state_dict
6
- # Removed: from gradio_imageslider import ImageSlider
 
7
  from huggingface_hub import hf_hub_download
8
 
9
  from controlnet_union import ControlNetModel_Union
@@ -12,7 +13,7 @@ from pipeline_fill_sd_xl import StableDiffusionXLFillPipeline
12
  from PIL import Image, ImageDraw
13
  import numpy as np
14
 
15
- # --- Model Loading (unchanged) ---
16
  config_file = hf_hub_download(
17
  "xinsir/controlnet-union-sdxl-1.0",
18
  filename="config_promax.json",
@@ -46,8 +47,7 @@ pipe = StableDiffusionXLFillPipeline.from_pretrained(
46
 
47
  pipe.scheduler = TCDScheduler.from_config(pipe.scheduler.config)
48
 
49
- # --- Helper Functions (unchanged, except infer) ---
50
-
51
  def can_expand(source_width, source_height, target_width, target_height, alignment):
52
  """Checks if the image can be expanded based on the alignment."""
53
  if alignment in ("Left", "Right") and source_width >= target_width:
@@ -129,28 +129,39 @@ def prepare_image_and_mask(image, width, height, overlap_percentage, resize_opti
129
  mask_draw = ImageDraw.Draw(mask)
130
 
131
  # Calculate overlap areas
132
- white_gaps_patch = 2
133
 
134
  left_overlap = margin_x + overlap_x if overlap_left else margin_x + white_gaps_patch
135
  right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width - white_gaps_patch
136
  top_overlap = margin_y + overlap_y if overlap_top else margin_y + white_gaps_patch
137
  bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height - white_gaps_patch
138
 
 
 
139
  if alignment == "Left":
140
- left_overlap = margin_x + overlap_x if overlap_left else margin_x
141
  elif alignment == "Right":
142
- right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width
143
  elif alignment == "Top":
144
- top_overlap = margin_y + overlap_y if overlap_top else margin_y
145
  elif alignment == "Bottom":
146
- bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height
 
 
 
 
 
 
147
 
 
 
 
 
 
 
148
 
149
- # Draw the mask
150
- mask_draw.rectangle([
151
- (left_overlap, top_overlap),
152
- (right_overlap, bottom_overlap)
153
- ], fill=0)
154
 
155
  return background, mask
156
 
@@ -160,15 +171,11 @@ def preview_image_and_mask(image, width, height, overlap_percentage, resize_opti
160
  # Create a preview image showing the mask
161
  preview = background.copy().convert('RGBA')
162
 
163
- # Create a semi-transparent red overlay
164
- red_overlay = Image.new('RGBA', background.size, (255, 0, 0, 64)) # Reduced alpha to 64 (25% opacity)
165
-
166
- # Convert black pixels in the mask to semi-transparent red
167
- red_mask = Image.new('RGBA', background.size, (0, 0, 0, 0))
168
- red_mask.paste(red_overlay, (0, 0), mask)
169
 
170
- # Overlay the red mask on the background
171
- preview = Image.alpha_composite(preview, red_mask)
172
 
173
  return preview
174
 
@@ -176,22 +183,29 @@ def preview_image_and_mask(image, width, height, overlap_percentage, resize_opti
176
  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):
177
  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)
178
 
179
- if not can_expand(background.width, background.height, width, height, alignment):
180
- alignment = "Middle" # Default to middle if expansion not possible with current alignment
181
-
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  cnet_image = background.copy()
183
- # Prepare the controlnet input image (original image with blacked-out mask area)
184
- # Note: The pipeline expects the original image content where the mask is 0 (black)
185
- # and the area to be filled where the mask is 255 (white).
186
- # However, the current pipeline_fill_sd_xl seems to use the mask differently internally.
187
- # Let's prepare the input image as per the original logic, which pastes black over the masked area.
188
- black_fill = Image.new('RGB', cnet_image.size, (0, 0, 0))
189
- # Invert the mask: white (255) becomes the area to keep, black (0) the area to fill
190
- inverted_mask = Image.eval(mask, lambda x: 255 - x)
191
- cnet_image.paste(black_fill, (0, 0), inverted_mask) # Paste black where the inverted mask is white (original mask was 0)
192
-
193
 
194
- final_prompt = f"{prompt_input} , high quality, 4k"
195
 
196
  (
197
  prompt_embeds,
@@ -200,62 +214,57 @@ def infer(image, width, height, overlap_percentage, num_inference_steps, resize_
200
  negative_pooled_prompt_embeds,
201
  ) = pipe.encode_prompt(final_prompt, "cuda", True)
202
 
203
- # Generate the image content for the masked area
204
- # The pipeline yields the generated content for the masked area
205
- # We only need the final result from the generator
206
- generated_content = None
207
- for res in pipe(
208
  prompt_embeds=prompt_embeds,
209
  negative_prompt_embeds=negative_prompt_embeds,
210
  pooled_prompt_embeds=pooled_prompt_embeds,
211
  negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
212
- image=cnet_image, # Pass the image with blacked-out area
213
- mask_image=mask, # Pass the mask (white = area to fill)
214
- num_inference_steps=num_inference_steps
215
- ):
216
- generated_content = res # Keep updating until the last step
217
-
218
- # The pipeline directly returns the final composite image in recent versions
219
- # If it returns only the filled part, we need to composite it
220
- # Let's assume the pipeline returns the final composited image based on its name "FillPipeline"
221
- final_image = generated_content
222
-
223
- # --- OLD compositing logic (keep commented in case pipeline behavior differs) ---
224
- # # Convert generated content to RGBA to handle potential transparency
225
- # generated_content = generated_content.convert("RGBA")
226
- # # Create the final composite image by pasting the generated content onto the background
227
- # final_image = background.copy().convert("RGBA")
228
- # # Paste the generated content using the original mask (white area = where to paste)
229
- # final_image.paste(generated_content, (0, 0), mask)
230
- # final_image = final_image.convert("RGB") # Convert back to RGB if needed
231
-
232
- # Yield only the final composited image
233
- yield final_image
234
 
235
 
236
  def clear_result():
237
- """Clears the result Image."""
238
  return gr.update(value=None)
239
 
 
240
  def preload_presets(target_ratio, ui_width, ui_height):
241
  """Updates the width and height sliders based on the selected aspect ratio."""
242
  if target_ratio == "9:16":
243
  changed_width = 720
244
  changed_height = 1280
245
- return changed_width, changed_height, gr.update()
246
  elif target_ratio == "16:9":
247
  changed_width = 1280
248
  changed_height = 720
249
- return changed_width, changed_height, gr.update()
250
  elif target_ratio == "1:1":
251
  changed_width = 1024
252
  changed_height = 1024
253
- return changed_width, changed_height, gr.update()
254
  elif target_ratio == "Custom":
255
- # When switching to custom, keep current slider values but open the accordion
256
  return ui_width, ui_height, gr.update(open=True)
257
 
258
  def select_the_right_preset(user_width, user_height):
 
259
  if user_width == 720 and user_height == 1280:
260
  return "9:16"
261
  elif user_width == 1280 and user_height == 720:
@@ -266,53 +275,65 @@ def select_the_right_preset(user_width, user_height):
266
  return "Custom"
267
 
268
  def toggle_custom_resize_slider(resize_option):
 
269
  return gr.update(visible=(resize_option == "Custom"))
270
 
271
  def update_history(new_image, history):
272
  """Updates the history gallery with the new image."""
273
  if history is None:
274
  history = []
275
- # Ensure new_image is a PIL Image before inserting
276
  if isinstance(new_image, Image.Image):
277
  history.insert(0, new_image)
278
- # Handle cases where the input might be None or not an image (e.g., during clearing)
279
- elif new_image is not None:
280
- print(f"Warning: Attempted to add non-image type to history: {type(new_image)}")
281
  return history
282
 
283
-
284
- # --- Gradio UI ---
285
  css = """
286
  .gradio-container {
287
  width: 1200px !important;
 
288
  }
289
  h1 { text-align: center; }
290
  footer { visibility: hidden; }
291
- """
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- title = """<h1 align="center">Diffusers Image Outpaint Lightning</h1>
294
  """
295
 
 
 
296
  with gr.Blocks(css=css) as demo:
297
  with gr.Column():
298
  gr.HTML(title)
299
 
300
  with gr.Row():
301
- with gr.Column():
302
  input_image = gr.Image(
303
  type="pil",
304
- label="Input Image"
 
305
  )
306
 
307
  with gr.Row():
308
  with gr.Column(scale=2):
309
- prompt_input = gr.Textbox(label="Prompt (Optional)")
310
  with gr.Column(scale=1):
311
- run_button = gr.Button("Generate")
312
 
313
  with gr.Row():
314
  target_ratio = gr.Radio(
315
- label="Expected Ratio",
316
  choices=["9:16", "16:9", "1:1", "Custom"],
317
  value="9:16",
318
  scale=2
@@ -321,130 +342,123 @@ with gr.Blocks(css=css) as demo:
321
  alignment_dropdown = gr.Dropdown(
322
  choices=["Middle", "Left", "Right", "Top", "Bottom"],
323
  value="Middle",
324
- label="Alignment"
325
  )
326
 
327
  with gr.Accordion(label="Advanced settings", open=False) as settings_panel:
328
- with gr.Column():
329
- with gr.Row():
330
- width_slider = gr.Slider(
331
- label="Target Width",
332
- minimum=720,
333
- maximum=1536,
334
- step=8,
335
- value=720, # Default for 9:16
336
- )
337
- height_slider = gr.Slider(
338
- label="Target Height",
339
- minimum=720,
340
- maximum=1536,
341
- step=8,
342
- value=1280, # Default for 9:16
343
- )
344
-
345
- num_inference_steps = gr.Slider(label="Steps", minimum=4, maximum=12, step=1, value=8)
346
- with gr.Group():
347
- overlap_percentage = gr.Slider(
348
- label="Mask overlap (%)",
349
- minimum=1,
350
- maximum=50,
351
- value=10,
352
- step=1
353
- )
354
- with gr.Row():
355
- overlap_top = gr.Checkbox(label="Overlap Top", value=True)
356
- overlap_right = gr.Checkbox(label="Overlap Right", value=True)
357
- with gr.Row(): # Changed nesting for better layout
358
- overlap_left = gr.Checkbox(label="Overlap Left", value=True)
359
- overlap_bottom = gr.Checkbox(label="Overlap Bottom", value=True)
360
  with gr.Row():
361
- resize_option = gr.Radio(
362
- label="Resize input image",
363
- choices=["Full", "50%", "33%", "25%", "Custom"],
364
- value="Full"
365
- )
366
- custom_resize_percentage = gr.Slider(
367
- label="Custom resize (%)",
368
- minimum=1,
369
- maximum=100,
370
- step=1,
371
- value=50,
372
- visible=False
373
- )
374
-
375
- with gr.Column(): # Keep preview button separate
376
- preview_button = gr.Button("Preview alignment and mask")
 
 
 
 
 
 
 
377
 
378
 
379
  gr.Examples(
380
  examples=[
381
- ["./examples/example_1.webp", 1280, 720, "Middle"],
382
- ["./examples/example_2.jpg", 1440, 810, "Left"],
383
- ["./examples/example_3.jpg", 1024, 1024, "Top"],
384
- ["./examples/example_3.jpg", 1024, 1024, "Bottom"],
 
385
  ],
386
- inputs=[input_image, width_slider, height_slider, alignment_dropdown],
387
- # Ensure examples don't try to set output components directly
388
- # outputs=[result], # Remove output mapping from examples
389
- # fn=infer, # Don't run infer on example click, just load inputs
390
  )
391
 
392
 
393
- with gr.Column():
394
- # *** MODIFICATION: Changed ImageSlider to Image ***
395
- result = gr.Image(label="Generated Image", interactive=False, type="pil")
396
- use_as_input_button = gr.Button("Use as Input Image", visible=False)
 
 
 
 
 
 
 
 
 
397
 
398
- history_gallery = gr.Gallery(label="History", columns=6, object_fit="contain", interactive=False, type="pil")
399
- preview_image = gr.Image(label="Preview", type="pil") # Ensure preview is also PIL
400
 
401
- # --- Event Handlers ---
402
 
403
  def use_output_as_input(output_image):
404
  """Sets the generated output as the new input image."""
405
- # *** MODIFICATION: Access the image directly, not output_image[1] ***
406
  return gr.update(value=output_image)
407
 
408
  use_as_input_button.click(
409
  fn=use_output_as_input,
410
- inputs=[result], # Input is the single result image
411
- outputs=[input_image]
412
  )
413
 
414
  target_ratio.change(
415
  fn=preload_presets,
416
  inputs=[target_ratio, width_slider, height_slider],
417
- outputs=[width_slider, height_slider, settings_panel],
418
  queue=False
419
  )
420
 
421
- # Link sliders change to update the ratio selection to "Custom"
422
  width_slider.change(
423
  fn=select_the_right_preset,
424
  inputs=[width_slider, height_slider],
425
  outputs=[target_ratio],
426
  queue=False
427
- ).then(
428
- fn=lambda: gr.update(open=True), # Also open accordion on slider change
429
- inputs=None,
430
- outputs=settings_panel,
431
- queue=False
432
  )
433
-
434
-
435
  height_slider.change(
436
  fn=select_the_right_preset,
437
  inputs=[width_slider, height_slider],
438
  outputs=[target_ratio],
439
  queue=False
440
- ).then(
441
- fn=lambda: gr.update(open=True), # Also open accordion on slider change
442
- inputs=None,
443
- outputs=settings_panel,
444
- queue=False
445
  )
446
 
447
-
448
  resize_option.change(
449
  fn=toggle_custom_resize_slider,
450
  inputs=[resize_option],
@@ -452,49 +466,58 @@ with gr.Blocks(css=css) as demo:
452
  queue=False
453
  )
454
 
455
- # Combine run logic for Button and Textbox submission
456
- run_inputs = [
457
  input_image, width_slider, height_slider, overlap_percentage, num_inference_steps,
458
  resize_option, custom_resize_percentage, prompt_input, alignment_dropdown,
459
  overlap_left, overlap_right, overlap_top, overlap_bottom
460
  ]
461
 
462
- def run_generation(img, w, h, ov_perc, steps, res_opt, cust_res_perc, prompt, align, ov_l, ov_r, ov_t, ov_b, history):
463
- # The infer function is a generator, we need to iterate to get the final value
464
- final_image = None
465
- for res_img in infer(img, w, h, ov_perc, steps, res_opt, cust_res_perc, prompt, align, ov_l, ov_r, ov_t, ov_b):
466
- final_image = res_img
467
-
468
- # Update history with the final image
469
- updated_history = update_history(final_image, history)
470
-
471
- # Return the final image for the result component and the updated history
472
- return final_image, updated_history, gr.update(visible=True) # Also make button visible
473
-
474
-
475
  run_button.click(
476
- fn=clear_result, # First clear the previous result
477
  inputs=None,
478
- outputs=result,
479
- queue=False # Clearing should be fast
480
  ).then(
481
- fn=run_generation, # Then run the generation and history update
482
- inputs=run_inputs + [history_gallery], # Pass current history
483
- outputs=[result, history_gallery, use_as_input_button], # Update result, history, and button visibility
 
 
 
 
 
 
 
 
 
 
 
 
484
  )
485
 
486
  prompt_input.submit(
487
- fn=clear_result, # First clear the previous result
488
  inputs=None,
489
- outputs=result,
490
- queue=False # Clearing should be fast
 
 
 
 
491
  ).then(
492
- fn=run_generation, # Then run the generation and history update
493
- inputs=run_inputs + [history_gallery], # Pass current history
494
- outputs=[result, history_gallery, use_as_input_button], # Update result, history, and button visibility
 
 
 
 
 
 
495
  )
496
 
497
-
498
  preview_button.click(
499
  fn=preview_image_and_mask,
500
  inputs=[input_image, width_slider, height_slider, overlap_percentage, resize_option, custom_resize_percentage, alignment_dropdown,
@@ -503,5 +526,4 @@ with gr.Blocks(css=css) as demo:
503
  queue=False # Preview should be fast
504
  )
505
 
506
- # Launch the demo
507
- demo.queue(max_size=20).launch(share=False, ssr_mode=False, show_error=True)
 
3
  import torch
4
  from diffusers import AutoencoderKL, TCDScheduler
5
  from diffusers.models.model_loading_utils import load_state_dict
6
+ # Remove ImageSlider import
7
+ # from gradio_imageslider import ImageSlider
8
  from huggingface_hub import hf_hub_download
9
 
10
  from controlnet_union import ControlNetModel_Union
 
13
  from PIL import Image, ImageDraw
14
  import numpy as np
15
 
16
+ # --- Model Loading (Unchanged) ---
17
  config_file = hf_hub_download(
18
  "xinsir/controlnet-union-sdxl-1.0",
19
  filename="config_promax.json",
 
47
 
48
  pipe.scheduler = TCDScheduler.from_config(pipe.scheduler.config)
49
 
50
+ # --- Helper Functions (Mostly Unchanged) ---
 
51
  def can_expand(source_width, source_height, target_width, target_height, alignment):
52
  """Checks if the image can be expanded based on the alignment."""
53
  if alignment in ("Left", "Right") and source_width >= target_width:
 
129
  mask_draw = ImageDraw.Draw(mask)
130
 
131
  # Calculate overlap areas
132
+ white_gaps_patch = 2 # Pixels to leave unmasked at edges if overlap is disabled for that edge
133
 
134
  left_overlap = margin_x + overlap_x if overlap_left else margin_x + white_gaps_patch
135
  right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width - white_gaps_patch
136
  top_overlap = margin_y + overlap_y if overlap_top else margin_y + white_gaps_patch
137
  bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height - white_gaps_patch
138
 
139
+ # Adjust overlap boundaries based on alignment when specific overlap directions are *disabled*
140
+ # This prevents unmasking the absolute edge of the canvas in alignment modes
141
  if alignment == "Left":
142
+ left_overlap = margin_x + overlap_x if overlap_left else margin_x # Keep edge masked if alignment is left
143
  elif alignment == "Right":
144
+ right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width # Keep edge masked
145
  elif alignment == "Top":
146
+ top_overlap = margin_y + overlap_y if overlap_top else margin_y # Keep edge masked
147
  elif alignment == "Bottom":
148
+ bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height # Keep edge masked
149
+
150
+ # Ensure coordinates are within bounds
151
+ left_overlap = max(0, left_overlap)
152
+ top_overlap = max(0, top_overlap)
153
+ right_overlap = min(target_size[0], right_overlap)
154
+ bottom_overlap = min(target_size[1], bottom_overlap)
155
 
156
+ # Draw the mask (black rectangle for the area to keep)
157
+ if right_overlap > left_overlap and bottom_overlap > top_overlap:
158
+ mask_draw.rectangle([
159
+ (left_overlap, top_overlap),
160
+ (right_overlap, bottom_overlap)
161
+ ], fill=0) # 0 means keep this area (not masked for inpainting)
162
 
163
+ # Invert the mask: White areas (255) will be inpainted. Black (0) is kept.
164
+ mask = Image.fromarray(255 - np.array(mask))
 
 
 
165
 
166
  return background, mask
167
 
 
171
  # Create a preview image showing the mask
172
  preview = background.copy().convert('RGBA')
173
 
174
+ # Create a semi-transparent red overlay for the masked (inpainting) area
175
+ red_overlay = Image.new('RGBA', background.size, (255, 0, 0, 100)) # 100 alpha (~40% opacity)
 
 
 
 
176
 
177
+ # The mask is now white (255) where inpainting happens. Use this directly.
178
+ preview.paste(red_overlay, (0, 0), mask)
179
 
180
  return preview
181
 
 
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
+ # Ensure alignment allows expansion, default to Middle if not
187
+ source_w, source_h = background.size # Use background size after initial resize/placement
188
+ target_w, target_h = width, height
189
+ if alignment in ("Left", "Right") and source_w >= target_w:
190
+ print(f"Warning: Source width ({source_w}) >= target width ({target_w}) with {alignment} alignment. Forcing Middle alignment.")
191
+ alignment = "Middle"
192
+ # Re-prepare mask/background with corrected alignment if needed (optional, depends if prepare func uses alignment early)
193
+ # background, mask = prepare_image_and_mask(...) # If needed
194
+ if alignment in ("Top", "Bottom") and source_h >= target_h:
195
+ print(f"Warning: Source height ({source_h}) >= target height ({target_h}) with {alignment} alignment. Forcing Middle alignment.")
196
+ alignment = "Middle"
197
+ # Re-prepare mask/background with corrected alignment if needed
198
+ # background, mask = prepare_image_and_mask(...) # If needed
199
+
200
+ # Image for ControlNet input (masked original content)
201
+ # The pipeline expects the original image content in the non-masked area
202
  cnet_image = background.copy()
203
+ # The pipeline's `image` argument is the *initial* content for the *masked* area (often noise, but here we provide the background)
204
+ # The `mask_image` tells the pipeline *where* to perform the inpainting/outpainting.
205
+ # The controlnet `image` needs the original content visible in the non-masked area.
206
+ # ControlNet Union seems to work well by just passing the background with the source image pasted.
 
 
 
 
 
 
207
 
208
+ final_prompt = f"{prompt_input} , high quality, 4k" if prompt_input else "high quality, 4k"
209
 
210
  (
211
  prompt_embeds,
 
214
  negative_pooled_prompt_embeds,
215
  ) = pipe.encode_prompt(final_prompt, "cuda", True)
216
 
217
+ # The pipeline call
218
+ # Note: The pipeline expects `image` (initial state for masked area) and `mask_image`
219
+ # The `control_image` is implicitly handled by the ControlNet attached to the pipeline
220
+ output_image = pipe(
 
221
  prompt_embeds=prompt_embeds,
222
  negative_prompt_embeds=negative_prompt_embeds,
223
  pooled_prompt_embeds=pooled_prompt_embeds,
224
  negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
225
+ image=background, # Provide the initial canvas state
226
+ mask_image=mask, # Provide the mask (white is area to change)
227
+ control_image=cnet_image, # Pass the control image explicitly if needed by pipeline logic
228
+ num_inference_steps=num_inference_steps,
229
+ output_type="pil" # Ensure PIL output
230
+ ).images[0]
231
+
232
+ # The pipeline should have already handled the compositing based on the mask
233
+ # If not, uncomment the paste operation below:
234
+ # final_image = background.copy().convert("RGBA") # Start with original background
235
+ # output_image = output_image.convert("RGBA")
236
+ # mask_rgba = mask.convert('L').point(lambda p: 255 if p > 128 else 0) # Ensure mask is binary 0/255
237
+ # final_image.paste(output_image, (0, 0), mask_rgba) # Paste generated content using the mask
238
+
239
+ # Return the single final image
240
+ return output_image
 
 
 
 
 
 
241
 
242
 
243
  def clear_result():
244
+ """Clears the result Image component."""
245
  return gr.update(value=None)
246
 
247
+ # --- UI Helper Functions (Unchanged) ---
248
  def preload_presets(target_ratio, ui_width, ui_height):
249
  """Updates the width and height sliders based on the selected aspect ratio."""
250
  if target_ratio == "9:16":
251
  changed_width = 720
252
  changed_height = 1280
253
+ return changed_width, changed_height, gr.update() # Close accordion
254
  elif target_ratio == "16:9":
255
  changed_width = 1280
256
  changed_height = 720
257
+ return changed_width, changed_height, gr.update() # Close accordion
258
  elif target_ratio == "1:1":
259
  changed_width = 1024
260
  changed_height = 1024
261
+ return changed_width, changed_height, gr.update() # Close accordion
262
  elif target_ratio == "Custom":
263
+ # Don't change sliders, just open accordion
264
  return ui_width, ui_height, gr.update(open=True)
265
 
266
  def select_the_right_preset(user_width, user_height):
267
+ """Updates the radio button based on the current slider values."""
268
  if user_width == 720 and user_height == 1280:
269
  return "9:16"
270
  elif user_width == 1280 and user_height == 720:
 
275
  return "Custom"
276
 
277
  def toggle_custom_resize_slider(resize_option):
278
+ """Shows/hides the custom resize slider."""
279
  return gr.update(visible=(resize_option == "Custom"))
280
 
281
  def update_history(new_image, history):
282
  """Updates the history gallery with the new image."""
283
  if history is None:
284
  history = []
285
+ # Ensure new_image is a PIL Image before adding
286
  if isinstance(new_image, Image.Image):
287
  history.insert(0, new_image)
 
 
 
288
  return history
289
 
290
+ # --- Gradio UI Definition ---
 
291
  css = """
292
  .gradio-container {
293
  width: 1200px !important;
294
+ margin: auto !important; /* Center the container */
295
  }
296
  h1 { text-align: center; }
297
  footer { visibility: hidden; }
298
+ /* Ensure result image takes reasonable space */
299
+ #result-image img {
300
+ max-height: 768px; /* Adjust max height as needed */
301
+ object-fit: contain;
302
+ width: auto;
303
+ height: auto;
304
+ }
305
+ #history-gallery .thumbnail-item { /* Style history items */
306
+ height: 100px !important;
307
+ }
308
+ #history-gallery .gallery {
309
+ grid-template-rows: repeat(auto-fill, 100px) !important;
310
+ }
311
 
 
312
  """
313
 
314
+ title = """<h1 align="center">Diffusers Image Outpaint Lightning</h1>"""
315
+
316
  with gr.Blocks(css=css) as demo:
317
  with gr.Column():
318
  gr.HTML(title)
319
 
320
  with gr.Row():
321
+ with gr.Column(scale=1): # Left column for inputs
322
  input_image = gr.Image(
323
  type="pil",
324
+ label="Input Image",
325
+ height=400 # Give input image reasonable height
326
  )
327
 
328
  with gr.Row():
329
  with gr.Column(scale=2):
330
+ prompt_input = gr.Textbox(label="Prompt (Optional)", placeholder="Describe the scene to expand...")
331
  with gr.Column(scale=1):
332
+ run_button = gr.Button("Generate", variant="primary") # Make primary
333
 
334
  with gr.Row():
335
  target_ratio = gr.Radio(
336
+ label="Target Ratio",
337
  choices=["9:16", "16:9", "1:1", "Custom"],
338
  value="9:16",
339
  scale=2
 
342
  alignment_dropdown = gr.Dropdown(
343
  choices=["Middle", "Left", "Right", "Top", "Bottom"],
344
  value="Middle",
345
+ label="Align Source Image"
346
  )
347
 
348
  with gr.Accordion(label="Advanced settings", open=False) as settings_panel:
349
+ with gr.Row():
350
+ width_slider = gr.Slider(
351
+ label="Target Width",
352
+ minimum=512, # Lowered minimum slightly
353
+ maximum=2048, # Increased maximum slightly
354
+ step=64, # Use steps of 64 common for SD
355
+ value=720,
356
+ )
357
+ height_slider = gr.Slider(
358
+ label="Target Height",
359
+ minimum=512,
360
+ maximum=2048,
361
+ step=64,
362
+ value=1280,
363
+ )
364
+ num_inference_steps = gr.Slider(label="Steps", minimum=1, maximum=12, step=1, value=4) # TCD/Lightning allows few steps
365
+
366
+ with gr.Group():
367
+ overlap_percentage = gr.Slider(
368
+ label="Mask overlap (%)",
369
+ minimum=1,
370
+ maximum=50,
371
+ value=12, # Default overlap
372
+ step=1
373
+ )
 
 
 
 
 
 
 
374
  with gr.Row():
375
+ overlap_top = gr.Checkbox(label="Top", value=True)
376
+ overlap_right = gr.Checkbox(label="Right", value=True)
377
+ overlap_bottom = gr.Checkbox(label="Bottom", value=True)
378
+ overlap_left = gr.Checkbox(label="Left", value=True)
379
+
380
+
381
+ with gr.Row():
382
+ resize_option = gr.Radio(
383
+ label="Resize input within target",
384
+ choices=["Full", "50%", "33%", "25%", "Custom"],
385
+ value="Full"
386
+ )
387
+ custom_resize_percentage = gr.Slider(
388
+ label="Custom resize (%)",
389
+ minimum=1,
390
+ maximum=100,
391
+ step=1,
392
+ value=50,
393
+ visible=False # Initially hidden
394
+ )
395
+
396
+ preview_button = gr.Button("Preview Mask & Alignment")
397
+ preview_image = gr.Image(label="Mask Preview (Red = Outpaint Area)", type="pil", interactive=False)
398
 
399
 
400
  gr.Examples(
401
  examples=[
402
+ ["./examples/example_1.webp", "A wide landscape view of the mountains", 1280, 720, "Middle"],
403
+ ["./examples/example_2.jpg", "Full body shot of the astronaut on the moon", 720, 1280, "Middle"],
404
+ ["./examples/example_3.jpg", "Expanding the sky and ground around the subject", 1024, 1024, "Middle"],
405
+ ["./examples/example_3.jpg", "Expanding downwards from the subject", 1024, 1024, "Top"], # Align subject Top
406
+ ["./examples/example_3.jpg", "Expanding upwards from the subject", 1024, 1024, "Bottom"], # Align subject Bottom
407
  ],
408
+ inputs=[input_image, prompt_input, width_slider, height_slider, alignment_dropdown],
409
+ label="Examples (Click to load)"
 
 
410
  )
411
 
412
 
413
+ with gr.Column(scale=1): # Right column for output
414
+ # Replace ImageSlider with gr.Image
415
+ result = gr.Image(label="Generated Image", type="pil", interactive=False, elem_id="result-image")
416
+ use_as_input_button = gr.Button("Use Result as Input Image", visible=False) # Initially hidden
417
+
418
+ history_gallery = gr.Gallery(
419
+ label="History",
420
+ columns=6,
421
+ object_fit="contain",
422
+ interactive=False,
423
+ height=110, # Fixed height for the row
424
+ elem_id="history-gallery"
425
+ )
426
 
 
 
427
 
428
+ # --- Event Handling ---
429
 
430
  def use_output_as_input(output_image):
431
  """Sets the generated output as the new input image."""
432
+ # output_image is now the single final image from gr.Image
433
  return gr.update(value=output_image)
434
 
435
  use_as_input_button.click(
436
  fn=use_output_as_input,
437
+ inputs=[result], # Input is the result image component
438
+ outputs=[input_image] # Output updates the input image component
439
  )
440
 
441
  target_ratio.change(
442
  fn=preload_presets,
443
  inputs=[target_ratio, width_slider, height_slider],
444
+ outputs=[width_slider, height_slider, settings_panel], # Also control accordion state
445
  queue=False
446
  )
447
 
448
+ # Link sliders back to the ratio selector
449
  width_slider.change(
450
  fn=select_the_right_preset,
451
  inputs=[width_slider, height_slider],
452
  outputs=[target_ratio],
453
  queue=False
 
 
 
 
 
454
  )
 
 
455
  height_slider.change(
456
  fn=select_the_right_preset,
457
  inputs=[width_slider, height_slider],
458
  outputs=[target_ratio],
459
  queue=False
 
 
 
 
 
460
  )
461
 
 
462
  resize_option.change(
463
  fn=toggle_custom_resize_slider,
464
  inputs=[resize_option],
 
466
  queue=False
467
  )
468
 
469
+ # Consolidate common inputs for generation
470
+ gen_inputs = [
471
  input_image, width_slider, height_slider, overlap_percentage, num_inference_steps,
472
  resize_option, custom_resize_percentage, prompt_input, alignment_dropdown,
473
  overlap_left, overlap_right, overlap_top, overlap_bottom
474
  ]
475
 
476
+ # Chain generation logic
 
 
 
 
 
 
 
 
 
 
 
 
477
  run_button.click(
478
+ fn=clear_result,
479
  inputs=None,
480
+ outputs=[result], # Clear the single image output
481
+ queue=False # Run clearing immediately
482
  ).then(
483
+ fn=infer,
484
+ inputs=gen_inputs,
485
+ outputs=[result], # Output the single image to the result component
486
+ ).then(
487
+ # Update history with the single result image
488
+ fn=lambda res_img, hist: update_history(res_img, hist),
489
+ inputs=[result, history_gallery],
490
+ outputs=[history_gallery],
491
+ queue=False # Update history immediately after generation
492
+ ).then(
493
+ # Show the 'Use as Input' button
494
+ fn=lambda: gr.update(visible=True),
495
+ inputs=None,
496
+ outputs=[use_as_input_button],
497
+ queue=False # Show button immediately
498
  )
499
 
500
  prompt_input.submit(
501
+ fn=clear_result,
502
  inputs=None,
503
+ outputs=[result],
504
+ queue=False
505
+ ).then(
506
+ fn=infer,
507
+ inputs=gen_inputs,
508
+ outputs=[result],
509
  ).then(
510
+ fn=lambda res_img, hist: update_history(res_img, hist),
511
+ inputs=[result, history_gallery],
512
+ outputs=[history_gallery],
513
+ queue=False
514
+ ).then(
515
+ fn=lambda: gr.update(visible=True),
516
+ inputs=None,
517
+ outputs=[use_as_input_button],
518
+ queue=False
519
  )
520
 
 
521
  preview_button.click(
522
  fn=preview_image_and_mask,
523
  inputs=[input_image, width_slider, height_slider, overlap_percentage, resize_option, custom_resize_percentage, alignment_dropdown,
 
526
  queue=False # Preview should be fast
527
  )
528
 
529
+ demo.queue(max_size=10).launch(ssr_mode=False, show_error=True) # Removed share=False for potential Hugging Face Spaces use