Spaces:
Running
on
Zero
Running
on
Zero
import gradio as gr | |
import spaces | |
import replicate | |
import os | |
from PIL import Image, ImageDraw | |
import requests | |
from io import BytesIO | |
import time | |
import tempfile | |
import base64 | |
import numpy as np | |
import gc | |
# Set up Replicate API key from environment variable | |
os.environ['REPLICATE_API_TOKEN'] = os.getenv('REPLICATE_API_TOKEN') | |
# --- Helper Functions --- | |
def get_with_retry(url, max_retries=3, timeout=60): | |
"""URL 요청을 재시도 로직과 함께 처리""" | |
for i in range(max_retries): | |
try: | |
response = requests.get(url, timeout=timeout) | |
if response.status_code == 200: | |
return response | |
except requests.exceptions.Timeout: | |
if i == max_retries - 1: | |
raise | |
time.sleep(2) # 재시도 전 대기 | |
except Exception as e: | |
if i == max_retries - 1: | |
raise | |
time.sleep(2) | |
return None | |
def validate_image_size(image, max_size=2048): | |
"""이미지 크기 검증 및 리사이즈""" | |
if image is None: | |
return None | |
if image.width > max_size or image.height > max_size: | |
ratio = min(max_size/image.width, max_size/image.height) | |
new_size = (int(image.width * ratio), int(image.height * ratio)) | |
image = image.resize(new_size, Image.LANCZOS) | |
return image | |
# --- Outpainting Functions --- | |
def can_expand(source_width, source_height, target_width, target_height, alignment): | |
"""Checks if the image can be expanded based on the alignment.""" | |
if alignment in ("Left", "Right") and source_width >= target_width: | |
return False | |
if alignment in ("Top", "Bottom") and source_height >= target_height: | |
return False | |
return True | |
def prepare_image_and_mask(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom): | |
"""Prepares the image with white margins and creates a mask for outpainting.""" | |
target_size = (width, height) | |
# Calculate the scaling factor to fit the image within the target size | |
scale_factor = min(target_size[0] / image.width, target_size[1] / image.height) | |
new_width = int(image.width * scale_factor) | |
new_height = int(image.height * scale_factor) | |
# Resize the source image to fit within target size | |
source = image.resize((new_width, new_height), Image.LANCZOS) | |
# Apply resize option using percentages | |
if resize_option == "Full": | |
resize_percentage = 100 | |
elif resize_option == "50%": | |
resize_percentage = 50 | |
elif resize_option == "33%": | |
resize_percentage = 33 | |
elif resize_option == "25%": | |
resize_percentage = 25 | |
else: # Custom | |
resize_percentage = custom_resize_percentage | |
# Calculate new dimensions based on percentage | |
resize_factor = resize_percentage / 100 | |
new_width = int(source.width * resize_factor) | |
new_height = int(source.height * resize_factor) | |
# Ensure minimum size of 64 pixels | |
new_width = max(new_width, 64) | |
new_height = max(new_height, 64) | |
# Resize the image | |
source = source.resize((new_width, new_height), Image.LANCZOS) | |
# Calculate the overlap in pixels based on the percentage | |
overlap_x = int(new_width * (overlap_percentage / 100)) | |
overlap_y = int(new_height * (overlap_percentage / 100)) | |
# Ensure minimum overlap of 1 pixel | |
overlap_x = max(overlap_x, 1) | |
overlap_y = max(overlap_y, 1) | |
# Calculate margins based on alignment | |
if alignment == "Middle": | |
margin_x = (target_size[0] - new_width) // 2 | |
margin_y = (target_size[1] - new_height) // 2 | |
elif alignment == "Left": | |
margin_x = 0 | |
margin_y = (target_size[1] - new_height) // 2 | |
elif alignment == "Right": | |
margin_x = target_size[0] - new_width | |
margin_y = (target_size[1] - new_height) // 2 | |
elif alignment == "Top": | |
margin_x = (target_size[0] - new_width) // 2 | |
margin_y = 0 | |
elif alignment == "Bottom": | |
margin_x = (target_size[0] - new_width) // 2 | |
margin_y = target_size[1] - new_height | |
# Adjust margins to eliminate gaps | |
margin_x = max(0, min(margin_x, target_size[0] - new_width)) | |
margin_y = max(0, min(margin_y, target_size[1] - new_height)) | |
# Create a new background image with white margins and paste the resized source image | |
background = Image.new('RGB', target_size, (255, 255, 255)) | |
background.paste(source, (margin_x, margin_y)) | |
# Create the mask | |
mask = Image.new('L', target_size, 255) | |
mask_draw = ImageDraw.Draw(mask) | |
# Calculate overlap areas | |
white_gaps_patch = 2 | |
left_overlap = margin_x + overlap_x if overlap_left else margin_x + white_gaps_patch | |
right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width - white_gaps_patch | |
top_overlap = margin_y + overlap_y if overlap_top else margin_y + white_gaps_patch | |
bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height - white_gaps_patch | |
if alignment == "Left": | |
left_overlap = margin_x + overlap_x if overlap_left else margin_x | |
elif alignment == "Right": | |
right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width | |
elif alignment == "Top": | |
top_overlap = margin_y + overlap_y if overlap_top else margin_y | |
elif alignment == "Bottom": | |
bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height | |
# Draw the mask | |
mask_draw.rectangle([ | |
(left_overlap, top_overlap), | |
(right_overlap, bottom_overlap) | |
], fill=0) | |
return background, mask | |
def preview_outpaint(image, width, height, overlap_percentage, resize_option, custom_resize_percentage, alignment, overlap_left, overlap_right, overlap_top, overlap_bottom): | |
"""Creates a preview showing the mask overlay for outpainting.""" | |
if not image: | |
return None | |
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) | |
# Create a preview image showing the mask | |
preview = background.copy().convert('RGBA') | |
# Create a semi-transparent red overlay | |
red_overlay = Image.new('RGBA', background.size, (255, 0, 0, 64)) # Reduced alpha to 64 (25% opacity) | |
# Convert black pixels in the mask to semi-transparent red | |
red_mask = Image.new('RGBA', background.size, (0, 0, 0, 0)) | |
red_mask.paste(red_overlay, (0, 0), mask) | |
# Overlay the red mask on the background | |
preview = Image.alpha_composite(preview, red_mask) | |
return preview | |
# --- Image Upload Functions --- | |
def upload_image_to_hosting(image): | |
""" | |
Upload image to multiple hosting services with fallback | |
""" | |
try: | |
# Method 1: Try imgbb.com (most reliable) | |
buffered = BytesIO() | |
image.save(buffered, format="PNG") | |
buffered.seek(0) | |
img_base64 = base64.b64encode(buffered.getvalue()).decode() | |
response = requests.post( | |
"https://api.imgbb.com/1/upload", | |
data={ | |
'key': '6d207e02198a847aa98d0a2a901485a5', | |
'image': img_base64, | |
}, | |
timeout=30 | |
) | |
if response.status_code == 200: | |
data = response.json() | |
if data.get('success'): | |
return data['data']['url'] | |
except: | |
pass | |
try: | |
# Method 2: Try 0x0.st (simple and reliable) | |
buffered = BytesIO() | |
image.save(buffered, format="PNG") | |
buffered.seek(0) | |
files = {'file': ('image.png', buffered, 'image/png')} | |
response = requests.post("https://0x0.st", files=files, timeout=30) | |
if response.status_code == 200: | |
return response.text.strip() | |
except: | |
pass | |
# Method 3: Fallback to base64 | |
buffered = BytesIO() | |
image.save(buffered, format="PNG") | |
buffered.seek(0) | |
img_base64 = base64.b64encode(buffered.getvalue()).decode() | |
return f"data:image/png;base64,{img_base64}" | |
def upscale_image(image): | |
""" | |
Upscale the generated image using Real-ESRGAN (mandatory) | |
""" | |
if not image: | |
return None, "No image to upscale" | |
if not os.getenv('REPLICATE_API_TOKEN'): | |
return None, "Please set REPLICATE_API_TOKEN" | |
start_time = time.time() | |
try: | |
# 시간 체크 | |
if time.time() - start_time > 80: | |
return image, "Time limit approaching, returning original image" | |
# Upload image to hosting | |
image_url = upload_image_to_hosting(image) | |
# Run Real-ESRGAN model | |
output = replicate.run( | |
"nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa", | |
input={ | |
"image": image_url, | |
"scale": 4 # 4x upscaling as default | |
} | |
) | |
if output is None: | |
return None, "No output received from upscaler" | |
# Get the upscaled image with retry logic | |
output_url = None | |
if isinstance(output, str): | |
output_url = output | |
elif isinstance(output, list) and len(output) > 0: | |
output_url = output[0] | |
elif hasattr(output, 'url'): | |
output_url = output.url() | |
if output_url: | |
response = get_with_retry(output_url, max_retries=3, timeout=60) | |
if response and response.status_code == 200: | |
img = Image.open(BytesIO(response.content)) | |
gc.collect() # 메모리 정리 | |
return img, "🔍 Upscaled 4x successfully!" | |
# Try direct read if available | |
if hasattr(output, 'read'): | |
img_data = output.read() | |
img = Image.open(BytesIO(img_data)) | |
gc.collect() | |
return img, "🔍 Upscaled 4x successfully!" | |
return None, "Could not process upscaled output" | |
except spaces.GPUError as e: | |
gc.collect() | |
return image, "GPU resources exhausted, returning original image" | |
except Exception as e: | |
gc.collect() | |
return image, f"Upscale error (returning original): {str(e)[:50]}" | |
def apply_outpainting_to_image(image, outpaint_prompt, target_width, target_height, | |
overlap_percentage, resize_option, custom_resize_percentage, | |
alignment, overlap_left, overlap_right, overlap_top, overlap_bottom): | |
""" | |
Apply outpainting to an image by preparing it with white margins | |
""" | |
if not image: | |
return None | |
# Check if expansion is possible | |
if not can_expand(image.width, image.height, target_width, target_height, alignment): | |
alignment = "Middle" | |
# Prepare the image with white margins for outpainting | |
outpaint_image, mask = prepare_image_and_mask( | |
image, target_width, target_height, overlap_percentage, | |
resize_option, custom_resize_percentage, alignment, | |
overlap_left, overlap_right, overlap_top, overlap_bottom | |
) | |
return outpaint_image | |
def process_images(prompt, image1, image2=None, enable_outpaint=False, outpaint_prompt="", | |
target_width=1280, target_height=720, overlap_percentage=10, | |
resize_option="Full", custom_resize_percentage=50, | |
alignment="Middle", overlap_left=True, overlap_right=True, | |
overlap_top=True, overlap_bottom=True, progress=gr.Progress()): | |
""" | |
Process uploaded images with Replicate API, apply optional outpainting, and mandatory upscaling | |
""" | |
if not image1: | |
return None, "Please upload at least one image" | |
if not os.getenv('REPLICATE_API_TOKEN'): | |
return None, "Please set REPLICATE_API_TOKEN" | |
start_time = time.time() | |
try: | |
progress(0.1, desc="Validating images...") | |
# Validate and resize images if needed | |
image1 = validate_image_size(image1) | |
if image2: | |
image2 = validate_image_size(image2) | |
# Time check | |
if time.time() - start_time > 170: | |
return None, "Processing time exceeded. Please try with smaller images." | |
progress(0.2, desc="Preparing images...") | |
# Step 1: Apply outpainting if enabled | |
if enable_outpaint: | |
# Apply outpainting to image1 | |
image1 = apply_outpainting_to_image( | |
image1, outpaint_prompt, target_width, target_height, | |
overlap_percentage, resize_option, custom_resize_percentage, | |
alignment, overlap_left, overlap_right, overlap_top, overlap_bottom | |
) | |
# Apply outpainting to image2 if it exists | |
if image2: | |
image2 = apply_outpainting_to_image( | |
image2, outpaint_prompt, target_width, target_height, | |
overlap_percentage, resize_option, custom_resize_percentage, | |
alignment, overlap_left, overlap_right, overlap_top, overlap_bottom | |
) | |
# Update the prompt if outpainting is enabled | |
if outpaint_prompt: | |
prompt = f"replace the white margins. {outpaint_prompt}. {prompt}" | |
progress(0.3, desc="Uploading to server...") | |
# Step 2: Upload images and process with Nano Banana | |
image_urls = [] | |
url1 = upload_image_to_hosting(image1) | |
image_urls.append(url1) | |
if image2: | |
url2 = upload_image_to_hosting(image2) | |
image_urls.append(url2) | |
# Time check | |
if time.time() - start_time > 170: | |
return None, "Processing time exceeded during upload." | |
progress(0.5, desc="Generating image...") | |
# Run the Nano Banana model | |
output = replicate.run( | |
"google/nano-banana", | |
input={ | |
"prompt": prompt, | |
"image_input": image_urls | |
} | |
) | |
if output is None: | |
return None, "No output received" | |
# Clear intermediate variables | |
del image_urls | |
gc.collect() | |
progress(0.7, desc="Processing output...") | |
# Get the generated image with retry logic | |
generated_image = None | |
# Try different methods to get the output | |
output_url = None | |
if isinstance(output, str): | |
output_url = output | |
elif isinstance(output, list) and len(output) > 0: | |
output_url = output[0] | |
elif hasattr(output, 'url'): | |
output_url = output.url() | |
if output_url: | |
response = get_with_retry(output_url, max_retries=3, timeout=60) | |
if response and response.status_code == 200: | |
generated_image = Image.open(BytesIO(response.content)) | |
# Try direct read if available | |
if not generated_image and hasattr(output, 'read'): | |
img_data = output.read() | |
generated_image = Image.open(BytesIO(img_data)) | |
if not generated_image: | |
return None, "Could not process output" | |
# Clear output variable | |
del output | |
gc.collect() | |
# Time check before upscaling | |
if time.time() - start_time > 170: | |
return generated_image, "✨ Generated (time limit reached, skipping upscale)" | |
progress(0.8, desc="Upscaling image...") | |
# Step 3: Apply mandatory upscaling | |
upscaled_image, upscale_status = upscale_image(generated_image) | |
# Clear generated image from memory | |
del generated_image | |
gc.collect() | |
progress(1.0, desc="Complete!") | |
if upscaled_image: | |
return upscaled_image, f"✨ Generated and {upscale_status}" | |
else: | |
# If upscaling fails, return the generated image with a warning | |
return generated_image, "✨ Generated (upscaling failed, returning original)" | |
except spaces.GPUError as e: | |
gc.collect() | |
return None, "GPU resources exhausted. Please try again later." | |
except requests.exceptions.Timeout: | |
gc.collect() | |
return None, "Request timeout. Please try with smaller images." | |
except Exception as e: | |
gc.collect() | |
return None, f"Error: {str(e)[:100]}" | |
finally: | |
gc.collect() # Final cleanup | |
def toggle_outpaint_options(enable): | |
"""Toggle visibility of outpainting options""" | |
return gr.update(visible=enable) | |
def preload_presets(target_ratio): | |
"""Updates the width and height based on the selected aspect ratio.""" | |
if target_ratio == "9:16": | |
return 720, 1280 | |
elif target_ratio == "16:9": | |
return 1280, 720 | |
elif target_ratio == "1:1": | |
return 1024, 1024 | |
else: # Custom | |
return 1280, 720 | |
def toggle_custom_resize_slider(resize_option): | |
return gr.update(visible=(resize_option == "Custom")) | |
# Enhanced CSS with modern, minimal design | |
css = """ | |
.gradio-container { | |
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
min-height: 100vh; | |
} | |
.header-container { | |
background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%); | |
padding: 2.5rem; | |
border-radius: 24px; | |
margin-bottom: 2.5rem; | |
box-shadow: 0 20px 60px rgba(255, 179, 71, 0.25); | |
} | |
.logo-text { | |
font-size: 3.5rem; | |
font-weight: 900; | |
color: #2d3436; | |
text-align: center; | |
margin: 0; | |
letter-spacing: -2px; | |
} | |
.subtitle { | |
color: #2d3436; | |
text-align: center; | |
font-size: 1rem; | |
margin-top: 0.5rem; | |
opacity: 0.8; | |
} | |
.main-content { | |
background: rgba(255, 255, 255, 0.95); | |
backdrop-filter: blur(20px); | |
border-radius: 24px; | |
padding: 2.5rem; | |
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08); | |
} | |
.gr-button-primary { | |
background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%) !important; | |
border: none !important; | |
color: #2d3436 !important; | |
font-weight: 700 !important; | |
font-size: 1.1rem !important; | |
padding: 1.2rem 2rem !important; | |
border-radius: 14px !important; | |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
width: 100%; | |
margin-top: 1rem !important; | |
} | |
.gr-button-primary:hover { | |
transform: translateY(-3px) !important; | |
box-shadow: 0 15px 40px rgba(255, 179, 71, 0.35) !important; | |
} | |
.gr-button-secondary { | |
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%) !important; | |
border: none !important; | |
color: white !important; | |
font-weight: 600 !important; | |
font-size: 0.95rem !important; | |
padding: 0.8rem 1.5rem !important; | |
border-radius: 12px !important; | |
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; | |
} | |
.gr-button-secondary:hover { | |
transform: translateY(-2px) !important; | |
box-shadow: 0 10px 30px rgba(9, 132, 227, 0.3) !important; | |
} | |
.gr-input, .gr-textarea { | |
background: #ffffff !important; | |
border: 2px solid #e1e8ed !important; | |
border-radius: 14px !important; | |
color: #2d3436 !important; | |
font-size: 1rem !important; | |
padding: 0.8rem 1rem !important; | |
} | |
.gr-input:focus, .gr-textarea:focus { | |
border-color: #ffd93d !important; | |
box-shadow: 0 0 0 4px rgba(255, 217, 61, 0.15) !important; | |
} | |
.gr-form { | |
background: transparent !important; | |
border: none !important; | |
} | |
.gr-panel { | |
background: #ffffff !important; | |
border: 2px solid #e1e8ed !important; | |
border-radius: 16px !important; | |
padding: 1.5rem !important; | |
} | |
.gr-box { | |
border-radius: 14px !important; | |
border-color: #e1e8ed !important; | |
} | |
label { | |
color: #636e72 !important; | |
font-weight: 600 !important; | |
font-size: 0.85rem !important; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
margin-bottom: 0.5rem !important; | |
} | |
.status-text { | |
font-family: 'SF Mono', 'Monaco', monospace; | |
color: #00b894; | |
font-size: 0.9rem; | |
} | |
.image-container { | |
border-radius: 14px !important; | |
overflow: hidden; | |
border: 2px solid #e1e8ed !important; | |
background: #fafbfc !important; | |
} | |
.preview-container { | |
border: 2px dashed #ff6b6b !important; | |
border-radius: 14px !important; | |
padding: 1rem !important; | |
background: rgba(255, 107, 107, 0.05) !important; | |
} | |
footer { | |
display: none !important; | |
} | |
/* Equal sizing for all image containers */ | |
.image-upload { | |
min-height: 200px !important; | |
max-height: 200px !important; | |
} | |
.output-image { | |
min-height: 420px !important; | |
max-height: 420px !important; | |
} | |
/* Ensure consistent spacing */ | |
.gr-row { | |
gap: 1rem !important; | |
} | |
.gr-column { | |
gap: 1rem !important; | |
} | |
/* Outpainting options styling */ | |
.outpaint-section { | |
background: rgba(116, 185, 255, 0.1) !important; | |
border: 2px solid #74b9ff !important; | |
border-radius: 14px !important; | |
padding: 1rem !important; | |
margin-top: 1rem !important; | |
} | |
""" | |
with gr.Blocks(css=css, theme=gr.themes.Base()) as demo: | |
with gr.Column(elem_classes="header-container"): | |
gr.HTML(""" | |
<h1 class="logo-text">🍌 Nano Banana PRO</h1> | |
<p class="subtitle">AI-Powered Image Style Transfer with Outpainting & Auto-Upscaling(X4)</p> | |
<div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px;"> | |
<a href="https://ginigen.com" target="_blank"> | |
<img src="https://img.shields.io/static/v1?label=ginigen&message=special%20ai%20sERVICE&color=%230000ff&labelColor=%23800080&logo=google&logoColor=white&style=for-the-badge" alt="badge"> | |
</a> | |
<a href="https://discord.gg/openfreeai" target="_blank"> | |
<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"> | |
</a> | |
</div> | |
""") | |
with gr.Column(elem_classes="main-content"): | |
with gr.Row(equal_height=True): | |
# Left Column - Inputs | |
with gr.Column(scale=1): | |
prompt = gr.Textbox( | |
label="Style Description", | |
placeholder="Describe your style...", | |
lines=3, | |
value="Make the sheets in the style of the logo. Make the scene natural.", | |
elem_classes="prompt-input" | |
) | |
with gr.Row(equal_height=True): | |
image1 = gr.Image( | |
label="Primary Image", | |
type="pil", | |
height=200, | |
elem_classes="image-container image-upload" | |
) | |
image2 = gr.Image( | |
label="Secondary Image (Optional)", | |
type="pil", | |
height=200, | |
elem_classes="image-container image-upload" | |
) | |
# Outpainting Options | |
enable_outpaint = gr.Checkbox( | |
label="🎨 Enable Outpainting (Expand Image)", | |
value=False | |
) | |
with gr.Column(visible=False, elem_classes="outpaint-section") as outpaint_options: | |
outpaint_prompt = gr.Textbox( | |
label="Outpaint Prompt", | |
placeholder="Describe what should appear in the extended areas", | |
value="extend the image naturally", | |
lines=2 | |
) | |
with gr.Row(): | |
target_ratio = gr.Radio( | |
label="Target Ratio", | |
choices=["9:16", "16:9", "1:1", "Custom"], | |
value="16:9" | |
) | |
alignment_dropdown = gr.Dropdown( | |
choices=["Middle", "Left", "Right", "Top", "Bottom"], | |
value="Middle", | |
label="Alignment" | |
) | |
with gr.Row(): | |
target_width = gr.Slider( | |
label="Target Width", | |
minimum=512, | |
maximum=2048, | |
step=8, | |
value=1280 | |
) | |
target_height = gr.Slider( | |
label="Target Height", | |
minimum=512, | |
maximum=2048, | |
step=8, | |
value=720 | |
) | |
with gr.Accordion("Advanced Outpaint Settings", open=False): | |
overlap_percentage = gr.Slider( | |
label="Mask overlap (%)", | |
minimum=1, | |
maximum=50, | |
value=10, | |
step=1, | |
info="Controls the blending area" | |
) | |
with gr.Row(): | |
overlap_top = gr.Checkbox(label="Overlap Top", value=True) | |
overlap_right = gr.Checkbox(label="Overlap Right", value=True) | |
with gr.Row(): | |
overlap_left = gr.Checkbox(label="Overlap Left", value=True) | |
overlap_bottom = gr.Checkbox(label="Overlap Bottom", value=True) | |
with gr.Row(): | |
resize_option = gr.Radio( | |
label="Resize input image", | |
choices=["Full", "50%", "33%", "25%", "Custom"], | |
value="Full" | |
) | |
custom_resize_percentage = gr.Slider( | |
label="Custom resize (%)", | |
minimum=1, | |
maximum=100, | |
step=1, | |
value=50, | |
visible=False | |
) | |
preview_outpaint_btn = gr.Button( | |
"👁️ Preview Outpaint Mask", | |
variant="secondary" | |
) | |
generate_btn = gr.Button( | |
"Generate Magic with Auto-Upscale ✨", | |
variant="primary", | |
size="lg" | |
) | |
# Right Column - Output | |
with gr.Column(scale=1): | |
output_image = gr.Image( | |
label="Generated & Upscaled Result", | |
type="pil", | |
height=420, | |
elem_classes="image-container output-image" | |
) | |
status = gr.Textbox( | |
label="Status", | |
interactive=False, | |
lines=1, | |
elem_classes="status-text", | |
value="Ready to generate..." | |
) | |
outpaint_preview = gr.Image( | |
label="Outpaint Preview (red area will be generated)", | |
type="pil", | |
visible=False, | |
elem_classes="preview-container" | |
) | |
gr.Markdown(""" | |
### 📌 Features: | |
- **Style Transfer**: Apply artistic styles to your images | |
- **Outpainting** (Optional): Expand your images with AI | |
- **Auto 4x Upscaling**: All outputs are automatically upscaled for maximum quality | |
- **Optimized for ZeroGPU**: Improved stability and reduced timeout issues | |
### 💡 Tips: | |
- Upload 1-2 images to apply style transfer | |
- Enable outpainting to expand image boundaries | |
- All generated images are automatically upscaled 4x | |
- Large images are automatically resized for optimal processing | |
""") | |
# Event handlers | |
enable_outpaint.change( | |
fn=toggle_outpaint_options, | |
inputs=[enable_outpaint], | |
outputs=[outpaint_options] | |
) | |
target_ratio.change( | |
fn=preload_presets, | |
inputs=[target_ratio], | |
outputs=[target_width, target_height] | |
) | |
resize_option.change( | |
fn=toggle_custom_resize_slider, | |
inputs=[resize_option], | |
outputs=[custom_resize_percentage] | |
) | |
preview_outpaint_btn.click( | |
fn=preview_outpaint, | |
inputs=[ | |
image1, target_width, target_height, overlap_percentage, | |
resize_option, custom_resize_percentage, alignment_dropdown, | |
overlap_left, overlap_right, overlap_top, overlap_bottom | |
], | |
outputs=[outpaint_preview] | |
).then( | |
fn=lambda: gr.update(visible=True), | |
outputs=[outpaint_preview] | |
) | |
generate_btn.click( | |
fn=process_images, | |
inputs=[ | |
prompt, image1, image2, enable_outpaint, outpaint_prompt, | |
target_width, target_height, overlap_percentage, | |
resize_option, custom_resize_percentage, alignment_dropdown, | |
overlap_left, overlap_right, overlap_top, overlap_bottom | |
], | |
outputs=[output_image, status] | |
) | |
# Launch | |
if __name__ == "__main__": | |
demo.launch( | |
share=True, | |
server_name="0.0.0.0", | |
server_port=7860 | |
) |