|
import gradio as gr |
|
from gradio_image_annotation import image_annotator |
|
import fal_client |
|
from PIL import Image |
|
import io |
|
import base64 |
|
import numpy as np |
|
import os |
|
|
|
def process_images(annotated_image, second_image, user_api_key=None, progress=gr.Progress()): |
|
""" |
|
Process the annotated image and second image using fal API |
|
""" |
|
|
|
if annotated_image is None: |
|
return None, "Please provide the first image and draw an annotation box" |
|
|
|
|
|
if second_image is None or (isinstance(second_image, np.ndarray) and second_image.size == 0): |
|
return None, "Please provide the second image" |
|
|
|
|
|
if not annotated_image.get("boxes") or len(annotated_image["boxes"]) == 0: |
|
return None, "Please draw an annotation box on the first image" |
|
|
|
|
|
box = annotated_image["boxes"][0] |
|
xmin = box.get("xmin") |
|
ymin = box.get("ymin") |
|
xmax = box.get("xmax") |
|
ymax = box.get("ymax") |
|
|
|
|
|
prompt = f"""add the <central object in the second image> in the first image only inside an imaginary box defined by pixels values "xmin": {xmin}, "ymin": {ymin}, "xmax": {xmax}, "ymax": {ymax}. Take care of shadows, lighting, style, and general concept of objects as per the first image.""" |
|
print(f"prompt - {prompt}") |
|
progress(0.2, desc="Gradio is preparing your images...") |
|
|
|
try: |
|
|
|
original_key = os.environ.get("FAL_KEY", "") |
|
|
|
if user_api_key and user_api_key.strip(): |
|
|
|
os.environ["FAL_KEY"] = user_api_key.strip() |
|
api_key_source = "user-provided" |
|
elif original_key: |
|
|
|
api_key_source = "environment" |
|
else: |
|
|
|
return None, "⚠️ No FAL API key found. Please either:\n1. Duplicate this app and set your FAL_KEY as a secret, or\n2. Enter your FAL API key in the field provided above." |
|
|
|
|
|
first_img = annotated_image["image"] |
|
if isinstance(first_img, np.ndarray): |
|
|
|
first_img_pil = Image.fromarray(first_img.astype('uint8')) |
|
|
|
img1_bytes = io.BytesIO() |
|
first_img_pil.save(img1_bytes, format='PNG') |
|
img1_bytes.seek(0) |
|
uploaded_file1 = fal_client.upload(img1_bytes.getvalue(), "image/png") |
|
elif isinstance(first_img, str): |
|
|
|
uploaded_file1 = fal_client.upload_file(first_img) |
|
else: |
|
|
|
img1_bytes = io.BytesIO() |
|
first_img.save(img1_bytes, format='PNG') |
|
img1_bytes.seek(0) |
|
uploaded_file1 = fal_client.upload(img1_bytes.getvalue(), "image/png") |
|
|
|
|
|
if isinstance(second_image, np.ndarray): |
|
second_img_pil = Image.fromarray(second_image.astype('uint8')) |
|
img2_bytes = io.BytesIO() |
|
second_img_pil.save(img2_bytes, format='PNG') |
|
img2_bytes.seek(0) |
|
uploaded_file2 = fal_client.upload(img2_bytes.getvalue(), "image/png") |
|
elif isinstance(second_image, str): |
|
uploaded_file2 = fal_client.upload_file(second_image) |
|
else: |
|
img2_bytes = io.BytesIO() |
|
second_image.save(img2_bytes, format='PNG') |
|
img2_bytes.seek(0) |
|
uploaded_file2 = fal_client.upload(img2_bytes.getvalue(), "image/png") |
|
|
|
progress(0.4, desc="Processing with nano-banana...") |
|
|
|
|
|
def on_queue_update(update): |
|
if isinstance(update, fal_client.InProgress): |
|
|
|
progress(0.6, desc="nano-banana is working on your image...") |
|
|
|
if hasattr(update, 'logs') and update.logs: |
|
for log in update.logs: |
|
print(log.get("message", "")) |
|
|
|
|
|
result = fal_client.subscribe( |
|
"fal-ai/nano-banana/edit", |
|
arguments={ |
|
"prompt": prompt, |
|
"image_urls": [f"{uploaded_file1}", f"{uploaded_file2}"] |
|
}, |
|
with_logs=True, |
|
on_queue_update=on_queue_update, |
|
) |
|
|
|
progress(0.95, desc="Finalizing...") |
|
|
|
|
|
if result and "images" in result and len(result["images"]) > 0: |
|
output_url = result["images"][0]["url"] |
|
description = result.get("description", "Image processed successfully!") |
|
progress(1.0, desc="Complete!") |
|
return output_url, description |
|
else: |
|
return None, "Failed to generate image. Please check your API key or try again." |
|
|
|
except Exception as e: |
|
error_message = str(e).lower() |
|
|
|
|
|
if "401" in error_message or "unauthorized" in error_message or "api key" in error_message: |
|
return None, f"⚠️ API Authentication Error: Invalid or missing FAL API key.\n\nPlease either:\n1. Duplicate this app and set your FAL_KEY as a secret, or\n2. Enter your valid FAL API key in the field provided above.\n\nGet your API key at: https://fal.ai" |
|
|
|
|
|
elif "429" in error_message or "rate limit" in error_message: |
|
return None, "⚠️ Rate limit exceeded. Please wait a moment and try again, or use your own API key for higher limits." |
|
|
|
|
|
elif "500" in error_message or "502" in error_message or "503" in error_message: |
|
return None, f"⚠️ FAL API server error. The service might be temporarily unavailable.\n\nPlease either:\n1. Try again in a few moments, or\n2. Use your own API key by entering it in the field above.\n\nError details: {str(e)}" |
|
|
|
|
|
else: |
|
return None, f"⚠️ Error occurred: {str(e)}\n\nIf the error persists, please either:\n1. Duplicate this app and set your FAL_KEY as a secret, or\n2. Enter your FAL API key in the field provided above.\n\nGet your API key at: https://fal.ai" |
|
|
|
finally: |
|
|
|
if user_api_key and user_api_key.strip(): |
|
if original_key: |
|
os.environ["FAL_KEY"] = original_key |
|
else: |
|
os.environ.pop("FAL_KEY", None) |
|
|
|
examples_image_banner=gr.HTML( |
|
""" |
|
<style> |
|
.animation-container { |
|
width: 100%; |
|
height: 400px; |
|
background: linear-gradient(135deg, #fff4e6 0%, #ffe8cc 25%, #ffeaa7 50%, #fdcb6e 75%, #ffecb3 100%); |
|
border-radius: 10px; |
|
margin-bottom: 20px; |
|
position: relative; |
|
overflow: hidden; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
} |
|
|
|
.mini-container { |
|
position: relative; |
|
width: 600px; |
|
height: 300px; |
|
perspective: 1000px; |
|
} |
|
|
|
.mini-image-wrapper { |
|
position: absolute; |
|
width: 300px; |
|
height: 300px; |
|
overflow: hidden; |
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2); |
|
background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.3) 100%); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.mini-image-wrapper img { |
|
width: 100%; |
|
height: 100%; |
|
object-fit: contain; |
|
} |
|
|
|
.mini-left-image { |
|
left: 0; |
|
border-radius: 15px 0 0 15px; |
|
} |
|
|
|
.mini-right-image { |
|
right: 0; |
|
border-radius: 0 15px 15px 0; |
|
} |
|
|
|
.mini-result-image { |
|
width: 600px; |
|
height: 300px; |
|
position: absolute; |
|
left: 0; |
|
top: 0; |
|
border-radius: 15px; |
|
box-shadow: 0 15px 40px rgba(0,0,0,0.25); |
|
background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.3) 100%); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.mini-result-image img { |
|
width: 100%; |
|
height: 100%; |
|
object-fit: contain; |
|
border-radius: 15px; |
|
} |
|
|
|
/* Animations for Set 1 */ |
|
.mini-left-image.set1 { |
|
animation: miniLeftSet1 18s infinite linear; |
|
} |
|
|
|
.mini-right-image.set1 { |
|
animation: miniRightSet1 18s infinite linear; |
|
} |
|
|
|
.mini-result-image.set1 { |
|
animation: miniResultSet1 18s infinite linear; |
|
} |
|
|
|
/* Animations for Set 2 */ |
|
.mini-left-image.set2 { |
|
animation: miniLeftSet2 18s infinite linear; |
|
} |
|
|
|
.mini-right-image.set2 { |
|
animation: miniRightSet2 18s infinite linear; |
|
} |
|
|
|
.mini-result-image.set2 { |
|
animation: miniResultSet2 18s infinite linear; |
|
} |
|
|
|
/* Animations for Set 3 */ |
|
.mini-left-image.set3 { |
|
animation: miniLeftSet3 18s infinite linear; |
|
} |
|
|
|
.mini-right-image.set3 { |
|
animation: miniRightSet3 18s infinite linear; |
|
} |
|
|
|
.mini-result-image.set3 { |
|
animation: miniResultSet3 18s infinite linear; |
|
} |
|
|
|
/* Set 1 Keyframes (0-6s of 18s) */ |
|
@keyframes miniLeftSet1 { |
|
0% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
5% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
22% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
28% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
33%, 100% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniRightSet1 { |
|
0% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
5% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
22% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
28% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
33%, 100% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniResultSet1 { |
|
0%, 22% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
28% { |
|
opacity: 1; |
|
transform: scale(1.05); |
|
z-index: 10; |
|
} |
|
30% { |
|
opacity: 1; |
|
transform: scale(1); |
|
z-index: 10; |
|
} |
|
32% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
33%, 100% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
} |
|
|
|
/* Set 2 Keyframes (6-12s of 18s) */ |
|
@keyframes miniLeftSet2 { |
|
0%, 33% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
38% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
55% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
61% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
66%, 100% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniRightSet2 { |
|
0%, 33% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
38% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
55% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
61% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
66%, 100% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniResultSet2 { |
|
0%, 55% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
61% { |
|
opacity: 1; |
|
transform: scale(1.05); |
|
z-index: 10; |
|
} |
|
63% { |
|
opacity: 1; |
|
transform: scale(1); |
|
z-index: 10; |
|
} |
|
65% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
66%, 100% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
} |
|
|
|
/* Set 3 Keyframes (12-18s of 18s) */ |
|
@keyframes miniLeftSet3 { |
|
0%, 66% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
72% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
88% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
94% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
100% { |
|
transform: translateX(-120px) rotateY(15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniRightSet3 { |
|
0%, 66% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
72% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
88% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 1; |
|
} |
|
94% { |
|
transform: translateX(0) rotateY(0); |
|
opacity: 0; |
|
} |
|
100% { |
|
transform: translateX(120px) rotateY(-15deg); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes miniResultSet3 { |
|
0%, 88% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
94% { |
|
opacity: 1; |
|
transform: scale(1.05); |
|
z-index: 10; |
|
} |
|
96% { |
|
opacity: 1; |
|
transform: scale(1); |
|
z-index: 10; |
|
} |
|
98% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
100% { |
|
opacity: 0; |
|
transform: scale(0.8); |
|
z-index: -1; |
|
} |
|
} |
|
|
|
.mini-progress-bar { |
|
position: absolute; |
|
bottom: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
width: 300px; |
|
height: 4px; |
|
background: rgba(255, 165, 0, 0.2); |
|
border-radius: 2px; |
|
overflow: hidden; |
|
} |
|
|
|
.mini-progress-fill { |
|
height: 100%; |
|
background: linear-gradient(90deg, #ffa502 0%, #ff6348 100%); |
|
border-radius: 2px; |
|
animation: miniProgressCycle 18s linear infinite; |
|
} |
|
|
|
@keyframes miniProgressCycle { |
|
0% { width: 0%; } |
|
100% { width: 100%; } |
|
} |
|
|
|
.animation-title { |
|
position: absolute; |
|
top: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
color: #ff6348; |
|
font-size: 18px; |
|
font-weight: 700; |
|
text-shadow: 0 2px 4px rgba(255,99,72,0.2); |
|
z-index: 100; |
|
} |
|
</style> |
|
|
|
<div class="animation-container"> |
|
<div class="animation-title">Nano Banana Magic ✨</div> |
|
<div class="mini-container" id="animContainer"> |
|
<!-- Set 1 Images --> |
|
<div class="mini-image-wrapper mini-left-image set1" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=one-1.png" alt="Set1 Left"> <!--src would be file/my_image.png*/ --> |
|
</div> |
|
<div class="mini-image-wrapper mini-right-image set1" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=one-2.png" alt="Set1 Right"> |
|
</div> |
|
<div class="mini-result-image set1" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=one-3.jpg" alt="Set1 Result"> |
|
</div> |
|
|
|
<!-- Set 2 Images --> |
|
<div class="mini-image-wrapper mini-left-image set2" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=two-1.png" alt="Set2 Left"> |
|
</div> |
|
<div class="mini-image-wrapper mini-right-image set2" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=two-2.png" alt="Set2 Right"> |
|
</div> |
|
<div class="mini-result-image set2" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=two-3.jpeg" alt="Set2 Result"> |
|
</div> |
|
|
|
<!-- Set 3 Images --> |
|
<div class="mini-image-wrapper mini-left-image set3" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=three-1.png" alt="Set3 Left"> |
|
</div> |
|
<div class="mini-image-wrapper mini-right-image set3" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=three-2.png" alt="Set3 Right"> |
|
</div> |
|
<div class="mini-result-image set3" style="animation-play-state: running;"> |
|
<img src="/gradio_api/file=three-3.jpg" alt="Set3 Result"> |
|
</div> |
|
|
|
<!-- Progress bar --> |
|
<div class="mini-progress-bar"> |
|
<div class="mini-progress-fill" style="animation-play-state: running;"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// Force restart animations on load |
|
setTimeout(function() { |
|
const elements = document.querySelectorAll('.mini-image-wrapper, .mini-result-image, .mini-progress-fill'); |
|
elements.forEach(el => { |
|
el.style.animationPlayState = 'running'; |
|
}); |
|
}, 100); |
|
</script> |
|
""" |
|
) |
|
|
|
|
|
with gr.Blocks(theme='ocean') as demo: |
|
|
|
navbar = gr.Navbar( |
|
value=[ |
|
("FAL.AI nano-banana", "https://fal.ai/models/fal-ai/nano-banana/edit/api"), |
|
("Learn more about Gradio Navbar", "https://www.gradio.app/guides/multipage-apps#customizing-the-navbar") |
|
], |
|
visible=True, |
|
main_page_name="🎨 guided nano banana" |
|
) |
|
with gr.Row(): |
|
|
|
examples_image_banner.render() |
|
|
|
with gr.Column(): |
|
gr.HTML( |
|
""" |
|
<h1><center>Guide Your Nano Banana👉🍌</center></h1> |
|
|
|
<b>How to use:</b><br> |
|
1. Upload or capture the first image and draw a box where you want to place an object<br> |
|
2. Upload the second image containing the object you want to insert<br> |
|
3. Click "Generate Composite Image" and wait for the Gradio and Nano-Banana to blend the images<br> |
|
|
|
<br>The Gradio app will intelligently place the object from the second image into the boxed area of the first image, |
|
taking care of lighting, shadows, and proper integration.<br>Kindly note that this app is experimental, so image edits might not always create the desired results. You can create a duplicate of the app and experiment with the prompt available <a href="https://huggingface.co/spaces/ysharma/guided-placement-nano-banana/blob/main/app.py#L34">here</a> to achieve better results. |
|
""" |
|
) |
|
|
|
|
|
with gr.Accordion("🔑 API Configuration (Optional)", open=False): |
|
gr.Markdown( |
|
""" |
|
**Note:** If you're experiencing API errors or want to use your own FAL account: |
|
- Enter your FAL API key below, or |
|
- [Duplicate this Space](https://huggingface.co/spaces) and set FAL_KEY as a secret |
|
- Get your API key at [fal.ai](https://fal.ai) |
|
""" |
|
) |
|
api_key_input = gr.Textbox( |
|
label="FAL API Key", |
|
placeholder="Enter your FAL key (optional)", |
|
type="password", |
|
interactive=True, |
|
info="Your key will be used only for this session and won't be stored" |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
gr.Markdown("### Step 1: Annotate First Image _(click on the upload image button if the app is stuck)_") |
|
|
|
from gradio_image_annotation import image_annotator |
|
|
|
first_image = image_annotator( |
|
value=None, |
|
label="Draw a box where you want to place the object", |
|
image_type="pil", |
|
single_box=True, |
|
disable_edit_boxes=True, |
|
show_download_button=False, |
|
show_share_button=False, |
|
box_thickness=3, |
|
box_selected_thickness=4, |
|
show_label=True, |
|
|
|
|
|
) |
|
|
|
with gr.Column(scale=1): |
|
gr.Markdown("### Step 2: Upload Second Image") |
|
|
|
second_image = gr.Image( |
|
label="Image containing the object to insert", |
|
type="numpy", |
|
height=400, |
|
) |
|
|
|
generate_btn = gr.Button("Step 3: 🚀 Generate Composite Image", variant="primary", size="lg") |
|
|
|
|
|
with gr.Column(): |
|
output_image = gr.Image( |
|
label="Generated Composite Image", |
|
type="filepath", |
|
height=500, |
|
) |
|
status_text = gr.Textbox( |
|
label="Status", |
|
placeholder="Results will appear here...", |
|
lines=3, |
|
) |
|
|
|
|
|
generate_btn.click( |
|
fn=process_images, |
|
inputs=[first_image, second_image, api_key_input], |
|
outputs=[output_image, status_text], |
|
show_progress=True, |
|
) |
|
|
|
examples = [ |
|
[ |
|
{ |
|
"image": "examples/example1-1.png", |
|
"boxes": [{"xmin": 61, "ymin": 298, "xmax": 228, "ymax": 462}], |
|
}, |
|
"examples/example1-2.png", |
|
], |
|
[ |
|
{ |
|
"image": "examples/example2-1.png", |
|
"boxes": [{"xmin": 205, "ymin": 791, "xmax": 813, "ymax": 1161}], |
|
}, |
|
"examples/example2-2.jpg", |
|
], |
|
[ |
|
{ |
|
"image": "examples/example3-1.png", |
|
"boxes": [{"xmin": 24, "ymin": 465, "xmax": 146, "ymax": 607}], |
|
}, |
|
"examples/example3-2.png", |
|
], |
|
] |
|
|
|
ex = gr.Examples( |
|
examples=examples, |
|
inputs=[first_image, second_image], |
|
) |
|
|
|
|
|
with demo.route("ℹ️Tips for Best Results", "/tips"): |
|
gr.Markdown( |
|
""" |
|
# ℹ️ Tips for Best Results |
|
- **Box Placement**: Draw the box exactly where you want the object to appear |
|
- **Image Quality**: Use high-resolution images for better results |
|
- **Object Selection**: The second image should clearly show the object you want to insert |
|
- **Lighting**: Images with similar lighting conditions work best |
|
- **Processing Time**: Generation typically takes 10-30 seconds |
|
- **API Key**: If you encounter errors, try using your own FAL API key |
|
""" |
|
) |
|
|
|
|
|
navbar = gr.Navbar( |
|
visible=True, |
|
main_page_name="Home", |
|
) |
|
|
|
if __name__ == "__main__": |
|
demo.launch(ssr_mode=False, allowed_paths=["."], debug=False) |