seawolf2357 commited on
Commit
be74e70
·
verified ·
1 Parent(s): 216a86d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +546 -523
app.py CHANGED
@@ -1,144 +1,16 @@
1
  import gradio as gr
2
- import numpy as np
3
- import random
4
- import torch
5
- import spaces
6
  import os
7
- import json
8
- import time
9
-
10
  from PIL import Image, ImageDraw
11
- import torch
12
- import math
13
-
14
- from optimization import optimize_pipeline_
15
- from qwenimage.pipeline_qwen_image_edit import QwenImageEditPipeline
16
- from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
17
- from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
18
-
19
- from huggingface_hub import InferenceClient
20
- import math
21
-
22
- # --- Prompt Enhancement using Hugging Face InferenceClient ---
23
- def polish_prompt_hf(original_prompt, system_prompt):
24
- """
25
- Rewrites the prompt using a Hugging Face InferenceClient.
26
- """
27
- # Ensure HF_TOKEN is set
28
- api_key = os.environ.get("HF_TOKEN")
29
- if not api_key:
30
- print("Warning: HF_TOKEN not set. Falling back to original prompt.")
31
- return original_prompt
32
-
33
- try:
34
- # Initialize the client
35
- client = InferenceClient(
36
- provider="cerebras",
37
- api_key=api_key,
38
- )
39
-
40
- # Format the messages for the chat completions API
41
- messages = [
42
- {"role": "system", "content": system_prompt},
43
- {"role": "user", "content": original_prompt}
44
- ]
45
-
46
- # Call the API
47
- completion = client.chat.completions.create(
48
- model="Qwen/Qwen3-235B-A22B-Instruct-2507",
49
- messages=messages,
50
- )
51
-
52
- # Parse the response
53
- result = completion.choices[0].message.content
54
-
55
- # Try to extract JSON if present
56
- if '{"Rewritten"' in result:
57
- try:
58
- # Clean up the response
59
- result = result.replace('```json', '').replace('```', '')
60
- result_json = json.loads(result)
61
- polished_prompt = result_json.get('Rewritten', result)
62
- except:
63
- polished_prompt = result
64
- else:
65
- polished_prompt = result
66
-
67
- polished_prompt = polished_prompt.strip().replace("\n", " ")
68
- return polished_prompt
69
-
70
- except Exception as e:
71
- print(f"Error during API call to Hugging Face: {e}")
72
- # Fallback to original prompt if enhancement fails
73
- return original_prompt
74
-
75
-
76
- def polish_prompt(prompt, img):
77
- """
78
- Main function to polish prompts for image editing using HF inference.
79
- """
80
- SYSTEM_PROMPT = '''
81
- # Edit Instruction Rewriter
82
- You are a professional edit instruction rewriter. Your task is to generate a precise, concise, and visually achievable professional-level edit instruction based on the user-provided instruction and the image to be edited.
83
-
84
- Please strictly follow the rewriting rules below:
85
-
86
- ## 1. General Principles
87
- - Keep the rewritten prompt **concise**. Avoid overly long sentences and reduce unnecessary descriptive language.
88
- - If the instruction is contradictory, vague, or unachievable, prioritize reasonable inference and correction, and supplement details when necessary.
89
- - Keep the core intention of the original instruction unchanged, only enhancing its clarity, rationality, and visual feasibility.
90
- - All added objects or modifications must align with the logic and style of the edited input image's overall scene.
91
-
92
- ## 2. Task Type Handling Rules
93
- ### 1. Add, Delete, Replace Tasks
94
- - If the instruction is clear (already includes task type, target entity, position, quantity, attributes), preserve the original intent and only refine the grammar.
95
- - If the description is vague, supplement with minimal but sufficient details (category, color, size, orientation, position, etc.). For example:
96
- > Original: "Add an animal"
97
- > Rewritten: "Add a light-gray cat in the bottom-right corner, sitting and facing the camera"
98
- - Remove meaningless instructions: e.g., "Add 0 objects" should be ignored or flagged as invalid.
99
- - For replacement tasks, specify "Replace Y with X" and briefly describe the key visual features of X.
100
-
101
- ### 2. Text Editing Tasks
102
- - All text content must be enclosed in English double quotes " ". Do not translate or alter the original language of the text, and do not change the capitalization.
103
- - **For text replacement tasks, always use the fixed template:**
104
- - Replace "xx" to "yy".
105
- - Replace the xx bounding box to "yy".
106
- - If the user does not specify text content, infer and add concise text based on the instruction and the input image's context. For example:
107
- > Original: "Add a line of text" (poster)
108
- > Rewritten: "Add text "LIMITED EDITION" at the top center with slight shadow"
109
- - Specify text position, color, and layout in a concise way.
110
-
111
- ### 3. Human Editing Tasks
112
- - Maintain the person's core visual consistency (ethnicity, gender, age, hairstyle, expression, outfit, etc.).
113
- - If modifying appearance (e.g., clothes, hairstyle), ensure the new element is consistent with the original style.
114
- - **For expression changes, they must be natural and subtle, never exaggerated.**
115
- - If deletion is not specifically emphasized, the most important subject in the original image (e.g., a person, an animal) should be preserved.
116
- - For background change tasks, emphasize maintaining subject consistency at first.
117
- - Example:
118
- > Original: "Change the person's hat"
119
- > Rewritten: "Replace the man's hat with a dark brown beret; keep smile, short hair, and gray jacket unchanged"
120
-
121
- ### 4. Style Transformation or Enhancement Tasks
122
- - If a style is specified, describe it concisely with key visual traits. For example:
123
- > Original: "Disco style"
124
- > Rewritten: "1970s disco: flashing lights, disco ball, mirrored walls, colorful tones"
125
- - If the instruction says "use reference style" or "keep current style," analyze the input image, extract main features (color, composition, texture, lighting, art style), and integrate them concisely.
126
- - **For coloring tasks, including restoring old photos, always use the fixed template:** "Restore old photograph, remove scratches, reduce noise, enhance details, high resolution, realistic, natural skin tones, clear facial features, no distortion, vintage photo restoration"
127
- - If there are other changes, place the style description at the end.
128
-
129
- ## 3. Rationality and Logic Checks
130
- - Resolve contradictory instructions: e.g., "Remove all trees but keep all trees" should be logically corrected.
131
- - Add missing key information: if position is unspecified, choose a reasonable area based on composition (near subject, empty space, center/edges).
132
 
133
- # Output Format
134
- Return only the rewritten instruction text directly, without JSON formatting or any other wrapper.
135
- '''
136
-
137
- # Note: We're not actually using the image in the HF version,
138
- # but keeping the interface consistent
139
- full_prompt = f"{SYSTEM_PROMPT}\n\nUser Input: {prompt}\n\nRewritten Prompt:"
140
-
141
- return polish_prompt_hf(full_prompt, SYSTEM_PROMPT)
142
 
143
  # --- Outpainting Functions ---
144
  def can_expand(source_width, source_height, target_width, target_height, alignment):
@@ -246,8 +118,11 @@ def prepare_image_and_mask(image, width, height, overlap_percentage, resize_opti
246
 
247
  return background, mask
248
 
249
- def preview_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
250
- """Creates a preview showing the mask overlay."""
 
 
 
251
  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)
252
 
253
  # Create a preview image showing the mask
@@ -265,248 +140,508 @@ def preview_image_and_mask(image, width, height, overlap_percentage, resize_opti
265
 
266
  return preview
267
 
268
- # --- Model Loading ---
269
- dtype = torch.bfloat16
270
- device = "cuda" if torch.cuda.is_available() else "cpu"
271
- pipe = QwenImageEditPipeline.from_pretrained("Qwen/Qwen-Image-Edit", torch_dtype=dtype).to(device)
272
- pipe.transformer.__class__ = QwenImageTransformer2DModel
273
- pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
274
-
275
- # --- Ahead-of-time compilation ---
276
- optimize_pipeline_(pipe, image=Image.new("RGB", (1024, 1024)), prompt="prompt")
277
-
278
- # --- UI Constants and Helpers ---
279
- MAX_SEED = np.iinfo(np.int32).max
280
-
281
- def clear_result():
282
- """Clears the result image."""
283
- return gr.update(value=None)
284
-
285
- def update_history(new_image, history):
286
- """Updates the history gallery with the new image."""
287
- time.sleep(0.5) # Small delay to ensure image is ready
288
- if history is None:
289
- history = []
290
- if new_image is not None:
291
- # Convert to list if needed (Gradio sometimes returns tuples)
292
- if not isinstance(history, list):
293
- history = list(history) if history else []
294
- history.insert(0, new_image)
295
- # Keep only the last 20 images in history
296
- history = history[:20]
297
- return history
298
-
299
- def use_history_as_input(evt: gr.SelectData, history):
300
- """Sets the selected history image as the new input image."""
301
- if history and evt.index < len(history):
302
- return gr.update(value=history[evt.index][0])
303
- return gr.update()
304
-
305
- def use_output_as_input(output_image):
306
- """Sets the generated output as the new input image."""
307
- if output_image is not None:
308
- return gr.update(value=output_image)
309
- return gr.update()
310
-
311
- def preload_presets(target_ratio, ui_width, ui_height):
312
- """Updates the width and height sliders based on the selected aspect ratio."""
313
- if target_ratio == "9:16":
314
- changed_width = 720
315
- changed_height = 1280
316
- return changed_width, changed_height, gr.update()
317
- elif target_ratio == "16:9":
318
- changed_width = 1280
319
- changed_height = 720
320
- return changed_width, changed_height, gr.update()
321
- elif target_ratio == "1:1":
322
- changed_width = 1024
323
- changed_height = 1024
324
- return changed_width, changed_height, gr.update()
325
- elif target_ratio == "Custom":
326
- return ui_width, ui_height, gr.update(open=True)
327
-
328
- def select_the_right_preset(user_width, user_height):
329
- if user_width == 720 and user_height == 1280:
330
- return "9:16"
331
- elif user_width == 1280 and user_height == 720:
332
- return "16:9"
333
- elif user_width == 1024 and user_height == 1024:
334
- return "1:1"
335
- else:
336
- return "Custom"
337
-
338
- def toggle_custom_resize_slider(resize_option):
339
- return gr.update(visible=(resize_option == "Custom"))
340
-
341
- # --- Main Inference Function (with outpainting preprocessing) ---
342
- @spaces.GPU(duration=120)
343
- def infer(
344
- image,
345
- prompt,
346
- width,
347
- height,
348
- overlap_percentage,
349
- resize_option,
350
- custom_resize_percentage,
351
- alignment,
352
- overlap_left,
353
- overlap_right,
354
- overlap_top,
355
- overlap_bottom,
356
- seed=42,
357
- randomize_seed=False,
358
- true_guidance_scale=4.0,
359
- num_inference_steps=50,
360
- rewrite_prompt=True,
361
- progress=gr.Progress(track_tqdm=True),
362
- ):
363
  """
364
- Generates an outpainted image using the Qwen-Image-Edit pipeline.
365
  """
366
- # Hardcode the negative prompt as requested
367
- negative_prompt = " "
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
 
369
- if randomize_seed:
370
- seed = random.randint(0, MAX_SEED)
371
-
372
- # Set up the generator for reproducibility
373
- generator = torch.Generator(device=device).manual_seed(seed)
374
 
375
- print(f"Original Prompt: '{prompt}'")
376
- print(f"Negative Prompt: '{negative_prompt}'")
377
- print(f"Seed: {seed}, Steps: {num_inference_steps}")
378
-
379
- if rewrite_prompt:
380
- prompt = polish_prompt(prompt, image)
381
- print(f"Rewritten Prompt: {prompt}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  # Prepare the image with white margins for outpainting
384
  outpaint_image, mask = prepare_image_and_mask(
385
- image, width, height, overlap_percentage,
386
  resize_option, custom_resize_percentage, alignment,
387
  overlap_left, overlap_right, overlap_top, overlap_bottom
388
  )
389
 
390
- # Check if expansion is possible
391
- if not can_expand(image.width, image.height, width, height, alignment):
392
- alignment = "Middle"
393
- outpaint_image, mask = prepare_image_and_mask(
394
- image, width, height, overlap_percentage,
395
- resize_option, custom_resize_percentage, "Middle",
396
- overlap_left, overlap_right, overlap_top, overlap_bottom
397
- )
 
 
 
 
 
 
 
398
 
399
- print(f"Outpaint dimensions: {outpaint_image.size}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- # Generate the image with outpainting preprocessing
402
- result_image = pipe(
403
- outpaint_image, # Use the preprocessed image with white margins
404
- prompt="replace the white margins. "+ prompt,
405
- negative_prompt=negative_prompt,
406
- num_inference_steps=num_inference_steps,
407
- generator=generator,
408
- true_cfg_scale=true_guidance_scale,
409
- ).images[0]
410
 
411
- return result_image, seed
 
 
 
 
 
 
 
 
 
412
 
413
- # --- Examples and UI Layout ---
414
- # You can add examples here if you have sample images
415
- # examples = [
416
- # ["path/to/example1.jpg", "extend the landscape", 1280, 720, "Middle"],
417
- # ["path/to/example2.jpg", "add more sky", 1024, 1024, "Top"],
418
- # ]
419
 
 
420
  css = """
421
- #col-container {
422
- margin: 0 auto;
423
- max-width: 1024px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  }
425
- #logo-title {
 
426
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  }
428
- #logo-title img {
429
- width: 400px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  }
431
- #edit_text{margin-top: -62px !important}
432
  .preview-container {
433
- border: 1px solid #e0e0e0;
434
- border-radius: 8px;
435
- padding: 10px;
436
- margin-top: 10px;
 
 
 
437
  }
438
- .gallery-container {
439
- margin-top: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  }
441
  """
442
 
443
- with gr.Blocks(css=css) as demo:
444
- with gr.Column(elem_id="col-container"):
445
  gr.HTML("""
446
- <div id="logo-title">
447
- <img src="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-Image/qwen_image_edit_logo.png" alt="Qwen-Image Edit Logo" width="400" style="display: block; margin: 0 auto;">
448
- <h2 style="font-style: italic;color: #5b47d1;margin-top: -27px !important;margin-left: 133px;">Outpaint [Fast]</h2>
449
- </div>
450
- """)
451
- gr.Markdown("""
452
-
453
- Outpaint images with Qwen Image Edit. [Learn more](https://github.com/QwenLM/Qwen-Image) about the Qwen-Image series.
454
-
455
- This demo uses the [Qwen-Image-Lightning](https://huggingface.co/lightx2v/Qwen-Image-Lightning) LoRA with AoT compilation and FA3 for accelerated 8-step inference.
456
- Try on [Qwen Chat](https://chat.qwen.ai/), or [download model](https://huggingface.co/Qwen/Qwen-Image-Edit) to run locally with ComfyUI or diffusers.
457
- """)
458
-
459
-
460
- with gr.Row():
461
- with gr.Column():
462
- input_image = gr.Image(label="Input Image", type="pil")
463
-
464
- prompt = gr.Text(
465
- label="Prompt",
466
- info="Describe what should appear in the extended areas",
467
- value="extend the image naturally",
 
 
 
 
 
 
 
 
 
468
  )
469
 
470
- with gr.Row():
471
- target_ratio = gr.Radio(
472
- label="Target Ratio",
473
- choices=["9:16", "16:9", "1:1", "Custom"],
474
- value="16:9",
475
- scale=2
476
  )
477
- alignment_dropdown = gr.Dropdown(
478
- choices=["Middle", "Left", "Right", "Top", "Bottom"],
479
- value="Middle",
480
- label="Alignment"
 
481
  )
482
 
483
- run_button = gr.Button("run", variant="primary")
 
 
 
 
484
 
485
- with gr.Accordion("Outpainting Settings", open=False) as settings_panel:
 
 
 
 
 
 
 
486
  with gr.Row():
487
- width_slider = gr.Slider(
 
 
 
 
 
 
 
 
 
 
 
 
488
  label="Target Width",
489
  minimum=512,
490
  maximum=2048,
491
  step=8,
492
- value=1280,
493
  )
494
- height_slider = gr.Slider(
495
  label="Target Height",
496
  minimum=512,
497
  maximum=2048,
498
  step=8,
499
- value=720,
500
  )
501
 
502
- with gr.Group():
503
  overlap_percentage = gr.Slider(
504
  label="Mask overlap (%)",
505
  minimum=1,
506
  maximum=50,
507
  value=10,
508
  step=1,
509
- info="Controls the blending area between original and new content"
510
  )
511
 
512
  with gr.Row():
@@ -515,228 +650,116 @@ with gr.Blocks(css=css) as demo:
515
  with gr.Row():
516
  overlap_left = gr.Checkbox(label="Overlap Left", value=True)
517
  overlap_bottom = gr.Checkbox(label="Overlap Bottom", value=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
- with gr.Row():
520
- resize_option = gr.Radio(
521
- label="Resize input image",
522
- choices=["Full", "50%", "33%", "25%", "Custom"],
523
- value="Full",
524
- info="How much of the target canvas the original image should occupy"
525
- )
526
- custom_resize_percentage = gr.Slider(
527
- label="Custom resize (%)",
528
- minimum=1,
529
- maximum=100,
530
- step=1,
531
- value=50,
532
- visible=False
533
- )
534
-
535
- preview_button = gr.Button("👁️ Preview alignment and mask", variant="secondary")
536
-
537
- with gr.Accordion("Advanced Settings", open=False):
538
- seed = gr.Slider(
539
- label="Seed",
540
- minimum=0,
541
- maximum=MAX_SEED,
542
- step=1,
543
- value=0,
544
  )
545
-
546
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
547
-
548
- with gr.Row():
549
- true_guidance_scale = gr.Slider(
550
- label="True guidance scale",
551
- minimum=1.0,
552
- maximum=10.0,
553
- step=0.1,
554
- value=1.0
555
- )
556
-
557
- num_inference_steps = gr.Slider(
558
- label="Number of inference steps",
559
- minimum=1,
560
- maximum=28,
561
- step=1,
562
- value=8,
563
- )
564
-
565
- rewrite_prompt = gr.Checkbox(
566
- label="Enhance prompt (using HF Inference)",
567
- value=True
568
- )
569
-
570
- with gr.Column():
571
- result = gr.Image(label="Result", type="pil", interactive=False)
572
 
573
- use_as_input_button = gr.Button("🔄 Use as Input Image", visible=False, variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
574
 
575
- with gr.Column(visible=False) as preview_container:
576
- preview_image = gr.Image(label="Preview (red area will be generated)", type="pil")
 
 
 
 
 
577
 
578
- gr.Markdown("---")
 
 
 
 
 
579
 
580
- with gr.Row():
581
- gr.Markdown("### 📜 History")
582
- clear_history_button = gr.Button("🗑️ Clear History", size="sm", variant="stop")
 
 
583
 
584
- history_gallery = gr.Gallery(
585
- label="Click any image to use as input",
586
- columns=4,
587
- rows=2,
588
- object_fit="contain",
589
- height="auto",
590
- interactive=False,
591
- show_label=True,
592
- elem_classes=["gallery-container"]
593
- )
594
-
595
- # Event handlers
596
- use_as_input_button.click(
597
- fn=use_output_as_input,
598
- inputs=[result],
599
- outputs=[input_image],
600
- show_api=False
601
- )
602
 
603
- history_gallery.select(
604
- fn=use_history_as_input,
605
- inputs=[history_gallery],
606
- outputs=[input_image],
607
- show_api=False
608
- )
609
-
610
- clear_history_button.click(
611
- fn=lambda: [],
612
- inputs=None,
613
- outputs=history_gallery,
614
- show_api=False
615
  )
616
 
617
  target_ratio.change(
618
  fn=preload_presets,
619
- inputs=[target_ratio, width_slider, height_slider],
620
- outputs=[width_slider, height_slider, settings_panel],
621
- queue=False,
622
- )
623
-
624
- width_slider.change(
625
- fn=select_the_right_preset,
626
- inputs=[width_slider, height_slider],
627
- outputs=[target_ratio],
628
- queue=False,
629
- )
630
-
631
- height_slider.change(
632
- fn=select_the_right_preset,
633
- inputs=[width_slider, height_slider],
634
- outputs=[target_ratio],
635
- queue=False,
636
  )
637
 
638
  resize_option.change(
639
  fn=toggle_custom_resize_slider,
640
  inputs=[resize_option],
641
- outputs=[custom_resize_percentage],
642
- queue=False,
643
  )
644
 
645
- preview_button.click(
646
- fn=lambda: gr.update(visible=True),
647
- inputs=None,
648
- outputs=[preview_container],
649
- queue=False,
650
- ).then(
651
- fn=preview_image_and_mask,
652
  inputs=[
653
- input_image, width_slider, height_slider, overlap_percentage,
654
  resize_option, custom_resize_percentage, alignment_dropdown,
655
  overlap_left, overlap_right, overlap_top, overlap_bottom
656
  ],
657
- outputs=preview_image,
658
- queue=False,
659
- )
660
-
661
- # Main generation pipeline with result clearing, history update, and button visibility
662
- run_button.click(
663
- fn=clear_result,
664
- inputs=None,
665
- outputs=result,
666
- show_api=False
667
- ).then(
668
- fn=infer,
669
- inputs=[
670
- input_image,
671
- prompt,
672
- width_slider,
673
- height_slider,
674
- overlap_percentage,
675
- resize_option,
676
- custom_resize_percentage,
677
- alignment_dropdown,
678
- overlap_left,
679
- overlap_right,
680
- overlap_top,
681
- overlap_bottom,
682
- seed,
683
- randomize_seed,
684
- true_guidance_scale,
685
- num_inference_steps,
686
- rewrite_prompt,
687
- ],
688
- outputs=[result, seed],
689
  ).then(
690
  fn=lambda: gr.update(visible=True),
691
- inputs=None,
692
- outputs=use_as_input_button,
693
- show_api=False
694
- ).then(
695
- fn=update_history,
696
- inputs=[result, history_gallery],
697
- outputs=history_gallery,
698
- show_api=False
699
  )
700
 
701
- # Also trigger on prompt submit
702
- prompt.submit(
703
- fn=clear_result,
704
- inputs=None,
705
- outputs=result,
706
- show_api=False
707
- ).then(
708
- fn=infer,
709
  inputs=[
710
- input_image,
711
- prompt,
712
- width_slider,
713
- height_slider,
714
- overlap_percentage,
715
- resize_option,
716
- custom_resize_percentage,
717
- alignment_dropdown,
718
- overlap_left,
719
- overlap_right,
720
- overlap_top,
721
- overlap_bottom,
722
- seed,
723
- randomize_seed,
724
- true_guidance_scale,
725
- num_inference_steps,
726
- rewrite_prompt,
727
  ],
728
- outputs=[result, seed],
729
- ).then(
730
- fn=lambda: gr.update(visible=True),
731
- inputs=None,
732
- outputs=use_as_input_button,
733
- show_api=False
734
- ).then(
735
- fn=update_history,
736
- inputs=[result, history_gallery],
737
- outputs=history_gallery,
738
- show_api=False
739
  )
740
 
 
741
  if __name__ == "__main__":
742
- demo.launch()
 
 
 
 
 
1
  import gradio as gr
2
+ import replicate
 
 
 
3
  import os
 
 
 
4
  from PIL import Image, ImageDraw
5
+ import requests
6
+ from io import BytesIO
7
+ import time
8
+ import tempfile
9
+ import base64
10
+ import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # Set up Replicate API key from environment variable
13
+ os.environ['REPLICATE_API_TOKEN'] = os.getenv('REPLICATE_API_TOKEN')
 
 
 
 
 
 
 
14
 
15
  # --- Outpainting Functions ---
16
  def can_expand(source_width, source_height, target_width, target_height, alignment):
 
118
 
119
  return background, mask
120
 
121
+ def preview_outpaint(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
122
+ """Creates a preview showing the mask overlay for outpainting."""
123
+ if not image:
124
+ return None
125
+
126
  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)
127
 
128
  # Create a preview image showing the mask
 
140
 
141
  return preview
142
 
143
+ # --- Image Upload Functions ---
144
+ def upload_image_to_hosting(image):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  """
146
+ Upload image to multiple hosting services with fallback
147
  """
148
+ # Method 1: Try imgbb.com (most reliable)
149
+ try:
150
+ buffered = BytesIO()
151
+ image.save(buffered, format="PNG")
152
+ buffered.seek(0)
153
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
154
+
155
+ response = requests.post(
156
+ "https://api.imgbb.com/1/upload",
157
+ data={
158
+ 'key': '6d207e02198a847aa98d0a2a901485a5',
159
+ 'image': img_base64,
160
+ }
161
+ )
162
+
163
+ if response.status_code == 200:
164
+ data = response.json()
165
+ if data.get('success'):
166
+ return data['data']['url']
167
+ except:
168
+ pass
169
+
170
+ # Method 2: Try 0x0.st (simple and reliable)
171
+ try:
172
+ buffered = BytesIO()
173
+ image.save(buffered, format="PNG")
174
+ buffered.seek(0)
175
+
176
+ files = {'file': ('image.png', buffered, 'image/png')}
177
+ response = requests.post("https://0x0.st", files=files)
178
+
179
+ if response.status_code == 200:
180
+ return response.text.strip()
181
+ except:
182
+ pass
183
+
184
+ # Method 3: Fallback to base64
185
+ buffered = BytesIO()
186
+ image.save(buffered, format="PNG")
187
+ buffered.seek(0)
188
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
189
+ return f"data:image/png;base64,{img_base64}"
190
+
191
+ def upscale_image(image):
192
+ """
193
+ Upscale the generated image using Real-ESRGAN (mandatory)
194
+ """
195
+ if not image:
196
+ return None, "No image to upscale"
197
 
198
+ if not os.getenv('REPLICATE_API_TOKEN'):
199
+ return None, "Please set REPLICATE_API_TOKEN"
 
 
 
200
 
201
+ try:
202
+ # Upload image to hosting
203
+ image_url = upload_image_to_hosting(image)
204
+
205
+ # Run Real-ESRGAN model
206
+ output = replicate.run(
207
+ "nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa",
208
+ input={
209
+ "image": image_url,
210
+ "scale": 4 # 4x upscaling as default
211
+ }
212
+ )
213
+
214
+ if output is None:
215
+ return None, "No output received from upscaler"
216
+
217
+ # Get the upscaled image
218
+ try:
219
+ if hasattr(output, 'read'):
220
+ img_data = output.read()
221
+ img = Image.open(BytesIO(img_data))
222
+ return img, "🔍 Upscaled 4x successfully!"
223
+ except:
224
+ pass
225
+
226
+ try:
227
+ if hasattr(output, 'url'):
228
+ output_url = output.url()
229
+ response = requests.get(output_url, timeout=30)
230
+ if response.status_code == 200:
231
+ img = Image.open(BytesIO(response.content))
232
+ return img, "🔍 Upscaled 4x successfully!"
233
+ except:
234
+ pass
235
+
236
+ output_url = None
237
+ if isinstance(output, str):
238
+ output_url = output
239
+ elif isinstance(output, list) and len(output) > 0:
240
+ output_url = output[0]
241
+
242
+ if output_url:
243
+ response = requests.get(output_url, timeout=30)
244
+ if response.status_code == 200:
245
+ img = Image.open(BytesIO(response.content))
246
+ return img, "🔍 Upscaled 4x successfully!"
247
+
248
+ return None, "Could not process upscaled output"
249
+
250
+ except Exception as e:
251
+ return None, f"Upscale error: {str(e)[:100]}"
252
 
253
+ def apply_outpainting_to_image(image, outpaint_prompt, target_width, target_height,
254
+ overlap_percentage, resize_option, custom_resize_percentage,
255
+ alignment, overlap_left, overlap_right, overlap_top, overlap_bottom):
256
+ """
257
+ Apply outpainting to an image by preparing it with white margins
258
+ """
259
+ if not image:
260
+ return None
261
+
262
+ # Check if expansion is possible
263
+ if not can_expand(image.width, image.height, target_width, target_height, alignment):
264
+ alignment = "Middle"
265
+
266
  # Prepare the image with white margins for outpainting
267
  outpaint_image, mask = prepare_image_and_mask(
268
+ image, target_width, target_height, overlap_percentage,
269
  resize_option, custom_resize_percentage, alignment,
270
  overlap_left, overlap_right, overlap_top, overlap_bottom
271
  )
272
 
273
+ return outpaint_image
274
+
275
+ def process_images(prompt, image1, image2=None, enable_outpaint=False, outpaint_prompt="",
276
+ target_width=1280, target_height=720, overlap_percentage=10,
277
+ resize_option="Full", custom_resize_percentage=50,
278
+ alignment="Middle", overlap_left=True, overlap_right=True,
279
+ overlap_top=True, overlap_bottom=True):
280
+ """
281
+ Process uploaded images with Replicate API, apply optional outpainting, and mandatory upscaling
282
+ """
283
+ if not image1:
284
+ return None, "Please upload at least one image"
285
+
286
+ if not os.getenv('REPLICATE_API_TOKEN'):
287
+ return None, "Please set REPLICATE_API_TOKEN"
288
 
289
+ try:
290
+ # Step 1: Apply outpainting if enabled
291
+ if enable_outpaint:
292
+ # Apply outpainting to image1
293
+ image1 = apply_outpainting_to_image(
294
+ image1, outpaint_prompt, target_width, target_height,
295
+ overlap_percentage, resize_option, custom_resize_percentage,
296
+ alignment, overlap_left, overlap_right, overlap_top, overlap_bottom
297
+ )
298
+
299
+ # Apply outpainting to image2 if it exists
300
+ if image2:
301
+ image2 = apply_outpainting_to_image(
302
+ image2, outpaint_prompt, target_width, target_height,
303
+ overlap_percentage, resize_option, custom_resize_percentage,
304
+ alignment, overlap_left, overlap_right, overlap_top, overlap_bottom
305
+ )
306
+
307
+ # Update the prompt if outpainting is enabled
308
+ if outpaint_prompt:
309
+ prompt = f"replace the white margins. {outpaint_prompt}. {prompt}"
310
+
311
+ # Step 2: Upload images and process with Nano Banana
312
+ image_urls = []
313
+
314
+ url1 = upload_image_to_hosting(image1)
315
+ image_urls.append(url1)
316
+
317
+ if image2:
318
+ url2 = upload_image_to_hosting(image2)
319
+ image_urls.append(url2)
320
+
321
+ # Run the Nano Banana model
322
+ output = replicate.run(
323
+ "google/nano-banana",
324
+ input={
325
+ "prompt": prompt,
326
+ "image_input": image_urls
327
+ }
328
+ )
329
+
330
+ if output is None:
331
+ return None, "No output received"
332
+
333
+ # Get the generated image
334
+ generated_image = None
335
+ try:
336
+ if hasattr(output, 'read'):
337
+ img_data = output.read()
338
+ generated_image = Image.open(BytesIO(img_data))
339
+ except:
340
+ pass
341
+
342
+ if not generated_image:
343
+ try:
344
+ if hasattr(output, 'url'):
345
+ output_url = output.url()
346
+ response = requests.get(output_url, timeout=30)
347
+ if response.status_code == 200:
348
+ generated_image = Image.open(BytesIO(response.content))
349
+ except:
350
+ pass
351
+
352
+ if not generated_image:
353
+ output_url = None
354
+ if isinstance(output, str):
355
+ output_url = output
356
+ elif isinstance(output, list) and len(output) > 0:
357
+ output_url = output[0]
358
+
359
+ if output_url:
360
+ response = requests.get(output_url, timeout=30)
361
+ if response.status_code == 200:
362
+ generated_image = Image.open(BytesIO(response.content))
363
+
364
+ if not generated_image:
365
+ return None, "Could not process output"
366
+
367
+ # Step 3: Apply mandatory upscaling
368
+ upscaled_image, upscale_status = upscale_image(generated_image)
369
+
370
+ if upscaled_image:
371
+ return upscaled_image, f"✨ Generated and {upscale_status}"
372
+ else:
373
+ # If upscaling fails, return the generated image with a warning
374
+ return generated_image, "✨ Generated (upscaling failed, returning original)"
375
+
376
+ except Exception as e:
377
+ return None, f"Error: {str(e)[:100]}"
378
 
379
+ def toggle_outpaint_options(enable):
380
+ """Toggle visibility of outpainting options"""
381
+ return gr.update(visible=enable)
 
 
 
 
 
 
382
 
383
+ def preload_presets(target_ratio):
384
+ """Updates the width and height based on the selected aspect ratio."""
385
+ if target_ratio == "9:16":
386
+ return 720, 1280
387
+ elif target_ratio == "16:9":
388
+ return 1280, 720
389
+ elif target_ratio == "1:1":
390
+ return 1024, 1024
391
+ else: # Custom
392
+ return 1280, 720
393
 
394
+ def toggle_custom_resize_slider(resize_option):
395
+ return gr.update(visible=(resize_option == "Custom"))
 
 
 
 
396
 
397
+ # Enhanced CSS with modern, minimal design
398
  css = """
399
+ .gradio-container {
400
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
401
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
402
+ min-height: 100vh;
403
+ }
404
+ .header-container {
405
+ background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%);
406
+ padding: 2.5rem;
407
+ border-radius: 24px;
408
+ margin-bottom: 2.5rem;
409
+ box-shadow: 0 20px 60px rgba(255, 179, 71, 0.25);
410
+ }
411
+ .logo-text {
412
+ font-size: 3.5rem;
413
+ font-weight: 900;
414
+ color: #2d3436;
415
+ text-align: center;
416
+ margin: 0;
417
+ letter-spacing: -2px;
418
  }
419
+ .subtitle {
420
+ color: #2d3436;
421
  text-align: center;
422
+ font-size: 1rem;
423
+ margin-top: 0.5rem;
424
+ opacity: 0.8;
425
+ }
426
+ .main-content {
427
+ background: rgba(255, 255, 255, 0.95);
428
+ backdrop-filter: blur(20px);
429
+ border-radius: 24px;
430
+ padding: 2.5rem;
431
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
432
+ }
433
+ .gr-button-primary {
434
+ background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%) !important;
435
+ border: none !important;
436
+ color: #2d3436 !important;
437
+ font-weight: 700 !important;
438
+ font-size: 1.1rem !important;
439
+ padding: 1.2rem 2rem !important;
440
+ border-radius: 14px !important;
441
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
442
+ text-transform: uppercase;
443
+ letter-spacing: 1px;
444
+ width: 100%;
445
+ margin-top: 1rem !important;
446
+ }
447
+ .gr-button-primary:hover {
448
+ transform: translateY(-3px) !important;
449
+ box-shadow: 0 15px 40px rgba(255, 179, 71, 0.35) !important;
450
+ }
451
+ .gr-button-secondary {
452
+ background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%) !important;
453
+ border: none !important;
454
+ color: white !important;
455
+ font-weight: 600 !important;
456
+ font-size: 0.95rem !important;
457
+ padding: 0.8rem 1.5rem !important;
458
+ border-radius: 12px !important;
459
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
460
+ }
461
+ .gr-button-secondary:hover {
462
+ transform: translateY(-2px) !important;
463
+ box-shadow: 0 10px 30px rgba(9, 132, 227, 0.3) !important;
464
+ }
465
+ .gr-input, .gr-textarea {
466
+ background: #ffffff !important;
467
+ border: 2px solid #e1e8ed !important;
468
+ border-radius: 14px !important;
469
+ color: #2d3436 !important;
470
+ font-size: 1rem !important;
471
+ padding: 0.8rem 1rem !important;
472
  }
473
+ .gr-input:focus, .gr-textarea:focus {
474
+ border-color: #ffd93d !important;
475
+ box-shadow: 0 0 0 4px rgba(255, 217, 61, 0.15) !important;
476
+ }
477
+ .gr-form {
478
+ background: transparent !important;
479
+ border: none !important;
480
+ }
481
+ .gr-panel {
482
+ background: #ffffff !important;
483
+ border: 2px solid #e1e8ed !important;
484
+ border-radius: 16px !important;
485
+ padding: 1.5rem !important;
486
+ }
487
+ .gr-box {
488
+ border-radius: 14px !important;
489
+ border-color: #e1e8ed !important;
490
+ }
491
+ label {
492
+ color: #636e72 !important;
493
+ font-weight: 600 !important;
494
+ font-size: 0.85rem !important;
495
+ text-transform: uppercase;
496
+ letter-spacing: 0.5px;
497
+ margin-bottom: 0.5rem !important;
498
+ }
499
+ .status-text {
500
+ font-family: 'SF Mono', 'Monaco', monospace;
501
+ color: #00b894;
502
+ font-size: 0.9rem;
503
+ }
504
+ .image-container {
505
+ border-radius: 14px !important;
506
+ overflow: hidden;
507
+ border: 2px solid #e1e8ed !important;
508
+ background: #fafbfc !important;
509
  }
 
510
  .preview-container {
511
+ border: 2px dashed #ff6b6b !important;
512
+ border-radius: 14px !important;
513
+ padding: 1rem !important;
514
+ background: rgba(255, 107, 107, 0.05) !important;
515
+ }
516
+ footer {
517
+ display: none !important;
518
  }
519
+ /* Equal sizing for all image containers */
520
+ .image-upload {
521
+ min-height: 200px !important;
522
+ max-height: 200px !important;
523
+ }
524
+ .output-image {
525
+ min-height: 420px !important;
526
+ max-height: 420px !important;
527
+ }
528
+ /* Ensure consistent spacing */
529
+ .gr-row {
530
+ gap: 1rem !important;
531
+ }
532
+ .gr-column {
533
+ gap: 1rem !important;
534
+ }
535
+ /* Outpainting options styling */
536
+ .outpaint-section {
537
+ background: rgba(116, 185, 255, 0.1) !important;
538
+ border: 2px solid #74b9ff !important;
539
+ border-radius: 14px !important;
540
+ padding: 1rem !important;
541
+ margin-top: 1rem !important;
542
  }
543
  """
544
 
545
+ with gr.Blocks(css=css, theme=gr.themes.Base()) as demo:
546
+ with gr.Column(elem_classes="header-container"):
547
  gr.HTML("""
548
+ <h1 class="logo-text">🍌 Nano Banana Upscale(X4)</h1>
549
+ <p class="subtitle">AI-Powered Image Style Transfer with Outpainting & Auto-Upscaling</p>
550
+ <div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px;">
551
+ <a href="https://huggingface.co/spaces/openfree/Nano-Banana-Upscale" target="_blank">
552
+ <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=UPSCALE&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Nano Banana Upscale">
553
+ </a>
554
+ <a href="https://huggingface.co/spaces/openfree/Free-Nano-Banana" target="_blank">
555
+ <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=FREE&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Free Nano Banana">
556
+ </a>
557
+ <a href="https://huggingface.co/spaces/aiqtech/Nano-Banana-API" target="_blank">
558
+ <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=API&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Nano Banana API">
559
+ </a>
560
+ <a href="https://huggingface.co/spaces/ginigen/Nano-Banana-Video" target="_blank">
561
+ <img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=VIDEO&color=%230000ff&labelColor=%23800080&logo=GOOGLE&logoColor=white&style=for-the-badge" alt="Nano Banana VIDEO">
562
+ </a>
563
+ <a href="https://discord.gg/openfreeai" target="_blank">
564
+ <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="Discord Openfree AI">
565
+ </a>
566
+ </div>
567
+ """)
568
+
569
+ with gr.Column(elem_classes="main-content"):
570
+ with gr.Row(equal_height=True):
571
+ # Left Column - Inputs
572
+ with gr.Column(scale=1):
573
+ prompt = gr.Textbox(
574
+ label="Style Description",
575
+ placeholder="Describe your style...",
576
+ lines=3,
577
+ value="Make the sheets in the style of the logo. Make the scene natural.",
578
+ elem_classes="prompt-input"
579
  )
580
 
581
+ with gr.Row(equal_height=True):
582
+ image1 = gr.Image(
583
+ label="Primary Image",
584
+ type="pil",
585
+ height=200,
586
+ elem_classes="image-container image-upload"
587
  )
588
+ image2 = gr.Image(
589
+ label="Secondary Image (Optional)",
590
+ type="pil",
591
+ height=200,
592
+ elem_classes="image-container image-upload"
593
  )
594
 
595
+ # Outpainting Options
596
+ enable_outpaint = gr.Checkbox(
597
+ label="🎨 Enable Outpainting (Expand Image)",
598
+ value=False
599
+ )
600
 
601
+ with gr.Column(visible=False, elem_classes="outpaint-section") as outpaint_options:
602
+ outpaint_prompt = gr.Textbox(
603
+ label="Outpaint Prompt",
604
+ placeholder="Describe what should appear in the extended areas",
605
+ value="extend the image naturally",
606
+ lines=2
607
+ )
608
+
609
  with gr.Row():
610
+ target_ratio = gr.Radio(
611
+ label="Target Ratio",
612
+ choices=["9:16", "16:9", "1:1", "Custom"],
613
+ value="16:9"
614
+ )
615
+ alignment_dropdown = gr.Dropdown(
616
+ choices=["Middle", "Left", "Right", "Top", "Bottom"],
617
+ value="Middle",
618
+ label="Alignment"
619
+ )
620
+
621
+ with gr.Row():
622
+ target_width = gr.Slider(
623
  label="Target Width",
624
  minimum=512,
625
  maximum=2048,
626
  step=8,
627
+ value=1280
628
  )
629
+ target_height = gr.Slider(
630
  label="Target Height",
631
  minimum=512,
632
  maximum=2048,
633
  step=8,
634
+ value=720
635
  )
636
 
637
+ with gr.Accordion("Advanced Outpaint Settings", open=False):
638
  overlap_percentage = gr.Slider(
639
  label="Mask overlap (%)",
640
  minimum=1,
641
  maximum=50,
642
  value=10,
643
  step=1,
644
+ info="Controls the blending area"
645
  )
646
 
647
  with gr.Row():
 
650
  with gr.Row():
651
  overlap_left = gr.Checkbox(label="Overlap Left", value=True)
652
  overlap_bottom = gr.Checkbox(label="Overlap Bottom", value=True)
653
+
654
+ with gr.Row():
655
+ resize_option = gr.Radio(
656
+ label="Resize input image",
657
+ choices=["Full", "50%", "33%", "25%", "Custom"],
658
+ value="Full"
659
+ )
660
+ custom_resize_percentage = gr.Slider(
661
+ label="Custom resize (%)",
662
+ minimum=1,
663
+ maximum=100,
664
+ step=1,
665
+ value=50,
666
+ visible=False
667
+ )
668
 
669
+ preview_outpaint_btn = gr.Button(
670
+ "👁️ Preview Outpaint Mask",
671
+ variant="secondary"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
+ generate_btn = gr.Button(
675
+ "Generate Magic with Auto-Upscale ✨",
676
+ variant="primary",
677
+ size="lg"
678
+ )
679
+
680
+ # Right Column - Output
681
+ with gr.Column(scale=1):
682
+ output_image = gr.Image(
683
+ label="Generated & Upscaled Result",
684
+ type="pil",
685
+ height=420,
686
+ elem_classes="image-container output-image"
687
+ )
688
 
689
+ status = gr.Textbox(
690
+ label="Status",
691
+ interactive=False,
692
+ lines=1,
693
+ elem_classes="status-text",
694
+ value="Ready to generate..."
695
+ )
696
 
697
+ outpaint_preview = gr.Image(
698
+ label="Outpaint Preview (red area will be generated)",
699
+ type="pil",
700
+ visible=False,
701
+ elem_classes="preview-container"
702
+ )
703
 
704
+ gr.Markdown("""
705
+ ### 📌 Features:
706
+ - **Style Transfer**: Apply artistic styles to your images
707
+ - **Outpainting** (Optional): Expand your images with AI
708
+ - **Auto 4x Upscaling**: All outputs are automatically upscaled for maximum quality
709
 
710
+ ### 💡 Tips:
711
+ - Upload 1-2 images to apply style transfer
712
+ - Enable outpainting to expand image boundaries
713
+ - All generated images are automatically upscaled 4x
714
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
715
 
716
+ # Event handlers
717
+ enable_outpaint.change(
718
+ fn=toggle_outpaint_options,
719
+ inputs=[enable_outpaint],
720
+ outputs=[outpaint_options]
 
 
 
 
 
 
 
721
  )
722
 
723
  target_ratio.change(
724
  fn=preload_presets,
725
+ inputs=[target_ratio],
726
+ outputs=[target_width, target_height]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
  )
728
 
729
  resize_option.change(
730
  fn=toggle_custom_resize_slider,
731
  inputs=[resize_option],
732
+ outputs=[custom_resize_percentage]
 
733
  )
734
 
735
+ preview_outpaint_btn.click(
736
+ fn=preview_outpaint,
 
 
 
 
 
737
  inputs=[
738
+ image1, target_width, target_height, overlap_percentage,
739
  resize_option, custom_resize_percentage, alignment_dropdown,
740
  overlap_left, overlap_right, overlap_top, overlap_bottom
741
  ],
742
+ outputs=[outpaint_preview]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  ).then(
744
  fn=lambda: gr.update(visible=True),
745
+ outputs=[outpaint_preview]
 
 
 
 
 
 
 
746
  )
747
 
748
+ generate_btn.click(
749
+ fn=process_images,
 
 
 
 
 
 
750
  inputs=[
751
+ prompt, image1, image2, enable_outpaint, outpaint_prompt,
752
+ target_width, target_height, overlap_percentage,
753
+ resize_option, custom_resize_percentage, alignment_dropdown,
754
+ overlap_left, overlap_right, overlap_top, overlap_bottom
 
 
 
 
 
 
 
 
 
 
 
 
 
755
  ],
756
+ outputs=[output_image, status]
 
 
 
 
 
 
 
 
 
 
757
  )
758
 
759
+ # Launch
760
  if __name__ == "__main__":
761
+ demo.launch(
762
+ share=True,
763
+ server_name="0.0.0.0",
764
+ server_port=7860
765
+ )