|
|
""" |
|
|
UI界面模块 |
|
|
""" |
|
|
|
|
|
import gradio as gr |
|
|
from typing import List, Tuple, Optional |
|
|
|
|
|
from .components import UIComponents |
|
|
from ..api import VideoProcessor |
|
|
from ..config import UI_CONFIG |
|
|
|
|
|
|
|
|
class Veo3Interface: |
|
|
"""Veo3界面类""" |
|
|
|
|
|
def __init__(self): |
|
|
self.components = UIComponents() |
|
|
self.video_processor = VideoProcessor() |
|
|
self.demo = None |
|
|
|
|
|
def create_interface(self) -> gr.Blocks: |
|
|
"""创建完整的界面""" |
|
|
|
|
|
with open("static/css/styles.css", "r", encoding="utf-8") as f: |
|
|
css = f.read() |
|
|
print(f"CSS样式读取成功\n") |
|
|
with gr.Blocks(css=css, theme=gr.themes.Base()) as demo: |
|
|
self.demo = demo |
|
|
|
|
|
|
|
|
with gr.Column(elem_classes="header-container"): |
|
|
self.components.create_header() |
|
|
|
|
|
with gr.Column(elem_classes="main-content"): |
|
|
|
|
|
self.components.create_info_box() |
|
|
|
|
|
with gr.Row(equal_height=True): |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
api_key, prompt, image_display, file_upload, delete_buttons, aspect_ratio, seeds, random_seed_btn, uploaded_file_state = self.components.create_input_components() |
|
|
|
|
|
|
|
|
generate_btn = self.components.create_generate_button() |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
output_video, status = self.components.create_output_section() |
|
|
|
|
|
|
|
|
examples = self.components.create_examples() |
|
|
examples.inputs = [prompt, api_key] |
|
|
|
|
|
|
|
|
self._setup_event_handlers( |
|
|
generate_btn, prompt, api_key, aspect_ratio, seeds, random_seed_btn, |
|
|
file_upload, image_display, delete_buttons, output_video, status, uploaded_file_state |
|
|
) |
|
|
|
|
|
|
|
|
self._add_javascript() |
|
|
|
|
|
return demo |
|
|
|
|
|
def _setup_event_handlers( |
|
|
self, |
|
|
generate_btn, |
|
|
prompt, |
|
|
api_key, |
|
|
aspect_ratio, |
|
|
seeds, |
|
|
random_seed_btn, |
|
|
file_upload, |
|
|
image_display, |
|
|
delete_buttons, |
|
|
output_video, |
|
|
status, |
|
|
uploaded_file_state |
|
|
): |
|
|
"""设置事件处理器""" |
|
|
|
|
|
|
|
|
self.components.setup_file_upload_handlers(file_upload, image_display, delete_buttons, uploaded_file_state) |
|
|
|
|
|
|
|
|
self.components.setup_delete_handlers(delete_buttons, image_display, uploaded_file_state) |
|
|
|
|
|
|
|
|
def generate_random_seed(): |
|
|
"""生成随机种子""" |
|
|
import random |
|
|
return random.randint(10000, 99999) |
|
|
|
|
|
random_seed_btn.click( |
|
|
fn=generate_random_seed, |
|
|
outputs=[seeds] |
|
|
) |
|
|
|
|
|
|
|
|
def prepare_and_generate(prompt, api_key, aspect_ratio, seeds, saved_file_path, progress=gr.Progress()): |
|
|
"""生成视频的主函数""" |
|
|
|
|
|
file_paths = [saved_file_path] if saved_file_path is not None else [] |
|
|
return self.video_processor.process_veo3_video( |
|
|
prompt, file_paths, api_key, aspect_ratio, None, seeds, False, progress |
|
|
) |
|
|
|
|
|
generate_btn.click( |
|
|
fn=prepare_and_generate, |
|
|
inputs=[prompt, api_key, aspect_ratio, seeds, uploaded_file_state], |
|
|
outputs=[output_video, status] |
|
|
) |
|
|
|
|
|
def _add_javascript(self): |
|
|
"""添加JavaScript代码""" |
|
|
self.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')); |
|
|
}); |
|
|
} |
|
|
}; |
|
|
|
|
|
// 美化文件上传区域 |
|
|
function enhanceFileUpload() { |
|
|
const fileInputs = document.querySelectorAll('.gr-file'); |
|
|
fileInputs.forEach(input => { |
|
|
// 替换中文文本为英文 |
|
|
const textElements = input.querySelectorAll('.wrap > div'); |
|
|
textElements.forEach((element, index) => { |
|
|
const text = element.textContent.trim(); |
|
|
// 替换各种可能的中文文本 |
|
|
if (text.includes('将文件拖放到此处') || text.includes('拖放文件到此处')) { |
|
|
element.textContent = 'Drag and drop files here'; |
|
|
} else if (text.includes('点击上传') || text.includes('点击选择文件')) { |
|
|
element.textContent = 'or click to upload'; |
|
|
} else if (text.includes('- 或 -') || text.includes('或')) { |
|
|
element.textContent = '- or -'; |
|
|
} |
|
|
}); |
|
|
|
|
|
// 如果还有中文文本,直接替换整个内容 |
|
|
const wrap = input.querySelector('.wrap'); |
|
|
if (wrap) { |
|
|
const allText = wrap.textContent; |
|
|
if (allText.includes('将文件') || allText.includes('点击上传')) { |
|
|
wrap.innerHTML = ` |
|
|
<div style="font-size: 0.9rem; font-weight: 500; color: #4a5568; margin: 0.5rem 0;">Drag and drop files here</div> |
|
|
<div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">- or -</div> |
|
|
<div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">or click to upload</div> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
// 添加拖拽事件监听 |
|
|
input.addEventListener('dragover', function(e) { |
|
|
e.preventDefault(); |
|
|
this.classList.add('dragover'); |
|
|
}); |
|
|
|
|
|
input.addEventListener('dragleave', function(e) { |
|
|
e.preventDefault(); |
|
|
this.classList.remove('dragover'); |
|
|
}); |
|
|
|
|
|
input.addEventListener('drop', function(e) { |
|
|
e.preventDefault(); |
|
|
this.classList.remove('dragover'); |
|
|
}); |
|
|
|
|
|
// 监听文件选择 |
|
|
const fileInput = input.querySelector('input[type="file"]'); |
|
|
if (fileInput) { |
|
|
fileInput.addEventListener('change', function() { |
|
|
if (this.files && this.files.length > 0) { |
|
|
input.classList.add('has-file'); |
|
|
} else { |
|
|
input.classList.remove('has-file'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
// 初始化美化 |
|
|
enhanceFileUpload(); |
|
|
|
|
|
// 使用定时器确保文本被替换 |
|
|
const textReplacer = setInterval(function() { |
|
|
const fileInputs = document.querySelectorAll('.gr-file'); |
|
|
let hasChineseText = false; |
|
|
|
|
|
fileInputs.forEach(input => { |
|
|
const wrap = input.querySelector('.wrap'); |
|
|
if (wrap) { |
|
|
const allText = wrap.textContent; |
|
|
// 检查所有可能的中文文本 |
|
|
if (allText.includes('将文件') || allText.includes('点击上传') || allText.includes('拖放') || |
|
|
allText.includes('或') || allText.includes('此处')) { |
|
|
hasChineseText = true; |
|
|
wrap.innerHTML = ` |
|
|
<div style="font-size: 0.9rem; font-weight: 500; color: #4a5568; margin: 0.5rem 0;">Drag and drop files here</div> |
|
|
<div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">- or -</div> |
|
|
<div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">or click to upload</div> |
|
|
`; |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
// 如果没有中文文本了,停止定时器 |
|
|
if (!hasChineseText) { |
|
|
clearInterval(textReplacer); |
|
|
} |
|
|
}, 50); // 更频繁的检查 |
|
|
|
|
|
// 监听DOM变化,处理动态添加的元素 |
|
|
const observer = new MutationObserver(function(mutations) { |
|
|
mutations.forEach(function(mutation) { |
|
|
if (mutation.type === 'childList') { |
|
|
mutation.addedNodes.forEach(function(node) { |
|
|
if (node.nodeType === 1 && node.classList && node.classList.contains('gr-file')) { |
|
|
enhanceFileUpload(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
observer.observe(document.body, { |
|
|
childList: true, |
|
|
subtree: true |
|
|
}); |
|
|
} |
|
|
""") |
|
|
|
|
|
def launch(self, share: bool = False, server_name: str = None, server_port: int = None): |
|
|
"""启动应用""" |
|
|
if server_name is None: |
|
|
server_name = UI_CONFIG["SERVER_HOST"] |
|
|
if server_port is None: |
|
|
server_port = UI_CONFIG["SERVER_PORT"] |
|
|
|
|
|
try: |
|
|
self.demo.launch( |
|
|
share=share, |
|
|
server_name=server_name, |
|
|
server_port=server_port, |
|
|
) |
|
|
except Exception as e: |
|
|
print(f"❌ Launch failed: {e}") |
|
|
print("🔄 Trying to restart with default configuration...") |
|
|
try: |
|
|
self.demo.launch( |
|
|
share=False, |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
show_error=True, |
|
|
quiet=False |
|
|
) |
|
|
except Exception as e2: |
|
|
print(f"❌ Restart also failed: {e2}") |
|
|
print("💡 Please check if the port is occupied, or try another port") |
|
|
raise |
|
|
|