Nano-Banana-API / app.py
aiqtech's picture
Update app.py
5ad5226 verified
raw
history blame
17.9 kB
import gradio as gr
import replicate
import os
from typing import Optional, List
from huggingface_hub import whoami
from PIL import Image
import requests
from io import BytesIO
import tempfile
# --- Replicate API Configuration ---
REPLICATE_API_TOKEN = os.getenv("REPLICATE_API_TOKEN")
if not REPLICATE_API_TOKEN:
raise ValueError("REPLICATE_API_TOKEN environment variable is not set.")
# Initialize Replicate client
os.environ["REPLICATE_API_TOKEN"] = REPLICATE_API_TOKEN
def verify_login_status(token: Optional[gr.OAuthToken]) -> bool:
"""Verifies if the user is logged in to Hugging Face."""
if not token:
return False
try:
user_info = whoami(token=token.token)
return True if user_info else False
except Exception as e:
print(f"Could not verify user's login status: {e}")
return False
def run_single_image_logic(prompt: str, image_path: Optional[str] = None, progress=gr.Progress()) -> str:
"""Handles text-to-image or single image-to-image using Replicate's Nano Banana."""
try:
progress(0.2, desc="๐ŸŽจ ์ค€๋น„ ์ค‘...")
# Prepare input for Replicate API
input_data = {
"prompt": prompt
}
# If there's an input image, add it to the input
if image_path:
# Upload the local image file and get a URL
# For Replicate, we need to provide URLs, not local paths
# We'll read the file and create a temporary URL or use file directly
input_data["image_input"] = [image_path]
progress(0.5, desc="โœจ ์ƒ์„ฑ ์ค‘...")
# Run the model on Replicate
output = replicate.run(
"google/nano-banana",
input=input_data
)
progress(0.8, desc="๐Ÿ–ผ๏ธ ๋งˆ๋ฌด๋ฆฌ ์ค‘...")
# Handle the output
if output:
# If output is a FileOutput object, get the URL
if hasattr(output, 'url'):
image_url = output.url()
elif isinstance(output, str):
image_url = output
elif isinstance(output, list) and len(output) > 0:
image_url = output[0]
else:
raise ValueError("Unexpected output format from Replicate")
# Download the image from URL
response = requests.get(image_url)
response.raise_for_status()
# Save to temporary file
img = Image.open(BytesIO(response.content))
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmpfile:
img.save(tmpfile.name)
progress(1.0, desc="โœ… ์™„๋ฃŒ!")
return tmpfile.name
else:
raise ValueError("No output received from Replicate API")
except Exception as e:
print(f"Error details: {e}")
print(f"Error type: {type(e)}")
raise gr.Error(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {e}")
def run_multi_image_logic(prompt: str, images: List[str], progress=gr.Progress()) -> str:
"""
Handles multi-image editing by sending a list of images and a prompt.
"""
if not images:
raise gr.Error("'์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€' ํƒญ์—์„œ ์ตœ์†Œ ํ•œ ๊ฐœ์˜ ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”.")
try:
progress(0.2, desc="๐ŸŽจ ์ด๋ฏธ์ง€ ์ค€๋น„ ์ค‘...")
# Prepare input for Replicate API with multiple images
input_data = {
"prompt": prompt,
"image_input": images # Pass the list of image paths directly
}
progress(0.5, desc="โœจ ์ƒ์„ฑ ์ค‘...")
# Run the model on Replicate
output = replicate.run(
"google/nano-banana",
input=input_data
)
progress(0.8, desc="๐Ÿ–ผ๏ธ ๋งˆ๋ฌด๋ฆฌ ์ค‘...")
# Handle the output
if output:
# If output is a FileOutput object, get the URL
if hasattr(output, 'url'):
image_url = output.url()
elif isinstance(output, str):
image_url = output
elif isinstance(output, list) and len(output) > 0:
image_url = output[0]
else:
raise ValueError("Unexpected output format from Replicate")
# Download the image from URL
response = requests.get(image_url)
response.raise_for_status()
# Save to temporary file
img = Image.open(BytesIO(response.content))
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmpfile:
img.save(tmpfile.name)
progress(1.0, desc="โœ… ์™„๋ฃŒ!")
return tmpfile.name
else:
raise ValueError("No output received from Replicate API")
except Exception as e:
print(f"Multi-image error details: {e}")
raise gr.Error(f"์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ: {e}")
# --- Gradio App UI ---
css = '''
/* Header Styling */
.main-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header-title {
font-size: 2.5rem !important;
font-weight: bold;
color: white;
text-align: center;
margin: 0 !important;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.header-subtitle {
color: rgba(255,255,255,0.9);
text-align: center;
margin-top: 0.5rem !important;
font-size: 1.1rem;
}
/* Card Styling */
.card {
background: white;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.05);
}
.dark .card {
background: #1f2937;
border: 1px solid #374151;
}
/* Tab Styling */
.tabs {
border-radius: 0.5rem;
overflow: hidden;
margin-bottom: 1rem;
}
.tabitem {
padding: 1rem !important;
}
button.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
}
/* Button Styling */
.generate-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
color: white !important;
font-size: 1.1rem !important;
font-weight: 600 !important;
padding: 0.8rem 2rem !important;
border-radius: 0.5rem !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
width: 100% !important;
margin-top: 1rem !important;
}
.generate-btn:hover {
transform: translateY(-2px) !important;
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4) !important;
}
.use-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
border: none !important;
color: white !important;
font-weight: 600 !important;
padding: 0.6rem 1.5rem !important;
border-radius: 0.5rem !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
width: 100% !important;
}
.use-btn:hover {
transform: translateY(-1px) !important;
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.4) !important;
}
/* Input Styling */
.prompt-input textarea {
border-radius: 0.5rem !important;
border: 2px solid #e5e7eb !important;
padding: 0.8rem !important;
font-size: 1rem !important;
transition: border-color 0.3s ease !important;
}
.prompt-input textarea:focus {
border-color: #667eea !important;
outline: none !important;
}
.dark .prompt-input textarea {
border-color: #374151 !important;
background: #1f2937 !important;
}
/* Image Output Styling */
#output {
border-radius: 0.5rem !important;
overflow: hidden !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
}
/* Progress Bar Styling */
.progress-bar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
}
/* Examples Styling */
.examples {
background: #f9fafb;
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.dark .examples {
background: #1f2937;
}
/* Login Message Styling */
.login-message {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 1rem;
padding: 2rem;
text-align: center;
border: 2px solid #f59e0b;
}
.dark .login-message {
background: linear-gradient(135deg, #7c2d12 0%, #92400e 100%);
border-color: #f59e0b;
}
/* Emoji Animations */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.emoji-icon {
display: inline-block;
animation: bounce 2s infinite;
}
/* Responsive Design */
@media (max-width: 768px) {
.header-title {
font-size: 2rem !important;
}
.main-container {
padding: 1rem !important;
}
}
'''
with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
# Header
gr.HTML('''
<div class="main-header">
<h1 class="header-title">
๐ŸŒ Real Nano Banana
</h1>
<p class="header-subtitle">
Google Nano Banana๋กœ ๊ตฌ๋™๋˜๋Š” AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ๊ธฐ
</p>
</div>
''')
# Login Notice
gr.HTML('''
<div style="background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem;
border-left: 4px solid #0284c7;">
<p style="margin: 0; color: #075985; font-weight: 600;">
๐Ÿ” ์ด ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Hugging Face ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.
</p>
</div>
''')
login_message = gr.Markdown(visible=False)
main_interface = gr.Column(visible=False, elem_classes="main-container")
with main_interface:
with gr.Row():
with gr.Column(scale=1):
gr.HTML('<div class="card">')
# Mode Selection
gr.HTML('<h3 style="margin-top: 0;">๐Ÿ“ธ ๋ชจ๋“œ ์„ ํƒ</h3>')
active_tab_state = gr.State(value="single")
with gr.Tabs(elem_classes="tabs") as tabs:
with gr.TabItem("๐Ÿ–ผ๏ธ ๋‹จ์ผ ์ด๋ฏธ์ง€", id="single") as single_tab:
image_input = gr.Image(
type="filepath",
label="์ž…๋ ฅ ์ด๋ฏธ์ง€ (์„ ํƒ์‚ฌํ•ญ)",
elem_classes="image-input"
)
gr.HTML('''
<p style="text-align: center; color: #6b7280; font-size: 0.9rem; margin-top: 0.5rem;">
๐Ÿ’ก ํ…์ŠคํŠธโ†’์ด๋ฏธ์ง€ ์ƒ์„ฑ์€ ๋น„์›Œ๋‘์„ธ์š”
</p>
''')
with gr.TabItem("๐ŸŽจ ๋‹ค์ค‘ ์ด๋ฏธ์ง€", id="multiple") as multi_tab:
gallery_input = gr.Gallery(
label="์ž…๋ ฅ ์ด๋ฏธ์ง€๋“ค",
file_types=["image"],
elem_classes="gallery-input"
)
gr.HTML('''
<p style="text-align: center; color: #6b7280; font-size: 0.9rem; margin-top: 0.5rem;">
๐Ÿ’ก ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญํ•˜์„ธ์š”
</p>
''')
# Prompt Input
gr.HTML('<h3>โœ๏ธ ํ”„๋กฌํ”„ํŠธ</h3>')
prompt_input = gr.Textbox(
label="",
info="AI์—๊ฒŒ ์›ํ•˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”",
placeholder="์˜ˆ: ๋ง›์žˆ์–ด ๋ณด์ด๋Š” ํ”ผ์ž, ์šฐ์ฃผ๋ฅผ ๋ฐฐ๊ฒฝ์œผ๋กœ ํ•œ ๊ณ ์–‘์ด, ๋ฏธ๋ž˜์ ์ธ ๋„์‹œ ํ’๊ฒฝ...",
lines=3,
elem_classes="prompt-input"
)
# Generate Button
generate_button = gr.Button(
"๐Ÿš€ ์ƒ์„ฑํ•˜๊ธฐ",
variant="primary",
elem_classes="generate-btn"
)
# Examples
with gr.Accordion("๐Ÿ’ก ์˜ˆ์ œ ํ”„๋กฌํ”„ํŠธ", open=False):
gr.Examples(
examples=[
["A delicious looking pizza with melting cheese"],
["A cat in a spacesuit walking on the moon surface"],
["Cyberpunk city at night with neon lights"],
["Japanese garden with cherry blossoms in spring"],
["Fantasy wizard tower in a magical world"],
["Make the scene more dramatic and cinematic"],
["Transform this into a watercolor painting style"],
],
inputs=prompt_input
)
gr.HTML('</div>')
with gr.Column(scale=1):
gr.HTML('<div class="card">')
gr.HTML('<h3 style="margin-top: 0;">๐ŸŽจ ์ƒ์„ฑ ๊ฒฐ๊ณผ</h3>')
output_image = gr.Image(
label="",
interactive=False,
elem_id="output"
)
use_image_button = gr.Button(
"โ™ป๏ธ ์ด ์ด๋ฏธ์ง€๋ฅผ ๋‹ค์Œ ํŽธ์ง‘์— ์‚ฌ์šฉ",
elem_classes="use-btn",
visible=False
)
# Tips
gr.HTML('''
<div style="background: #f0f9ff; border-radius: 0.5rem; padding: 1rem; margin-top: 1rem;">
<h4 style="margin-top: 0; color: #0369a1;">๐Ÿ’ก ํŒ</h4>
<ul style="margin: 0; padding-left: 1.5rem; color: #0c4a6e;">
<li>๊ตฌ์ฒด์ ์ด๊ณ  ์ƒ์„ธํ•œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”</li>
<li>์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜๋ณต์ ์œผ๋กœ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค</li>
<li>๋‹ค์ค‘ ์ด๋ฏธ์ง€ ๋ชจ๋“œ๋กœ ์—ฌ๋Ÿฌ ์ฐธ์กฐ ์ด๋ฏธ์ง€๋ฅผ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค</li>
<li>์˜์–ด ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋” ์ข‹์€ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค</li>
</ul>
</div>
''')
gr.HTML('</div>')
# Footer
gr.HTML('''
<div style="text-align: center; margin-top: 2rem; padding: 1rem;
border-top: 1px solid #e5e7eb;">
<p style="color: #6b7280;">
Made with ๐Ÿ’œ using Replicate API | Powered by Google Nano Banana
</p>
</div>
''')
login_button = gr.LoginButton()
# --- Event Handlers ---
def unified_generator(
prompt: str,
single_image: Optional[str],
multi_images: Optional[List[str]],
active_tab: str,
oauth_token: Optional[gr.OAuthToken] = None,
):
if not verify_login_status(oauth_token):
raise gr.Error("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ƒ๋‹จ์˜ 'Sign in with Hugging Face' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ด์ฃผ์„ธ์š”.")
if not prompt:
raise gr.Error("ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
if active_tab == "multiple" and multi_images:
result = run_multi_image_logic(prompt, multi_images)
else:
result = run_single_image_logic(prompt, single_image)
return result, gr.update(visible=True)
single_tab.select(lambda: "single", None, active_tab_state)
multi_tab.select(lambda: "multiple", None, active_tab_state)
generate_button.click(
unified_generator,
inputs=[prompt_input, image_input, gallery_input, active_tab_state],
outputs=[output_image, use_image_button],
)
use_image_button.click(
lambda img: (img, gr.update(visible=False)),
inputs=[output_image],
outputs=[image_input, use_image_button]
)
# --- Access Control Logic ---
def control_access(
profile: Optional[gr.OAuthProfile] = None,
oauth_token: Optional[gr.OAuthToken] = None
):
if not profile:
return gr.update(visible=False), gr.update(visible=False)
if verify_login_status(oauth_token):
return gr.update(visible=True), gr.update(visible=False)
else:
message = '''
<div class="login-message">
<h2>๐Ÿ” ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค</h2>
<p style="font-size: 1.1rem; margin: 1rem 0;">
์ด AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Hugging Face ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.
</p>
<p style="margin: 1rem 0;">
๋กœ๊ทธ์ธํ•˜๋ฉด ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
</p>
<ul style="text-align: left; display: inline-block; margin: 1rem 0;">
<li>๐Ÿš€ Google Nano Banana๋ฅผ ํ†ตํ•œ ๊ณ ํ’ˆ์งˆ ์ด๋ฏธ์ง€ ์ƒ์„ฑ</li>
<li>โšก ๋น ๋ฅธ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฐ ํŽธ์ง‘</li>
<li>๐ŸŽจ ํ…์ŠคํŠธ๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜</li>
<li>๐Ÿ”ง ๋‹ค์ค‘ ์ด๋ฏธ์ง€ ํŽธ์ง‘ ๋ฐ ๊ฒฐํ•ฉ</li>
</ul>
<p style="margin-top: 1.5rem; font-weight: bold;">
์ƒ๋‹จ์˜ "Sign in with Hugging Face" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์‹œ์ž‘ํ•˜์„ธ์š”!
</p>
</div>
'''
return gr.update(visible=False), gr.update(visible=True, value=message)
demo.load(control_access, inputs=None, outputs=[main_interface, login_message])
if __name__ == "__main__":
demo.queue(max_size=None, default_concurrency_limit=None)
demo.launch()