dmmmmm commited on
Commit
0ab5288
·
verified ·
1 Parent(s): b2f4ba4

Upload 28 files

Browse files
app.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Veo3 AI Video Generator - 主应用文件
3
+ 重构后的模块化版本
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ import socket
9
+ import random
10
+
11
+ # 添加src目录到Python路径
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
13
+
14
+ from src.ui import Veo3Interface
15
+ from src.utils import start_cleanup_scheduler
16
+
17
+
18
+ def find_free_port(start_port=55555, max_attempts=10):
19
+ """查找可用端口"""
20
+ for i in range(max_attempts):
21
+ port = start_port + i
22
+ try:
23
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
24
+ s.bind(('127.0.0.1', port))
25
+ return port
26
+ except OSError:
27
+ continue
28
+ return None
29
+
30
+
31
+ def main():
32
+ """主函数"""
33
+ print("🎬 Starting Veo3 AI Video Generator...")
34
+
35
+ # 启动定时清理任务
36
+ start_cleanup_scheduler()
37
+ print("✅ Cleanup scheduler started")
38
+
39
+ # 创建界面
40
+ interface = Veo3Interface()
41
+ interface.create_interface()
42
+ print("✅ Interface created")
43
+
44
+ # 查找可用端口
45
+ free_port = find_free_port()
46
+ if free_port is None:
47
+ return
48
+ # 启动应用
49
+ print("🚀 Launching application...")
50
+ try:
51
+ interface.launch(share=False, server_name="127.0.0.1", server_port=free_port)
52
+ except Exception as e:
53
+ print(f"❌ 启动失败: {e}")
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
readme.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Veo3 AI Video Generator
3
+ emoji: 🎬
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ short_description: AI-powered video generation with Google's Veo3 model
12
+ tags:
13
+ - video-generation
14
+ - ai-video
15
+ - text-to-video
16
+ - image-to-video
17
+ - gradio
18
+ - computer-vision
19
+ ---
20
+
21
+ # 🎬 Veo3 AI Video Generator
22
+
23
+ **AI-Powered Video Generation & Creation**
24
+
25
+ Generate high-quality videos with cutting-edge AI technology using Google's Veo3 model. Create videos from text prompts or transform images into dynamic videos with various aspect ratios and intelligent fallback options.
26
+
27
+ ## ✨ Features
28
+
29
+ - 🎥 **Text-to-Video**: Generate videos from text descriptions
30
+ - 🖼️ **Image-to-Video**: Transform images into dynamic videos
31
+ - 📐 **Multiple Aspect Ratios**: Support for 16:9 and 9:16 formats
32
+ - 🔄 **Intelligent Fallback**: Enhanced success rates with fallback mechanism
33
+ - 🚀 **High Quality**: Professional-grade video generation
34
+ - 🌐 **Easy to Use**: Simple web interface with real-time progress
35
+ - 📱 **Mobile Friendly**: Works on all devices
36
+
37
+ ## 🚀 How to Use
38
+
39
+ 1. **Get API Key**: Visit [KIE.AI](https://kie.ai/veo3) to get your Veo3 API key
40
+ 2. **Enter Prompt**: Describe the video you want to generate
41
+ 3. **Upload Image (Optional)**: Add 1 reference image for image-to-video
42
+ 4. **Configure Settings**: Choose aspect ratio, watermark, and other options
43
+ 5. **Generate**: Click start and wait for AI video creation!
44
+
45
+ ## 🎯 Example Prompts
46
+
47
+ - "A dog playing in a park with beautiful sunset lighting"
48
+ - "A cat walking through a magical forest with glowing mushrooms"
49
+ - "Ocean waves crashing against rocks during a storm"
50
+ - "A futuristic city with flying cars and neon lights"
51
+ - "A peaceful mountain landscape with snow falling gently"
52
+
53
+ ## ⚙️ Advanced Features
54
+
55
+ - **Aspect Ratio Control**: Choose between 16:9 (landscape) and 9:16 (portrait)
56
+ - **Watermark Support**: Add custom watermarks to your videos
57
+ - **Seed Control**: Use specific seeds for reproducible results
58
+ - **Fallback Mechanism**: Automatic retry with alternative backend for better success rates
59
+
60
+ ---
61
+
62
+ *Powered by Google's Veo3 model via KIE.AI*
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio
2
+ replicate
3
+ huggingface_hub
4
+ pillow
5
+ requests
src/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Veo3 AI Video Generator - 主模块
3
+ """
4
+
5
+ from .config import *
6
+ from .api import *
7
+ from .ui import *
8
+ from .utils import *
9
+
10
+ __version__ = "1.0.0"
11
+ __author__ = "Veo3 AI Video Generator Team"
src/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (370 Bytes). View file
 
src/api/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API模块
3
+ """
4
+
5
+ from .veo3_client import Veo3Client
6
+ from .video_processor import VideoProcessor
7
+
8
+ __all__ = [
9
+ 'Veo3Client',
10
+ 'VideoProcessor'
11
+ ]
src/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (331 Bytes). View file
 
src/api/__pycache__/veo3_client.cpython-312.pyc ADDED
Binary file (7.3 kB). View file
 
src/api/__pycache__/video_processor.cpython-312.pyc ADDED
Binary file (4.96 kB). View file
 
src/api/veo3_client.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Veo3 API客户端
3
+ """
4
+
5
+ import json
6
+ import time
7
+ import os
8
+ import uuid
9
+ import base64
10
+ import requests
11
+ from typing import List, Tuple, Optional
12
+
13
+ from ..config import API_CONFIG, MIME_TYPES
14
+
15
+
16
+ class Veo3Client:
17
+ """Veo3 API客户端类"""
18
+
19
+ def __init__(self, api_key: str):
20
+ self.api_key = api_key
21
+ self.headers = {
22
+ "Authorization": f"Bearer {api_key}",
23
+ "Content-Type": "application/json"
24
+ }
25
+
26
+ def upload_file(self, file_path: str) -> Tuple[bool, str]:
27
+ """
28
+ 上传文件到 KIE AI 的文件存储服务(使用Base64上传)
29
+ 返回文件的公开访问URL
30
+ """
31
+ url = API_CONFIG["FILE_UPLOAD_URL"]
32
+ headers = self.headers.copy()
33
+
34
+ # 获取文件名和扩展名
35
+ file_name = os.path.basename(file_path)
36
+ file_ext = os.path.splitext(file_name)[1].lower()
37
+ if file_ext is None or file_ext == "":
38
+ file_ext = ".jpg"
39
+ file_name = uuid.uuid4().hex + file_ext
40
+
41
+ # 根据文件扩展名确定MIME类型
42
+ mime_type = MIME_TYPES.get(file_ext, 'image/jpeg')
43
+
44
+ try:
45
+ print("hellow")
46
+ with open(file_path, 'rb') as file_handle:
47
+ # 读取文件并转换为Base64
48
+ file_data = file_handle.read()
49
+ base64_data = base64.b64encode(file_data).decode('utf-8')
50
+
51
+ # 构建data URL格式
52
+ data_url = f"data:{mime_type};base64,{base64_data}"
53
+
54
+ # 准备请求数据
55
+ payload = {
56
+ "base64Data": data_url,
57
+ "uploadPath": "images/veo3",
58
+ "fileName": file_name
59
+ }
60
+
61
+ response = requests.post(
62
+ url,
63
+ headers=headers,
64
+ json=payload,
65
+ timeout=API_CONFIG["TIMEOUT"]
66
+ )
67
+ print(response.status_code)
68
+ if response.status_code == 200:
69
+ resp_json = response.json()
70
+ print(resp_json)
71
+ if resp_json.get("success") and resp_json.get("data"):
72
+ # 返回下载URL
73
+ return True, resp_json["data"]["downloadUrl"]
74
+ else:
75
+ return False, resp_json.get("msg", "Upload failed")
76
+ else:
77
+ return False, f"Upload error, please try again later"
78
+ except Exception as e:
79
+ return False, f"Upload error: {str(e)}"
80
+
81
+ def create_task(
82
+ self,
83
+ prompt: str,
84
+ image_urls: List[str] = None,
85
+ aspect_ratio: str = "16:9",
86
+ watermark: str = "",
87
+ seeds: Optional[int] = None,
88
+ enable_fallback: bool = False
89
+ ) -> Tuple[int, str]:
90
+ """
91
+ 创建Veo3视频生成任务
92
+ """
93
+ headers = {
94
+ "Content-Type": "application/json",
95
+ "Authorization": f"Bearer {self.api_key}"
96
+ }
97
+ url = API_CONFIG["VEO3_GENERATE_URL"]
98
+
99
+ # 构建Veo3 API请求参数
100
+ payload = {
101
+ "prompt": prompt,
102
+ "model": "veo3",
103
+ "aspectRatio": aspect_ratio,
104
+ "enableFallback": enable_fallback
105
+ }
106
+
107
+ # 添加可选参数
108
+ if image_urls:
109
+ payload["imageUrls"] = image_urls
110
+ if watermark:
111
+ payload["watermark"] = watermark
112
+ if seeds is not None and seeds > 0:
113
+ payload["seeds"] = seeds
114
+ print(json.dumps(payload), headers)
115
+ try:
116
+ response = requests.post(
117
+ url,
118
+ headers=headers,
119
+ data=json.dumps(payload),
120
+ timeout=30
121
+ )
122
+ if response.status_code == 200:
123
+ resp_json = response.json()
124
+ print(resp_json)
125
+ if resp_json.get("code") == 200:
126
+ return 200, resp_json["data"]["taskId"]
127
+ else:
128
+ return 500, resp_json.get("msg", "API request failed")
129
+ else:
130
+ return response.status_code, f"HTTP {response.status_code}: {response.text}"
131
+ except Exception as e:
132
+ return 500, f"Request failed: {str(e)}"
133
+
134
+ def get_task_result(self, task_id: str) -> Tuple[int, str]:
135
+ """
136
+ 获取任务结果
137
+ """
138
+ start_time = time.time()
139
+ url = API_CONFIG["VEO3_DETAILS_URL"]
140
+ params = {"taskId": task_id}
141
+ headers = {"Authorization": f"Bearer {self.api_key}"}
142
+
143
+ while time.time() - start_time < API_CONFIG["TASK_TIMEOUT"]:
144
+ try:
145
+ response = requests.get(
146
+ url,
147
+ headers=headers,
148
+ params=params,
149
+ timeout=API_CONFIG["TIMEOUT"]
150
+ )
151
+ if response.status_code == 200:
152
+ resp_json = response.json()
153
+ print(resp_json)
154
+ if resp_json.get("code") == 200:
155
+ data = resp_json.get('data', {})
156
+ success_flag = data.get('successFlag')
157
+
158
+ if success_flag == 1: # 成功
159
+ # 获取视频URL
160
+ response_data = data.get('response', {})
161
+ result_urls = response_data.get('resultUrls', [])
162
+ if result_urls:
163
+ video_url = result_urls[0] # 取第一个视频URL
164
+ return 200, video_url
165
+ else:
166
+ return 500, "Video URL not found in response"
167
+ elif success_flag == 2 or success_flag == 3: # 失败
168
+ error_msg = data.get('errorMessage', 'Task failed')
169
+ return 500, error_msg
170
+ else: # success_flag == 0 或 None,任务仍在处理中
171
+ time.sleep(API_CONFIG["POLL_INTERVAL"])
172
+ continue
173
+ else:
174
+ return 500, resp_json.get("msg", "API request failed")
175
+ else:
176
+ return response.status_code, f"HTTP {response.status_code}: {response.text}"
177
+ except Exception as e:
178
+ pass
179
+ time.sleep(API_CONFIG["POLL_INTERVAL"])
180
+
181
+ return 500, "Task timeout, please try again later"
src/api/video_processor.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 视频处理模块
3
+ """
4
+
5
+ import gradio as gr
6
+ from typing import List, Tuple, Optional
7
+
8
+ from .veo3_client import Veo3Client
9
+ from ..utils.file_utils import cleanup_temp_file
10
+
11
+
12
+ class VideoProcessor:
13
+ """视频处理器类"""
14
+
15
+ def __init__(self):
16
+ self.client = None
17
+
18
+ def set_api_key(self, api_key: str):
19
+ """设置API密钥"""
20
+ self.client = Veo3Client(api_key)
21
+
22
+ def process_veo3_video(
23
+ self,
24
+ prompt: str,
25
+ uploaded_files: List[str],
26
+ api_key: str,
27
+ aspect_ratio: str = "16:9",
28
+ watermark: str = "",
29
+ seeds: Optional[int] = None,
30
+ enable_fallback: bool = False,
31
+ progress: gr.Progress = None
32
+ ) -> Tuple[Optional[str], str]:
33
+ """
34
+ 处理Veo3视频生成的主函数
35
+ """
36
+ # 验证输入
37
+ if not api_key or not api_key.strip():
38
+ return None, "❌ Please enter API Key"
39
+
40
+ if not prompt or not prompt.strip():
41
+ return None, "❌ Please enter a prompt"
42
+
43
+ # 设置客户端
44
+ self.set_api_key(api_key.strip())
45
+
46
+ # 图片是可选的,用于image-to-video
47
+ image_urls = []
48
+ file_paths = []
49
+
50
+ print(f"DEBUG: uploaded_files = {uploaded_files}, type = {type(uploaded_files)}")
51
+
52
+ if uploaded_files:
53
+ # 确保是列表格式
54
+ file_paths = uploaded_files if isinstance(uploaded_files, list) else [uploaded_files]
55
+ # 验证图片数量
56
+ if len(file_paths) > 1:
57
+ return None, "❌ Maximum 1 image allowed for video generation"
58
+
59
+ try:
60
+ if progress:
61
+ progress(0.1, desc="📤 Processing input...")
62
+
63
+ # 如果有图片,上传到 KIE AI 并获取公开URL
64
+ if file_paths:
65
+ for i, file_path in enumerate(file_paths):
66
+ if progress:
67
+ progress(0.1 + (0.2 * i / len(file_paths)), desc=f"📤 Uploading image {i + 1}/{len(file_paths)}...")
68
+
69
+ success, result = self.client.upload_file(file_path)
70
+ if success:
71
+ image_urls.append(result)
72
+ # 上传成功后删除本地临时文件
73
+ cleanup_temp_file(file_path)
74
+ else:
75
+ return None, f"❌ Failed to upload image {i + 1}: {result}"
76
+
77
+ if progress:
78
+ progress(0.3, desc="🚀 Creating video generation task...")
79
+
80
+ # 创建Veo3任务
81
+ status_code, result = self.client.create_task(
82
+ prompt.strip(),
83
+ image_urls,
84
+ aspect_ratio=aspect_ratio,
85
+ watermark=watermark,
86
+ seeds=seeds,
87
+ enable_fallback=enable_fallback
88
+ )
89
+
90
+ if status_code != 200:
91
+ return None, f"❌ Failed to create task: {result}"
92
+
93
+ task_id = result
94
+ if progress:
95
+ progress(0.4, desc=f"📋 Task ID: {task_id}")
96
+
97
+ if progress:
98
+ progress(0.5, desc="⏳ Generating video, please wait...")
99
+
100
+ # 轮询获取结果
101
+ status_code, result = self.client.get_task_result(task_id)
102
+
103
+ if status_code != 200:
104
+ return None, f"❌ Video generation failed: {result}"
105
+
106
+ if progress:
107
+ progress(0.9, desc="📥 Processing video...")
108
+
109
+ # 处理返回的视频URL
110
+ if result:
111
+ if progress:
112
+ progress(1.0, desc="✅ Complete!")
113
+ return result, f"✅ Successfully generated video! Task ID: {task_id}"
114
+ else:
115
+ return None, "❌ No video URL received"
116
+
117
+ except Exception as e:
118
+ return None, f"❌ Error occurred during processing: {str(e)}"
119
+ finally:
120
+ # 最终清理:删除任何剩余的临时文件
121
+ try:
122
+ for file_path in file_paths:
123
+ cleanup_temp_file(file_path)
124
+ except:
125
+ pass
src/config/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置模块
3
+ """
4
+
5
+ from .settings import (
6
+ API_CONFIG,
7
+ FILE_CONFIG,
8
+ UI_CONFIG,
9
+ EXAMPLE_PROMPTS,
10
+ ASPECT_RATIOS,
11
+ MIME_TYPES
12
+ )
13
+
14
+ __all__ = [
15
+ 'API_CONFIG',
16
+ 'FILE_CONFIG',
17
+ 'UI_CONFIG',
18
+ 'EXAMPLE_PROMPTS',
19
+ 'ASPECT_RATIOS',
20
+ 'MIME_TYPES'
21
+ ]
src/config/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (379 Bytes). View file
 
src/config/__pycache__/settings.cpython-312.pyc ADDED
Binary file (1.57 kB). View file
 
src/config/settings.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Veo3 AI Video Generator - 配置文件
3
+ """
4
+
5
+ # API配置
6
+ API_CONFIG = {
7
+ "VEO3_GENERATE_URL": "https://api.kie.ai/api/v1/veo/generate",
8
+ "VEO3_DETAILS_URL": "https://api.kie.ai/api/v1/veo/record-info",
9
+ "FILE_UPLOAD_URL": "https://kieai.redpandaai.co/api/file-base64-upload",
10
+ "TIMEOUT": 60,
11
+ "TASK_TIMEOUT": 600, # 10分钟
12
+ "POLL_INTERVAL": 5, # 5秒轮询间隔
13
+ }
14
+
15
+ # 文件配置
16
+ FILE_CONFIG = {
17
+ "MAX_IMAGE_SIZE": 10 * 1024 * 1024, # 10MB
18
+ "ALLOWED_IMAGE_TYPES": ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
19
+ "MAX_IMAGES": 1, # Veo3只支持1张图片
20
+ "TEMP_CLEANUP_INTERVAL": 1800, # 30分钟清理间隔
21
+ }
22
+
23
+ # UI配置
24
+ UI_CONFIG = {
25
+ "APP_TITLE": "🎬 Veo3 AI Video Generator",
26
+ "APP_SUBTITLE": "Powered by Google's Official Veo3 AI Video Model",
27
+ "APP_DESCRIPTION": "🎥 Veo3 AI generates high-quality videos from text prompts and images. Support for 16:9 and 9:16 aspect ratios with intelligent fallback.",
28
+ "DEFAULT_PROMPT": "A dog playing in a park with beautiful sunset lighting",
29
+ "DEFAULT_ASPECT_RATIO": "16:9",
30
+ "SERVER_PORT": 7860,
31
+ "SERVER_HOST": "0.0.0.0",
32
+ }
33
+
34
+ # 示例提示词
35
+ EXAMPLE_PROMPTS = [
36
+ "A dog playing in a park with beautiful sunset lighting",
37
+ "A cat walking through a magical forest with glowing mushrooms",
38
+ "Ocean waves crashing against rocks during a storm",
39
+ "A futuristic city with flying cars and neon lights",
40
+ "A peaceful mountain landscape with snow falling gently",
41
+ ]
42
+
43
+ # 支持的宽高比
44
+ ASPECT_RATIOS = [
45
+ "16:9",
46
+ "9:16",
47
+ ]
48
+
49
+ # MIME类型映射
50
+ MIME_TYPES = {
51
+ '.jpg': 'image/jpeg',
52
+ '.jpeg': 'image/jpeg',
53
+ '.png': 'image/png',
54
+ '.gif': 'image/gif',
55
+ '.webp': 'image/webp'
56
+ }
src/ui/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI模块
3
+ """
4
+
5
+ from .components import UIComponents
6
+ from .interface import Veo3Interface
7
+
8
+ __all__ = [
9
+ 'UIComponents',
10
+ 'Veo3Interface'
11
+ ]
src/ui/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (323 Bytes). View file
 
src/ui/__pycache__/components.cpython-312.pyc ADDED
Binary file (8.51 kB). View file
 
src/ui/__pycache__/interface.cpython-312.pyc ADDED
Binary file (13.1 kB). View file
 
src/ui/components.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI组件模块
3
+ """
4
+
5
+ import gradio as gr
6
+ from typing import List, Tuple
7
+
8
+ from ..config import (
9
+ UI_CONFIG,
10
+ EXAMPLE_PROMPTS,
11
+ ASPECT_RATIOS
12
+ )
13
+ from ..utils import handle_file_upload, create_image_html
14
+
15
+
16
+ class UIComponents:
17
+ """UI组件类"""
18
+
19
+ def __init__(self):
20
+ self.delete_buttons = []
21
+
22
+ def create_header(self) -> gr.HTML:
23
+ """创建页面头部"""
24
+ return gr.HTML(f"""
25
+ <h1 class="logo-text">{UI_CONFIG['APP_TITLE']}</h1>
26
+ <p class="subtitle">{UI_CONFIG['APP_SUBTITLE']}</p>
27
+ <div class="mode-indicator">
28
+ {UI_CONFIG['APP_DESCRIPTION']}
29
+ </div>
30
+ """)
31
+
32
+ def create_info_box(self) -> gr.HTML:
33
+ """创建信息提示框"""
34
+ return gr.HTML("""
35
+ <div class="info-box">
36
+ <strong>Usage Instructions:</strong><br>
37
+ • Get your Veo3 API Key <a href="https://kie.ai/veo3" target="_blank">👉 here 👈</a><br>
38
+ • Enter a text prompt to generate video from text, or upload 1 image for image-to-video.<br>
39
+ • Upload 1 reference image (Max 10MB, formats: JPG, PNG, WebP) for image-to-video<br>
40
+ • Click Generate and wait ~2–5 minutes for video processing
41
+ </div>
42
+ """)
43
+
44
+ def create_input_components(self) -> Tuple[gr.Textbox, gr.Textbox, gr.HTML, gr.File, List[gr.Button], gr.Dropdown, gr.Number, gr.Button]:
45
+ """创建输入组件"""
46
+ # API Key输入
47
+ api_key = gr.Textbox(
48
+ label="API Key",
49
+ placeholder="Please enter your KIE AI API Key",
50
+ type="password",
51
+ elem_classes="api-key-input"
52
+ )
53
+
54
+ # 提示词输入
55
+ prompt = gr.Textbox(
56
+ label="Video Prompt",
57
+ placeholder="Describe the video you want to generate, e.g.: A dog playing in a park...",
58
+ lines=3,
59
+ value=UI_CONFIG["DEFAULT_PROMPT"],
60
+ elem_classes="prompt-input"
61
+ )
62
+
63
+ # 图片上传区域 - 放在提示词和宽高比之间
64
+ with gr.Group(elem_classes="image-upload-container"):
65
+ gr.Markdown("### 📸 Image Upload (Optional)")
66
+ gr.Markdown("*Upload 1 image for image-to-video generation, or leave empty for text-to-video*")
67
+
68
+ # 自定义图片展示区
69
+ image_display = gr.HTML(
70
+ value="<div id='image-display-area'><div class='no-images'>No image uploaded - will generate from text only</div></div>",
71
+ elem_id="image-display"
72
+ )
73
+
74
+ # 上传按钮
75
+ file_upload = gr.File(
76
+ show_label=False,
77
+ file_count="single",
78
+ file_types=["image"],
79
+ type="filepath",
80
+ height=120,
81
+ )
82
+
83
+ # 删除按钮组
84
+ with gr.Row(elem_id="delete-buttons-row"):
85
+ delete_label = gr.Markdown("**Delete Images:**", visible=True, elem_id="delete-label")
86
+ delete_buttons = []
87
+ for i in range(1): # 最多支持1张图片
88
+ btn = gr.Button(f"Delete {i + 1}", visible=True, size="sm", elem_id=f"delete-btn-{i}")
89
+ delete_buttons.append(btn)
90
+
91
+ # 宽高比选择
92
+ aspect_ratio = gr.Dropdown(
93
+ choices=ASPECT_RATIOS,
94
+ value=UI_CONFIG["DEFAULT_ASPECT_RATIO"],
95
+ label="Aspect Ratio",
96
+ info="16:9 for landscape, 9:16 for portrait"
97
+ )
98
+
99
+ # 种子输入区域 - 使用Row让按钮在输入框右边
100
+ with gr.Row():
101
+ seeds = gr.Number(
102
+ label="Seed (Optional)",
103
+ value=10001,
104
+ info="Random seed for reproducible results (10000-99999)",
105
+ scale=4
106
+ )
107
+ random_seed_btn = gr.Button(
108
+ "🎲",
109
+ size="sm",
110
+ scale=1,
111
+ elem_id="random-seed-btn"
112
+ )
113
+
114
+ # 添加一个状态来保存上传的文件路径
115
+ uploaded_file_state = gr.State(None)
116
+
117
+ return api_key, prompt, image_display, file_upload, delete_buttons, aspect_ratio, seeds, random_seed_btn, uploaded_file_state
118
+
119
+ def create_output_section(self) -> Tuple[gr.Video, gr.Textbox]:
120
+ """创建输出区域"""
121
+ # 输出视频
122
+ output_video = gr.Video(
123
+ show_label=False,
124
+ elem_id="output-video",
125
+ height=400,
126
+ container=True
127
+ )
128
+
129
+ # 状态信息
130
+ status = gr.Textbox(
131
+ label="Processing Status",
132
+ interactive=False,
133
+ lines=2,
134
+ value="Ready, please enter a prompt to generate video..."
135
+ )
136
+
137
+ return output_video, status
138
+
139
+ def create_examples(self) -> gr.Examples:
140
+ """创建示例"""
141
+ return gr.Examples(
142
+ examples=[[prompt, None] for prompt in EXAMPLE_PROMPTS],
143
+ inputs=[], # 将在主应用中设置
144
+ label="Video Prompt Examples"
145
+ )
146
+
147
+ def create_generate_button(self) -> gr.Button:
148
+ """创建生成按钮"""
149
+ return gr.Button(
150
+ "🚀 Start Generation",
151
+ variant="primary",
152
+ size="lg"
153
+ )
154
+
155
+ def setup_file_upload_handlers(self, file_upload, image_display, delete_buttons, uploaded_file_state):
156
+ """设置文件上传处理器"""
157
+ def on_file_upload(new_files):
158
+ if not new_files:
159
+ return gr.update(value=None), "<div id='image-display-area'><div class='no-images'>No image uploaded - will generate from text only</div></div>", gr.update(visible=False), None
160
+
161
+ # 只处理第一张图片
162
+ file_path = new_files[0] if isinstance(new_files, list) else new_files
163
+
164
+ # 显示图片预览,实际上传在生成时进行
165
+ html_content = create_image_html([file_path])
166
+ return gr.update(value=None), html_content, gr.update(visible=True, value="Delete Image 1"), file_path
167
+
168
+ file_upload.upload(
169
+ fn=on_file_upload,
170
+ inputs=[file_upload],
171
+ outputs=[file_upload, image_display, delete_buttons[0], uploaded_file_state]
172
+ )
173
+
174
+ def setup_delete_handlers(self, delete_buttons, image_display, uploaded_file_state):
175
+ """设置删除按钮处理器"""
176
+ def delete_image():
177
+ return "<div id='image-display-area'><div class='no-images'>No image uploaded - will generate from text only</div></div>", gr.update(visible=False), None
178
+
179
+ # 绑定删除按钮事件
180
+ delete_buttons[0].click(
181
+ fn=delete_image,
182
+ outputs=[image_display, delete_buttons[0], uploaded_file_state]
183
+ )
src/ui/interface.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI界面模块
3
+ """
4
+
5
+ import gradio as gr
6
+ from typing import List, Tuple, Optional
7
+
8
+ from .components import UIComponents
9
+ from ..api import VideoProcessor
10
+ from ..config import UI_CONFIG
11
+
12
+
13
+ class Veo3Interface:
14
+ """Veo3界面类"""
15
+
16
+ def __init__(self):
17
+ self.components = UIComponents()
18
+ self.video_processor = VideoProcessor()
19
+ self.demo = None
20
+
21
+ def create_interface(self) -> gr.Blocks:
22
+ """创建完整的界面"""
23
+ # 读取CSS样式
24
+ with open("static/css/styles.css", "r", encoding="utf-8") as f:
25
+ css = f.read()
26
+
27
+ with gr.Blocks(css=css, theme=gr.themes.Base()) as demo:
28
+ self.demo = demo
29
+
30
+ # 页面头部
31
+ with gr.Column(elem_classes="header-container"):
32
+ self.components.create_header()
33
+
34
+ with gr.Column(elem_classes="main-content"):
35
+ # 信息提示框
36
+ self.components.create_info_box()
37
+
38
+ with gr.Row(equal_height=True):
39
+ # 左侧 - 输入区域
40
+ with gr.Column(scale=1):
41
+ # 输入组件(包含图片上传)
42
+ api_key, prompt, image_display, file_upload, delete_buttons, aspect_ratio, seeds, random_seed_btn, uploaded_file_state = self.components.create_input_components()
43
+
44
+ # 生成按钮
45
+ generate_btn = self.components.create_generate_button()
46
+
47
+ # 右侧 - 输出区域
48
+ with gr.Column(scale=1):
49
+ # 输出区域
50
+ output_video, status = self.components.create_output_section()
51
+
52
+ # 示例
53
+ examples = self.components.create_examples()
54
+ examples.inputs = [prompt, api_key]
55
+
56
+ # 设置事件处理器
57
+ self._setup_event_handlers(
58
+ generate_btn, prompt, api_key, aspect_ratio, seeds, random_seed_btn,
59
+ file_upload, image_display, delete_buttons, output_video, status, uploaded_file_state
60
+ )
61
+
62
+ # 添加JavaScript代码
63
+ self._add_javascript()
64
+
65
+ return demo
66
+
67
+ def _setup_event_handlers(
68
+ self,
69
+ generate_btn,
70
+ prompt,
71
+ api_key,
72
+ aspect_ratio,
73
+ seeds,
74
+ random_seed_btn,
75
+ file_upload,
76
+ image_display,
77
+ delete_buttons,
78
+ output_video,
79
+ status,
80
+ uploaded_file_state
81
+ ):
82
+ """设置事件处理器"""
83
+
84
+ # 文件上传处理器
85
+ self.components.setup_file_upload_handlers(file_upload, image_display, delete_buttons, uploaded_file_state)
86
+
87
+ # 删除按钮处理器
88
+ self.components.setup_delete_handlers(delete_buttons, image_display, uploaded_file_state)
89
+
90
+ # 随机种子按钮处理器
91
+ def generate_random_seed():
92
+ """生成随机种子"""
93
+ import random
94
+ return random.randint(10000, 99999) # API要求的范围
95
+
96
+ random_seed_btn.click(
97
+ fn=generate_random_seed,
98
+ outputs=[seeds]
99
+ )
100
+
101
+ # 生成按钮事件
102
+ def prepare_and_generate(prompt, api_key, aspect_ratio, seeds, saved_file_path, progress=gr.Progress()):
103
+ """生成视频的主函数"""
104
+ # 处理文件上传参数,如果为None则转换为空列表
105
+ file_paths = [saved_file_path] if saved_file_path is not None else []
106
+ return self.video_processor.process_veo3_video(
107
+ prompt, file_paths, api_key, aspect_ratio, None, seeds, False, progress
108
+ )
109
+
110
+ generate_btn.click(
111
+ fn=prepare_and_generate,
112
+ inputs=[prompt, api_key, aspect_ratio, seeds, uploaded_file_state],
113
+ outputs=[output_video, status]
114
+ )
115
+
116
+ def _add_javascript(self):
117
+ """添加JavaScript代码"""
118
+ self.demo.load(None, None, None, js="""
119
+ () => {
120
+ // 创建全局删除函数
121
+ window.deleteImageByIndex = function(index) {
122
+ // Gradio的elem_id设置在包装器上,需要找到内部的button
123
+ let deleteBtn = null;
124
+
125
+ // 方法1: 通过ID找到包装器,然后找内部的button
126
+ const wrapper = document.getElementById(`delete-btn-${index}`);
127
+ if (wrapper) {
128
+ deleteBtn = wrapper.querySelector('button');
129
+ }
130
+
131
+ // 方法2: 如果方法1失败,查找所有按钮并通过文本内容匹配
132
+ if (!deleteBtn) {
133
+ const allButtons = document.querySelectorAll('button');
134
+ for (let btn of allButtons) {
135
+ if (btn.textContent.includes(`Delete Image ${index + 1}`)) {
136
+ deleteBtn = btn;
137
+ break;
138
+ }
139
+ }
140
+ }
141
+
142
+ if (deleteBtn) {
143
+ deleteBtn.click();
144
+ } else {
145
+ // 调试信息
146
+ document.querySelectorAll('[id^="delete-btn-"]').forEach(elem => {
147
+ console.log(elem.id, elem.tagName, elem.querySelector('button'));
148
+ });
149
+ }
150
+ };
151
+
152
+ // 美化文件上传区域
153
+ function enhanceFileUpload() {
154
+ const fileInputs = document.querySelectorAll('.gr-file');
155
+ fileInputs.forEach(input => {
156
+ // 替换中文文本为英文
157
+ const textElements = input.querySelectorAll('.wrap > div');
158
+ textElements.forEach((element, index) => {
159
+ const text = element.textContent.trim();
160
+ // 替换各种可能的中文文本
161
+ if (text.includes('将文件拖放到此处') || text.includes('拖放文件到此处')) {
162
+ element.textContent = 'Drag and drop files here';
163
+ } else if (text.includes('点击上传') || text.includes('点击选择文件')) {
164
+ element.textContent = 'or click to upload';
165
+ } else if (text.includes('- 或 -') || text.includes('或')) {
166
+ element.textContent = '- or -';
167
+ }
168
+ });
169
+
170
+ // 如果还有中文文本,直接替换整个内容
171
+ const wrap = input.querySelector('.wrap');
172
+ if (wrap) {
173
+ const allText = wrap.textContent;
174
+ if (allText.includes('将文件') || allText.includes('点击上传')) {
175
+ wrap.innerHTML = `
176
+ <div style="font-size: 0.9rem; font-weight: 500; color: #4a5568; margin: 0.5rem 0;">Drag and drop files here</div>
177
+ <div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">- or -</div>
178
+ <div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">or click to upload</div>
179
+ `;
180
+ }
181
+ }
182
+
183
+ // 添加拖拽事件监听
184
+ input.addEventListener('dragover', function(e) {
185
+ e.preventDefault();
186
+ this.classList.add('dragover');
187
+ });
188
+
189
+ input.addEventListener('dragleave', function(e) {
190
+ e.preventDefault();
191
+ this.classList.remove('dragover');
192
+ });
193
+
194
+ input.addEventListener('drop', function(e) {
195
+ e.preventDefault();
196
+ this.classList.remove('dragover');
197
+ });
198
+
199
+ // 监听文件选择
200
+ const fileInput = input.querySelector('input[type="file"]');
201
+ if (fileInput) {
202
+ fileInput.addEventListener('change', function() {
203
+ if (this.files && this.files.length > 0) {
204
+ input.classList.add('has-file');
205
+ } else {
206
+ input.classList.remove('has-file');
207
+ }
208
+ });
209
+ }
210
+ });
211
+ }
212
+
213
+ // 初始化美化
214
+ enhanceFileUpload();
215
+
216
+ // 使用定时器确保文本被替换
217
+ const textReplacer = setInterval(function() {
218
+ const fileInputs = document.querySelectorAll('.gr-file');
219
+ let hasChineseText = false;
220
+
221
+ fileInputs.forEach(input => {
222
+ const wrap = input.querySelector('.wrap');
223
+ if (wrap) {
224
+ const allText = wrap.textContent;
225
+ if (allText.includes('将文件') || allText.includes('点击上传') || allText.includes('拖放')) {
226
+ hasChineseText = true;
227
+ wrap.innerHTML = `
228
+ <div style="font-size: 0.9rem; font-weight: 500; color: #4a5568; margin: 0.5rem 0;">Drag and drop files here</div>
229
+ <div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">- or -</div>
230
+ <div style="font-size: 0.8rem; color: #718096; margin: 0.3rem 0;">or click to upload</div>
231
+ `;
232
+ }
233
+ }
234
+ });
235
+
236
+ // 如果没有中文文本了,停止定时器
237
+ if (!hasChineseText) {
238
+ clearInterval(textReplacer);
239
+ }
240
+ }, 100);
241
+
242
+ // 监听DOM变化,处理动态添加的元素
243
+ const observer = new MutationObserver(function(mutations) {
244
+ mutations.forEach(function(mutation) {
245
+ if (mutation.type === 'childList') {
246
+ mutation.addedNodes.forEach(function(node) {
247
+ if (node.nodeType === 1 && node.classList && node.classList.contains('gr-file')) {
248
+ enhanceFileUpload();
249
+ }
250
+ });
251
+ }
252
+ });
253
+ });
254
+
255
+ observer.observe(document.body, {
256
+ childList: true,
257
+ subtree: true
258
+ });
259
+ }
260
+ """)
261
+
262
+ def launch(self, share: bool = False, server_name: str = None, server_port: int = None):
263
+ """启动应用"""
264
+ if server_name is None:
265
+ server_name = UI_CONFIG["SERVER_HOST"]
266
+ if server_port is None:
267
+ server_port = UI_CONFIG["SERVER_PORT"]
268
+
269
+ try:
270
+ self.demo.launch(
271
+ share=share,
272
+ server_name=server_name,
273
+ server_port=server_port,
274
+ show_error=True,
275
+ quiet=False
276
+ )
277
+ except Exception as e:
278
+ print(f"❌ Launch failed: {e}")
279
+ print("🔄 Trying to restart with default configuration...")
280
+ try:
281
+ self.demo.launch(
282
+ share=False,
283
+ server_name="127.0.0.1",
284
+ server_port=7861,
285
+ show_error=True,
286
+ quiet=False
287
+ )
288
+ except Exception as e2:
289
+ print(f"❌ Restart also failed: {e2}")
290
+ print("💡 Please check if the port is occupied, or try another port")
291
+ raise
src/utils/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 工具函数模块
3
+ """
4
+
5
+ from .file_utils import (
6
+ cleanup_temp_files,
7
+ start_cleanup_scheduler,
8
+ handle_file_upload,
9
+ cleanup_temp_file,
10
+ get_temp_dirs
11
+ )
12
+
13
+ from .image_utils import (
14
+ get_image_list,
15
+ create_image_html
16
+ )
17
+
18
+ __all__ = [
19
+ 'cleanup_temp_files',
20
+ 'start_cleanup_scheduler',
21
+ 'handle_file_upload',
22
+ 'cleanup_temp_file',
23
+ 'get_temp_dirs',
24
+ 'get_image_list',
25
+ 'create_image_html'
26
+ ]
src/utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (514 Bytes). View file
 
src/utils/__pycache__/file_utils.cpython-312.pyc ADDED
Binary file (5.84 kB). View file
 
src/utils/__pycache__/image_utils.cpython-312.pyc ADDED
Binary file (2.6 kB). View file
 
src/utils/file_utils.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 文件处理工具函数
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import threading
8
+ from typing import List, Set
9
+
10
+
11
+ # 全局变量记录使用过的临时目录
12
+ used_temp_dirs: Set[str] = set()
13
+
14
+
15
+ def cleanup_temp_files(temp_dirs=None):
16
+ """
17
+ 清理指定的临时文件夹中的旧文件
18
+ temp_dirs: 要清理的目录列表,如果为None则只清理记录的目录
19
+ """
20
+ try:
21
+ # 如果没有指定目录,只清理我们记录的目录
22
+ if temp_dirs is None:
23
+ temp_dirs = []
24
+
25
+ current_time = time.time()
26
+ cleaned_count = 0
27
+
28
+ for temp_dir in temp_dirs:
29
+ try:
30
+ # 确保目录存在且是目录
31
+ if not os.path.exists(temp_dir) or not os.path.isdir(temp_dir):
32
+ continue
33
+
34
+ # 遍历目录中的文件
35
+ for root, dirs, files in os.walk(temp_dir):
36
+ for file_name in files:
37
+ file_path = os.path.join(root, file_name)
38
+ try:
39
+ # 检查文件修改时间
40
+ file_mtime = os.path.getmtime(file_path)
41
+ # 如果文件超过30分钟未修改,则删除
42
+ if current_time - file_mtime > 1800: # 1800秒 = 30分钟
43
+ # 检查是否是图片文件或临时文件
44
+ if any(file_path.lower().endswith(ext) for ext in
45
+ ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tmp']):
46
+ os.remove(file_path)
47
+ cleaned_count += 1
48
+ print(f"Cleaned old temp file: {file_path}")
49
+ except Exception as e:
50
+ print(f"Error cleaning {file_path}: {e}")
51
+
52
+ # 清理空目录
53
+ for dir_name in dirs:
54
+ dir_path = os.path.join(root, dir_name)
55
+ try:
56
+ if os.path.exists(dir_path) and not os.listdir(dir_path):
57
+ os.rmdir(dir_path)
58
+ print(f"Removed empty temp directory: {dir_path}")
59
+ except Exception as e:
60
+ print(f"Error removing directory {dir_path}: {e}")
61
+
62
+ except Exception as e:
63
+ print(f"Error processing directory {temp_dir}: {e}")
64
+
65
+ if cleaned_count > 0:
66
+ print(f"Cleanup completed: removed {cleaned_count} temporary files")
67
+
68
+ except Exception as e:
69
+ print(f"Error during temp cleanup: {e}")
70
+
71
+
72
+ def start_cleanup_scheduler():
73
+ """
74
+ 启动定时清理任务
75
+ """
76
+ def cleanup_worker():
77
+ while True:
78
+ try:
79
+ # 每30分钟清理一次
80
+ time.sleep(1800) # 1800秒 = 30分钟
81
+ print("Starting scheduled cleanup...")
82
+ # 只清理我们记录的临时目录
83
+ cleanup_temp_files(list(used_temp_dirs))
84
+ except Exception as e:
85
+ print(f"Error in cleanup scheduler: {e}")
86
+
87
+ # 创建守护线程
88
+ cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
89
+ cleanup_thread.start()
90
+
91
+
92
+ def handle_file_upload(current_files, new_files):
93
+ """简化的文件上传处理 - 支持单张图片"""
94
+ if not new_files:
95
+ return [], "No image uploaded - will generate from text only"
96
+
97
+ # 确保是列表格式
98
+ current_list = current_files or []
99
+ new_list = new_files if isinstance(new_files, list) else [new_files]
100
+
101
+ # 合并并限制数量为1张图片
102
+ all_files = current_list + [f for f in new_list if f]
103
+ if len(all_files) > 1:
104
+ all_files = all_files[:1]
105
+ message = f"Uploaded 1 image (limit reached)"
106
+ else:
107
+ message = f"Uploaded {len(all_files)} image"
108
+
109
+ return all_files, message
110
+
111
+
112
+ def cleanup_temp_file(file_path: str):
113
+ """
114
+ 清理单个临时文件
115
+ """
116
+ try:
117
+ if os.path.exists(file_path):
118
+ # 记录文件所在的目录
119
+ temp_dir = os.path.dirname(file_path)
120
+ used_temp_dirs.add(temp_dir)
121
+ os.remove(file_path)
122
+ except Exception as e:
123
+ print(f"Error cleaning temp file {file_path}: {e}")
124
+
125
+
126
+ def get_temp_dirs() -> Set[str]:
127
+ """
128
+ 获取所有使用过的临时目录
129
+ """
130
+ return used_temp_dirs.copy()
src/utils/image_utils.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 图片处理工具函数
3
+ """
4
+
5
+ import base64
6
+ from typing import List
7
+ from PIL import Image
8
+
9
+
10
+ def get_image_list(file_paths: List[str]) -> List[Image.Image]:
11
+ """从文件路径列表获取PIL图片列表用于展示"""
12
+ if not file_paths:
13
+ return []
14
+
15
+ images = []
16
+ for path in file_paths:
17
+ try:
18
+ img = Image.open(path)
19
+ images.append(img)
20
+ except Exception:
21
+ continue
22
+ return images
23
+
24
+
25
+ def create_image_html(file_paths: List[str]) -> str:
26
+ """创建图片展示的HTML代码"""
27
+ if not file_paths:
28
+ return "<div id='image-display-area'><div class='no-images'>No image uploaded - will generate from text only</div></div>"
29
+
30
+ html_items = []
31
+ for i, path in enumerate(file_paths):
32
+ try:
33
+ # 将图片转换为base64编码
34
+ with open(path, "rb") as img_file:
35
+ img_data = base64.b64encode(img_file.read()).decode()
36
+ img_ext = path.split('.')[-1].lower()
37
+ if img_ext in ['jpg', 'jpeg']:
38
+ mime_type = 'image/jpeg'
39
+ elif img_ext == 'png':
40
+ mime_type = 'image/png'
41
+ elif img_ext == 'gif':
42
+ mime_type = 'image/gif'
43
+ elif img_ext == 'webp':
44
+ mime_type = 'image/webp'
45
+ else:
46
+ mime_type = 'image/jpeg'
47
+
48
+ img_src = f"data:{mime_type};base64,{img_data}"
49
+
50
+ html_items.append(f"""
51
+ <div class="image-item" data-index="{i}">
52
+ <img src="{img_src}" alt="Uploaded image {i + 1}">
53
+ <div class="delete-btn" onclick="deleteImageByIndex({i})" data-index="{i}">×</div>
54
+ </div>
55
+ """)
56
+ except:
57
+ continue
58
+
59
+ if not html_items:
60
+ return "<div id='image-display-area'><div class='no-images'>No image uploaded - will generate from text only</div></div>"
61
+
62
+ html_content = f"""
63
+ <div id='image-display-area'>
64
+ {''.join(html_items)}
65
+ </div>
66
+ """
67
+
68
+ return html_content
static/css/styles.css ADDED
@@ -0,0 +1,699 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Veo3 AI Video Generator - 现代美观CSS样式 */
2
+
3
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
4
+
5
+ :root {
6
+ --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
7
+ --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
8
+ --success-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
9
+ --warning-gradient: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
10
+ --dark-gradient: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
11
+ --glass-bg: rgba(255, 255, 255, 0.85);
12
+ --glass-border: rgba(255, 255, 255, 0.3);
13
+ --shadow-light: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
14
+ --shadow-heavy: 0 20px 60px rgba(102, 126, 234, 0.25);
15
+ --border-radius: 20px;
16
+ --border-radius-small: 12px;
17
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ .gradio-container {
25
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
26
+ background-size: 400% 400%;
27
+ animation: gradientShift 15s ease infinite;
28
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ min-height: 100vh;
30
+ padding: 20px;
31
+ position: relative;
32
+ overflow-x: hidden;
33
+ -webkit-font-smoothing: antialiased;
34
+ -moz-osx-font-smoothing: grayscale;
35
+ text-rendering: optimizeLegibility;
36
+ }
37
+
38
+ @keyframes gradientShift {
39
+ 0% { background-position: 0% 50%; }
40
+ 50% { background-position: 100% 50%; }
41
+ 100% { background-position: 0% 50%; }
42
+ }
43
+
44
+ .gradio-container::before {
45
+ content: '';
46
+ position: fixed;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.05)"/><circle cx="10" cy="60" r="0.5" fill="rgba(255,255,255,0.05)"/><circle cx="90" cy="40" r="0.5" fill="rgba(255,255,255,0.05)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
52
+ pointer-events: none;
53
+ z-index: 0;
54
+ }
55
+
56
+ .header-container {
57
+ background: #ffffff;
58
+ backdrop-filter: none;
59
+ border: 1px solid #e2e8f0;
60
+ padding: 3rem 2.5rem;
61
+ border-radius: var(--border-radius);
62
+ margin-bottom: 2rem;
63
+ box-shadow: var(--shadow-light);
64
+ position: relative;
65
+ z-index: 1;
66
+ text-align: center;
67
+ overflow: hidden;
68
+ }
69
+
70
+ .header-container::before {
71
+ content: '';
72
+ position: absolute;
73
+ top: 0;
74
+ left: 0;
75
+ right: 0;
76
+ height: 4px;
77
+ background: var(--primary-gradient);
78
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
79
+ }
80
+
81
+ .logo-text {
82
+ font-size: 3.5rem;
83
+ font-weight: 900;
84
+ color: #2d3436;
85
+ text-align: center;
86
+ margin: 0;
87
+ letter-spacing: -2px;
88
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
89
+ filter: none;
90
+ }
91
+
92
+ @keyframes textGlow {
93
+ from { filter: brightness(1); }
94
+ to { filter: brightness(1.2); }
95
+ }
96
+
97
+ .subtitle {
98
+ color: #4a5568;
99
+ text-align: center;
100
+ font-size: 1.1rem;
101
+ margin-top: 1rem;
102
+ font-weight: 500;
103
+ text-shadow: none;
104
+ }
105
+
106
+ .main-content {
107
+ background: #ffffff;
108
+ backdrop-filter: none;
109
+ border: 1px solid #e2e8f0;
110
+ border-radius: var(--border-radius);
111
+ padding: 2.5rem;
112
+ box-shadow: var(--shadow-light);
113
+ position: relative;
114
+ z-index: 1;
115
+ margin-bottom: 2rem;
116
+ }
117
+
118
+ .mode-indicator {
119
+ background: rgba(255, 255, 255, 0.8);
120
+ backdrop-filter: blur(4px);
121
+ border-radius: 12px;
122
+ padding: 0.8rem 1.2rem;
123
+ margin-top: 1rem;
124
+ text-align: center;
125
+ font-weight: 600;
126
+ color: #2d3436;
127
+ border: 1px solid rgba(255, 255, 255, 0.3);
128
+ }
129
+
130
+ .gr-button-primary {
131
+ background: var(--primary-gradient) !important;
132
+ border: none !important;
133
+ color: white !important;
134
+ font-weight: 700 !important;
135
+ font-size: 1.1rem !important;
136
+ padding: 1.2rem 2rem !important;
137
+ border-radius: var(--border-radius-small) !important;
138
+ text-transform: uppercase;
139
+ letter-spacing: 1px;
140
+ width: 100%;
141
+ margin-top: 1rem !important;
142
+ position: relative;
143
+ overflow: hidden;
144
+ transition: var(--transition);
145
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
146
+ }
147
+
148
+ .gr-button-primary::before {
149
+ content: '';
150
+ position: absolute;
151
+ top: 0;
152
+ left: -100%;
153
+ width: 100%;
154
+ height: 100%;
155
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
156
+ transition: left 0.5s;
157
+ }
158
+
159
+ .gr-button-primary:hover {
160
+ transform: translateY(-2px);
161
+ box-shadow: 0 12px 35px rgba(102, 126, 234, 0.4);
162
+ }
163
+
164
+ .gr-button-primary:hover::before {
165
+ left: 100%;
166
+ }
167
+
168
+ .gr-button-primary:active {
169
+ transform: translateY(0);
170
+ }
171
+
172
+ .gr-input, .gr-textarea {
173
+ background: #ffffff !important;
174
+ backdrop-filter: none !important;
175
+ border: 2px solid #e2e8f0 !important;
176
+ border-radius: var(--border-radius-small) !important;
177
+ color: #1a1a1a !important;
178
+ font-size: 1rem !important;
179
+ padding: 1rem 1.2rem !important;
180
+ transition: var(--transition) !important;
181
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
182
+ }
183
+
184
+ .gr-input:focus, .gr-textarea:focus {
185
+ border-color: rgba(102, 126, 234, 0.6) !important;
186
+ outline: none !important;
187
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.2) !important;
188
+ transform: translateY(-1px) !important;
189
+ }
190
+
191
+ .gr-input:hover, .gr-textarea:hover {
192
+ border-color: rgba(102, 126, 234, 0.4) !important;
193
+ transform: translateY(-1px) !important;
194
+ }
195
+
196
+ .gr-form {
197
+ background: transparent !important;
198
+ border: none !important;
199
+ }
200
+
201
+ .gr-panel {
202
+ background: #ffffff !important;
203
+ backdrop-filter: none !important;
204
+ border: 1px solid #e2e8f0 !important;
205
+ border-radius: var(--border-radius) !important;
206
+ padding: 2rem !important;
207
+ box-shadow: var(--shadow-light) !important;
208
+ transition: var(--transition) !important;
209
+ }
210
+
211
+ .gr-panel:hover {
212
+ transform: translateY(-2px) !important;
213
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15) !important;
214
+ }
215
+
216
+ .gr-box {
217
+ border-radius: var(--border-radius-small) !important;
218
+ border-color: rgba(255, 255, 255, 0.2) !important;
219
+ background: rgba(255, 255, 255, 0.1) !important;
220
+ backdrop-filter: blur(10px) !important;
221
+ }
222
+
223
+ label {
224
+ color: #2d3436 !important;
225
+ font-weight: 600 !important;
226
+ font-size: 0.9rem !important;
227
+ text-transform: uppercase;
228
+ letter-spacing: 0.5px;
229
+ margin-bottom: 0.8rem !important;
230
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
231
+ }
232
+
233
+ .status-text {
234
+ font-family: 'SF Mono', 'Monaco', monospace;
235
+ font-size: 0.95rem;
236
+ }
237
+
238
+ .image-container {
239
+ border-radius: 14px !important;
240
+ overflow: hidden;
241
+ border: 2px solid #e1e8ed !important;
242
+ background: #fafbfc !important;
243
+ }
244
+
245
+ footer {
246
+ display: none !important;
247
+ }
248
+
249
+ .info-box {
250
+ background: #ffffff;
251
+ backdrop-filter: none;
252
+ border: 1px solid #e2e8f0;
253
+ border-radius: var(--border-radius-small);
254
+ padding: 1.5rem;
255
+ margin-bottom: 1.5rem;
256
+ border-left: 4px solid #667eea;
257
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
258
+ color: #2d3436;
259
+ position: relative;
260
+ overflow: hidden;
261
+ font-weight: 500;
262
+ line-height: 1.6;
263
+ }
264
+
265
+ .info-box strong {
266
+ color: #1a202c;
267
+ font-weight: 700;
268
+ font-size: 1.1rem;
269
+ text-shadow: none;
270
+ filter: none;
271
+ }
272
+
273
+ .info-box a {
274
+ color: #3182ce;
275
+ text-decoration: underline;
276
+ font-weight: 600;
277
+ text-shadow: none;
278
+ filter: none;
279
+ }
280
+
281
+ .info-box a:hover {
282
+ color: #2c5282;
283
+ text-decoration: underline;
284
+ }
285
+
286
+ .info-box::before {
287
+ content: '';
288
+ position: absolute;
289
+ top: 0;
290
+ left: 0;
291
+ width: 100%;
292
+ height: 2px;
293
+ background: var(--primary-gradient);
294
+ }
295
+
296
+ .warning-box {
297
+ background: #fff3cd;
298
+ border-radius: 12px;
299
+ padding: 1rem;
300
+ margin-top: 1rem;
301
+ border-left: 4px solid #ffc107;
302
+ color: #856404;
303
+ }
304
+
305
+ /* 简化的图片展示区域 */
306
+ .image-upload-container {
307
+ background: #ffffff !important;
308
+ backdrop-filter: none !important;
309
+ border: 1px solid #e2e8f0 !important;
310
+ border-radius: var(--border-radius) !important;
311
+ padding: 2rem !important;
312
+ box-shadow: var(--shadow-light) !important;
313
+ transition: var(--transition) !important;
314
+ }
315
+
316
+ .image-upload-container:hover {
317
+ transform: translateY(-2px) !important;
318
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15) !important;
319
+ }
320
+
321
+ /* 自定义图片展示区 - 无边框设计 */
322
+ #image-display-area {
323
+ display: flex !important;
324
+ flex-wrap: nowrap !important;
325
+ gap: 12px !important;
326
+ padding: 15px 5px !important;
327
+ overflow-x: auto !important;
328
+ overflow-y: hidden !important;
329
+ min-height: 120px !important;
330
+ max-height: 120px !important;
331
+ align-items: center !important;
332
+ background: transparent !important;
333
+ }
334
+
335
+ /* 无图片时的提示 */
336
+ .no-images {
337
+ color: #6b7280 !important;
338
+ font-size: 14px !important;
339
+ text-align: center !important;
340
+ width: 100% !important;
341
+ padding: 20px !important;
342
+ font-weight: 500 !important;
343
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
344
+ }
345
+
346
+ /* 图片项容器 */
347
+ .image-item {
348
+ position: relative !important;
349
+ flex-shrink: 0 !important;
350
+ width: 100px !important;
351
+ height: 100px !important;
352
+ border-radius: var(--border-radius-small) !important;
353
+ overflow: hidden !important;
354
+ cursor: pointer !important;
355
+ transition: var(--transition) !important;
356
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
357
+ border: 2px solid rgba(255, 255, 255, 0.2) !important;
358
+ }
359
+
360
+ .image-item:hover {
361
+ transform: scale(1.05) translateY(-2px) !important;
362
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.2) !important;
363
+ border-color: rgba(102, 126, 234, 0.6) !important;
364
+ }
365
+
366
+ /* 图片样式 */
367
+ .image-item img {
368
+ width: 100% !important;
369
+ height: 100% !important;
370
+ object-fit: cover !important;
371
+ border-radius: 8px !important;
372
+ display: block !important;
373
+ }
374
+
375
+ /* 删除按钮 */
376
+ .image-item .delete-btn {
377
+ position: absolute !important;
378
+ top: -8px !important;
379
+ right: -8px !important;
380
+ width: 28px !important;
381
+ height: 28px !important;
382
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%) !important;
383
+ color: white !important;
384
+ border: 2px solid rgba(255, 255, 255, 0.9) !important;
385
+ border-radius: 50% !important;
386
+ display: flex !important;
387
+ align-items: center !important;
388
+ justify-content: center !important;
389
+ font-size: 14px !important;
390
+ font-weight: bold !important;
391
+ cursor: pointer !important;
392
+ opacity: 0 !important;
393
+ transition: var(--transition) !important;
394
+ z-index: 10 !important;
395
+ line-height: 1 !important;
396
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3) !important;
397
+ }
398
+
399
+ .image-item:hover .delete-btn {
400
+ opacity: 1 !important;
401
+ }
402
+
403
+ .image-item .delete-btn:hover {
404
+ background: linear-gradient(135deg, #ff5252 0%, #e53935 100%) !important;
405
+ transform: scale(1.15) !important;
406
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4) !important;
407
+ }
408
+
409
+ /* 隐藏独立的删除按钮组 */
410
+ #delete-buttons-row {
411
+ display: none !important;
412
+ }
413
+
414
+ /* 滚动条样式 */
415
+ #image-display-area::-webkit-scrollbar {
416
+ height: 6px !important;
417
+ }
418
+
419
+ #image-display-area::-webkit-scrollbar-track {
420
+ background: rgba(0, 0, 0, 0.05) !important;
421
+ border-radius: 3px !important;
422
+ }
423
+
424
+ #image-display-area::-webkit-scrollbar-thumb {
425
+ background: rgba(0, 0, 0, 0.2) !important;
426
+ border-radius: 3px !important;
427
+ }
428
+
429
+ #image-display-area::-webkit-scrollbar-thumb:hover {
430
+ background: rgba(0, 0, 0, 0.3) !important;
431
+ }
432
+
433
+ /* 美化的上传文件框 */
434
+ .gr-file {
435
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important;
436
+ backdrop-filter: blur(10px) !important;
437
+ border: 2px dashed #cbd5e0 !important;
438
+ border-radius: var(--border-radius) !important;
439
+ padding: 2rem !important;
440
+ font-size: 1rem !important;
441
+ min-height: 120px !important;
442
+ height: auto !important;
443
+ max-width: 100% !important;
444
+ transition: var(--transition) !important;
445
+ color: #4a5568 !important;
446
+ position: relative !important;
447
+ overflow: hidden !important;
448
+ cursor: pointer !important;
449
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
450
+ }
451
+
452
+ .gr-file::before {
453
+ content: '';
454
+ position: absolute;
455
+ top: 0;
456
+ left: 0;
457
+ right: 0;
458
+ bottom: 0;
459
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
460
+ opacity: 0;
461
+ transition: var(--transition);
462
+ z-index: 0;
463
+ }
464
+
465
+ .gr-file:hover {
466
+ border-color: #667eea !important;
467
+ background: linear-gradient(135deg, #ffffff 0%, #f7fafc 100%) !important;
468
+ transform: translateY(-3px) !important;
469
+ box-shadow: 0 8px 30px rgba(102, 126, 234, 0.2) !important;
470
+ }
471
+
472
+ .gr-file:hover::before {
473
+ opacity: 1;
474
+ }
475
+
476
+ .gr-file .wrap {
477
+ min-height: 80px !important;
478
+ padding: 0 !important;
479
+ display: flex !important;
480
+ flex-direction: column !important;
481
+ align-items: center !important;
482
+ justify-content: center !important;
483
+ position: relative !important;
484
+ z-index: 1 !important;
485
+ text-align: center !important;
486
+ }
487
+
488
+ .gr-file .wrap > div {
489
+ font-size: 0.9rem !important;
490
+ color: #4a5568 !important;
491
+ line-height: 1.5 !important;
492
+ font-weight: 500 !important;
493
+ margin: 0.5rem 0 !important;
494
+ }
495
+
496
+ /* 上传图标样式 */
497
+ .gr-file .wrap::before {
498
+ content: '📁';
499
+ font-size: 2.5rem !important;
500
+ margin-bottom: 0.5rem !important;
501
+ opacity: 0.7 !important;
502
+ transition: var(--transition) !important;
503
+ }
504
+
505
+ .gr-file:hover .wrap::before {
506
+ content: '📤';
507
+ opacity: 1 !important;
508
+ transform: scale(1.1) !important;
509
+ }
510
+
511
+ /* 上传区域文字样式 */
512
+ .gr-file .wrap > div:first-of-type {
513
+ font-size: 1.1rem !important;
514
+ font-weight: 600 !important;
515
+ color: #2d3748 !important;
516
+ margin-bottom: 0.3rem !important;
517
+ }
518
+
519
+ .gr-file .wrap > div:last-of-type {
520
+ font-size: 0.85rem !important;
521
+ color: #718096 !important;
522
+ font-weight: 400 !important;
523
+ }
524
+
525
+ /* 文本样式保持原样,通过JavaScript替换内容 */
526
+
527
+ /* 拖拽状态 */
528
+ .gr-file.dragover {
529
+ border-color: #667eea !important;
530
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%) !important;
531
+ transform: scale(1.02) !important;
532
+ }
533
+
534
+ /* 文件已选择状态 */
535
+ .gr-file.has-file {
536
+ border-color: #48bb78 !important;
537
+ background: linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, rgba(56, 178, 172, 0.1) 100%) !important;
538
+ }
539
+
540
+ .gr-file.has-file .wrap::before {
541
+ content: '✅';
542
+ color: #48bb78 !important;
543
+ }
544
+
545
+ /* 输出标题样式 */
546
+ .output-title {
547
+ margin-bottom: 1rem !important;
548
+ margin-top: 0 !important;
549
+ }
550
+
551
+ .output-title h3 {
552
+ margin: 0 !important;
553
+ padding: 0 !important;
554
+ font-size: 1.3rem !important;
555
+ color: #2d3436 !important;
556
+ font-weight: 700 !important;
557
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
558
+ }
559
+
560
+ /* 输出视频容器 */
561
+ #output-video {
562
+ border: 2px solid #e2e8f0 !important;
563
+ border-radius: var(--border-radius) !important;
564
+ padding: 1.5rem !important;
565
+ background: #ffffff !important;
566
+ backdrop-filter: none !important;
567
+ height: 400px !important;
568
+ overflow: hidden !important;
569
+ position: relative !important;
570
+ box-shadow: var(--shadow-light) !important;
571
+ transition: var(--transition) !important;
572
+ }
573
+
574
+ #output-video:hover {
575
+ transform: translateY(-2px) !important;
576
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15) !important;
577
+ }
578
+
579
+ #output-video::before {
580
+ content: '';
581
+ position: absolute;
582
+ top: 0;
583
+ left: 0;
584
+ right: 0;
585
+ height: 3px;
586
+ background: var(--primary-gradient);
587
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
588
+ }
589
+
590
+ .gr-group {
591
+ background: #ffffff !important;
592
+ backdrop-filter: none !important;
593
+ border: 1px solid #e2e8f0 !important;
594
+ border-radius: var(--border-radius) !important;
595
+ padding: 1.5rem !important;
596
+ margin-bottom: 1.5rem !important;
597
+ box-shadow: var(--shadow-light) !important;
598
+ transition: var(--transition) !important;
599
+ }
600
+
601
+ .gr-group:hover {
602
+ transform: translateY(-1px) !important;
603
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important;
604
+ }
605
+
606
+ /* 响应式设计 */
607
+ @media (max-width: 768px) {
608
+ .gradio-container {
609
+ padding: 10px;
610
+ }
611
+
612
+ .header-container {
613
+ padding: 2rem 1.5rem;
614
+ }
615
+
616
+ .logo-text {
617
+ font-size: 2.5rem;
618
+ }
619
+
620
+ .main-content {
621
+ padding: 1.5rem;
622
+ }
623
+
624
+ .gr-panel {
625
+ padding: 1.5rem;
626
+ }
627
+
628
+ .gr-group {
629
+ padding: 1rem;
630
+ }
631
+ }
632
+
633
+ /* 加载动画 */
634
+ @keyframes pulse {
635
+ 0% { opacity: 1; }
636
+ 50% { opacity: 0.5; }
637
+ 100% { opacity: 1; }
638
+ }
639
+
640
+ .loading {
641
+ animation: pulse 2s infinite;
642
+ }
643
+
644
+ /* 成功状态 */
645
+ .success {
646
+ background: var(--success-gradient) !important;
647
+ color: white !important;
648
+ }
649
+
650
+ /* 错误状态 */
651
+ .error {
652
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%) !important;
653
+ color: white !important;
654
+ }
655
+
656
+ /* 自定义滚动条 */
657
+ ::-webkit-scrollbar {
658
+ width: 8px;
659
+ height: 8px;
660
+ }
661
+
662
+ ::-webkit-scrollbar-track {
663
+ background: rgba(255, 255, 255, 0.1);
664
+ border-radius: 4px;
665
+ }
666
+
667
+ ::-webkit-scrollbar-thumb {
668
+ background: var(--primary-gradient);
669
+ border-radius: 4px;
670
+ }
671
+
672
+ ::-webkit-scrollbar-thumb:hover {
673
+ background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
674
+ }
675
+
676
+ /* 随机种子按钮样式 - 在Row中显示 */
677
+ #random-seed-btn {
678
+ background: var(--primary-gradient) !important;
679
+ border: none !important;
680
+ color: white !important;
681
+ font-weight: 600 !important;
682
+ font-size: 1.2rem !important;
683
+ padding: 0.5rem !important;
684
+ border-radius: var(--border-radius-small) !important;
685
+ transition: var(--transition) !important;
686
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
687
+ min-width: 50px !important;
688
+ height: 40px !important;
689
+ margin-top: 20px !important; /* 与输入框对齐 */
690
+ }
691
+
692
+ #random-seed-btn:hover {
693
+ transform: translateY(-2px) !important;
694
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
695
+ }
696
+
697
+ #random-seed-btn:active {
698
+ transform: translateY(0) !important;
699
+ }