Nano-Banana-PRO / app.py
ginipick's picture
Update app.py
01504d4 verified
raw
history blame
30.6 kB
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}"
@spaces.GPU(duration=90)
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
@spaces.GPU(duration=180)
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
)