Spaces:
Sleeping
Sleeping
import gradio as gr | |
import spaces | |
import torch | |
import numpy as np | |
from PIL import Image, ImageDraw | |
import json | |
import warnings | |
from typing import Optional | |
from dream_renderer import DreamRendererPipeline | |
DEMO_MODE = False | |
warnings.filterwarnings("ignore") | |
# 全局变量 | |
pipeline = None | |
current_bbox_data = [] | |
def create_demo_image(prompt: str, bbox_data: list, width: int = 512, height: int = 512): | |
"""创建演示图像""" | |
# 创建一个简单的演示图像 | |
image = Image.new('RGB', (width, height), color='lightblue') | |
draw = ImageDraw.Draw(image) | |
# 绘制背景文字 | |
try: | |
# 尝试绘制提示词 | |
draw.text((10, 10), f"演示模式: {prompt[:50]}", fill='darkblue') | |
draw.text((10, 30), f"边界框数量: {len(bbox_data)}", fill='darkblue') | |
# 绘制边界框 | |
for i, bbox in enumerate(bbox_data): | |
x = int(bbox['x'] * width) | |
y = int(bbox['y'] * height) | |
w = int(bbox['width'] * width) | |
h = int(bbox['height'] * height) | |
# 绘制边界框 | |
color = f"hsl({i * 60}, 70%, 50%)" | |
# 简单的颜色映射 | |
colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange'] | |
bbox_color = colors[i % len(colors)] | |
draw.rectangle([x, y, x+w, y+h], outline=bbox_color, width=2) | |
draw.text((x+5, y+5), bbox.get('label', f'区域{i+1}'), fill=bbox_color) | |
except Exception as e: | |
draw.text((10, 50), f"绘制错误: {str(e)}", fill='red') | |
return image | |
def initialize_pipeline(): | |
"""初始化DreamRenderer管道""" | |
global pipeline | |
try: | |
if pipeline is None: | |
pipeline = DreamRendererPipeline() | |
# 预加载模型以节省时间 | |
success = pipeline.load_model() | |
if success: | |
return "✅ DreamRenderer管道已成功初始化并加载模型!" | |
else: | |
return "⚠️ DreamRenderer管道已初始化,但模型加载失败。将使用演示模式。" | |
else: | |
return "✅ DreamRenderer管道已经初始化完成!" | |
except Exception as e: | |
return f"❌ 初始化失败: {str(e)}" | |
def generate_image_with_bbox(prompt: str, negative_prompt: str, | |
num_inference_steps: int, guidance_scale: float, | |
width: int, height: int, seed: int, use_seed: bool): | |
"""使用边界框生成图像""" | |
global pipeline, current_bbox_data | |
if pipeline is None: | |
return None, "❌ 请先初始化DreamRenderer管道!" | |
if not prompt.strip(): | |
return None, "❌ 请输入提示词!" | |
try: | |
# 设置种子 | |
actual_seed = seed if use_seed else None | |
# 生成图像 | |
image = pipeline.generate_image( | |
prompt=prompt, | |
bbox_data=current_bbox_data, | |
negative_prompt=negative_prompt, | |
num_inference_steps=num_inference_steps, | |
guidance_scale=guidance_scale, | |
width=width, | |
height=height, | |
seed=actual_seed | |
) | |
info = f"✅ 图像生成成功!\n" | |
info += f"🔸 使用边界框: {len(current_bbox_data)}个\n" | |
info += f"🔸 推理步数: {num_inference_steps}\n" | |
info += f"🔸 引导强度: {guidance_scale}\n" | |
info += f"🔸 图像尺寸: {width}×{height}\n" | |
if actual_seed is not None: | |
info += f"🔸 随机种子: {actual_seed}" | |
return image, info | |
except Exception as e: | |
return None, f"❌ 生成图像时出错: {str(e)}" | |
def load_bbox_component(): | |
"""加载边界框绘制组件""" | |
try: | |
with open('bbox_component.html', 'r', encoding='utf-8') as f: | |
content = f.read() | |
return content | |
except FileNotFoundError: | |
# 返回简化的HTML内容 | |
return """ | |
<div style="text-align: center; padding: 20px; border: 2px dashed #ccc; border-radius: 8px;"> | |
<p>边界框组件加载失败</p> | |
<p>请检查 bbox_component.html 文件是否存在</p> | |
</div> | |
""" | |
def update_bbox_data(bbox_json: str): | |
"""更新边界框数据显示""" | |
global current_bbox_data | |
try: | |
if not bbox_json or bbox_json.strip() == "": | |
current_bbox_data = [] | |
return "📦 暂无边界框数据\n\n💡 提示:在画布上拖拽鼠标绘制边界框", "" | |
bbox_data = json.loads(bbox_json) | |
current_bbox_data = bbox_data # 重要:更新全局变量 | |
if not bbox_data: | |
current_bbox_data = [] | |
return "📦 暂无边界框数据\n\n💡 提示:在画布上拖拽鼠标绘制边界框", "" | |
info_lines = [ | |
f"📦 边界框数据 ({len(bbox_data)} 个)", | |
"=" * 40, | |
"" | |
] | |
# 生成边界框编辑界面HTML | |
edit_html_lines = [ | |
'<div style="max-height: 400px; overflow-y: auto; padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: #f9f9f9;">', | |
'<h4 style="color: #333; margin-top: 0;">🎯 边界框描述编辑</h4>' | |
] | |
for i, bbox in enumerate(bbox_data, 1): | |
x = bbox.get('x', 0) | |
y = bbox.get('y', 0) | |
width = bbox.get('width', 0) | |
height = bbox.get('height', 0) | |
label = bbox.get('label', f'区域{i}') | |
prompt = bbox.get('prompt', '') # 获取已有的提示词 | |
info_lines.extend([ | |
f"🎯 边界框 {i}:", | |
f" 📍 位置: ({x:.3f}, {y:.3f})", | |
f" 📏 大小: {width:.3f} × {height:.3f}", | |
f" 🏷️ 标签: {label}", | |
f" 💬 描述: {prompt or '(请在下方输入描述)'}", | |
"" | |
]) | |
# 为每个边界框生成编辑界面 | |
color = f"hsl({(i-1) * 60}, 70%, 50%)" | |
edit_html_lines.extend([ | |
f'<div style="margin: 15px 0; padding: 15px; border-left: 4px solid {color}; background: white; border-radius: 8px;">', | |
f' <div style="display: flex; align-items: center; margin-bottom: 10px;">', | |
f' <div style="width: 20px; height: 20px; background: {color}; border-radius: 4px; margin-right: 10px;"></div>', | |
f' <strong style="color: #333;">边界框 {i} - {label}</strong>', | |
f' <span style="margin-left: auto; font-size: 0.9em; color: #666;">({x:.2f}, {y:.2f}) {width:.2f}×{height:.2f}</span>', | |
f' </div>', | |
f' <div style="margin-bottom: 8px;">', | |
f' <label style="display: block; font-weight: bold; color: #555; margin-bottom: 5px;">🏷️ 区域标签:</label>', | |
f' <input type="text" id="bbox_label_{i-1}" value="{label}" placeholder="为这个区域命名..." ', | |
f' style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;" ', | |
f' onchange="updateBboxField({i-1}, \'label\', this.value)">', | |
f' </div>', | |
f' <div style="margin-bottom: 8px;">', | |
f' <label style="display: block; font-weight: bold; color: #555; margin-bottom: 5px;">💬 详细描述:</label>', | |
f' <textarea id="bbox_prompt_{i-1}" placeholder="描述这个区域应该生成什么内容..." ', | |
f' style="width: 100%; height: 80px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; resize: vertical;" ', | |
f' onchange="updateBboxField({i-1}, \'prompt\', this.value)">{prompt}</textarea>', | |
f' </div>', | |
f' <div style="text-align: right;">', | |
f' <button onclick="deleteBbox({i-1})" style="background: #ff4757; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">', | |
f' 🗑️ 删除此框', | |
f' </button>', | |
f' </div>', | |
f'</div>' | |
]) | |
edit_html_lines.extend([ | |
'<div style="margin-top: 20px; padding: 15px; background: #e8f5e8; border-radius: 8px;">', | |
' <div style="display: flex; justify-content: space-between; align-items: center;">', | |
' <div>', | |
f' <strong style="color: #2d5a2d;">✅ 共 {len(bbox_data)} 个边界框</strong>', | |
' <br><small style="color: #5a5a5a;">修改描述后会自动保存</small>', | |
' </div>', | |
' <button onclick="clearAllBboxes()" style="background: #ff6b6b; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer;">', | |
' 🗑️ 清空所有', | |
' </button>', | |
' </div>', | |
'</div>', | |
'</div>' | |
]) | |
info_lines.extend([ | |
"💡 使用说明:", | |
"• 在画布上拖拽绘制新的边界框", | |
"• 在右侧为每个框输入具体描述", | |
"• 每个框可以有不同的生成内容", | |
"• 描述越详细,生成效果越好" | |
]) | |
print(f"DEBUG: 边界框数据已更新: {len(current_bbox_data)}个") # 调试信息 | |
return "\n".join(info_lines), "\n".join(edit_html_lines) | |
except json.JSONDecodeError: | |
current_bbox_data = [] | |
return f"❌ 边界框数据格式错误\n\n原始数据: {bbox_json[:200]}...", "" | |
except Exception as e: | |
current_bbox_data = [] | |
return f"❌ 处理边界框数据时出错: {str(e)}", "" | |
def create_interface(): | |
"""创建Gradio界面""" | |
# 自定义CSS | |
css = """ | |
.main-container { | |
max-width: 1400px; | |
margin: 0 auto; | |
} | |
.bbox-container { | |
border: 2px solid #e1e5e9; | |
border-radius: 12px; | |
padding: 20px; | |
margin: 15px 0; | |
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); | |
} | |
.generate-btn { | |
background: linear-gradient(45deg, #FF6B6B, #4ECDC4); | |
border: none; | |
border-radius: 25px; | |
padding: 15px 35px; | |
color: white; | |
font-weight: bold; | |
font-size: 18px; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
transition: all 0.3s ease; | |
} | |
.generate-btn:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 6px 20px rgba(0,0,0,0.3); | |
} | |
.init-btn { | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
border: none; | |
border-radius: 20px; | |
color: white; | |
font-weight: bold; | |
padding: 12px 25px; | |
} | |
""" | |
with gr.Blocks(css=css, title="DreamRenderer - Multi-Instance Control", theme=gr.themes.Soft()) as demo: | |
# 根据模式显示不同的标题 | |
if DEMO_MODE: | |
gr.HTML(""" | |
<div style="text-align: center; padding: 20px;"> | |
<h1 style="background: linear-gradient(45deg, #FF6B6B, #4ECDC4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 3em; margin-bottom: 10px;"> | |
🎨 DreamRenderer (演示模式) | |
</h1> | |
<h2 style="color: #666; margin-bottom: 20px;">Multi-Instance Attribute Control</h2> | |
<div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 15px; margin: 20px auto; max-width: 800px;"> | |
<p style="margin: 0; color: #856404; font-size: 1.1em;"> | |
⚠️ <strong>当前运行在演示模式下</strong><br> | |
由于缺少实际的AI模型,系统将生成简单的演示图像来展示界面功能。<br> | |
您仍然可以测试边界框绘制和参数设置功能。 | |
</p> | |
</div> | |
</div> | |
""") | |
else: | |
gr.HTML(""" | |
<div style="text-align: center; padding: 20px;"> | |
<h1 style="background: linear-gradient(45deg, #FF6B6B, #4ECDC4); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 3em; margin-bottom: 10px;"> | |
🎨 DreamRenderer | |
</h1> | |
<h2 style="color: #666; margin-bottom: 20px;">Multi-Instance Attribute Control</h2> | |
<p style="font-size: 1.2em; color: #888; max-width: 800px; margin: 0 auto;"> | |
基于ZeroGPU的高质量多实例属性控制文本到图像生成工具 | |
</p> | |
</div> | |
""") | |
# 使用说明 | |
with gr.Accordion("📖 使用说明", open=False): | |
if DEMO_MODE: | |
gr.Markdown(""" | |
### 🚀 演示模式说明: | |
1. **功能测试**: 点击"初始化"按钮启动演示模式 | |
2. **绘制区域**: 在画布上拖拽鼠标绘制边界框 | |
3. **添加描述**: 为每个边界框输入描述文本 | |
4. **设置参数**: 调整生成参数(用于演示) | |
5. **生成图像**: 输入主提示词并点击生成演示图像 | |
### ⚠️ 演示模式限制: | |
- 🎯 **界面功能**: 所有界面功能都可以正常使用 | |
- 🖼️ **图像生成**: 生成的是简单的演示图像,非AI生成 | |
- 📦 **边界框**: 边界框绘制和编辑功能完全正常 | |
- 🔧 **参数调节**: 参数设置功能正常,但不影响实际生成 | |
### 📌 完整功能需要: | |
- 安装完整的AI模型(dream_renderer模块) | |
- 配置ZeroGPU环境 | |
""") | |
else: | |
gr.Markdown(""" | |
### 🚀 快速开始: | |
1. **初始化**: 点击"初始化DreamRenderer"按钮加载模型 | |
2. **绘制区域**: 在画布上拖拽鼠标绘制边界框 | |
3. **添加描述**: 为每个边界框输入描述文本 | |
4. **设置参数**: 调整生成参数(可选) | |
5. **生成图像**: 输入主提示词并点击生成 | |
### ✨ 功能特点: | |
- 🎯 **精确控制**: 通过边界框精确控制每个实例的位置和属性 | |
- 🚀 **ZeroGPU加速**: 利用Hugging Face的ZeroGPU实现快速推理 | |
- 🎨 **高质量生成**: 基于FLUX模型的高质量图像生成 | |
- 🔧 **灵活参数**: 丰富的参数调节选项 | |
""") | |
with gr.Row(): | |
# 左侧:边界框绘制和控制 | |
with gr.Column(scale=1): | |
# 初始化部分 | |
with gr.Group(): | |
gr.Markdown("### 🚀 模型初始化") | |
if DEMO_MODE: | |
init_btn = gr.Button("🚀 启动演示模式", variant="primary") | |
else: | |
init_btn = gr.Button("🚀 初始化DreamRenderer", variant="primary") | |
init_status = gr.Textbox(label="初始化状态", interactive=False, lines=2) | |
# 边界框绘制区域 | |
with gr.Group(): | |
gr.Markdown("### 📦 边界框绘制") | |
gr.HTML(""" | |
<div style="background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); padding: 10px; border-radius: 8px; margin: 10px 0;"> | |
<p style="margin: 0; color: #1976d2;"><strong>步骤1:</strong> 在画布上拖拽鼠标绘制边界框</p> | |
<p style="margin: 5px 0 0 0; color: #1976d2;"><strong>步骤2:</strong> 在右侧为每个框输入详细描述</p> | |
</div> | |
""") | |
bbox_component = gr.HTML(load_bbox_component()) | |
# 隐藏的输入框用于接收边界框数据 | |
bbox_data_input = gr.Textbox(visible=False, elem_id="bbox_data") | |
bbox_info = gr.Textbox(label="📦 边界框信息", interactive=False, lines=8, placeholder="边界框信息将在这里显示...") | |
# 右侧:边界框编辑和生成参数 | |
with gr.Column(scale=1): | |
# 边界框编辑区域 | |
with gr.Group(): | |
gr.Markdown("### ✏️ 边界框描述编辑") | |
bbox_editor = gr.HTML( | |
value="<div style='text-align: center; padding: 40px; color: #666;'>绘制边界框后,编辑界面将出现在这里</div>", | |
elem_id="bbox_editor" | |
) | |
# 提示词设置 | |
with gr.Group(): | |
gr.Markdown("### 📝 提示词设置") | |
prompt = gr.Textbox( | |
label="主提示词", | |
placeholder="描述你想要生成的整体场景...", | |
lines=3, | |
value="a beautiful landscape" | |
) | |
negative_prompt = gr.Textbox( | |
label="负向提示词", | |
placeholder="描述你不想看到的内容...", | |
lines=2, | |
value="blurry, low quality, distorted" | |
) | |
# 生成参数 | |
with gr.Group(): | |
gr.Markdown("### ⚙️ 生成参数") | |
with gr.Row(): | |
num_steps = gr.Slider( | |
minimum=1, maximum=100, value=20, step=1, | |
label="推理步数", | |
info="更多步数通常能获得更好的质量" | |
) | |
guidance_scale = gr.Slider( | |
minimum=1.0, maximum=30.0, value=7.5, step=0.5, | |
label="引导强度", | |
info="控制对提示词的遵循程度" | |
) | |
with gr.Row(): | |
width = gr.Slider( | |
minimum=256, maximum=1024, value=512, step=64, | |
label="宽度" | |
) | |
height = gr.Slider( | |
minimum=256, maximum=1024, value=512, step=64, | |
label="高度" | |
) | |
with gr.Row(): | |
use_seed = gr.Checkbox(label="使用固定种子", value=False) | |
seed = gr.Number(label="随机种子", value=42, precision=0) | |
# 生成按钮 | |
generate_btn = gr.Button( | |
"🎨 生成图像", | |
variant="primary", | |
size="lg" | |
) | |
# 结果显示 | |
with gr.Group(): | |
gr.Markdown("### 🖼️ 生成结果") | |
output_image = gr.Image(label="生成的图像", height=500, show_label=False) | |
generation_info = gr.Textbox(label="生成信息", interactive=False, lines=6) | |
# 示例和更多选项 | |
with gr.Accordion("🎯 示例和技巧", open=False): | |
gr.Markdown(""" | |
### 📌 提示词示例: | |
- **风景场景**: "a serene mountain landscape with a lake, golden hour lighting" | |
- **城市场景**: "modern city skyline at sunset, futuristic architecture" | |
- **人物场景**: "a group of people in a park, casual clothing, natural lighting" | |
### 🎨 使用技巧: | |
1. **边界框大小**: 合适的边界框大小有助于更好的控制效果 | |
2. **描述精确性**: 为每个区域提供具体而精确的描述 | |
3. **参数调节**: 较高的引导强度可以提高对提示词的遵循度 | |
4. **种子控制**: 使用固定种子可以获得可重复的结果 | |
""") | |
# 事件绑定 | |
init_btn.click( | |
fn=initialize_pipeline, | |
outputs=init_status, | |
show_progress=True | |
) | |
bbox_data_input.change( | |
fn=update_bbox_data, | |
inputs=bbox_data_input, | |
outputs=[bbox_info, bbox_editor] | |
) | |
generate_btn.click( | |
fn=generate_image_with_bbox, | |
inputs=[prompt, negative_prompt, num_steps, guidance_scale, width, height, seed, use_seed], | |
outputs=[output_image, generation_info], | |
show_progress=True | |
) | |
# JavaScript代码用于处理边界框数据通信 | |
demo.load(None, None, None, js=""" | |
function() { | |
console.log('DreamRenderer界面已加载'); | |
// 给DOM一些时间加载 | |
setTimeout(function() { | |
try { | |
// 全局变量 | |
let isDrawing = false; | |
let startX, startY, currentRect; | |
let bboxes = []; | |
const canvas = document.getElementById('bboxCanvas'); | |
if (!canvas) { | |
console.error('画布元素未找到'); | |
return; | |
} | |
const ctx = canvas.getContext('2d'); | |
// 清除之前的监听器并添加新的 | |
canvas.replaceWith(canvas.cloneNode(true)); | |
const newCanvas = document.getElementById('bboxCanvas'); | |
const newCtx = newCanvas.getContext('2d'); | |
// 重新设置样式 | |
newCanvas.style.display = 'block'; | |
newCanvas.style.border = '2px solid #4ECDC4'; | |
newCanvas.style.backgroundColor = 'white'; | |
newCanvas.style.cursor = 'crosshair'; | |
newCanvas.style.borderRadius = '8px'; | |
// 边界框编辑函数 | |
window.updateBboxField = function(index, field, value) { | |
if (index >= 0 && index < bboxes.length) { | |
bboxes[index][field] = value; | |
console.log(`更新边界框 ${index} 的 ${field}:`, value); | |
updateBboxData(); | |
} | |
}; | |
window.deleteBbox = function(index) { | |
if (index >= 0 && index < bboxes.length) { | |
bboxes.splice(index, 1); | |
console.log(`删除边界框 ${index}`); | |
redrawCanvas(); | |
updateBboxData(); | |
} | |
}; | |
window.clearAllBboxes = function() { | |
bboxes = []; | |
console.log('清空所有边界框'); | |
redrawCanvas(); | |
updateBboxData(); | |
}; | |
// 重绘画布 | |
function redrawCanvas() { | |
newCtx.clearRect(0, 0, newCanvas.width, newCanvas.height); | |
bboxes.forEach((bbox, index) => { | |
newCtx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`; | |
newCtx.lineWidth = 2; | |
newCtx.strokeRect(bbox.x, bbox.y, bbox.width, bbox.height); | |
// 绘制标签 | |
newCtx.fillStyle = `hsl(${index * 60}, 70%, 50%)`; | |
newCtx.font = '12px Arial'; | |
newCtx.fillText(bbox.label || `区域${index + 1}`, bbox.x + 5, bbox.y - 5); | |
}); | |
} | |
// 更新边界框数据 | |
function updateBboxData() { | |
const relativeBboxes = bboxes.map(b => ({ | |
x: b.x / newCanvas.width, | |
y: b.y / newCanvas.height, | |
width: b.width / newCanvas.width, | |
height: b.height / newCanvas.height, | |
label: b.label || '', | |
prompt: b.prompt || '' | |
})); | |
const dataString = JSON.stringify(relativeBboxes); | |
console.log('📤 更新数据:', relativeBboxes.length, '个边界框'); | |
const textarea = document.querySelector('#bbox_data textarea'); | |
if (textarea) { | |
textarea.value = dataString; | |
textarea.dispatchEvent(new Event('input', { bubbles: true })); | |
} | |
} | |
// 添加绘制事件监听器 | |
newCanvas.addEventListener('mousedown', function(e) { | |
isDrawing = true; | |
startX = e.offsetX; | |
startY = e.offsetY; | |
console.log('🎯 开始绘制:', startX, startY); | |
}); | |
newCanvas.addEventListener('mousemove', function(e) { | |
if (!isDrawing) return; | |
const currentX = e.offsetX; | |
const currentY = e.offsetY; | |
// 清除画布并重绘所有边界框 | |
redrawCanvas(); | |
// 绘制当前正在绘制的框 | |
newCtx.strokeStyle = '#007bff'; | |
newCtx.lineWidth = 2; | |
newCtx.setLineDash([5, 5]); | |
const width = currentX - startX; | |
const height = currentY - startY; | |
newCtx.strokeRect(startX, startY, width, height); | |
newCtx.setLineDash([]); | |
}); | |
newCanvas.addEventListener('mouseup', function(e) { | |
if (!isDrawing) return; | |
isDrawing = false; | |
const endX = e.offsetX; | |
const endY = e.offsetY; | |
const width = endX - startX; | |
const height = endY - startY; | |
// 只有当框足够大时才添加 | |
if (Math.abs(width) > 10 && Math.abs(height) > 10) { | |
const bbox = { | |
x: Math.min(startX, endX), | |
y: Math.min(startY, endY), | |
width: Math.abs(width), | |
height: Math.abs(height), | |
label: `区域${bboxes.length + 1}`, | |
prompt: '' | |
}; | |
bboxes.push(bbox); | |
console.log('✅ 添加边界框:', bbox); | |
redrawCanvas(); | |
updateBboxData(); | |
} | |
console.log('🎯 绘制结束,当前边界框数量:', bboxes.length); | |
}); | |
console.log('🚀 DreamRenderer边界框功能已就绪!'); | |
} catch (error) { | |
console.error('初始化边界框功能时出错:', error); | |
} | |
}, 1000); // 延迟1秒执行 | |
} | |
""") | |
return demo | |
if __name__ == "__main__": | |
# 创建并启动应用 | |
demo = create_interface() | |
demo.launch( | |
show_error=True | |
) |