dmmmmm commited on
Commit
24f71ec
·
verified ·
1 Parent(s): 82ce405

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -969
app.py CHANGED
@@ -1,980 +1,44 @@
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
- )
 
 
 
 
 
 
 
1
+ """
2
+ Veo3 AI Video Generator - 主应用文件
3
+ 重构后的模块化版本
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
+ import sys
7
+ import os
8
+ import socket
9
+ import random
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # 添加src目录到Python路径
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
 
 
 
13
 
14
+ from src.ui import Veo3Interface
15
+ from src.utils import start_cleanup_scheduler
 
 
 
 
 
 
 
 
16
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ def main():
19
+ """主函数"""
20
+ print("🎬 Starting Veo3 AI Video Generator...")
21
 
22
  # 启动定时清理任务
23
  start_cleanup_scheduler()
24
+ print("✅ Cleanup scheduler started")
25
+
26
+ # 创建界面
27
+ interface = Veo3Interface()
28
+ interface.create_interface()
29
+ print("✅ Interface created")
30
+
31
+ # 启动应用
32
+ print("🚀 Launching application...")
33
+ try:
34
+ interface.launch(
35
  share=True,
36
  server_name="0.0.0.0",
37
+ server_port=7860
38
+ )
39
+ except Exception as e:
40
+ print(f"❌ 启动失败: {e}")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()