ysharma HF Staff commited on
Commit
075259f
·
verified ·
1 Parent(s): c33a0ec

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +546 -0
app.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from gradio_image_annotation import image_annotator
3
+ import fal_client
4
+ from PIL import Image
5
+ import io
6
+ import base64
7
+ import numpy as np
8
+ import os
9
+
10
+ def process_images(annotated_image, second_image, user_api_key=None, progress=gr.Progress()):
11
+ """
12
+ Process the annotated image and second image using fal API
13
+ """
14
+ # Check if annotated_image is provided
15
+ if annotated_image is None:
16
+ return None, "Please provide the first image and draw an annotation box"
17
+
18
+ # Check if second_image is provided (could be None or numpy array)
19
+ if second_image is None or (isinstance(second_image, np.ndarray) and second_image.size == 0):
20
+ return None, "Please provide the second image"
21
+
22
+ # Check if annotation box exists
23
+ if not annotated_image.get("boxes") or len(annotated_image["boxes"]) == 0:
24
+ return None, "Please draw an annotation box on the first image"
25
+
26
+ # Extract bounding box coordinates
27
+ box = annotated_image["boxes"][0] # Get the first (and only) box
28
+ xmin = box.get("xmin")
29
+ ymin = box.get("ymin")
30
+ xmax = box.get("xmax")
31
+ ymax = box.get("ymax")
32
+
33
+ # Construct the dynamic prompt with the actual box coordinates
34
+ 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."""
35
+
36
+ progress(0.2, desc="Gradio is preparing your images...")
37
+
38
+ try:
39
+ # Set API key - prioritize user input, then environment variable
40
+ original_key = os.environ.get("FAL_KEY", "")
41
+
42
+ if user_api_key and user_api_key.strip():
43
+ # Use user-provided key
44
+ os.environ["FAL_KEY"] = user_api_key.strip()
45
+ api_key_source = "user-provided"
46
+ elif original_key:
47
+ # Use environment variable (secret)
48
+ api_key_source = "environment"
49
+ else:
50
+ # No API key available
51
+ 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."
52
+
53
+ # Convert first image to file for upload
54
+ first_img = annotated_image["image"]
55
+ if isinstance(first_img, np.ndarray):
56
+ # Convert numpy array to PIL Image
57
+ first_img_pil = Image.fromarray(first_img.astype('uint8'))
58
+ # Save to bytes
59
+ img1_bytes = io.BytesIO()
60
+ first_img_pil.save(img1_bytes, format='PNG')
61
+ img1_bytes.seek(0)
62
+ uploaded_file1 = fal_client.upload(img1_bytes.getvalue(), "image/png")
63
+ elif isinstance(first_img, str):
64
+ # If it's a file path
65
+ uploaded_file1 = fal_client.upload_file(first_img)
66
+ else:
67
+ # If it's already a PIL Image
68
+ img1_bytes = io.BytesIO()
69
+ first_img.save(img1_bytes, format='PNG')
70
+ img1_bytes.seek(0)
71
+ uploaded_file1 = fal_client.upload(img1_bytes.getvalue(), "image/png")
72
+
73
+ # Convert second image to file for upload
74
+ if isinstance(second_image, np.ndarray):
75
+ second_img_pil = Image.fromarray(second_image.astype('uint8'))
76
+ img2_bytes = io.BytesIO()
77
+ second_img_pil.save(img2_bytes, format='PNG')
78
+ img2_bytes.seek(0)
79
+ uploaded_file2 = fal_client.upload(img2_bytes.getvalue(), "image/png")
80
+ elif isinstance(second_image, str):
81
+ uploaded_file2 = fal_client.upload_file(second_image)
82
+ else:
83
+ img2_bytes = io.BytesIO()
84
+ second_image.save(img2_bytes, format='PNG')
85
+ img2_bytes.seek(0)
86
+ uploaded_file2 = fal_client.upload(img2_bytes.getvalue(), "image/png")
87
+
88
+ progress(0.4, desc="Processing with nano-banana...")
89
+
90
+ # Setup progress callback
91
+ def on_queue_update(update):
92
+ if isinstance(update, fal_client.InProgress):
93
+ # InProgress updates don't have a progress attribute, just show we're processing
94
+ progress(0.6, desc="nano-banana is working on your image...")
95
+ # Optionally log any messages if they exist
96
+ if hasattr(update, 'logs') and update.logs:
97
+ for log in update.logs:
98
+ print(log.get("message", ""))
99
+
100
+ # Call fal API with the dynamic prompt including box coordinates
101
+ result = fal_client.subscribe(
102
+ "fal-ai/nano-banana/edit",
103
+ arguments={
104
+ "prompt": prompt,
105
+ "image_urls": [f"{uploaded_file1}", f"{uploaded_file2}"]
106
+ },
107
+ with_logs=True,
108
+ on_queue_update=on_queue_update,
109
+ )
110
+
111
+ progress(0.95, desc="Finalizing...")
112
+
113
+ # Extract the result image URL
114
+ if result and "images" in result and len(result["images"]) > 0:
115
+ output_url = result["images"][0]["url"]
116
+ description = result.get("description", "Image processed successfully!")
117
+ progress(1.0, desc="Complete!")
118
+ return output_url, description
119
+ else:
120
+ return None, "Failed to generate image. Please check your API key or try again."
121
+
122
+ except Exception as e:
123
+ error_message = str(e).lower()
124
+
125
+ # Check for authentication errors
126
+ if "401" in error_message or "unauthorized" in error_message or "api key" in error_message:
127
+ 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"
128
+
129
+ # Check for rate limit errors
130
+ elif "429" in error_message or "rate limit" in error_message:
131
+ return None, "⚠️ Rate limit exceeded. Please wait a moment and try again, or use your own API key for higher limits."
132
+
133
+ # Check for server errors
134
+ elif "500" in error_message or "502" in error_message or "503" in error_message:
135
+ 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)}"
136
+
137
+ # Generic error with fallback message
138
+ else:
139
+ 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"
140
+
141
+ finally:
142
+ # Restore original API key if we temporarily changed it
143
+ if user_api_key and user_api_key.strip():
144
+ if original_key:
145
+ os.environ["FAL_KEY"] = original_key
146
+ else:
147
+ os.environ.pop("FAL_KEY", None)
148
+
149
+
150
+ # Create the Gradio interface
151
+ with gr.Blocks(theme='ocean') as demo:
152
+ # Add navigation bar
153
+ navbar = gr.Navbar(
154
+ value=[
155
+ ("Documentation", "https://docs.fal.ai"),
156
+ ("FAL.AI nano-banana", "https://fal.ai/models/fal-ai/nano-banana/edit/api"),
157
+ ("Learn more about Gradio Navbar", "https://www.gradio.app/guides/multipage-apps#customizing-the-navbar")
158
+ ],
159
+ visible=True,
160
+ main_page_name="🎨 guided nano banana"
161
+ )
162
+
163
+ # Add the animated banner
164
+ gr.HTML(
165
+ """
166
+ <style>
167
+ .animation-container {
168
+ width: 100%;
169
+ height: 400px;
170
+ background: linear-gradient(135deg, #fff4e6 0%, #ffe8cc 25%, #ffeaa7 50%, #fdcb6e 75%, #ffecb3 100%);
171
+ border-radius: 10px;
172
+ margin-bottom: 20px;
173
+ position: relative;
174
+ overflow: hidden;
175
+ display: flex;
176
+ justify-content: center;
177
+ align-items: center;
178
+ }
179
+
180
+ .mini-container {
181
+ position: relative;
182
+ width: 600px;
183
+ height: 300px;
184
+ perspective: 1000px;
185
+ transform: scale(1.0);
186
+ }
187
+
188
+ .mini-image-wrapper {
189
+ position: absolute;
190
+ width: 300px;
191
+ height: 300px;
192
+ overflow: hidden;
193
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
194
+ transition: all 1.5s cubic-bezier(0.4, 0, 0.2, 1);
195
+ background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.3) 100%);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ }
200
+
201
+ .mini-image-wrapper img {
202
+ width: 100%;
203
+ height: 100%;
204
+ object-fit: contain;
205
+ }
206
+
207
+ .mini-left-image {
208
+ left: 0;
209
+ border-radius: 15px 0 0 15px;
210
+ }
211
+
212
+ .mini-right-image {
213
+ right: 0;
214
+ border-radius: 0 15px 15px 0;
215
+ }
216
+
217
+ .mini-result-image {
218
+ width: 600px;
219
+ height: 300px;
220
+ position: absolute;
221
+ left: 0;
222
+ top: 0;
223
+ border-radius: 15px;
224
+ box-shadow: 0 15px 40px rgba(0,0,0,0.25);
225
+ background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.3) 100%);
226
+ display: flex;
227
+ align-items: center;
228
+ justify-content: center;
229
+ }
230
+
231
+ .mini-result-image img {
232
+ width: 100%;
233
+ height: 100%;
234
+ object-fit: contain;
235
+ border-radius: 15px;
236
+ }
237
+
238
+ /* Animations for Set 1 */
239
+ .mini-left-image.set1 {
240
+ animation: miniLeftSet1 18s infinite;
241
+ }
242
+
243
+ .mini-right-image.set1 {
244
+ animation: miniRightSet1 18s infinite;
245
+ }
246
+
247
+ .mini-result-image.set1 {
248
+ animation: miniResultSet1 18s infinite;
249
+ }
250
+
251
+ /* Animations for Set 2 */
252
+ .mini-left-image.set2 {
253
+ animation: miniLeftSet2 18s infinite;
254
+ }
255
+
256
+ .mini-right-image.set2 {
257
+ animation: miniRightSet2 18s infinite;
258
+ }
259
+
260
+ .mini-result-image.set2 {
261
+ animation: miniResultSet2 18s infinite;
262
+ }
263
+
264
+ /* Animations for Set 3 */
265
+ .mini-left-image.set3 {
266
+ animation: miniLeftSet3 18s infinite;
267
+ }
268
+
269
+ .mini-right-image.set3 {
270
+ animation: miniRightSet3 18s infinite;
271
+ }
272
+
273
+ .mini-result-image.set3 {
274
+ animation: miniResultSet3 18s infinite;
275
+ }
276
+
277
+ /* Set 1 Keyframes (0-6s) */
278
+ @keyframes miniLeftSet1 {
279
+ 0% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
280
+ 2% { transform: translateX(0) rotateY(0); opacity: 1; }
281
+ 16% { transform: translateX(0) rotateY(0); opacity: 1; }
282
+ 22% { transform: translateX(0) rotateY(0); opacity: 0; }
283
+ 33%, 100% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
284
+ }
285
+
286
+ @keyframes miniRightSet1 {
287
+ 0% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
288
+ 2% { transform: translateX(0) rotateY(0); opacity: 1; }
289
+ 16% { transform: translateX(0) rotateY(0); opacity: 1; }
290
+ 22% { transform: translateX(0) rotateY(0); opacity: 0; }
291
+ 33%, 100% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
292
+ }
293
+
294
+ @keyframes miniResultSet1 {
295
+ 0%, 16% { opacity: 0; transform: scale(0.8); z-index: -1; }
296
+ 22% { opacity: 1; transform: scale(1.05); z-index: 10; }
297
+ 27% { opacity: 1; transform: scale(1); z-index: 10; }
298
+ 30% { opacity: 0; transform: scale(0.8); z-index: -1; }
299
+ 100% { opacity: 0; transform: scale(0.8); z-index: -1; }
300
+ }
301
+
302
+ /* Set 2 Keyframes (6-12s) */
303
+ @keyframes miniLeftSet2 {
304
+ 0%, 33% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
305
+ 35% { transform: translateX(0) rotateY(0); opacity: 1; }
306
+ 50% { transform: translateX(0) rotateY(0); opacity: 1; }
307
+ 55% { transform: translateX(0) rotateY(0); opacity: 0; }
308
+ 66%, 100% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
309
+ }
310
+
311
+ @keyframes miniRightSet2 {
312
+ 0%, 33% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
313
+ 35% { transform: translateX(0) rotateY(0); opacity: 1; }
314
+ 50% { transform: translateX(0) rotateY(0); opacity: 1; }
315
+ 55% { transform: translateX(0) rotateY(0); opacity: 0; }
316
+ 66%, 100% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
317
+ }
318
+
319
+ @keyframes miniResultSet2 {
320
+ 0%, 50% { opacity: 0; transform: scale(0.8); z-index: -1; }
321
+ 55% { opacity: 1; transform: scale(1.05); z-index: 10; }
322
+ 61% { opacity: 1; transform: scale(1); z-index: 10; }
323
+ 63% { opacity: 0; transform: scale(0.8); z-index: -1; }
324
+ 100% { opacity: 0; transform: scale(0.8); z-index: -1; }
325
+ }
326
+
327
+ /* Set 3 Keyframes (12-18s) */
328
+ @keyframes miniLeftSet3 {
329
+ 0%, 66% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
330
+ 69% { transform: translateX(0) rotateY(0); opacity: 1; }
331
+ 83% { transform: translateX(0) rotateY(0); opacity: 1; }
332
+ 88% { transform: translateX(0) rotateY(0); opacity: 0; }
333
+ 96%, 100% { transform: translateX(-120px) rotateY(15deg); opacity: 0; }
334
+ }
335
+
336
+ @keyframes miniRightSet3 {
337
+ 0%, 66% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
338
+ 69% { transform: translateX(0) rotateY(0); opacity: 1; }
339
+ 83% { transform: translateX(0) rotateY(0); opacity: 1; }
340
+ 88% { transform: translateX(0) rotateY(0); opacity: 0; }
341
+ 96%, 100% { transform: translateX(120px) rotateY(-15deg); opacity: 0; }
342
+ }
343
+
344
+ @keyframes miniResultSet3 {
345
+ 0%, 83% { opacity: 0; transform: scale(0.8); z-index: -1; }
346
+ 88% { opacity: 1; transform: scale(1.05); z-index: 10; }
347
+ 94% { opacity: 1; transform: scale(1); z-index: 10; }
348
+ 96% { opacity: 0; transform: scale(0.8); z-index: -1; }
349
+ 100% { opacity: 0; transform: scale(0.8); z-index: -1; }
350
+ }
351
+
352
+ .mini-progress-bar {
353
+ position: absolute;
354
+ bottom: 20px;
355
+ left: 50%;
356
+ transform: translateX(-50%);
357
+ width: 300px;
358
+ height: 4px;
359
+ background: rgba(255, 165, 0, 0.2);
360
+ border-radius: 2px;
361
+ overflow: hidden;
362
+ }
363
+
364
+ .mini-progress-fill {
365
+ height: 100%;
366
+ background: linear-gradient(90deg, #ffa502 0%, #ff6348 100%);
367
+ border-radius: 2px;
368
+ animation: miniProgressCycle 18s linear infinite;
369
+ }
370
+
371
+ @keyframes miniProgressCycle {
372
+ 0% { width: 0%; }
373
+ 100% { width: 100%; }
374
+ }
375
+
376
+ .animation-title {
377
+ position: absolute;
378
+ top: 20px;
379
+ left: 50%;
380
+ transform: translateX(-50%);
381
+ color: #ff6348;
382
+ font-size: 18px;
383
+ font-weight: 700;
384
+ text-shadow: 0 2px 4px rgba(255,99,72,0.2);
385
+ z-index: 100;
386
+ }
387
+ </style>
388
+
389
+ <div class="animation-container">
390
+ <div class="animation-title">Image Fusion Magic ✨</div>
391
+ <div class="mini-container">
392
+ <!-- Set 1 Images -->
393
+ <div class="image-wrapper left-image set1">
394
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/aPCNIVXzUkwGrjDvm4a2q.png" alt="Set1 Left" id="set1LeftImg">
395
+ </div>
396
+ <div class="image-wrapper right-image set1">
397
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/yhuEbtc1s2kC9OUdcIPL2.png" alt="Set1 Right" id="set1RightImg">
398
+ </div>
399
+ <div class="result-image set1">
400
+ <div class="glow set1"></div>
401
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/Kk5WkJOsbCexc_14HYsVf.jpeg" alt="Set1 Result" id="set1ResultImg">
402
+ </div>
403
+
404
+ <!-- Set 2 Images -->
405
+ <div class="image-wrapper left-image set2">
406
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/c4RM2XbJKLOiT5ePGkIoF.png" alt="Set2 Left" id="set2LeftImg">
407
+ </div>
408
+ <div class="image-wrapper right-image set2">
409
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/Eiv6EYFjpHEVHNRYPxpLA.png" alt="Set2 Right" id="set2RightImg">
410
+ </div>
411
+ <div class="result-image set2">
412
+ <div class="glow set2"></div>
413
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/zWCbcioy4Ary_1_x_0-5a.jpeg" alt="Set2 Result" id="set2ResultImg">
414
+ </div>
415
+
416
+ <!-- Set 3 Images -->
417
+ <div class="image-wrapper left-image set3">
418
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/Yk5FqD9brcUNiub2D94K9.png" alt="Set3 Left" id="set3LeftImg">
419
+ </div>
420
+ <div class="image-wrapper right-image set3">
421
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/AYdhb7p7vD5EecbNUqhHb.png" alt="Set3 Right" id="set3RightImg">
422
+ </div>
423
+ <div class="result-image set3">
424
+ <div class="glow set3"></div>
425
+ <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/99wlXmLCptDGnYkE6IOfE.jpeg" alt="Set3 Result" id="set3ResultImg">
426
+ </div>
427
+
428
+ <!-- Progress bar -->
429
+ <div class="mini-progress-bar">
430
+ <div class="mini-progress-fill"></div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ """
435
+ )
436
+
437
+ gr.HTML(
438
+ """
439
+ <h1><center>Guide Your Nano Banana👉🍌</center></h1>
440
+
441
+ <b>How to use:</b><br>
442
+ 1. Upload or capture the first image and draw a box where you want to place an object<br>
443
+ 2. Upload the second image containing the object you want to insert<br>
444
+ 3. Click "Generate Composite Image" and wait for the Gradio and Nano-Banana to blend the images<br>
445
+
446
+ The Gradio app will intelligently place the object from the second image into the boxed area of the first image,
447
+ taking care of lighting, shadows, and proper integration.
448
+ """
449
+ )
450
+
451
+ # API Key input section
452
+ with gr.Row():
453
+ with gr.Column():
454
+ with gr.Accordion("🔑 API Configuration (Optional)", open=False):
455
+ gr.Markdown(
456
+ """
457
+ **Note:** If you're experiencing API errors or want to use your own FAL account:
458
+ - Enter your FAL API key below, or
459
+ - [Duplicate this Space](https://huggingface.co/spaces) and set FAL_KEY as a secret
460
+ - Get your API key at [fal.ai](https://fal.ai)
461
+ """
462
+ )
463
+ api_key_input = gr.Textbox(
464
+ label="FAL API Key",
465
+ placeholder="Enter your FAL key (optional)",
466
+ type="password",
467
+ interactive=True,
468
+ info="Your key will be used only for this session and won't be stored"
469
+ )
470
+
471
+ with gr.Row():
472
+ with gr.Column(scale=1):
473
+ with gr.Row():
474
+ with gr.Column(scale=1):
475
+ gr.Markdown("### Step 1: Annotate First Image")
476
+ # Image annotator for first image
477
+ from gradio_image_annotation import image_annotator
478
+ #first_image = ImageAnnotator(
479
+ first_image = image_annotator(
480
+ value=None,
481
+ label="Draw a box where you want to place the object",
482
+ image_type="pil",
483
+ single_box=True, # Only allow one box
484
+ disable_edit_boxes=True,
485
+ show_download_button=False,
486
+ show_share_button=False,
487
+ box_thickness=3,
488
+ box_selected_thickness=4,
489
+ show_label=True,
490
+ #image_mode="RGB",
491
+ #box_min_size=20,
492
+ )
493
+
494
+ with gr.Column(scale=1):
495
+ gr.Markdown("### Step 2: Upload Second Image")
496
+ # Regular image input for second image
497
+ second_image = gr.Image(
498
+ label="Image containing the object to insert",
499
+ type="numpy",
500
+ height=400,
501
+ )
502
+ # Generate button
503
+ generate_btn = gr.Button("Step 3: 🚀 Generate Composite Image", variant="primary", size="lg")
504
+
505
+ # Output section
506
+ with gr.Column():
507
+ output_image = gr.Image(
508
+ label="Generated Composite Image",
509
+ type="filepath",
510
+ height=500,
511
+ )
512
+ status_text = gr.Textbox(
513
+ label="Status",
514
+ placeholder="Results will appear here...",
515
+ lines=3,
516
+ )
517
+
518
+ # Connect the button to the processing function
519
+ generate_btn.click(
520
+ fn=process_images,
521
+ inputs=[first_image, second_image, api_key_input],
522
+ outputs=[output_image, status_text],
523
+ show_progress=True,
524
+ )
525
+
526
+ with demo.route("Tips", "/tips"):
527
+ gr.Markdown(
528
+ """
529
+ # ℹ️ Tips for Best Results
530
+ - **Box Placement**: Draw the box exactly where you want the object to appear
531
+ - **Image Quality**: Use high-resolution images for better results
532
+ - **Object Selection**: The second image should clearly show the object you want to insert
533
+ - **Lighting**: Images with similar lighting conditions work best
534
+ - **Processing Time**: Generation typically takes 10-30 seconds
535
+ - **API Key**: If you encounter errors, try using your own FAL API key
536
+ """
537
+ )
538
+
539
+ # Different navbar for the Settings page
540
+ navbar = gr.Navbar(
541
+ visible=True,
542
+ main_page_name="Home",
543
+ )
544
+
545
+ if __name__ == "__main__":
546
+ demo.launch(ssr_mode=False)