|
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_TOKEN = os.getenv("REPLICATE_API_TOKEN") |
|
|
|
if not REPLICATE_API_TOKEN: |
|
raise ValueError("REPLICATE_API_TOKEN environment variable is not set.") |
|
|
|
|
|
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="๐จ ์ค๋น ์ค...") |
|
|
|
|
|
input_data = { |
|
"prompt": prompt |
|
} |
|
|
|
|
|
if image_path: |
|
|
|
|
|
|
|
input_data["image_input"] = [image_path] |
|
|
|
progress(0.5, desc="โจ ์์ฑ ์ค...") |
|
|
|
|
|
output = replicate.run( |
|
"google/nano-banana", |
|
input=input_data |
|
) |
|
|
|
progress(0.8, desc="๐ผ๏ธ ๋ง๋ฌด๋ฆฌ ์ค...") |
|
|
|
|
|
if output: |
|
|
|
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") |
|
|
|
|
|
response = requests.get(image_url) |
|
response.raise_for_status() |
|
|
|
|
|
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="๐จ ์ด๋ฏธ์ง ์ค๋น ์ค...") |
|
|
|
|
|
input_data = { |
|
"prompt": prompt, |
|
"image_input": images |
|
} |
|
|
|
progress(0.5, desc="โจ ์์ฑ ์ค...") |
|
|
|
|
|
output = replicate.run( |
|
"google/nano-banana", |
|
input=input_data |
|
) |
|
|
|
progress(0.8, desc="๐ผ๏ธ ๋ง๋ฌด๋ฆฌ ์ค...") |
|
|
|
|
|
if output: |
|
|
|
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") |
|
|
|
|
|
response = requests.get(image_url) |
|
response.raise_for_status() |
|
|
|
|
|
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}") |
|
|
|
|
|
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: |
|
|
|
gr.HTML(''' |
|
<div class="main-header"> |
|
<h1 class="header-title"> |
|
๐ Real Nano Banana |
|
</h1> |
|
<p class="header-subtitle"> |
|
Google Nano Banana๋ก ๊ตฌ๋๋๋ AI ์ด๋ฏธ์ง ์์ฑ๊ธฐ |
|
</p> |
|
</div> |
|
''') |
|
|
|
|
|
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">') |
|
|
|
|
|
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> |
|
''') |
|
|
|
|
|
gr.HTML('<h3>โ๏ธ ํ๋กฌํํธ</h3>') |
|
prompt_input = gr.Textbox( |
|
label="", |
|
info="AI์๊ฒ ์ํ๋ ์ด๋ฏธ์ง๋ฅผ ์ค๋ช
ํ์ธ์", |
|
placeholder="์: ๋ง์์ด ๋ณด์ด๋ ํผ์, ์ฐ์ฃผ๋ฅผ ๋ฐฐ๊ฒฝ์ผ๋ก ํ ๊ณ ์์ด, ๋ฏธ๋์ ์ธ ๋์ ํ๊ฒฝ...", |
|
lines=3, |
|
elem_classes="prompt-input" |
|
) |
|
|
|
|
|
generate_button = gr.Button( |
|
"๐ ์์ฑํ๊ธฐ", |
|
variant="primary", |
|
elem_classes="generate-btn" |
|
) |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
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>') |
|
|
|
|
|
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() |
|
|
|
|
|
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] |
|
) |
|
|
|
|
|
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() |