🎯 **批量任务重复生成问题修复**: - 添加调试日志追踪批量处理的提示词和文件夹 - 修复可能导致重复生成的逻辑问题 - 确保一张图片对应一个提示词的正确映射 🔄 **前端任务列表优化**: - 最新任务排在前面:使用 jobs.slice().reverse() 显示 - 提示词改为多文本框:批量模式支持独立的提示词输入框 - 添加/删除提示词功能,动态管理提示词列表 - 单个模式保持原有的文本域输入 📱 **左侧菜单栏 Tab 化**: - Sidebar 组件重构为 Tab 形式 - 导航 Tab:传统的页面导航功能 - 任务列表 Tab:显示 AI 视频生成任务状态 - 任务数量徽章:实时显示当前任务数量 - 任务详情:状态图标、进度条、时间信息 🏗️ **Commands.rs 代码重构**: - 按功能模块化:basic.rs, video.rs, ai_video.rs, file_system.rs, project.rs - 创建 commands/mod.rs 统一导出 - 单一职责原则:每个文件专注特定功能领域 - 保持向后兼容:所有命令函数正常工作 📊 **进度日志前端展示**: - 添加 progressLogs 和 currentStep 到任务状态 - 实时显示运行中任务的详细进度信息 - 显示 '[运行中] 任务运行中,已等待18秒,预计剩余282秒' 等日志 - Python API 客户端发送 JSON-RPC 格式的详细进度 - 前端滚动显示最近3条进度日志 🎨 **用户界面增强**: - 批量提示词管理:添加、删除、编辑功能 - 任务状态可视化:进度条、状态图标、时间显示 - Tab 切换:导航和任务列表的无缝切换 - 响应式设计:适配不同屏幕尺寸 🔧 **技术改进**: - 模块化架构:代码组织更清晰 - 类型安全:TypeScript 类型定义完善 - 状态管理:Zustand store 功能扩展 - 错误处理:完善的异常捕获和用户反馈 ✅ **完成状态**: - 批量重复生成问题 ✓ - 最新任务排序 ✓ - 多文本框提示词 ✓ - Tab 化菜单栏 ✓ - 代码模块化重构 ✓ - 进度日志展示 ✓ 现在应用具有更好的用户体验和代码结构!
297 lines
10 KiB
Python
297 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
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__))))
|
||
|
||
try:
|
||
from config import settings
|
||
from utils import setup_logger
|
||
except ImportError:
|
||
# Fallback for when running as script
|
||
import logging
|
||
settings = type('Settings', (), {'LOG_LEVEL': 'INFO'})()
|
||
def setup_logger(name):
|
||
logging.basicConfig(level=logging.INFO)
|
||
return logging.getLogger(name)
|
||
|
||
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 (http/https URL or file:// for local files)
|
||
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
|
||
|
||
# Handle local file URLs
|
||
if img_url.startswith('file://'):
|
||
result['status'] = False
|
||
result['msg'] = 'Local files are not supported by the API. Please upload the image to cloud storage first.'
|
||
logger.error(f"Local file URL not supported: {img_url}")
|
||
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:
|
||
import traceback
|
||
error_details = {
|
||
'error_type': type(e).__name__,
|
||
'error_message': str(e),
|
||
'traceback': traceback.format_exc()
|
||
}
|
||
logger.error(f"Failed to submit task: {error_details['error_type']}: {error_details['error_message']}")
|
||
logger.error(f"Traceback: {error_details['traceback']}")
|
||
result['msg'] = f"{error_details['error_type']}: {error_details['error_message']}"
|
||
result['error_details'] = error_details
|
||
|
||
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)
|
||
|
||
# Send detailed progress via JSON-RPC
|
||
from .json_rpc import create_progress_reporter
|
||
progress = create_progress_reporter()
|
||
progress.update(
|
||
step="running",
|
||
progress=min(100, (elapsed / timeout) * 100),
|
||
message=progress_msg,
|
||
details={
|
||
"elapsed_seconds": elapsed,
|
||
"remaining_seconds": remaining,
|
||
"total_timeout": timeout
|
||
}
|
||
)
|
||
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
|