feat: 集成 AI 视频生成功能到 MixVideo V2
🎬 主要功能: - ✅ 完整的 AI 视频生成模块 (Python) - ✅ 图片转视频 API 集成 (字节跳动 Seedance) - ✅ 云存储支持 (腾讯云 COS) - ✅ 单张图片和批量处理模式 - ✅ 现代化 React 界面组件 - ✅ Tauri 桥接通信 🛠️ 技术实现: - Python 模块:VideoGenerator, CloudStorage, APIClient - Rust 命令:generate_ai_video, batch_generate_ai_videos - React 组件:AIVideoGenerator, AIVideoPage - 状态管理:useAIVideoStore (Zustand) - 路由集成:/ai-video 页面 �� 新增文件: - python_core/ai_video/ - AI 视频生成核心模块 - src/components/AIVideoGenerator.tsx - 主要 UI 组件 - src/pages/AIVideoPage.tsx - AI 视频生成页面 - src/stores/useAIVideoStore.ts - 状态管理 🎯 功能特性: - 支持 Lite (720p) 和 Pro (1080p) 模型 - 可配置视频时长 (5秒/10秒) - 实时进度跟踪和任务管理 - 批量处理多张图片 - 云存储自动上传下载 - 错误处理和重试机制 🔗 界面集成: - 侧边栏导航添加 'AI 视频' 入口 - 首页快速操作卡片 - 完整的用户引导和帮助文档 这是从原始 Tkinter GUI 到现代 Web 应用的完整迁移!
This commit is contained in:
258
python_core/ai_video/api_client.py
Normal file
258
python_core/ai_video/api_client.py
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
API Client Module
|
||||
API 客户端模块
|
||||
|
||||
Handles communication with AI video generation APIs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
from utils import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
class APIClient:
|
||||
"""Client for AI video generation API."""
|
||||
|
||||
def __init__(self, api_key: str = None, base_url: str = None):
|
||||
"""
|
||||
Initialize API client.
|
||||
|
||||
Args:
|
||||
api_key: API key for authentication
|
||||
base_url: Base URL for API endpoints
|
||||
"""
|
||||
self.api_key = api_key or os.getenv('AI_VIDEO_API_KEY', '21575c22-14aa-40ca-8aa8-f00ca27a3a17')
|
||||
self.base_url = base_url or os.getenv('AI_VIDEO_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3')
|
||||
|
||||
# Model configurations
|
||||
self.models = {
|
||||
'lite': {
|
||||
'name': 'doubao-seedance-1-0-lite-i2v-250428',
|
||||
'resolution': '720p'
|
||||
},
|
||||
'pro': {
|
||||
'name': 'doubao-seedance-1-0-pro-250528',
|
||||
'resolution': '1080p'
|
||||
}
|
||||
}
|
||||
|
||||
def submit_task(self, prompt: str, img_url: str, duration: str = '5', model_type: str = 'lite') -> Dict[str, Any]:
|
||||
"""
|
||||
Submit video generation task.
|
||||
|
||||
Args:
|
||||
prompt: Text prompt for video generation
|
||||
img_url: URL of the input image
|
||||
duration: Video duration ('5' or '10')
|
||||
model_type: Model type ('lite' or 'pro')
|
||||
|
||||
Returns:
|
||||
Dictionary with task submission result
|
||||
"""
|
||||
result = {'status': False, 'data': None, 'msg': ''}
|
||||
|
||||
if duration not in ('5', '10'):
|
||||
result['msg'] = 'Duration must be either 5 or 10'
|
||||
return result
|
||||
|
||||
if model_type not in self.models:
|
||||
result['msg'] = f'Model type must be one of: {list(self.models.keys())}'
|
||||
return result
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
model_config = self.models[model_type]
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
}
|
||||
|
||||
json_data = {
|
||||
'model': model_config['name'],
|
||||
'content': [
|
||||
{
|
||||
'type': 'text',
|
||||
'text': f'{prompt} --resolution {model_config["resolution"]} --dur {duration} --camerafixed false',
|
||||
},
|
||||
{
|
||||
'type': 'image_url',
|
||||
'image_url': {
|
||||
'url': img_url,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f'{self.base_url}/contents/generations/tasks',
|
||||
headers=headers,
|
||||
json=json_data,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Check HTTP status code
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API request failed, status code: {response.status_code}, response: {response.text}"
|
||||
logger.error(error_msg)
|
||||
result['msg'] = error_msg
|
||||
return result
|
||||
|
||||
resp_json = response.json()
|
||||
|
||||
# Check if response contains id field
|
||||
if 'id' not in resp_json:
|
||||
error_msg = f"API response missing id field, response: {resp_json}"
|
||||
logger.error(error_msg)
|
||||
result['msg'] = error_msg
|
||||
return result
|
||||
|
||||
job_id = resp_json['id']
|
||||
result['status'] = True
|
||||
result['data'] = job_id
|
||||
result['msg'] = 'Task submitted successfully'
|
||||
|
||||
logger.info(f"Task submitted successfully, job ID: {job_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to submit task: {str(e)}")
|
||||
result['msg'] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def query_task_status(self, job_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Query task status.
|
||||
|
||||
Args:
|
||||
job_id: Task ID to query
|
||||
|
||||
Returns:
|
||||
Dictionary with task status
|
||||
"""
|
||||
result = {'status': False, 'data': None, 'msg': ''}
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
f'{self.base_url}/contents/generations/tasks/{job_id}',
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
result['msg'] = f"API request failed, status code: {response.status_code}"
|
||||
return result
|
||||
|
||||
resp_json = response.json()
|
||||
|
||||
# Parse response
|
||||
task_status = resp_json.get('status', 'unknown')
|
||||
result['msg'] = task_status
|
||||
|
||||
if task_status == 'succeeded':
|
||||
result['status'] = True
|
||||
result['data'] = resp_json.get('content', {}).get('video_url')
|
||||
elif task_status in ['failed', 'cancelled']:
|
||||
result['status'] = False
|
||||
result['data'] = None
|
||||
else:
|
||||
# Still running, pending, or queued
|
||||
result['status'] = False
|
||||
result['data'] = None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to query task status: {str(e)}")
|
||||
result['msg'] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def wait_for_completion(self, job_id: str, timeout: int = 180, interval: int = 2,
|
||||
progress_callback: Optional[Callable[[str], None]] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Wait for task completion with progress updates.
|
||||
|
||||
Args:
|
||||
job_id: Task ID to wait for
|
||||
timeout: Maximum wait time in seconds
|
||||
interval: Check interval in seconds
|
||||
progress_callback: Optional callback for progress updates
|
||||
|
||||
Returns:
|
||||
Dictionary with final result
|
||||
"""
|
||||
result = {'status': False, 'data': None, 'msg': ''}
|
||||
|
||||
end_time = time.time() + timeout
|
||||
wait_count = 0
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(f"开始查询任务状态,任务ID: {job_id}")
|
||||
|
||||
while time.time() < end_time:
|
||||
status_result = self.query_task_status(job_id)
|
||||
|
||||
if status_result['status']:
|
||||
# Task completed successfully
|
||||
result['status'] = True
|
||||
result['data'] = status_result['data']
|
||||
result['msg'] = 'succeeded'
|
||||
if progress_callback:
|
||||
progress_callback("✓ 视频生成完成!")
|
||||
break
|
||||
|
||||
elif status_result['msg'] == 'running':
|
||||
wait_count += 1
|
||||
elapsed = wait_count * interval
|
||||
remaining = max(0, timeout - elapsed)
|
||||
progress_msg = f"⏳ 任务运行中,已等待{elapsed}秒,预计剩余{remaining}秒..."
|
||||
logger.info(progress_msg)
|
||||
if progress_callback:
|
||||
progress_callback(progress_msg)
|
||||
time.sleep(interval)
|
||||
|
||||
elif status_result['msg'] == 'failed':
|
||||
result['msg'] = '任务执行失败'
|
||||
if progress_callback:
|
||||
progress_callback("✗ 任务执行失败")
|
||||
break
|
||||
|
||||
elif status_result['msg'] in ['pending', 'queued']:
|
||||
wait_count += 1
|
||||
elapsed = wait_count * interval
|
||||
remaining = max(0, timeout - elapsed)
|
||||
progress_msg = f"⏳ 任务排队中,已等待{elapsed}秒,预计剩余{remaining}秒..."
|
||||
logger.info(progress_msg)
|
||||
if progress_callback:
|
||||
progress_callback(progress_msg)
|
||||
time.sleep(interval)
|
||||
|
||||
else:
|
||||
# Unknown status, continue waiting
|
||||
wait_count += 1
|
||||
logger.info(f"未知状态: {status_result['msg']},继续等待...")
|
||||
if progress_callback:
|
||||
progress_callback(f"❔ 状态: {status_result['msg']},继续等待...")
|
||||
time.sleep(interval)
|
||||
|
||||
if not result['status'] and result['msg'] == '':
|
||||
result['msg'] = '任务超时'
|
||||
if progress_callback:
|
||||
progress_callback(f"⏰ 任务查询超时({timeout}秒)")
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user