""" 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: """创建完整的界面""" # 读取CSS样式 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 ) # 添加JavaScript代码 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) # API要求的范围 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()): """生成视频的主函数""" # 处理文件上传参数,如果为None则转换为空列表 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 = `
Drag and drop files here
- or -
or click to upload
`; } } // 添加拖拽事件监听 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 = `
Drag and drop files here
- or -
or click to upload
`; } } }); // 如果没有中文文本了,停止定时器 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