Nano-Banana / app.py
dmmmmm's picture
Update app.py
38f0e14 verified
import base64
import json
import time
import os
import uuid
import threading
from io import BytesIO
import requests
import gradio as gr
from PIL import Image
# 全局变量存储应用实例
app_instance = None
def cleanup_temp_files(temp_dirs=None):
"""
清理指定的临时文件夹中的旧文件
temp_dirs: 要清理的目录列表,如果为None则只清理记录的目录
"""
try:
# 如果没有指定目录,只清理我们记录的目录
if temp_dirs is None:
temp_dirs = []
current_time = time.time()
cleaned_count = 0
for temp_dir in temp_dirs:
try:
# 确保目录存在且是目录
if not os.path.exists(temp_dir) or not os.path.isdir(temp_dir):
continue
# 遍历目录中的文件
for root, dirs, files in os.walk(temp_dir):
for file_name in files:
file_path = os.path.join(root, file_name)
try:
# 检查文件修改时间
file_mtime = os.path.getmtime(file_path)
# 如果文件超过30分钟未修改,则删除
if current_time - file_mtime > 1800: # 1800秒 = 30分钟
# 检查是否是图片文件或临时文件
if any(file_path.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tmp']):
os.remove(file_path)
cleaned_count += 1
print(f"Cleaned old temp file: {file_path}")
except Exception as e:
print(f"Error cleaning {file_path}: {e}")
# 清理空目录
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
try:
if os.path.exists(dir_path) and not os.listdir(dir_path):
os.rmdir(dir_path)
print(f"Removed empty temp directory: {dir_path}")
except Exception as e:
print(f"Error removing directory {dir_path}: {e}")
except Exception as e:
print(f"Error processing directory {temp_dir}: {e}")
if cleaned_count > 0:
print(f"Cleanup completed: removed {cleaned_count} temporary files")
except Exception as e:
print(f"Error during temp cleanup: {e}")
# 全局变量记录使用过的临时目录
used_temp_dirs = set()
def start_cleanup_scheduler():
"""
启动定时清理任务
"""
def cleanup_worker():
while True:
try:
# 每30分钟清理一次
time.sleep(1800) # 1800秒 = 30分钟
print("Starting scheduled cleanup...")
# 只清理我们记录的临时目录
cleanup_temp_files(list(used_temp_dirs))
except Exception as e:
print(f"Error in cleanup scheduler: {e}")
# 创建守护线程
cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
cleanup_thread.start()
def upload_file_to_kie(file_path: str, api_key: str):
"""
上传文件到 KIE AI 的文件存储服务
返回文件的公开访问URL
"""
url = "https://kieai.redpandaai.co/api/file-stream-upload"
headers = {
"Authorization": f"Bearer {api_key}"
}
# 获取文件名和扩展名
file_name = os.path.basename(file_path)
file_ext = os.path.splitext(file_name)[1].lower()
if file_ext is None or file_ext == "":
file_ext = ".jpg"
file_name = uuid.uuid4().hex + file_ext
# 根据文件扩展名确定MIME类型
mime_types = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp'
}
mime_type = mime_types.get(file_ext, 'image/jpg')
try:
with open(file_path, 'rb') as file_handle:
# 准备文件上传数据
files = {
'file': (file_name, file_handle, mime_type)
}
data = {
'uploadPath': 'images/nano_banana',
"fileName": file_name
}
response = requests.post(url, headers=headers, files=files, data=data, timeout=60)
if response.status_code == 200:
resp_json = response.json()
if resp_json.get("success") and resp_json.get("data"):
# 返回下载URL
return True, resp_json["data"]["downloadUrl"]
else:
return False, resp_json.get("msg", "Upload failed")
else:
return False, f"Upload error, please try again later"
except:
return False, f"Upload error, please try again later"
def create_task(prompt: str, image_urls: list[str], api_key: str):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
url = "https://api.kie.ai/api/v1/playground/createTask"
payload = {
"model": "google/nano-banana-edit",
"callBackUrl": "",
"input": {
"prompt": prompt,
"image_urls": image_urls
}
}
try:
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=60)
if response.status_code == 200:
resp_json = response.json()
if resp_json.get("code") == 200:
return 200, resp_json["data"]["taskId"]
else:
return 500, resp_json["message"]
except Exception:
pass
return 500, "Internal Server Error"
def get_task_result(taskId: str, api_key: str):
start_time = time.time()
url = "https://api.kie.ai/api/v1/playground/recordInfo"
params = {"taskId": taskId}
headers = {"Authorization": f"Bearer {api_key}"}
while time.time() - start_time < 600:
try:
response = requests.get(url, headers=headers, params=params, timeout=60)
if response.status_code == 200:
resp_json = response.json()
if resp_json.get("code") == 200:
if resp_json['data']['state'] == 'success':
# 解析resultJson字符串
result_json = json.loads(resp_json['data']['resultJson'])
return result_json.get('resultUrls', []) # 图片连接数组
elif resp_json['data']['state'] == 'fail':
return 500, resp_json['data']['failMsg']
else:
time.sleep(5) # 等待2秒后再次查询
continue
else:
return 500, resp_json["message"]
else:
break
except Exception as e:
pass
time.sleep(2) # 等待2秒后再次查询
return 500, "Task timeout, please try again later"
def get_image_list(file_paths):
"""从文件路径列表获取PIL图片列表用于展示"""
if not file_paths:
return []
images = []
for path in file_paths:
try:
img = Image.open(path)
images.append(img)
except Exception:
continue
return images
def create_image_html(file_paths):
"""创建图片展示的HTML代码"""
if not file_paths:
return "<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>"
html_items = []
for i, path in enumerate(file_paths):
try:
# 将图片转换为base64编码
with open(path, "rb") as img_file:
img_data = base64.b64encode(img_file.read()).decode()
img_ext = path.split('.')[-1].lower()
if img_ext in ['jpg', 'jpeg']:
mime_type = 'image/jpeg'
elif img_ext == 'png':
mime_type = 'image/png'
elif img_ext == 'gif':
mime_type = 'image/gif'
elif img_ext == 'webp':
mime_type = 'image/webp'
else:
mime_type = 'image/jpeg'
img_src = f"data:{mime_type};base64,{img_data}"
html_items.append(f"""
<div class="image-item" data-index="{i}">
<img src="{img_src}" alt="Uploaded image {i + 1}">
<div class="delete-btn" onclick="deleteImageByIndex({i})" data-index="{i}">×</div>
</div>
""")
except:
continue
if not html_items:
return "<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>"
html_content = f"""
<div id='image-display-area'>
{''.join(html_items)}
</div>
"""
return html_content
def handle_file_upload(current_files, new_files):
"""简化的文件上传处理"""
if not new_files:
return current_files or [], "Please select images"
# 确保是列表格式
current_list = current_files or []
new_list = new_files if isinstance(new_files, list) else [new_files]
# 合并并限制数量
all_files = current_list + [f for f in new_list if f]
if len(all_files) > 5:
all_files = all_files[:5]
message = f"Uploaded {len(all_files)} images (limit reached)"
else:
message = f"Uploaded {len(all_files)} images"
return all_files, message
def process_nano_banana(prompt, uploaded_files, api_key, progress=gr.Progress()):
"""
处理Nano Banana图像生成的主函数
"""
# 验证输入
if not api_key or not api_key.strip():
return [], "❌ Please enter API Key"
if not prompt or not prompt.strip():
return [], "❌ Please enter a prompt"
# 验证图片列表
if not uploaded_files or len(uploaded_files) == 0:
return [], "❌ Please upload at least one image"
# 确保是列表格式
file_paths = uploaded_files if isinstance(uploaded_files, list) else [uploaded_files]
# 验证图片数量
if len(file_paths) > 5:
return [], "❌ Maximum 5 images allowed"
try:
progress(0.1, desc="📤 Processing images...")
# 上传文件到 KIE AI 并获取公开URL
image_urls = []
for i, file_path in enumerate(file_paths):
progress(0.1 + (0.3 * i / len(file_paths)), desc=f"📤 Uploading image {i + 1}/{len(file_paths)}...")
success, result = upload_file_to_kie(file_path, api_key.strip())
if success:
image_urls.append(result)
# 上传成功后删除本地临时文件
try:
if os.path.exists(file_path):
# 记录文件所在的目录
temp_dir = os.path.dirname(file_path)
used_temp_dirs.add(temp_dir)
os.remove(file_path)
except:
pass
else:
return [], f"❌ Failed to upload image {i + 1}: {result}"
progress(0.5, desc="🚀 Creating processing task...")
# 创建任务
status_code, result = create_task(prompt.strip(), image_urls, api_key.strip())
if status_code != 200:
return [], f"❌ Failed to create task: {result}"
task_id = result
progress(0.6, desc=f"📋 Task ID: {task_id}")
progress(0.7, desc="⏳ Processing images, please wait...")
# 轮询获取结果
result = get_task_result(task_id, api_key.strip())
if isinstance(result, tuple) and result[0] == 500:
return [], f"❌ Task processing failed: {result[1]}"
progress(0.9, desc="📥 Downloading generated images...")
# 处理返回的图片URL数组
generated_images = []
if isinstance(result, list) and len(result) > 0:
total_images = len(result)
for i, img_url in enumerate(result):
try:
progress(0.9 + (0.1 * i / total_images), desc=f"📥 Downloading image {i + 1}/{total_images}...")
response = requests.get(img_url, timeout=60)
if response.status_code == 200:
img = Image.open(BytesIO(response.content))
generated_images.append(img)
except Exception as e:
pass
if generated_images:
progress(1.0, desc="✅ Complete!")
return generated_images, f"✅ Successfully generated {len(generated_images)} images!"
else:
return [], "❌ Unable to download any generated images"
else:
return [], "❌ No generated images received"
except:
return [], f"❌ Error occurred during processing"
finally:
# 最终清理:删除任何剩余的临时文件
try:
for file_path in file_paths:
if os.path.exists(file_path):
# 记录文件所在的目录
temp_dir = os.path.dirname(file_path)
used_temp_dirs.add(temp_dir)
os.remove(file_path)
except:
pass
# CSS样式,参考app.py的风格
css = """
.gradio-container {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-height: 100vh;
}
.header-container {
background: linear-gradient(135deg, #ffd93d 0%, #ffb347 100%);
padding: 2.5rem;
border-radius: 24px;
margin-bottom: 2.5rem;
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.25);
}
.logo-text {
font-size: 3.5rem;
font-weight: 900;
color: #2d3436;
text-align: center;
margin: 0;
letter-spacing: -2px;
}
.subtitle {
color: #2d3436;
text-align: center;
font-size: 1rem;
margin-top: 0.5rem;
opacity: 0.8;
}
.main-content {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
padding: 2.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
}
.mode-indicator {
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 0.5rem 1rem;
margin-top: 1rem;
text-align: center;
font-weight: 600;
color: #2d3436;
}
.gr-button-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
color: white !important;
font-weight: 700 !important;
font-size: 1.1rem !important;
padding: 1.2rem 2rem !important;
border-radius: 14px !important;
text-transform: uppercase;
letter-spacing: 1px;
width: 100%;
margin-top: 1rem !important;
}
.gr-button-primary:hover {
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important;
}
.gr-input, .gr-textarea {
background: #ffffff !important;
border: 1px solid #d1d5db !important;
border-radius: 8px !important;
color: #374151 !important;
font-size: 1rem !important;
padding: 0.75rem 1rem !important;
}
.gr-input:focus, .gr-textarea:focus {
border-color: #667eea !important;
outline: none !important;
}
.gr-form {
background: transparent !important;
border: none !important;
}
.gr-panel {
background: #ffffff !important;
border: 2px solid #e1e8ed !important;
border-radius: 16px !important;
padding: 1.5rem !important;
}
.gr-box {
border-radius: 14px !important;
border-color: #e1e8ed !important;
}
label {
color: #636e72 !important;
font-weight: 600 !important;
font-size: 0.85rem !important;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem !important;
}
.status-text {
font-family: 'SF Mono', 'Monaco', monospace;
font-size: 0.95rem;
}
.image-container {
border-radius: 14px !important;
overflow: hidden;
border: 2px solid #e1e8ed !important;
background: #fafbfc !important;
}
footer {
display: none !important;
}
.info-box {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
border-left: 4px solid #2196f3;
}
.warning-box {
background: #fff3cd;
border-radius: 12px;
padding: 1rem;
margin-top: 1rem;
border-left: 4px solid #ffc107;
color: #856404;
}
/* 简化的图片展示区域 */
.image-upload-container {
background: white !important;
border-radius: 16px !important;
padding: 1.5rem !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
}
/* 自定义图片展示区 - 无边框设计 */
#image-display-area {
display: flex !important;
flex-wrap: nowrap !important;
gap: 12px !important;
padding: 15px 5px !important;
overflow-x: auto !important;
overflow-y: hidden !important;
min-height: 120px !important;
max-height: 120px !important;
align-items: center !important;
background: transparent !important;
}
/* 无图片时的提示 */
.no-images {
color: #9ca3af !important;
font-size: 14px !important;
text-align: center !important;
width: 100% !important;
padding: 20px !important;
}
/* 图片项容器 */
.image-item {
position: relative !important;
flex-shrink: 0 !important;
width: 100px !important;
height: 100px !important;
border-radius: 8px !important;
overflow: hidden !important;
cursor: pointer !important;
transition: transform 0.2s ease, box-shadow 0.2s ease !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
}
.image-item:hover {
transform: scale(1.05) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
/* 图片样式 */
.image-item img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
border-radius: 8px !important;
display: block !important;
}
/* 删除按钮 */
.image-item .delete-btn {
position: absolute !important;
top: -8px !important;
right: -8px !important;
width: 24px !important;
height: 24px !important;
background: #ef4444 !important;
color: white !important;
border: 2px solid white !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 12px !important;
font-weight: bold !important;
cursor: pointer !important;
opacity: 0 !important;
transition: all 0.2s ease !important;
z-index: 10 !important;
line-height: 1 !important;
}
.image-item:hover .delete-btn {
opacity: 1 !important;
}
.image-item .delete-btn:hover {
background: #dc2626 !important;
transform: scale(1.1) !important;
}
/* 隐藏独立的删除按钮组 */
#delete-buttons-row {
display: none !important;
}
/* 滚动条样式 */
#image-display-area::-webkit-scrollbar {
height: 6px !important;
}
#image-display-area::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05) !important;
border-radius: 3px !important;
}
#image-display-area::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2) !important;
border-radius: 3px !important;
}
#image-display-area::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3) !important;
}
/* 更小的上传文件框 */
.gr-file {
background: white !important;
border: 1px dashed #d1d5db !important;
border-radius: 4px !important;
padding: 0.1rem 0.3rem !important;
font-size: 0.65rem !important;
min-height: 16px !important;
height: 16px !important;
max-width: 60px !important;
}
.gr-file:hover {
border-color: #667eea !important;
background: #f9fafb !important;
}
.gr-file .wrap {
min-height: 14px !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.gr-file .wrap > div {
font-size: 0.65rem !important;
color: #6b7280 !important;
line-height: 1 !important;
}
/* 输出标题样式 */
.output-title {
margin-bottom: 0.5rem !important;
margin-top: 0 !important;
}
.output-title h3 {
margin: 0 !important;
padding: 0 !important;
font-size: 1.1rem !important;
color: #374151 !important;
font-weight: 600 !important;
}
/* 输出图片画廊 */
#output-gallery {
border: 2px solid #e1e8ed !important;
border-radius: 14px !important;
padding: 1rem !important;
background: #fafbfc !important;
height: 500px !important; /* 固定高度 */
overflow: hidden !important; /* 隐藏外层滚动条 */
position: relative !important;
}
/* 输出画廊内部容器控制 */
#output-gallery > div {
height: 100% !important;
overflow: hidden !important;
}
#output-gallery .gr-gallery {
height: 100% !important;
position: relative !important;
}
/* 输出画廊滚动容器 - 只允许垂直滚动 */
#output-gallery .gr-gallery-container {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
gap: 1rem !important;
padding: 5px !important;
}
/* 美化输出画廊的滚动条 */
#output-gallery .gr-gallery-container::-webkit-scrollbar {
width: 8px !important;
}
#output-gallery .gr-gallery-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05) !important;
border-radius: 4px !important;
}
#output-gallery .gr-gallery-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2) !important;
border-radius: 4px !important;
}
#output-gallery .gr-gallery-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3) !important;
}
.gr-group {
background: #f8f9fa !important;
border-radius: 14px !important;
padding: 1rem !important;
margin-bottom: 1rem !important;
}
"""
# 创建Gradio界面
with gr.Blocks(css=css, theme=gr.themes.Base()) as demo:
with gr.Column(elem_classes="header-container"):
gr.HTML("""
<h1 class="logo-text">🍌 Nano Banana API Free Online Test</h1>
<p class="subtitle">Powered by Google’s Official Gemini 2.5 Flash Image Model</p>
<div class="mode-indicator">
💡 Nano Banana AI makes editing smarter — upload up to 5 photos, refine details, and maintain subject consistency. Free to try: 80 credits = 20 images.
</div>
""")
with gr.Column(elem_classes="main-content"):
# 信息提示框
gr.HTML("""
<div class="info-box">
<strong>Usage Instructions:</strong><br>
• Get your Nano Banana API Key <a href="https://kie.ai/nano-banana" target="_blank">👉 here 👈</a><br>
• Add your image and enter a prompt to generate or edit.<br>
• Upload 1–5 reference images (Max 10MB each, formats: JPG, PNG, WebP)<br>
• Click Generate and wait ~1–2 minutes for processing
</div>
""")
with gr.Row(equal_height=True):
# 左侧 - 输入区域
with gr.Column(scale=1):
# API Key输入
api_key = gr.Textbox(
label="API Key",
placeholder="Please enter your KIE AI API Key",
type="password",
elem_classes="api-key-input"
)
# 提示词输入
prompt = gr.Textbox(
label="Editing Prompt",
placeholder="Describe the image effect you want, e.g.: Convert image to cartoon style...",
lines=3,
value="Transform the image into a dreamy oil painting style with vibrant colors and clear brushstrokes",
elem_classes="prompt-input"
)
# 重新设计的图片上传区域
with gr.Group(elem_classes="image-upload-container"):
gr.Markdown("### 📸 Image Upload")
# 自定义图片展示区(无边框)
image_display = gr.HTML(
value="<div id='image-display-area'><div class='no-images'>No images yet, please upload images</div></div>",
elem_id="image-display"
)
# 简单的上传按钮
file_upload = gr.File(
show_label=False,
file_count="multiple",
file_types=["image"],
type="filepath",
height=120, # 缩小高度
)
# 添加删除按钮组(在图片上传区域内)- 通过CSS隐藏而不是visible=False
with gr.Row(elem_id="delete-buttons-row"):
delete_label = gr.Markdown("**Delete Images:**", visible=True, elem_id="delete-label")
delete_buttons = []
for i in range(5): # 最多支持5张图片
btn = gr.Button(f"Delete {i + 1}", visible=True, size="sm", elem_id=f"delete-btn-{i}")
delete_buttons.append(btn)
# 生成按钮
generate_btn = gr.Button(
"🚀 Start Generation",
variant="primary",
size="lg"
)
# 右侧 - 输出区域
with gr.Column(scale=1):
# 添加输出标题
gr.Markdown("### 🎨 Generation Results", elem_classes="output-title")
# 输出图片画廊
output_gallery = gr.Gallery(
show_label=False,
elem_id="output-gallery",
columns=2,
rows=None,
object_fit="contain",
height=500,
preview=True,
container=True
)
# 状态信息
status = gr.Textbox(
label="Processing Status",
interactive=False,
lines=2,
value="Ready, please upload images and enter a prompt..."
)
# 示例
gr.Examples(
examples=[
["Convert image to cartoon anime style", None],
["Apply Van Gogh's starry night style", None],
["Convert to black and white sketch", None],
["Add neon light effects, cyberpunk style", None],
["Convert to watercolor painting style with soft tones", None],
],
inputs=[prompt, api_key],
label="Prompt Examples"
)
# 状态变量
current_files = gr.State([])
# 更新的事件处理函数
def on_file_upload(current_paths, new_files):
"""处理文件上传并更新HTML显示"""
updated_files, message = handle_file_upload(current_paths, new_files)
html_content = create_image_html(updated_files)
return updated_files, gr.update(value=None), html_content
# 为每个删除按钮绑定事件
def create_delete_handler(index):
def delete_image_at_index(current_paths):
if not current_paths or index >= len(current_paths):
return current_paths, create_image_html(current_paths), *update_delete_buttons(current_paths)
# 删除指定索引的图片
updated_files = current_paths[:index] + current_paths[index + 1:]
return updated_files, create_image_html(updated_files), *update_delete_buttons(updated_files)
return delete_image_at_index
def update_delete_buttons(file_paths):
"""更新删除按钮 - 保持所有按钮visible=True,通过CSS控制显示"""
updates = []
updates.append(gr.update()) # 标签保持不变
for i in range(5):
if i < len(file_paths):
updates.append(gr.update(value=f"Delete Image {i + 1}"))
else:
updates.append(gr.update()) # 保持不变
return updates
# 绑定删除按钮事件
outputs_list = [current_files, image_display, delete_label] + delete_buttons
for i, btn in enumerate(delete_buttons):
btn.click(
fn=create_delete_handler(i),
inputs=[current_files],
outputs=outputs_list
)
# 更新文件上传事件,同时更新删除按钮
def on_file_upload_with_buttons(current_paths, new_files):
updated_files, message = handle_file_upload(current_paths, new_files)
html_content = create_image_html(updated_files)
button_updates = update_delete_buttons(updated_files)
return updated_files, gr.update(value=None), html_content, *button_updates
file_upload.upload(
fn=on_file_upload_with_buttons,
inputs=[current_files, file_upload],
outputs=[current_files, file_upload, image_display, delete_label] + delete_buttons
)
# 生成按钮事件
def prepare_and_generate(prompt, paths, api_key, progress=gr.Progress()):
"""生成图片的主函数"""
if not paths:
return [], "❌ Please upload at least one image"
return process_nano_banana(prompt, paths, api_key, progress)
generate_btn.click(
fn=prepare_and_generate,
inputs=[prompt, current_files, api_key],
outputs=[output_gallery, status]
)
# 添加JavaScript代码来连接图片上的删除按钮和隐藏的独立删除按钮
demo.load(None, None, None, js="""
() => {
// 创建全局删除函数
window.deleteImageByIndex = function(index) {
// Gradio的elem_id设置在包装器上,需要找到内部的button
let deleteBtn = null;
// 方法1: 通过ID找到包装器,然后找内部的button
const wrapper = document.getElementById(`delete-btn-${index}`);
if (wrapper) {
deleteBtn = wrapper.querySelector('button');
}
// 方法2: 如果方法1失败,查找所有按钮并通过文本内容匹配
if (!deleteBtn) {
const allButtons = document.querySelectorAll('button');
for (let btn of allButtons) {
if (btn.textContent.includes(`Delete Image ${index + 1}`)) {
deleteBtn = btn;
break;
}
}
}
if (deleteBtn) {
deleteBtn.click();
} else {
// 调试信息
document.querySelectorAll('[id^="delete-btn-"]').forEach(elem => {
console.log(elem.id, elem.tagName, elem.querySelector('button'));
});
}
};
}
""")
# 启动应用
if __name__ == "__main__":
app_instance = demo
# 启动定时清理任务
start_cleanup_scheduler()
demo.launch(
share=True,
server_name="0.0.0.0",
server_port=7860 # 使用不同的端口避免冲突
)