dmmmmm commited on
Commit
82ce405
·
verified ·
1 Parent(s): 3e22ba4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +970 -29
app.py CHANGED
@@ -1,39 +1,980 @@
1
- import sys
 
 
2
  import os
3
- import socket
4
- import random
 
 
 
 
5
 
6
- # 添加src目录到Python路径
7
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
8
- print(os.path.dirname(__file__))
9
- from src.ui import Veo3Interface
10
- from src.utils import start_cleanup_scheduler
11
 
12
 
13
- def main():
14
- """主函数"""
15
- print("🎬 Starting Veo3 AI Video Generator...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- # 启动定时清理任务
18
- start_cleanup_scheduler()
19
- print("✅ Cleanup scheduler started")
20
-
21
- # 创建界面
22
- interface = Veo3Interface()
23
- interface.create_interface()
24
- print("✅ Interface created")
25
-
26
- # 启动应用
27
- print("🚀 Launching application...")
28
  try:
29
- interface.launch(
30
- share=False,
31
- server_name="0.0.0.0",
32
- server_port=12345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  )
34
- except Exception as e:
35
- print(f"❌ 启动失败: {e}")
 
 
 
 
 
 
 
 
 
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  if __name__ == "__main__":
39
- main()
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import time
4
  import os
5
+ import uuid
6
+ import threading
7
+ from io import BytesIO
8
+ import requests
9
+ import gradio as gr
10
+ from PIL import Image
11
 
12
+ # 全局变量存储应用实例
13
+ app_instance = None
 
 
 
14
 
15
 
16
+ def cleanup_temp_files(temp_dirs=None):
17
+ """
18
+ 清理指定的临时文件夹中的旧文件
19
+ temp_dirs: 要清理的目录列表,如果为None则只清理记录的目录
20
+ """
21
+ try:
22
+ # 如果没有指定目录,只清理我们记录的目录
23
+ if temp_dirs is None:
24
+ temp_dirs = []
25
+
26
+ current_time = time.time()
27
+ cleaned_count = 0
28
+
29
+ for temp_dir in temp_dirs:
30
+ try:
31
+ # 确保目录存在且是目录
32
+ if not os.path.exists(temp_dir) or not os.path.isdir(temp_dir):
33
+ continue
34
+
35
+ # 遍历目录中的文件
36
+ for root, dirs, files in os.walk(temp_dir):
37
+ for file_name in files:
38
+ file_path = os.path.join(root, file_name)
39
+ try:
40
+ # 检查文件修改时间
41
+ file_mtime = os.path.getmtime(file_path)
42
+ # 如果文件超过30分钟未修改,则删除
43
+ if current_time - file_mtime > 1800: # 1800秒 = 30分钟
44
+ # 检查是否是图片文件或临时文件
45
+ if any(file_path.lower().endswith(ext) for ext in
46
+ ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tmp']):
47
+ os.remove(file_path)
48
+ cleaned_count += 1
49
+ print(f"Cleaned old temp file: {file_path}")
50
+ except Exception as e:
51
+ print(f"Error cleaning {file_path}: {e}")
52
+
53
+ # 清理空目录
54
+ for dir_name in dirs:
55
+ dir_path = os.path.join(root, dir_name)
56
+ try:
57
+ if os.path.exists(dir_path) and not os.listdir(dir_path):
58
+ os.rmdir(dir_path)
59
+ print(f"Removed empty temp directory: {dir_path}")
60
+ except Exception as e:
61
+ print(f"Error removing directory {dir_path}: {e}")
62
+
63
+ except Exception as e:
64
+ print(f"Error processing directory {temp_dir}: {e}")
65
+
66
+ if cleaned_count > 0:
67
+ print(f"Cleanup completed: removed {cleaned_count} temporary files")
68
+
69
+ except Exception as e:
70
+ print(f"Error during temp cleanup: {e}")
71
+
72
+
73
+ # 全局变量记录使用过的临时目录
74
+ used_temp_dirs = set()
75
+
76
+
77
+ def start_cleanup_scheduler():
78
+ """
79
+ 启动定时清理任务
80
+ """
81
+
82
+ def cleanup_worker():
83
+ while True:
84
+ try:
85
+ # 每30分钟清理一次
86
+ time.sleep(1800) # 1800秒 = 30分钟
87
+ print("Starting scheduled cleanup...")
88
+ # 只清理我们记录的临时目录
89
+ cleanup_temp_files(list(used_temp_dirs))
90
+ except Exception as e:
91
+ print(f"Error in cleanup scheduler: {e}")
92
+
93
+ # 创建守护线程
94
+ cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
95
+ cleanup_thread.start()
96
+
97
+
98
+ def upload_file_to_kie(file_path: str, api_key: str):
99
+ """
100
+ 上传文件到 KIE AI 的文件存储服务
101
+ 返回文件的公开访问URL
102
+ """
103
+ url = "https://kieai.redpandaai.co/api/file-stream-upload"
104
+ headers = {
105
+ "Authorization": f"Bearer {api_key}"
106
+ }
107
+
108
+ # 获取文件名和扩展名
109
+ file_name = os.path.basename(file_path)
110
+ file_ext = os.path.splitext(file_name)[1].lower()
111
+ if file_ext is None or file_ext == "":
112
+ file_ext = ".jpg"
113
+ file_name = uuid.uuid4().hex + file_ext
114
+
115
+ # 根据文件扩展名确定MIME类型
116
+ mime_types = {
117
+ '.jpg': 'image/jpeg',
118
+ '.jpeg': 'image/jpeg',
119
+ '.png': 'image/png',
120
+ '.gif': 'image/gif',
121
+ '.webp': 'image/webp'
122
+ }
123
+ mime_type = mime_types.get(file_ext, 'image/jpg')
124
+
125
+ try:
126
+ with open(file_path, 'rb') as file_handle:
127
+ # 准备文件上传数据
128
+ files = {
129
+ 'file': (file_name, file_handle, mime_type)
130
+ }
131
+ data = {
132
+ 'uploadPath': 'images/nano_banana',
133
+ "fileName": file_name
134
+ }
135
+
136
+ response = requests.post(url, headers=headers, files=files, data=data, timeout=60)
137
+ if response.status_code == 200:
138
+ resp_json = response.json()
139
+ if resp_json.get("success") and resp_json.get("data"):
140
+ # 返回下载URL
141
+ return True, resp_json["data"]["downloadUrl"]
142
+ else:
143
+ return False, resp_json.get("msg", "Upload failed")
144
+ else:
145
+ return False, f"Upload error, please try again later"
146
+ except:
147
+ return False, f"Upload error, please try again later"
148
+
149
+
150
+ def create_task(prompt: str, image_urls: list[str], api_key: str):
151
+ headers = {
152
+ "Content-Type": "application/json",
153
+ "Authorization": f"Bearer {api_key}"
154
+ }
155
+ url = "https://api.kie.ai/api/v1/playground/createTask"
156
+ payload = {
157
+ "model": "google/nano-banana-edit",
158
+ "callBackUrl": "",
159
+ "input": {
160
+ "prompt": prompt,
161
+ "image_urls": image_urls
162
+ }
163
+ }
164
+ try:
165
+ response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)
166
+ if response.status_code == 200:
167
+ resp_json = response.json()
168
+ if resp_json.get("code") == 200:
169
+ return 200, resp_json["data"]["taskId"]
170
+ else:
171
+ return 500, resp_json["message"]
172
+ except Exception:
173
+ pass
174
+ return 500, "Internal Server Error"
175
+
176
+
177
+ def get_task_result(taskId: str, api_key: str):
178
+ start_time = time.time()
179
+ url = "https://api.kie.ai/api/v1/playground/recordInfo"
180
+ params = {"taskId": taskId}
181
+ headers = {"Authorization": f"Bearer {api_key}"}
182
+ while time.time() - start_time < 600:
183
+ try:
184
+ response = requests.get(url, headers=headers, params=params, timeout=60)
185
+ if response.status_code == 200:
186
+ resp_json = response.json()
187
+ if resp_json.get("code") == 200:
188
+ if resp_json['data']['state'] == 'success':
189
+ # 解析resultJson字符串
190
+ result_json = json.loads(resp_json['data']['resultJson'])
191
+ return result_json.get('resultUrls', []) # 图片连接数组
192
+ elif resp_json['data']['state'] == 'fail':
193
+ return 500, resp_json['data']['failMsg']
194
+ else:
195
+ time.sleep(5) # 等待2秒后再次查询
196
+ continue
197
+ else:
198
+ return 500, resp_json["message"]
199
+ else:
200
+ break
201
+ except Exception as e:
202
+ pass
203
+ time.sleep(2) # 等待2秒后再次查询
204
+ return 500, "Task timeout, please try again later"
205
+
206
+
207
+ def get_image_list(file_paths):
208
+ """从文件路径列表获取PIL图片列表用于展示"""
209
+ if not file_paths:
210
+ return []
211
+
212
+ images = []
213
+ for path in file_paths:
214
+ try:
215
+ img = Image.open(path)
216
+ images.append(img)
217
+ except Exception:
218
+ continue
219
+ return images
220
+
221
+
222
+ def create_image_html(file_paths):
223
+ """创建图片展示的HTML代码"""
224
+ if not file_paths:
225
+ return "<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>"
226
+
227
+ html_items = []
228
+ for i, path in enumerate(file_paths):
229
+ try:
230
+ # 将图片转换为base64编码
231
+ with open(path, "rb") as img_file:
232
+ img_data = base64.b64encode(img_file.read()).decode()
233
+ img_ext = path.split('.')[-1].lower()
234
+ if img_ext in ['jpg', 'jpeg']:
235
+ mime_type = 'image/jpeg'
236
+ elif img_ext == 'png':
237
+ mime_type = 'image/png'
238
+ elif img_ext == 'gif':
239
+ mime_type = 'image/gif'
240
+ elif img_ext == 'webp':
241
+ mime_type = 'image/webp'
242
+ else:
243
+ mime_type = 'image/jpeg'
244
+
245
+ img_src = f"data:{mime_type};base64,{img_data}"
246
+
247
+ html_items.append(f"""
248
+ <div class="image-item" data-index="{i}">
249
+ <img src="{img_src}" alt="Uploaded image {i + 1}">
250
+ <div class="delete-btn" onclick="deleteImageByIndex({i})" data-index="{i}">×</div>
251
+ </div>
252
+ """)
253
+ except:
254
+ continue
255
+
256
+ if not html_items:
257
+ return "<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>"
258
+
259
+ html_content = f"""
260
+ <div id='image-display-area'>
261
+ {''.join(html_items)}
262
+ </div>
263
+ """
264
+
265
+ return html_content
266
+
267
+
268
+ def handle_file_upload(current_files, new_files):
269
+ """简化的文件上传处理"""
270
+ if not new_files:
271
+ return current_files or [], "Please select images"
272
+
273
+ # 确保是列表格式
274
+ current_list = current_files or []
275
+ new_list = new_files if isinstance(new_files, list) else [new_files]
276
+
277
+ # 合并并限制数量
278
+ all_files = current_list + [f for f in new_list if f]
279
+ if len(all_files) > 5:
280
+ all_files = all_files[:5]
281
+ message = f"Uploaded {len(all_files)} images (limit reached)"
282
+ else:
283
+ message = f"Uploaded {len(all_files)} images"
284
+
285
+ return all_files, message
286
+
287
+
288
+ def process_nano_banana(prompt, uploaded_files, api_key, progress=gr.Progress()):
289
+ """
290
+ 处理Nano Banana图像生成的主函数
291
+ """
292
+ # 验证输入
293
+ if not api_key or not api_key.strip():
294
+ return [], "❌ Please enter API Key"
295
+
296
+ if not prompt or not prompt.strip():
297
+ return [], "❌ Please enter a prompt"
298
+
299
+ # 验证图片列表
300
+ if not uploaded_files or len(uploaded_files) == 0:
301
+ return [], "❌ Please upload at least one image"
302
+
303
+ # 确保是列表格式
304
+ file_paths = uploaded_files if isinstance(uploaded_files, list) else [uploaded_files]
305
+ # 验证图片数量
306
+ if len(file_paths) > 5:
307
+ return [], "❌ Maximum 5 images allowed"
308
 
 
 
 
 
 
 
 
 
 
 
 
309
  try:
310
+ progress(0.1, desc="📤 Processing images...")
311
+
312
+ # 上传文件到 KIE AI 并获取公开URL
313
+ image_urls = []
314
+
315
+ for i, file_path in enumerate(file_paths):
316
+ progress(0.1 + (0.3 * i / len(file_paths)), desc=f"📤 Uploading image {i + 1}/{len(file_paths)}...")
317
+
318
+ success, result = upload_file_to_kie(file_path, api_key.strip())
319
+ if success:
320
+ image_urls.append(result)
321
+ # 上传成功后删除本地临时文件
322
+ try:
323
+ if os.path.exists(file_path):
324
+ # 记录文件所在的目录
325
+ temp_dir = os.path.dirname(file_path)
326
+ used_temp_dirs.add(temp_dir)
327
+
328
+ os.remove(file_path)
329
+ except:
330
+ pass
331
+ else:
332
+ return [], f"❌ Failed to upload image {i + 1}: {result}"
333
+
334
+ progress(0.5, desc="🚀 Creating processing task...")
335
+
336
+ # 创建任务
337
+ status_code, result = create_task(prompt.strip(), image_urls, api_key.strip())
338
+
339
+ if status_code != 200:
340
+ return [], f"❌ Failed to create task: {result}"
341
+
342
+ task_id = result
343
+ progress(0.6, desc=f"📋 Task ID: {task_id}")
344
+
345
+ progress(0.7, desc="⏳ Processing images, please wait...")
346
+
347
+ # 轮询获取结果
348
+ result = get_task_result(task_id, api_key.strip())
349
+
350
+ if isinstance(result, tuple) and result[0] == 500:
351
+ return [], f"❌ Task processing failed: {result[1]}"
352
+
353
+ progress(0.9, desc="📥 Downloading generated images...")
354
+
355
+ # 处理返回的图片URL数组
356
+ generated_images = []
357
+ if isinstance(result, list) and len(result) > 0:
358
+ total_images = len(result)
359
+ for i, img_url in enumerate(result):
360
+ try:
361
+ progress(0.9 + (0.1 * i / total_images), desc=f"📥 Downloading image {i + 1}/{total_images}...")
362
+ response = requests.get(img_url, timeout=60)
363
+ if response.status_code == 200:
364
+ img = Image.open(BytesIO(response.content))
365
+ generated_images.append(img)
366
+ except Exception as e:
367
+ pass
368
+
369
+ if generated_images:
370
+ progress(1.0, desc="✅ Complete!")
371
+ return generated_images, f"✅ Successfully generated {len(generated_images)} images!"
372
+ else:
373
+ return [], "❌ Unable to download any generated images"
374
+ else:
375
+ return [], "❌ No generated images received"
376
+
377
+ except:
378
+ return [], f"❌ Error occurred during processing"
379
+ finally:
380
+ # 最终清理:删除任何剩余的临时文件
381
+ try:
382
+ for file_path in file_paths:
383
+ if os.path.exists(file_path):
384
+ # 记录文件所在的目录
385
+ temp_dir = os.path.dirname(file_path)
386
+ used_temp_dirs.add(temp_dir)
387
+
388
+ os.remove(file_path)
389
+ except:
390
+ pass
391
+
392
+
393
+ # CSS样式,参考app.py的风格
394
+ css = """
395
+ .gradio-container {
396
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
397
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
398
+ min-height: 100vh;
399
+ }
400
+ .header-container {
401
+ background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%);
402
+ padding: 2.5rem;
403
+ border-radius: 24px;
404
+ margin-bottom: 2.5rem;
405
+ box-shadow: 0 20px 60px rgba(102, 126, 234, 0.25);
406
+ }
407
+ .logo-text {
408
+ font-size: 3.5rem;
409
+ font-weight: 900;
410
+ color: #2d3436;
411
+ text-align: center;
412
+ margin: 0;
413
+ letter-spacing: -2px;
414
+ }
415
+ .subtitle {
416
+ color: #2d3436;
417
+ text-align: center;
418
+ font-size: 1rem;
419
+ margin-top: 0.5rem;
420
+ opacity: 0.8;
421
+ }
422
+ .main-content {
423
+ background: rgba(255, 255, 255, 0.95);
424
+ backdrop-filter: blur(20px);
425
+ border-radius: 24px;
426
+ padding: 2.5rem;
427
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
428
+ }
429
+ .mode-indicator {
430
+ background: rgba(255, 255, 255, 0.3);
431
+ backdrop-filter: blur(10px);
432
+ border-radius: 12px;
433
+ padding: 0.5rem 1rem;
434
+ margin-top: 1rem;
435
+ text-align: center;
436
+ font-weight: 600;
437
+ color: #2d3436;
438
+ }
439
+ .gr-button-primary {
440
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
441
+ border: none !important;
442
+ color: white !important;
443
+ font-weight: 700 !important;
444
+ font-size: 1.1rem !important;
445
+ padding: 1.2rem 2rem !important;
446
+ border-radius: 14px !important;
447
+ text-transform: uppercase;
448
+ letter-spacing: 1px;
449
+ width: 100%;
450
+ margin-top: 1rem !important;
451
+ }
452
+ .gr-button-primary:hover {
453
+ background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important;
454
+ }
455
+ .gr-input, .gr-textarea {
456
+ background: #ffffff !important;
457
+ border: 1px solid #d1d5db !important;
458
+ border-radius: 8px !important;
459
+ color: #374151 !important;
460
+ font-size: 1rem !important;
461
+ padding: 0.75rem 1rem !important;
462
+ }
463
+ .gr-input:focus, .gr-textarea:focus {
464
+ border-color: #667eea !important;
465
+ outline: none !important;
466
+ }
467
+ .gr-form {
468
+ background: transparent !important;
469
+ border: none !important;
470
+ }
471
+ .gr-panel {
472
+ background: #ffffff !important;
473
+ border: 2px solid #e1e8ed !important;
474
+ border-radius: 16px !important;
475
+ padding: 1.5rem !important;
476
+ }
477
+ .gr-box {
478
+ border-radius: 14px !important;
479
+ border-color: #e1e8ed !important;
480
+ }
481
+ label {
482
+ color: #636e72 !important;
483
+ font-weight: 600 !important;
484
+ font-size: 0.85rem !important;
485
+ text-transform: uppercase;
486
+ letter-spacing: 0.5px;
487
+ margin-bottom: 0.5rem !important;
488
+ }
489
+ .status-text {
490
+ font-family: 'SF Mono', 'Monaco', monospace;
491
+ font-size: 0.95rem;
492
+ }
493
+ .image-container {
494
+ border-radius: 14px !important;
495
+ overflow: hidden;
496
+ border: 2px solid #e1e8ed !important;
497
+ background: #fafbfc !important;
498
+ }
499
+ footer {
500
+ display: none !important;
501
+ }
502
+ .info-box {
503
+ background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
504
+ border-radius: 12px;
505
+ padding: 1rem;
506
+ margin-bottom: 1rem;
507
+ border-left: 4px solid #2196f3;
508
+ }
509
+ .warning-box {
510
+ background: #fff3cd;
511
+ border-radius: 12px;
512
+ padding: 1rem;
513
+ margin-top: 1rem;
514
+ border-left: 4px solid #ffc107;
515
+ color: #856404;
516
+ }
517
+ /* 简化的图片展示区域 */
518
+ .image-upload-container {
519
+ background: white !important;
520
+ border-radius: 16px !important;
521
+ padding: 1.5rem !important;
522
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
523
+ }
524
+
525
+ /* 自定义图片展示区 - 无边框设计 */
526
+ #image-display-area {
527
+ display: flex !important;
528
+ flex-wrap: nowrap !important;
529
+ gap: 12px !important;
530
+ padding: 15px 5px !important;
531
+ overflow-x: auto !important;
532
+ overflow-y: hidden !important;
533
+ min-height: 120px !important;
534
+ max-height: 120px !important;
535
+ align-items: center !important;
536
+ background: transparent !important;
537
+ }
538
+
539
+ /* 无图片时的提示 */
540
+ .no-images {
541
+ color: #9ca3af !important;
542
+ font-size: 14px !important;
543
+ text-align: center !important;
544
+ width: 100% !important;
545
+ padding: 20px !important;
546
+ }
547
+
548
+ /* 图片项容器 */
549
+ .image-item {
550
+ position: relative !important;
551
+ flex-shrink: 0 !important;
552
+ width: 100px !important;
553
+ height: 100px !important;
554
+ border-radius: 8px !important;
555
+ overflow: hidden !important;
556
+ cursor: pointer !important;
557
+ transition: transform 0.2s ease, box-shadow 0.2s ease !important;
558
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
559
+ }
560
+
561
+ .image-item:hover {
562
+ transform: scale(1.05) !important;
563
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
564
+ }
565
+
566
+ /* 图片样式 */
567
+ .image-item img {
568
+ width: 100% !important;
569
+ height: 100% !important;
570
+ object-fit: cover !important;
571
+ border-radius: 8px !important;
572
+ display: block !important;
573
+ }
574
+
575
+ /* 删除按钮 */
576
+ .image-item .delete-btn {
577
+ position: absolute !important;
578
+ top: -8px !important;
579
+ right: -8px !important;
580
+ width: 24px !important;
581
+ height: 24px !important;
582
+ background: #ef4444 !important;
583
+ color: white !important;
584
+ border: 2px solid white !important;
585
+ border-radius: 50% !important;
586
+ display: flex !important;
587
+ align-items: center !important;
588
+ justify-content: center !important;
589
+ font-size: 12px !important;
590
+ font-weight: bold !important;
591
+ cursor: pointer !important;
592
+ opacity: 0 !important;
593
+ transition: all 0.2s ease !important;
594
+ z-index: 10 !important;
595
+ line-height: 1 !important;
596
+ }
597
+
598
+ .image-item:hover .delete-btn {
599
+ opacity: 1 !important;
600
+ }
601
+
602
+ .image-item .delete-btn:hover {
603
+ background: #dc2626 !important;
604
+ transform: scale(1.1) !important;
605
+ }
606
+
607
+ /* 隐藏独立的删除按钮组 */
608
+ #delete-buttons-row {
609
+ display: none !important;
610
+ }
611
+
612
+ /* 滚动条样式 */
613
+ #image-display-area::-webkit-scrollbar {
614
+ height: 6px !important;
615
+ }
616
+
617
+ #image-display-area::-webkit-scrollbar-track {
618
+ background: rgba(0, 0, 0, 0.05) !important;
619
+ border-radius: 3px !important;
620
+ }
621
+
622
+ #image-display-area::-webkit-scrollbar-thumb {
623
+ background: rgba(0, 0, 0, 0.2) !important;
624
+ border-radius: 3px !important;
625
+ }
626
+
627
+ #image-display-area::-webkit-scrollbar-thumb:hover {
628
+ background: rgba(0, 0, 0, 0.3) !important;
629
+ }
630
+
631
+ /* 更小的上传文件框 */
632
+ .gr-file {
633
+ background: white !important;
634
+ border: 1px dashed #d1d5db !important;
635
+ border-radius: 4px !important;
636
+ padding: 0.1rem 0.3rem !important;
637
+ font-size: 0.65rem !important;
638
+ min-height: 16px !important;
639
+ height: 16px !important;
640
+ max-width: 60px !important;
641
+ }
642
+
643
+ .gr-file:hover {
644
+ border-color: #667eea !important;
645
+ background: #f9fafb !important;
646
+ }
647
+
648
+ .gr-file .wrap {
649
+ min-height: 14px !important;
650
+ padding: 0 !important;
651
+ display: flex !important;
652
+ align-items: center !important;
653
+ justify-content: center !important;
654
+ }
655
+
656
+ .gr-file .wrap > div {
657
+ font-size: 0.65rem !important;
658
+ color: #6b7280 !important;
659
+ line-height: 1 !important;
660
+ }
661
+
662
+
663
+ /* 输出标题样式 */
664
+ .output-title {
665
+ margin-bottom: 0.5rem !important;
666
+ margin-top: 0 !important;
667
+ }
668
+
669
+ .output-title h3 {
670
+ margin: 0 !important;
671
+ padding: 0 !important;
672
+ font-size: 1.1rem !important;
673
+ color: #374151 !important;
674
+ font-weight: 600 !important;
675
+ }
676
+
677
+ /* 输出图片画廊 */
678
+ #output-gallery {
679
+ border: 2px solid #e1e8ed !important;
680
+ border-radius: 14px !important;
681
+ padding: 1rem !important;
682
+ background: #fafbfc !important;
683
+ height: 500px !important; /* 固定高度 */
684
+ overflow: hidden !important; /* 隐藏外层滚动条 */
685
+ position: relative !important;
686
+ }
687
+
688
+ /* 输出画廊内部容器控制 */
689
+ #output-gallery > div {
690
+ height: 100% !important;
691
+ overflow: hidden !important;
692
+ }
693
+
694
+ #output-gallery .gr-gallery {
695
+ height: 100% !important;
696
+ position: relative !important;
697
+ }
698
+
699
+ /* 输出画廊滚动容器 - 只允许垂直滚动 */
700
+ #output-gallery .gr-gallery-container {
701
+ position: absolute !important;
702
+ top: 0 !important;
703
+ left: 0 !important;
704
+ right: 0 !important;
705
+ bottom: 0 !important;
706
+ overflow-y: auto !important;
707
+ overflow-x: hidden !important;
708
+ gap: 1rem !important;
709
+ padding: 5px !important;
710
+ }
711
+
712
+ /* 美化输出画廊的滚动条 */
713
+ #output-gallery .gr-gallery-container::-webkit-scrollbar {
714
+ width: 8px !important;
715
+ }
716
+
717
+ #output-gallery .gr-gallery-container::-webkit-scrollbar-track {
718
+ background: rgba(0, 0, 0, 0.05) !important;
719
+ border-radius: 4px !important;
720
+ }
721
+
722
+ #output-gallery .gr-gallery-container::-webkit-scrollbar-thumb {
723
+ background: rgba(0, 0, 0, 0.2) !important;
724
+ border-radius: 4px !important;
725
+ }
726
+
727
+ #output-gallery .gr-gallery-container::-webkit-scrollbar-thumb:hover {
728
+ background: rgba(0, 0, 0, 0.3) !important;
729
+ }
730
+
731
+ .gr-group {
732
+ background: #f8f9fa !important;
733
+ border-radius: 14px !important;
734
+ padding: 1rem !important;
735
+ margin-bottom: 1rem !important;
736
+ }
737
+ """
738
+
739
+ # 创建Gradio界面
740
+ with gr.Blocks(css=css, theme=gr.themes.Base()) as demo:
741
+ with gr.Column(elem_classes="header-container"):
742
+ gr.HTML("""
743
+ <h1 class="logo-text">🍌 Nano Banana API Free Online Test</h1>
744
+ <p class="subtitle">Powered by Google’s Official Gemini 2.5 Flash Image Model</p>
745
+ <div class="mode-indicator">
746
+ 💡 Nano Banana AI makes editing smarter — upload up to 5 photos, refine details, and maintain subject consistency. Free to try: 80 credits = 20 images.
747
+ </div>
748
+ """)
749
+
750
+ with gr.Column(elem_classes="main-content"):
751
+ # 信息提示框
752
+ gr.HTML("""
753
+ <div class="info-box">
754
+ <strong>Usage Instructions:</strong><br>
755
+ • Get your Nano Banana API Key <a href="https://kie.ai/nano-banana" target="_blank">👉 here 👈</a><br>
756
+ • Add your image and enter a prompt to generate or edit.<br>
757
+ • Upload 1–5 reference images (Max 10MB each, formats: JPG, PNG, WebP)<br>
758
+ • Click Generate and wait ~1–2 minutes for processing
759
+ </div>
760
+ """)
761
+
762
+ with gr.Row(equal_height=True):
763
+ # 左侧 - 输入区域
764
+ with gr.Column(scale=1):
765
+ # API Key输入
766
+ api_key = gr.Textbox(
767
+ label="API Key",
768
+ placeholder="Please enter your KIE AI API Key",
769
+ type="password",
770
+ elem_classes="api-key-input"
771
+ )
772
+
773
+ # 提示词输入
774
+ prompt = gr.Textbox(
775
+ label="Editing Prompt",
776
+ placeholder="Describe the image effect you want, e.g.: Convert image to cartoon style...",
777
+ lines=3,
778
+ value="Transform the image into a dreamy oil painting style with vibrant colors and clear brushstrokes",
779
+ elem_classes="prompt-input"
780
+ )
781
+
782
+ # 重新设计的图片上传区域
783
+ with gr.Group(elem_classes="image-upload-container"):
784
+ gr.Markdown("### 📸 Image Upload")
785
+
786
+ # 自定义图片展示区(无边框)
787
+ image_display = gr.HTML(
788
+ value="<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>",
789
+ elem_id="image-display"
790
+ )
791
+
792
+ # 简单的上传按钮
793
+ file_upload = gr.File(
794
+ show_label=False,
795
+ file_count="multiple",
796
+ file_types=["image"],
797
+ type="filepath",
798
+ height=120, # 缩小高度
799
+ )
800
+
801
+ # 添加删除按钮组(在图片上传区域内)- 通过CSS隐藏而不是visible=False
802
+ with gr.Row(elem_id="delete-buttons-row"):
803
+ delete_label = gr.Markdown("**Delete Images:**", visible=True, elem_id="delete-label")
804
+ delete_buttons = []
805
+ for i in range(5): # 最多支持5张图片
806
+ btn = gr.Button(f"Delete {i + 1}", visible=True, size="sm", elem_id=f"delete-btn-{i}")
807
+ delete_buttons.append(btn)
808
+
809
+ # 生成按钮
810
+ generate_btn = gr.Button(
811
+ "🚀 Start Generation",
812
+ variant="primary",
813
+ size="lg"
814
+ )
815
+
816
+ # 右侧 - 输出区域
817
+ with gr.Column(scale=1):
818
+ # 添加输出标题
819
+ gr.Markdown("### 🎨 Generation Results", elem_classes="output-title")
820
+
821
+ # 输出图片画廊
822
+ output_gallery = gr.Gallery(
823
+ show_label=False,
824
+ elem_id="output-gallery",
825
+ columns=2,
826
+ rows=None,
827
+ object_fit="contain",
828
+ height=500,
829
+ preview=True,
830
+ container=True
831
+ )
832
+
833
+ # 状态信息
834
+ status = gr.Textbox(
835
+ label="Processing Status",
836
+ interactive=False,
837
+ lines=2,
838
+ value="Ready, please upload images and enter a prompt..."
839
+ )
840
+
841
+ # 示例
842
+ gr.Examples(
843
+ examples=[
844
+ ["Convert image to cartoon anime style", None],
845
+ ["Apply Van Gogh's starry night style", None],
846
+ ["Convert to black and white sketch", None],
847
+ ["Add neon light effects, cyberpunk style", None],
848
+ ["Convert to watercolor painting style with soft tones", None],
849
+ ],
850
+ inputs=[prompt, api_key],
851
+ label="Prompt Examples"
852
  )
853
+
854
+ # 状态变量
855
+ current_files = gr.State([])
856
+
857
+
858
+ # 更新的事件处理函数
859
+ def on_file_upload(current_paths, new_files):
860
+ """处理文件上传并更新HTML显示"""
861
+ updated_files, message = handle_file_upload(current_paths, new_files)
862
+ html_content = create_image_html(updated_files)
863
+ return updated_files, gr.update(value=None), html_content
864
 
865
 
866
+ # 为每个删除按钮绑定事件
867
+ def create_delete_handler(index):
868
+ def delete_image_at_index(current_paths):
869
+ if not current_paths or index >= len(current_paths):
870
+ return current_paths, create_image_html(current_paths), *update_delete_buttons(current_paths)
871
+
872
+ # 删除指定索引的图片
873
+ updated_files = current_paths[:index] + current_paths[index + 1:]
874
+ return updated_files, create_image_html(updated_files), *update_delete_buttons(updated_files)
875
+
876
+ return delete_image_at_index
877
+
878
+
879
+ def update_delete_buttons(file_paths):
880
+ """更新删除按钮 - 保持所有按钮visible=True,通过CSS控制显示"""
881
+ updates = []
882
+ updates.append(gr.update()) # 标签保持不变
883
+
884
+ for i in range(5):
885
+ if i < len(file_paths):
886
+ updates.append(gr.update(value=f"Delete Image {i + 1}"))
887
+ else:
888
+ updates.append(gr.update()) # 保持不变
889
+ return updates
890
+
891
+
892
+ # 绑定删除按钮事件
893
+ outputs_list = [current_files, image_display, delete_label] + delete_buttons
894
+
895
+ for i, btn in enumerate(delete_buttons):
896
+ btn.click(
897
+ fn=create_delete_handler(i),
898
+ inputs=[current_files],
899
+ outputs=outputs_list
900
+ )
901
+
902
+
903
+ # 更新文件上传事件,同时更新删除按钮
904
+ def on_file_upload_with_buttons(current_paths, new_files):
905
+ updated_files, message = handle_file_upload(current_paths, new_files)
906
+ html_content = create_image_html(updated_files)
907
+ button_updates = update_delete_buttons(updated_files)
908
+ return updated_files, gr.update(value=None), html_content, *button_updates
909
+
910
+
911
+ file_upload.upload(
912
+ fn=on_file_upload_with_buttons,
913
+ inputs=[current_files, file_upload],
914
+ outputs=[current_files, file_upload, image_display, delete_label] + delete_buttons
915
+ )
916
+
917
+
918
+ # 生成按钮事件
919
+ def prepare_and_generate(prompt, paths, api_key, progress=gr.Progress()):
920
+ """生成图片的主函数"""
921
+ if not paths:
922
+ return [], "❌ Please upload at least one image"
923
+ return process_nano_banana(prompt, paths, api_key, progress)
924
+
925
+
926
+ generate_btn.click(
927
+ fn=prepare_and_generate,
928
+ inputs=[prompt, current_files, api_key],
929
+ outputs=[output_gallery, status]
930
+ )
931
+
932
+ # 添加JavaScript代码来连接图片上的删除按钮和隐藏的独立删除按钮
933
+ demo.load(None, None, None, js="""
934
+ () => {
935
+ // 创建全局删除函数
936
+ window.deleteImageByIndex = function(index) {
937
+ // Gradio的elem_id设置在包装器上,需要找到内部的button
938
+ let deleteBtn = null;
939
+
940
+ // 方法1: 通过ID找到包装器,然后找内部的button
941
+ const wrapper = document.getElementById(`delete-btn-${index}`);
942
+ if (wrapper) {
943
+ deleteBtn = wrapper.querySelector('button');
944
+ }
945
+
946
+ // 方法2: 如果方法1失败,查找所有按钮并通过文本内容匹配
947
+ if (!deleteBtn) {
948
+ const allButtons = document.querySelectorAll('button');
949
+ for (let btn of allButtons) {
950
+ if (btn.textContent.includes(`Delete Image ${index + 1}`)) {
951
+ deleteBtn = btn;
952
+ break;
953
+ }
954
+ }
955
+ }
956
+
957
+ if (deleteBtn) {
958
+ deleteBtn.click();
959
+ } else {
960
+ // 调试信息
961
+ document.querySelectorAll('[id^="delete-btn-"]').forEach(elem => {
962
+ console.log(elem.id, elem.tagName, elem.querySelector('button'));
963
+ });
964
+ }
965
+ };
966
+ }
967
+ """)
968
+
969
+ # 启动应用
970
  if __name__ == "__main__":
971
+ app_instance = demo
972
+
973
+ # 启动定时清理任务
974
+ start_cleanup_scheduler()
975
+
976
+ demo.launch(
977
+ share=True,
978
+ server_name="0.0.0.0",
979
+ server_port=7865 # 使用不同的端口避免冲突
980
+ )