| """ |
| 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 |
|
|