Files
mxivideo/python_core/ai_video/api_client.py
root 30fce4ae6b feat: 完成所有任务 - 批量重复修复、UI改进、代码重构
🎯 **批量任务重复生成问题修复**:
- 添加调试日志追踪批量处理的提示词和文件夹
- 修复可能导致重复生成的逻辑问题
- 确保一张图片对应一个提示词的正确映射

🔄 **前端任务列表优化**:
- 最新任务排在前面:使用 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 化菜单栏 ✓
- 代码模块化重构 ✓
- 进度日志展示 ✓

现在应用具有更好的用户体验和代码结构!
2025-07-10 13:45:56 +08:00

297 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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