|
import gradio as gr |
|
import replicate |
|
import os |
|
from PIL import Image |
|
import requests |
|
from io import BytesIO |
|
import time |
|
import tempfile |
|
import base64 |
|
|
|
|
|
os.environ['REPLICATE_API_TOKEN'] = os.getenv('REPLICATE_API_TOKEN') |
|
|
|
def upload_image_to_hosting(image): |
|
""" |
|
Upload image to multiple hosting services with fallback |
|
""" |
|
|
|
try: |
|
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, |
|
} |
|
) |
|
|
|
if response.status_code == 200: |
|
data = response.json() |
|
if data.get('success'): |
|
return data['data']['url'] |
|
except: |
|
pass |
|
|
|
|
|
try: |
|
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) |
|
|
|
if response.status_code == 200: |
|
return response.text.strip() |
|
except: |
|
pass |
|
|
|
|
|
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 |
|
""" |
|
if not image: |
|
return None, "Please generate an image first" |
|
|
|
if not os.getenv('REPLICATE_API_TOKEN'): |
|
return None, "Please set REPLICATE_API_TOKEN" |
|
|
|
try: |
|
|
|
image_url = upload_image_to_hosting(image) |
|
|
|
|
|
output = replicate.run( |
|
"nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa", |
|
input={ |
|
"image": image_url, |
|
"scale": 4 |
|
} |
|
) |
|
|
|
if output is None: |
|
return None, "No output received from upscaler" |
|
|
|
|
|
try: |
|
if hasattr(output, 'read'): |
|
img_data = output.read() |
|
img = Image.open(BytesIO(img_data)) |
|
return img, "๐ Upscaled 4x successfully!" |
|
except: |
|
pass |
|
|
|
try: |
|
if hasattr(output, 'url'): |
|
output_url = output.url() |
|
response = requests.get(output_url, timeout=30) |
|
if response.status_code == 200: |
|
img = Image.open(BytesIO(response.content)) |
|
return img, "๐ Upscaled 4x successfully!" |
|
except: |
|
pass |
|
|
|
output_url = None |
|
if isinstance(output, str): |
|
output_url = output |
|
elif isinstance(output, list) and len(output) > 0: |
|
output_url = output[0] |
|
|
|
if output_url: |
|
response = requests.get(output_url, timeout=30) |
|
if response.status_code == 200: |
|
img = Image.open(BytesIO(response.content)) |
|
return img, "๐ Upscaled 4x successfully!" |
|
|
|
return None, "Could not process upscaled output" |
|
|
|
except Exception as e: |
|
return None, f"Upscale error: {str(e)[:100]}" |
|
|
|
def process_images(prompt, image1, image2=None): |
|
""" |
|
Process uploaded images with Replicate API |
|
""" |
|
if not image1: |
|
return None, "Please upload at least one image", gr.update(visible=False) |
|
|
|
if not os.getenv('REPLICATE_API_TOKEN'): |
|
return None, "Please set REPLICATE_API_TOKEN", gr.update(visible=False) |
|
|
|
try: |
|
image_urls = [] |
|
|
|
|
|
url1 = upload_image_to_hosting(image1) |
|
image_urls.append(url1) |
|
|
|
if image2: |
|
url2 = upload_image_to_hosting(image2) |
|
image_urls.append(url2) |
|
|
|
|
|
output = replicate.run( |
|
"google/nano-banana", |
|
input={ |
|
"prompt": prompt, |
|
"image_input": image_urls |
|
} |
|
) |
|
|
|
if output is None: |
|
return None, "No output received", gr.update(visible=False) |
|
|
|
|
|
try: |
|
if hasattr(output, 'read'): |
|
img_data = output.read() |
|
img = Image.open(BytesIO(img_data)) |
|
return img, "โจ Generated successfully!", gr.update(visible=True) |
|
except: |
|
pass |
|
|
|
try: |
|
if hasattr(output, 'url'): |
|
output_url = output.url() |
|
response = requests.get(output_url, timeout=30) |
|
if response.status_code == 200: |
|
img = Image.open(BytesIO(response.content)) |
|
return img, "โจ Generated successfully!", gr.update(visible=True) |
|
except: |
|
pass |
|
|
|
output_url = None |
|
if isinstance(output, str): |
|
output_url = output |
|
elif isinstance(output, list) and len(output) > 0: |
|
output_url = output[0] |
|
|
|
if output_url: |
|
response = requests.get(output_url, timeout=30) |
|
if response.status_code == 200: |
|
img = Image.open(BytesIO(response.content)) |
|
return img, "โจ Generated successfully!", gr.update(visible=True) |
|
|
|
return None, "Could not process output", gr.update(visible=False) |
|
|
|
except Exception as e: |
|
return None, f"Error: {str(e)[:100]}", gr.update(visible=False) |
|
|
|
|
|
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; |
|
} |
|
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; |
|
} |
|
""" |
|
|
|
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 Upscale(X4)</h1> |
|
<p class="subtitle">AI-Powered Image Style Transfer</p> |
|
<div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px;"> |
|
<a href="https://huggingface.co/spaces/ginigen/Nano-Banana-PRO" target="_blank"> |
|
<img src="https://img.shields.io/static/v1?label=NANO%20BANANA&message=PRO&color=%230000ff&labelColor=%23800080&logo=HUGGINGFACE&logoColor=white&style=for-the-badge" alt="badge"> |
|
</a> |
|
<a href="https://huggingface.co/spaces/openfree/Free-Nano-Banana" target="_blank"> |
|
<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"> |
|
</a> |
|
<a href="https://huggingface.co/spaces/aiqtech/Nano-Banana-API" target="_blank"> |
|
<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"> |
|
</a> |
|
<a href="https://huggingface.co/spaces/ginigen/Nano-Banana-Video" target="_blank"> |
|
<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"> |
|
</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): |
|
|
|
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", |
|
type="pil", |
|
height=200, |
|
elem_classes="image-container image-upload" |
|
) |
|
|
|
generate_btn = gr.Button( |
|
"Generate Magic โจ", |
|
variant="primary", |
|
size="lg" |
|
) |
|
|
|
|
|
with gr.Column(scale=1): |
|
output_image = gr.Image( |
|
label="Generated Result", |
|
type="pil", |
|
height=420, |
|
elem_classes="image-container output-image" |
|
) |
|
|
|
with gr.Row(): |
|
status = gr.Textbox( |
|
label="Status", |
|
interactive=False, |
|
lines=1, |
|
elem_classes="status-text", |
|
value="Ready to generate...", |
|
scale=3 |
|
) |
|
|
|
upscale_btn = gr.Button( |
|
"๐ Upscale (4x)", |
|
variant="secondary", |
|
size="lg", |
|
visible=False, |
|
scale=1 |
|
) |
|
|
|
|
|
generate_btn.click( |
|
fn=process_images, |
|
inputs=[prompt, image1, image2], |
|
outputs=[output_image, status, upscale_btn] |
|
) |
|
|
|
upscale_btn.click( |
|
fn=upscale_image, |
|
inputs=[output_image], |
|
outputs=[output_image, status] |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
demo.launch( |
|
share=True, |
|
server_name="0.0.0.0", |
|
server_port=7860 |
|
) |