feat: 增强 AI 视频生成错误诊断和调试功能
🔍 问题分析: - 原始错误信息过于简单 (Unknown error) - 缺乏详细的错误追踪和调试信息 - Python 模块相对导入问题 🛠️ 解决方案: 1. 全面错误处理增强: - Python 模块:添加详细错误类型、消息和堆栈跟踪 - Rust 命令:增加执行日志和错误详情 - 前端服务:完整的错误信息传递和显示 2. 环境诊断工具: - 新增 test_ai_video_environment 命令 - 检查 Python 版本、模块导入、依赖包 - 前端环境测试按钮和结果显示 3. 导入问题修复: - 修复相对导入在直接运行时的问题 - 添加 fallback 机制支持独立脚本运行 - 统一模块导入逻辑 4. 调试功能: - 详细的执行日志输出 - 错误分类和追踪 - 前端控制台错误详情 ✅ 测试结果: - Python 模块可正常导入 ✓ - 脚本可独立运行 ✓ - 云存储上传成功 ✓ - API 任务提交成功 ✓ - 错误信息详细可追踪 ✓ 现在可以精确定位 AI 视频生成过程中的具体问题!
This commit is contained in:
907
gemini.md
Normal file
907
gemini.md
Normal file
@@ -0,0 +1,907 @@
|
|||||||
|
"""
|
||||||
|
Gemini AI服务
|
||||||
|
|
||||||
|
集成Google Gemini API,提供视频内容分析和分类功能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
from typing import Dict, List, Any, Optional, Union, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
import google.generativeai as genai
|
||||||
|
from google.generativeai.types import HarmCategory, HarmBlockThreshold
|
||||||
|
GEMINI_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
GEMINI_AVAILABLE = False
|
||||||
|
genai = None
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from src.core.di import Injectable, Inject, Service
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GeminiConfig:
|
||||||
|
"""Gemini配置类 - 参考demo.py实现"""
|
||||||
|
# 认证配置
|
||||||
|
cloudflare_project_id: str = ""
|
||||||
|
cloudflare_gateway_id: str = ""
|
||||||
|
google_project_id: str = ""
|
||||||
|
regions: List[str] = None
|
||||||
|
access_token: str = ""
|
||||||
|
|
||||||
|
# API配置
|
||||||
|
model_name: str = "gemini-2.5-flash"
|
||||||
|
base_url: str = "https://bowongai-dev--bowong-ai-video-gemini-fastapi-webapp.modal.run"
|
||||||
|
bearer_token: str = "bowong7777"
|
||||||
|
timeout: int = 120
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
enable_cache: bool = True
|
||||||
|
cache_dir: str = ".cache/gemini_analysis"
|
||||||
|
cache_expiry: int = 7 * 24 * 3600 # 7天
|
||||||
|
|
||||||
|
# 上传缓存配置
|
||||||
|
enable_upload_cache: bool = True
|
||||||
|
upload_cache_dir: str = ".cache/gemini_uploads"
|
||||||
|
upload_cache_expiry: int = 24 * 3600 # 1天
|
||||||
|
|
||||||
|
# 重试配置
|
||||||
|
max_retries: int = 3
|
||||||
|
retry_delay: int = 5
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.regions is None:
|
||||||
|
self.regions = ["us-central1", "us-east1", "europe-west1"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AnalysisProgress:
|
||||||
|
"""分析进度"""
|
||||||
|
step: str
|
||||||
|
progress: int # 0-100
|
||||||
|
description: str = ""
|
||||||
|
current_file: str = ""
|
||||||
|
stage: str = "upload" # upload, analysis, complete
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CacheEntry:
|
||||||
|
"""缓存条目"""
|
||||||
|
video_path: str
|
||||||
|
file_uri: str
|
||||||
|
prompt: str
|
||||||
|
result: Dict[str, Any]
|
||||||
|
timestamp: float
|
||||||
|
checksum: str
|
||||||
|
model_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UploadCacheEntry:
|
||||||
|
"""上传缓存条目"""
|
||||||
|
video_path: str
|
||||||
|
file_uri: str
|
||||||
|
timestamp: float
|
||||||
|
checksum: str
|
||||||
|
file_size: int
|
||||||
|
|
||||||
|
|
||||||
|
@Service("gemini_service")
|
||||||
|
class GeminiService:
|
||||||
|
"""
|
||||||
|
Gemini AI服务
|
||||||
|
|
||||||
|
提供基于Google Gemini API的视频内容分析和分类功能。
|
||||||
|
支持两种模式:
|
||||||
|
1. 传统模式:使用google-generativeai库
|
||||||
|
2. 新模式:使用Cloudflare Gateway + Vertex AI (参考demo.py)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
config: Dict[str, Any] = Inject("config"),
|
||||||
|
logger: logging.Logger = Inject("logger")):
|
||||||
|
self.config = config
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
# 传统Gemini配置
|
||||||
|
self.gemini_config = config.get("gemini", {})
|
||||||
|
self.api_key = self.gemini_config.get("api_key", "")
|
||||||
|
self.model_name = self.gemini_config.get("model", "gemini-2.5-flash")
|
||||||
|
self.max_tokens = self.gemini_config.get("max_tokens", 1000)
|
||||||
|
self.temperature = self.gemini_config.get("temperature", 0.1)
|
||||||
|
|
||||||
|
# 新模式配置 - 参考demo.py
|
||||||
|
self.new_mode_config = GeminiConfig(
|
||||||
|
cloudflare_project_id=self.gemini_config.get("cloudflare_project_id", ""),
|
||||||
|
cloudflare_gateway_id=self.gemini_config.get("cloudflare_gateway_id", ""),
|
||||||
|
google_project_id=self.gemini_config.get("google_project_id", ""),
|
||||||
|
regions=self.gemini_config.get("regions", ["us-central1", "us-east1", "europe-west1"]),
|
||||||
|
model_name=self.model_name,
|
||||||
|
base_url=self.gemini_config.get("base_url", "https://bowongai-dev--bowong-ai-video-gemini-fastapi-webapp.modal.run"),
|
||||||
|
bearer_token=self.gemini_config.get("bearer_token", "bowong7777"),
|
||||||
|
timeout=self.gemini_config.get("timeout", 120),
|
||||||
|
enable_cache=self.gemini_config.get("enable_cache", True),
|
||||||
|
cache_dir=self.gemini_config.get("cache_dir", ".cache/gemini_analysis"),
|
||||||
|
enable_upload_cache=self.gemini_config.get("enable_upload_cache", True),
|
||||||
|
upload_cache_dir=self.gemini_config.get("upload_cache_dir", ".cache/gemini_uploads"),
|
||||||
|
upload_cache_expiry=self.gemini_config.get("upload_cache_expiry", 86400),
|
||||||
|
max_retries=self.gemini_config.get("max_retries", 3),
|
||||||
|
retry_delay=self.gemini_config.get("retry_delay", 5)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查是否启用新模式
|
||||||
|
self.use_new_mode = self.gemini_config.get("use_new_mode", False)
|
||||||
|
|
||||||
|
# 缓存相关
|
||||||
|
self._access_token = None
|
||||||
|
self._token_expires_at = None
|
||||||
|
|
||||||
|
# 确保缓存目录存在
|
||||||
|
if self.new_mode_config.enable_cache:
|
||||||
|
os.makedirs(self.new_mode_config.cache_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 确保上传缓存目录存在
|
||||||
|
if self.new_mode_config.enable_upload_cache:
|
||||||
|
os.makedirs(self.new_mode_config.upload_cache_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 速率限制配置(仅传统模式使用)
|
||||||
|
self.rate_limit_config = self.gemini_config.get("rate_limit", {})
|
||||||
|
self.requests_per_minute = self.rate_limit_config.get("requests_per_minute", 60)
|
||||||
|
self.requests_per_day = self.rate_limit_config.get("requests_per_day", 1500)
|
||||||
|
|
||||||
|
# 请求历史记录(用于速率限制)
|
||||||
|
self.request_history = []
|
||||||
|
|
||||||
|
# 初始化客户端(传统模式)
|
||||||
|
self.model = None
|
||||||
|
if not self.use_new_mode:
|
||||||
|
self._initialize_client()
|
||||||
|
|
||||||
|
def _initialize_client(self) -> None:
|
||||||
|
"""初始化Gemini客户端"""
|
||||||
|
if not GEMINI_AVAILABLE:
|
||||||
|
self.logger.error("Google Generative AI库未安装,请运行: pip install google-generativeai")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
self.logger.warning("未配置Gemini API密钥,AI分类功能将不可用")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 配置API密钥
|
||||||
|
|
||||||
|
# 创建模型实例
|
||||||
|
generation_config = {
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 64,
|
||||||
|
"max_output_tokens": self.max_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
safety_settings = {
|
||||||
|
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
||||||
|
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
||||||
|
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
||||||
|
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.logger.info(f"Gemini客户端初始化成功,模型: {self.model_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Gemini客户端初始化失败: {e}")
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
"""检查Gemini服务是否可用"""
|
||||||
|
if self.use_new_mode:
|
||||||
|
# 新模式:检查配置是否完整
|
||||||
|
return (self.new_mode_config.cloudflare_project_id and
|
||||||
|
self.new_mode_config.cloudflare_gateway_id and
|
||||||
|
self.new_mode_config.google_project_id and
|
||||||
|
self.new_mode_config.bearer_token)
|
||||||
|
else:
|
||||||
|
# 传统模式
|
||||||
|
return GEMINI_AVAILABLE and self.model is not None
|
||||||
|
|
||||||
|
async def get_access_token(self) -> str:
|
||||||
|
"""
|
||||||
|
获取Google访问令牌,参考demo.py实现
|
||||||
|
"""
|
||||||
|
# 检查缓存的令牌是否仍然有效
|
||||||
|
if (self._access_token and self._token_expires_at and
|
||||||
|
time.time() < self._token_expires_at - 300): # 提前5分钟刷新
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.new_mode_config.bearer_token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
f"{self.new_mode_config.base_url}/google/access-token",
|
||||||
|
headers=headers,
|
||||||
|
timeout=self.new_mode_config.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(f"获取访问令牌失败: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
token_data = response.json()
|
||||||
|
self._access_token = token_data["access_token"]
|
||||||
|
self._token_expires_at = time.time() + token_data.get("expires_in", 3600)
|
||||||
|
|
||||||
|
self.logger.info("✅ 成功获取Google访问令牌")
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ 获取访问令牌失败: {e}")
|
||||||
|
raise Exception(f"获取访问令牌失败: {str(e)}")
|
||||||
|
|
||||||
|
def _create_gemini_client(self, access_token: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
创建Gemini客户端配置,参考demo.py实现
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
# 随机选择区域
|
||||||
|
region = random.choice(self.new_mode_config.regions)
|
||||||
|
|
||||||
|
gateway_url = (
|
||||||
|
f"https://gateway.ai.cloudflare.com/v1/"
|
||||||
|
f"{self.new_mode_config.cloudflare_project_id}/"
|
||||||
|
f"{self.new_mode_config.cloudflare_gateway_id}/"
|
||||||
|
f"google-vertex-ai/v1/projects/"
|
||||||
|
f"{self.new_mode_config.google_project_id}/"
|
||||||
|
f"locations/{region}/publishers/google/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"gateway_url": gateway_url,
|
||||||
|
"access_token": access_token,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _check_rate_limit(self) -> bool:
|
||||||
|
"""检查速率限制"""
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# 清理过期的请求记录
|
||||||
|
self.request_history = [
|
||||||
|
req_time for req_time in self.request_history
|
||||||
|
if current_time - req_time < 86400 # 24小时
|
||||||
|
]
|
||||||
|
|
||||||
|
# 检查每日限制
|
||||||
|
if len(self.request_history) >= self.requests_per_day:
|
||||||
|
self.logger.warning("已达到Gemini API每日请求限制")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查每分钟限制
|
||||||
|
recent_requests = [
|
||||||
|
req_time for req_time in self.request_history
|
||||||
|
if current_time - req_time < 60 # 1分钟
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(recent_requests) >= self.requests_per_minute:
|
||||||
|
self.logger.warning("已达到Gemini API每分钟请求限制")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _calculate_file_checksum(self, file_path: str) -> str:
|
||||||
|
"""计算文件校验和"""
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
return hash_md5.hexdigest()
|
||||||
|
|
||||||
|
def _generate_cache_key(self, video_path: str, prompt: str, model_name: str) -> str:
|
||||||
|
"""生成缓存键"""
|
||||||
|
key_data = f"{video_path}:{prompt}:{model_name}"
|
||||||
|
return hashlib.md5(key_data.encode()).hexdigest()
|
||||||
|
|
||||||
|
def _check_analysis_cache(self, video_path: str, prompt: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""检查分析缓存"""
|
||||||
|
if not self.new_mode_config.enable_cache:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_key = self._generate_cache_key(video_path, prompt, self.new_mode_config.model_name)
|
||||||
|
cache_file = os.path.join(self.new_mode_config.cache_dir, f"{cache_key}.json")
|
||||||
|
|
||||||
|
if not os.path.exists(cache_file):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
cache_entry_data = json.load(f)
|
||||||
|
cache_entry = CacheEntry(**cache_entry_data)
|
||||||
|
|
||||||
|
# 检查缓存是否过期
|
||||||
|
if time.time() - cache_entry.timestamp > self.new_mode_config.cache_expiry:
|
||||||
|
os.unlink(cache_file)
|
||||||
|
self.logger.info(f"⏰ 缓存已过期: {Path(video_path).name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查文件是否发生变化
|
||||||
|
current_checksum = self._calculate_file_checksum(video_path)
|
||||||
|
if current_checksum != cache_entry.checksum:
|
||||||
|
os.unlink(cache_file)
|
||||||
|
self.logger.info(f"🔄 文件已变更: {Path(video_path).name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.logger.info(f"🎯 使用缓存的分析结果: {Path(video_path).name}")
|
||||||
|
return cache_entry.result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"检查分析缓存失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_analysis_cache(self, video_path: str, file_uri: str, prompt: str, result: Dict[str, Any]) -> None:
|
||||||
|
"""保存分析结果到缓存"""
|
||||||
|
if not self.new_mode_config.enable_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_key = self._generate_cache_key(video_path, prompt, self.new_mode_config.model_name)
|
||||||
|
cache_file = os.path.join(self.new_mode_config.cache_dir, f"{cache_key}.json")
|
||||||
|
|
||||||
|
checksum = self._calculate_file_checksum(video_path)
|
||||||
|
cache_entry = CacheEntry(
|
||||||
|
video_path=video_path,
|
||||||
|
file_uri=file_uri,
|
||||||
|
prompt=prompt,
|
||||||
|
result=result,
|
||||||
|
timestamp=time.time(),
|
||||||
|
checksum=checksum,
|
||||||
|
model_name=self.new_mode_config.model_name
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(asdict(cache_entry), f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
self.logger.info(f"💾 分析结果已缓存: {Path(video_path).name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"保存分析缓存失败: {e}")
|
||||||
|
|
||||||
|
def _generate_upload_cache_key(self, video_path: str) -> str:
|
||||||
|
"""生成上传缓存键"""
|
||||||
|
# 使用文件路径和修改时间生成唯一键
|
||||||
|
file_stat = os.stat(video_path)
|
||||||
|
key_data = f"{video_path}:{file_stat.st_mtime}:{file_stat.st_size}"
|
||||||
|
return hashlib.md5(key_data.encode()).hexdigest()
|
||||||
|
|
||||||
|
def _check_upload_cache(self, video_path: str) -> Optional[str]:
|
||||||
|
"""检查上传缓存"""
|
||||||
|
if not self.new_mode_config.enable_upload_cache:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_key = self._generate_upload_cache_key(video_path)
|
||||||
|
cache_file = os.path.join(self.new_mode_config.upload_cache_dir, f"{cache_key}.json")
|
||||||
|
|
||||||
|
if not os.path.exists(cache_file):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
cache_entry_data = json.load(f)
|
||||||
|
cache_entry = UploadCacheEntry(**cache_entry_data)
|
||||||
|
|
||||||
|
# 检查缓存是否过期
|
||||||
|
if time.time() - cache_entry.timestamp > self.new_mode_config.upload_cache_expiry:
|
||||||
|
os.unlink(cache_file)
|
||||||
|
self.logger.info(f"⏰ 上传缓存已过期: {Path(video_path).name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 检查文件是否发生变化
|
||||||
|
current_checksum = self._calculate_file_checksum(video_path)
|
||||||
|
current_size = os.path.getsize(video_path)
|
||||||
|
|
||||||
|
if (current_checksum != cache_entry.checksum or
|
||||||
|
current_size != cache_entry.file_size):
|
||||||
|
os.unlink(cache_file)
|
||||||
|
self.logger.info(f"🔄 文件已变更,清除上传缓存: {Path(video_path).name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.logger.info(f"🎯 使用缓存的上传URI: {Path(video_path).name} -> {cache_entry.file_uri}")
|
||||||
|
return cache_entry.file_uri
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"检查上传缓存失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _save_upload_cache(self, video_path: str, file_uri: str) -> None:
|
||||||
|
"""保存上传缓存"""
|
||||||
|
if not self.new_mode_config.enable_upload_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cache_key = self._generate_upload_cache_key(video_path)
|
||||||
|
cache_file = os.path.join(self.new_mode_config.upload_cache_dir, f"{cache_key}.json")
|
||||||
|
|
||||||
|
checksum = self._calculate_file_checksum(video_path)
|
||||||
|
file_size = os.path.getsize(video_path)
|
||||||
|
|
||||||
|
cache_entry = UploadCacheEntry(
|
||||||
|
video_path=video_path,
|
||||||
|
file_uri=file_uri,
|
||||||
|
timestamp=time.time(),
|
||||||
|
checksum=checksum,
|
||||||
|
file_size=file_size
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(asdict(cache_entry), f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
self.logger.info(f"💾 上传URI已缓存: {Path(video_path).name} -> {file_uri}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"保存上传缓存失败: {e}")
|
||||||
|
|
||||||
|
async def _upload_video_file_new_mode(self, video_path: str) -> str:
|
||||||
|
"""
|
||||||
|
上传视频文件到Gemini,参考demo.py实现
|
||||||
|
支持上传缓存,避免重复上传相同文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 检查上传缓存
|
||||||
|
cached_uri = self._check_upload_cache(video_path)
|
||||||
|
if cached_uri:
|
||||||
|
return cached_uri
|
||||||
|
|
||||||
|
# 检查文件大小
|
||||||
|
file_size = os.path.getsize(video_path)
|
||||||
|
max_size = 100 * 1024 * 1024 # 100MB限制
|
||||||
|
|
||||||
|
if file_size > max_size:
|
||||||
|
raise Exception(f"视频文件过大 ({file_size / 1024 / 1024:.1f}MB),请使用小于100MB的文件")
|
||||||
|
|
||||||
|
# 获取访问令牌
|
||||||
|
access_token = await self.get_access_token()
|
||||||
|
|
||||||
|
# 准备FormData
|
||||||
|
with open(video_path, 'rb') as f:
|
||||||
|
video_data = f.read()
|
||||||
|
|
||||||
|
# 使用新的上传API
|
||||||
|
files = {
|
||||||
|
'file': (Path(video_path).name, video_data, 'video/mp4')
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"x-google-api-key": access_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 上传到Vertex AI
|
||||||
|
upload_url = f"{self.new_mode_config.base_url}/google/vertex-ai/upload"
|
||||||
|
params = {
|
||||||
|
"bucket": "dy-media-storage",
|
||||||
|
"prefix": "video-analysis"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
upload_url,
|
||||||
|
files=files,
|
||||||
|
headers=headers,
|
||||||
|
params=params,
|
||||||
|
timeout=self.new_mode_config.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise Exception(f"文件上传失败: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
upload_result = response.json()
|
||||||
|
file_uri = upload_result.get('urn') or upload_result.get('uri')
|
||||||
|
|
||||||
|
if not file_uri:
|
||||||
|
raise Exception("上传成功但未获取到文件URI")
|
||||||
|
|
||||||
|
self.logger.info(f"✅ 视频上传成功: {Path(video_path).name} -> {file_uri}")
|
||||||
|
|
||||||
|
# 保存到上传缓存
|
||||||
|
self._save_upload_cache(video_path, file_uri)
|
||||||
|
|
||||||
|
return file_uri
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ 视频上传失败: {e}")
|
||||||
|
raise Exception(f"视频上传失败: {str(e)}")
|
||||||
|
|
||||||
|
async def _generate_content_new_mode(self, file_uri: str, prompt: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
生成内容分析,参考demo.py实现
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取访问令牌
|
||||||
|
access_token = await self.get_access_token()
|
||||||
|
|
||||||
|
# 创建客户端配置
|
||||||
|
client_config = self._create_gemini_client(access_token)
|
||||||
|
|
||||||
|
# 格式化GCS URI
|
||||||
|
formatted_uri = self._format_gcs_uri(file_uri)
|
||||||
|
|
||||||
|
# 准备请求数据,参考demo.py实现
|
||||||
|
request_data = {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"text": prompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileData": {
|
||||||
|
"mimeType": "video/mp4",
|
||||||
|
"fileUri": formatted_uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"generationConfig": {
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"topK": 32,
|
||||||
|
"topP": 1,
|
||||||
|
"maxOutputTokens": self.max_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送请求到Cloudflare Gateway
|
||||||
|
generate_url = f"{client_config['gateway_url']}/{self.new_mode_config.model_name}:generateContent"
|
||||||
|
|
||||||
|
self.logger.info(f"📤 发送 Gemini API 请求: {formatted_uri}")
|
||||||
|
|
||||||
|
# 重试机制
|
||||||
|
last_exception = None
|
||||||
|
for attempt in range(self.new_mode_config.max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
generate_url,
|
||||||
|
headers=client_config["headers"],
|
||||||
|
json=request_data,
|
||||||
|
timeout=self.new_mode_config.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if 'candidates' not in result or not result['candidates']:
|
||||||
|
raise Exception("API返回结果为空")
|
||||||
|
|
||||||
|
self.logger.info("✅ 成功获取Gemini分析结果")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
error_msg = f"API请求失败: {response.status_code} - {response.text}"
|
||||||
|
self.logger.warning(f"⚠️ 尝试 {attempt + 1}/{self.new_mode_config.max_retries}: {error_msg}")
|
||||||
|
|
||||||
|
if attempt == self.new_mode_config.max_retries - 1:
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
await asyncio.sleep(self.new_mode_config.retry_delay)
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout as e:
|
||||||
|
last_exception = e
|
||||||
|
self.logger.warning(f"⚠️ 请求超时,尝试 {attempt + 1}/{self.new_mode_config.max_retries}")
|
||||||
|
if attempt < self.new_mode_config.max_retries - 1:
|
||||||
|
await asyncio.sleep(self.new_mode_config.retry_delay)
|
||||||
|
except Exception as e:
|
||||||
|
last_exception = e
|
||||||
|
self.logger.warning(f"⚠️ 请求失败,尝试 {attempt + 1}/{self.new_mode_config.max_retries}: {e}")
|
||||||
|
if attempt < self.new_mode_config.max_retries - 1:
|
||||||
|
await asyncio.sleep(self.new_mode_config.retry_delay)
|
||||||
|
|
||||||
|
raise Exception(f"内容生成失败,已重试{self.new_mode_config.max_retries}次: {last_exception}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ 内容生成失败: {e}")
|
||||||
|
raise Exception(f"内容生成失败: {str(e)}")
|
||||||
|
|
||||||
|
def _format_gcs_uri(self, file_uri: str) -> str:
|
||||||
|
"""格式化GCS URI"""
|
||||||
|
if file_uri.startswith('gs://'):
|
||||||
|
return file_uri
|
||||||
|
elif file_uri.startswith('https://storage.googleapis.com/'):
|
||||||
|
# 转换为gs://格式
|
||||||
|
path = file_uri.replace('https://storage.googleapis.com/', '')
|
||||||
|
return f"gs://{path}"
|
||||||
|
else:
|
||||||
|
# 假设已经是正确格式
|
||||||
|
return file_uri
|
||||||
|
|
||||||
|
def _parse_analysis_result_new_mode(self, api_result: Dict[str, Any], video_path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
解析分析结果,参考demo.py实现
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 提取文本内容
|
||||||
|
candidates = api_result.get('candidates', [])
|
||||||
|
if not candidates:
|
||||||
|
raise Exception("无有效的分析结果")
|
||||||
|
|
||||||
|
content = candidates[0].get('content', {})
|
||||||
|
parts = content.get('parts', [])
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
raise Exception("分析结果为空")
|
||||||
|
|
||||||
|
analysis_text = parts[0].get('text', '')
|
||||||
|
|
||||||
|
if not analysis_text:
|
||||||
|
raise Exception("未获取到分析文本")
|
||||||
|
|
||||||
|
self.logger.info(f"✅ 成功获取响应文本,长度: {len(analysis_text)}")
|
||||||
|
|
||||||
|
# 尝试解析JSON格式的结果
|
||||||
|
analysis_data = None
|
||||||
|
try:
|
||||||
|
# 清理文本,移除可能的markdown标记
|
||||||
|
cleaned_text = analysis_text.strip()
|
||||||
|
if cleaned_text.startswith('```json'):
|
||||||
|
cleaned_text = cleaned_text[7:]
|
||||||
|
if cleaned_text.endswith('```'):
|
||||||
|
cleaned_text = cleaned_text[:-3]
|
||||||
|
cleaned_text = cleaned_text.strip()
|
||||||
|
|
||||||
|
# 直接尝试解析JSON
|
||||||
|
if cleaned_text.startswith('{') or cleaned_text.startswith('['):
|
||||||
|
analysis_data = json.loads(cleaned_text)
|
||||||
|
self.logger.info("✅ 成功解析JSON格式的分析结果")
|
||||||
|
else:
|
||||||
|
# 使用正则表达式提取JSON部分
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'\{.*\}', cleaned_text, re.DOTALL)
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group()
|
||||||
|
analysis_data = json.loads(json_str)
|
||||||
|
self.logger.info("✅ 成功解析JSON格式的分析结果")
|
||||||
|
else:
|
||||||
|
raise json.JSONDecodeError("No JSON found", "", 0)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# JSON解析失败,使用文本格式
|
||||||
|
self.logger.info("📝 使用文本格式的分析结果")
|
||||||
|
analysis_data = {
|
||||||
|
"content_analysis": {
|
||||||
|
"summary": analysis_text[:500] + "..." if len(analysis_text) > 500 else analysis_text,
|
||||||
|
"full_text": analysis_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取新增字段
|
||||||
|
"""
|
||||||
|
{{
|
||||||
|
"category": "分类结果",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"reasoning": "详细的分类理由,包括商品匹配情况和内容特征",
|
||||||
|
"features": ["观察到的关键特征1", "关键特征2", "关键特征3"],
|
||||||
|
"product_match": true/false,
|
||||||
|
"quality_score": 0.9
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
product_match = analysis_data.get("product_match", True) # 默认为True保持兼容性
|
||||||
|
quality_score = analysis_data.get("quality_score", 1.0) # 默认为1.0保持兼容性
|
||||||
|
|
||||||
|
# 如果商品不匹配或质量太低,强制分类为废弃素材
|
||||||
|
category = analysis_data.get("category", "unclassified")
|
||||||
|
confidence = analysis_data.get("confidence", 0.8)
|
||||||
|
|
||||||
|
if not product_match or quality_score < 0.5:
|
||||||
|
category = "废弃素材"
|
||||||
|
confidence = max(confidence, 0.8) # 提高废弃素材的置信度
|
||||||
|
self.logger.info(f"商品不匹配或质量不合格,分类为废弃素材: product_match={product_match}, quality_score={quality_score}")
|
||||||
|
|
||||||
|
# 构建标准化结果格式
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"category": category,
|
||||||
|
"confidence": confidence,
|
||||||
|
"reasoning": analysis_data.get("reasoning", "AI分析结果"),
|
||||||
|
"features": analysis_data.get("features", []),
|
||||||
|
"product_match": product_match,
|
||||||
|
"quality_score": quality_score,
|
||||||
|
"video_info": {
|
||||||
|
"file_name": Path(video_path).name,
|
||||||
|
"file_path": str(video_path),
|
||||||
|
"file_size": os.path.getsize(video_path),
|
||||||
|
"analysis_time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"model_used": self.new_mode_config.model_name
|
||||||
|
},
|
||||||
|
"analysis_result": analysis_data,
|
||||||
|
"metadata": {
|
||||||
|
"response_length": len(analysis_text),
|
||||||
|
"candidates_count": len(candidates),
|
||||||
|
"mode": "new_mode"
|
||||||
|
},
|
||||||
|
"raw_response": analysis_text
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ 结果解析失败: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"category": "unclassified",
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_request(self, prompt: str, images: List[Image.Image] = None) -> Optional[str]:
|
||||||
|
"""发送请求到Gemini API"""
|
||||||
|
if not self.is_available():
|
||||||
|
raise RuntimeError("Gemini服务不可用")
|
||||||
|
|
||||||
|
# 检查速率限制
|
||||||
|
if not await self._check_rate_limit():
|
||||||
|
raise RuntimeError("已达到API速率限制")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 准备输入内容
|
||||||
|
content = [prompt]
|
||||||
|
|
||||||
|
if images:
|
||||||
|
for image in images:
|
||||||
|
content.append(image)
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = await asyncio.to_thread(
|
||||||
|
self.model.generate_content,
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
# 记录请求时间
|
||||||
|
self.request_history.append(time.time())
|
||||||
|
|
||||||
|
# 检查响应
|
||||||
|
if response.candidates and len(response.candidates) > 0:
|
||||||
|
candidate = response.candidates[0]
|
||||||
|
if hasattr(candidate, 'content') and candidate.content.parts:
|
||||||
|
return candidate.content.parts[0].text
|
||||||
|
|
||||||
|
self.logger.warning("Gemini API返回空响应")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Gemini API请求失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def analyze_video(self, video_path: str, prompt: str = None, product_title: str = "", **kwargs) -> Dict[str, Any]:
|
||||||
|
return await self.analyze_video_content(video_path, product_title=product_title, **kwargs)
|
||||||
|
|
||||||
|
async def analyze_video_content(self, video_path: str, product_title: str = "", **kwargs) -> Dict[str, Any]:
|
||||||
|
"""分析视频内容"""
|
||||||
|
try:
|
||||||
|
return await self._analyze_video_new_mode(video_path, product_title=product_title, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"视频分析失败: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"category": "unclassified",
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _analyze_video_new_mode(self, video_path: str, product_title: str = "", **kwargs) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
# 构建分析提示词
|
||||||
|
prompt = ""
|
||||||
|
|
||||||
|
# 检查缓存
|
||||||
|
cached_result = self._check_analysis_cache(video_path, prompt)
|
||||||
|
if cached_result:
|
||||||
|
return cached_result
|
||||||
|
|
||||||
|
# 上传视频文件
|
||||||
|
file_uri = await self._upload_video_file_new_mode(video_path)
|
||||||
|
|
||||||
|
# 发送分析请求
|
||||||
|
result = await self._generate_content_new_mode(file_uri, prompt)
|
||||||
|
|
||||||
|
# 解析结果
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"category": category,
|
||||||
|
"confidence": confidence,
|
||||||
|
"reasoning": analysis_data.get("reasoning", "AI分析结果"),
|
||||||
|
"features": analysis_data.get("features", []),
|
||||||
|
"product_match": product_match,
|
||||||
|
"quality_score": quality_score,
|
||||||
|
"video_info": {
|
||||||
|
"file_name": Path(video_path).name,
|
||||||
|
"file_path": str(video_path),
|
||||||
|
"file_size": os.path.getsize(video_path),
|
||||||
|
"analysis_time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"model_used": self.new_mode_config.model_name
|
||||||
|
},
|
||||||
|
"analysis_result": analysis_data,
|
||||||
|
"metadata": {
|
||||||
|
"response_length": len(analysis_text),
|
||||||
|
"candidates_count": len(candidates),
|
||||||
|
"mode": "new_mode"
|
||||||
|
},
|
||||||
|
"raw_response": analysis_text
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
parsed_result = self._parse_analysis_result_new_mode(result, video_path)
|
||||||
|
|
||||||
|
# 保存到缓存
|
||||||
|
self._save_analysis_cache(video_path, file_uri, prompt, parsed_result)
|
||||||
|
|
||||||
|
return parsed_result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"新模式视频分析失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def batch_analyze_videos(self, video_paths: List[str], **kwargs) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""批量分析视频"""
|
||||||
|
self.logger.info(f"开始批量分析 {len(video_paths)} 个视频")
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# 控制并发数以避免API限制
|
||||||
|
max_concurrent = kwargs.get("max_concurrent", 3)
|
||||||
|
semaphore = asyncio.Semaphore(max_concurrent)
|
||||||
|
|
||||||
|
async def analyze_single(video_path: str):
|
||||||
|
async with semaphore:
|
||||||
|
try:
|
||||||
|
result = await self.analyze_video_content(video_path, **kwargs)
|
||||||
|
return video_path, result
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"分析视频失败 {video_path}: {e}")
|
||||||
|
return video_path, {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"category": "unclassified",
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建并发任务
|
||||||
|
tasks = [analyze_single(video_path) for video_path in video_paths]
|
||||||
|
|
||||||
|
# 等待所有任务完成
|
||||||
|
completed_tasks = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
for task_result in completed_tasks:
|
||||||
|
if isinstance(task_result, Exception):
|
||||||
|
self.logger.error(f"批量分析任务失败: {task_result}")
|
||||||
|
else:
|
||||||
|
video_path, result = task_result
|
||||||
|
results[video_path] = result
|
||||||
|
|
||||||
|
success_count = len([r for r in results.values() if r.get("success", False)])
|
||||||
|
self.logger.info(f"批量分析完成: 成功 {success_count}/{len(video_paths)}")
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -13,8 +13,16 @@ from typing import Dict, Any, Optional, Callable
|
|||||||
import sys
|
import sys
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from config import settings
|
try:
|
||||||
from utils import setup_logger
|
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__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
@@ -124,9 +132,17 @@ class APIClient:
|
|||||||
logger.info(f"Task submitted successfully, job ID: {job_id}")
|
logger.info(f"Task submitted successfully, job ID: {job_id}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to submit task: {str(e)}")
|
import traceback
|
||||||
result['msg'] = str(e)
|
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
|
return result
|
||||||
|
|
||||||
def query_task_status(self, job_id: str) -> Dict[str, Any]:
|
def query_task_status(self, job_id: str) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -14,8 +14,16 @@ from typing import Dict, Any, Optional
|
|||||||
import sys
|
import sys
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from config import settings
|
try:
|
||||||
from utils import setup_logger
|
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__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
@@ -117,8 +125,16 @@ class CloudStorage:
|
|||||||
logger.info(f"File uploaded successfully: {url}")
|
logger.info(f"File uploaded successfully: {url}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to upload file: {str(e)}")
|
import traceback
|
||||||
result['msg'] = str(e)
|
error_details = {
|
||||||
|
'error_type': type(e).__name__,
|
||||||
|
'error_message': str(e),
|
||||||
|
'traceback': traceback.format_exc()
|
||||||
|
}
|
||||||
|
logger.error(f"Failed to upload file: {error_details['error_type']}: {error_details['error_message']}")
|
||||||
|
logger.error(f"Traceback: {error_details['traceback']}")
|
||||||
|
result['msg'] = f"Upload failed - {error_details['error_type']}: {error_details['error_message']}"
|
||||||
|
result['error_details'] = error_details
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if remove_src_file and result['status'] and os.path.exists(file_path):
|
if remove_src_file and result['status'] and os.path.exists(file_path):
|
||||||
|
|||||||
@@ -13,10 +13,24 @@ from typing import Dict, Any, List, Optional, Callable
|
|||||||
import sys
|
import sys
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from config import settings
|
try:
|
||||||
from utils import setup_logger
|
from config import settings
|
||||||
from .cloud_storage import CloudStorage
|
from utils import setup_logger
|
||||||
from .api_client import APIClient
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .cloud_storage import CloudStorage
|
||||||
|
from .api_client import APIClient
|
||||||
|
except ImportError:
|
||||||
|
# Fallback for when running as script
|
||||||
|
from cloud_storage import CloudStorage
|
||||||
|
from api_client import APIClient
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
@@ -152,9 +166,17 @@ class VideoGenerator:
|
|||||||
result['msg'] = '视频生成成功'
|
result['msg'] = '视频生成成功'
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result['msg'] = f'处理过程中发生异常: {str(e)}'
|
import traceback
|
||||||
logger.error(result['msg'])
|
error_details = {
|
||||||
|
'error_type': type(e).__name__,
|
||||||
|
'error_message': str(e),
|
||||||
|
'traceback': traceback.format_exc()
|
||||||
|
}
|
||||||
|
result['msg'] = f'处理过程中发生异常: {error_details["error_type"]}: {error_details["error_message"]}'
|
||||||
|
result['error_details'] = error_details
|
||||||
|
logger.error(f"Video generation failed: {result['msg']}")
|
||||||
|
logger.error(f"Traceback: {error_details['traceback']}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def batch_generate_videos(self,
|
def batch_generate_videos(self,
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ pub struct AudioTrack {
|
|||||||
|
|
||||||
// Helper function to execute Python commands
|
// Helper function to execute Python commands
|
||||||
async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
||||||
|
println!("Executing Python command with args: {:?}", args);
|
||||||
|
|
||||||
let mut cmd = Command::new("python3");
|
let mut cmd = Command::new("python3");
|
||||||
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
@@ -74,14 +76,32 @@ async fn execute_python_command(args: &[String]) -> Result<String, String> {
|
|||||||
|
|
||||||
let output = cmd
|
let output = cmd
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute Python script: {}", e))?;
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to execute Python script: {} (args: {:?})", e, args);
|
||||||
|
println!("Command execution failed: {}", error_msg);
|
||||||
|
error_msg
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
println!("Python script stdout: {}", stdout);
|
||||||
|
if !stderr.is_empty() {
|
||||||
|
println!("Python script stderr: {}", stderr);
|
||||||
|
}
|
||||||
|
println!("Python script exit code: {:?}", output.status.code());
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let result = String::from_utf8_lossy(&output.stdout);
|
Ok(stdout.to_string())
|
||||||
Ok(result.to_string())
|
|
||||||
} else {
|
} else {
|
||||||
let error = String::from_utf8_lossy(&output.stderr);
|
let error_msg = format!(
|
||||||
Err(format!("Python script error: {}", error))
|
"Python script failed with exit code {:?}. Stderr: {}. Stdout: {}",
|
||||||
|
output.status.code(),
|
||||||
|
stderr,
|
||||||
|
stdout
|
||||||
|
);
|
||||||
|
println!("Python script error: {}", error_msg);
|
||||||
|
Err(error_msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,3 +283,48 @@ pub async fn batch_generate_ai_videos(request: BatchAIVideoRequest) -> Result<St
|
|||||||
|
|
||||||
execute_python_command(&args).await
|
execute_python_command(&args).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn test_ai_video_environment() -> Result<String, String> {
|
||||||
|
println!("Testing AI video environment...");
|
||||||
|
|
||||||
|
// Test 1: Python availability
|
||||||
|
let python_test = Command::new("python3")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Python3 not found: {}", e))?;
|
||||||
|
|
||||||
|
if !python_test.status.success() {
|
||||||
|
return Err("Python3 is not available".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let python_version = String::from_utf8_lossy(&python_test.stdout);
|
||||||
|
println!("Python version: {}", python_version);
|
||||||
|
|
||||||
|
// Test 2: Check if AI video module can be imported
|
||||||
|
let module_test = Command::new("python3")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("import sys; sys.path.append('python_core'); from ai_video import VideoGenerator; print('AI video module imported successfully')")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to test module import: {}", e))?;
|
||||||
|
|
||||||
|
let module_output = String::from_utf8_lossy(&module_test.stdout);
|
||||||
|
let module_error = String::from_utf8_lossy(&module_test.stderr);
|
||||||
|
|
||||||
|
println!("Module test stdout: {}", module_output);
|
||||||
|
println!("Module test stderr: {}", module_error);
|
||||||
|
|
||||||
|
if !module_test.status.success() {
|
||||||
|
return Err(format!("AI video module import failed: {}", module_error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = serde_json::json!({
|
||||||
|
"status": "success",
|
||||||
|
"python_version": python_version.trim(),
|
||||||
|
"module_import": module_test.status.success(),
|
||||||
|
"module_output": module_output.trim(),
|
||||||
|
"module_error": module_error.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(result.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ pub fn run() {
|
|||||||
commands::save_project,
|
commands::save_project,
|
||||||
commands::load_project,
|
commands::load_project,
|
||||||
commands::generate_ai_video,
|
commands::generate_ai_video,
|
||||||
commands::batch_generate_ai_videos
|
commands::batch_generate_ai_videos,
|
||||||
|
commands::test_ai_video_environment
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -104,7 +104,25 @@ const AIVideoGenerator: React.FC<AIVideoGeneratorProps> = ({ className = '' }) =
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Generation failed:', error)
|
console.error('Generation failed:', error)
|
||||||
alert(`生成失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
||||||
|
let errorMessage = '未知错误'
|
||||||
|
if (error instanceof Error) {
|
||||||
|
errorMessage = error.message
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
errorMessage = error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract more details from the error
|
||||||
|
const errorDetails = {
|
||||||
|
message: errorMessage,
|
||||||
|
type: error instanceof Error ? error.name : typeof error,
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Detailed error information:', errorDetails)
|
||||||
|
|
||||||
|
alert(`生成失败: ${errorMessage}\n\n详细信息已输出到控制台,请检查开发者工具。`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Sparkles, Info, Settings, HelpCircle } from 'lucide-react'
|
import { Sparkles, Info, Settings, HelpCircle, TestTube } from 'lucide-react'
|
||||||
import AIVideoGenerator from '../components/AIVideoGenerator'
|
import AIVideoGenerator from '../components/AIVideoGenerator'
|
||||||
|
import { AIVideoService } from '../services/tauri'
|
||||||
|
|
||||||
const AIVideoPage: React.FC = () => {
|
const AIVideoPage: React.FC = () => {
|
||||||
|
const [testResult, setTestResult] = useState<any>(null)
|
||||||
|
const [isTesting, setIsTesting] = useState(false)
|
||||||
|
|
||||||
|
const handleTestEnvironment = async () => {
|
||||||
|
setIsTesting(true)
|
||||||
|
setTestResult(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await AIVideoService.testEnvironment()
|
||||||
|
setTestResult(result)
|
||||||
|
console.log('Environment test result:', result)
|
||||||
|
} catch (error) {
|
||||||
|
setTestResult({
|
||||||
|
status: 'error',
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
console.error('Environment test failed:', error)
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-secondary-50">
|
<div className="h-full flex flex-col bg-secondary-50">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -19,6 +42,14 @@ const AIVideoPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleTestEnvironment}
|
||||||
|
disabled={isTesting}
|
||||||
|
className="btn-ghost px-3 py-2 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<TestTube size={16} className="mr-2" />
|
||||||
|
{isTesting ? '测试中...' : '环境测试'}
|
||||||
|
</button>
|
||||||
<button className="btn-ghost px-3 py-2">
|
<button className="btn-ghost px-3 py-2">
|
||||||
<Settings size={16} className="mr-2" />
|
<Settings size={16} className="mr-2" />
|
||||||
设置
|
设置
|
||||||
@@ -35,6 +66,32 @@ const AIVideoPage: React.FC = () => {
|
|||||||
<div className="h-full flex">
|
<div className="h-full flex">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 p-6 overflow-y-auto">
|
<div className="flex-1 p-6 overflow-y-auto">
|
||||||
|
{/* Environment Test Result */}
|
||||||
|
{testResult && (
|
||||||
|
<div className={`mb-6 p-4 rounded-lg border max-w-4xl mx-auto ${
|
||||||
|
testResult.status === 'success'
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: 'bg-red-50 border-red-200'
|
||||||
|
}`}>
|
||||||
|
<h3 className="font-medium mb-2">
|
||||||
|
{testResult.status === 'success' ? '✅ 环境测试通过' : '❌ 环境测试失败'}
|
||||||
|
</h3>
|
||||||
|
<div className="text-sm space-y-1">
|
||||||
|
{testResult.status === 'success' ? (
|
||||||
|
<>
|
||||||
|
<p>Python 版本: {testResult.python_version}</p>
|
||||||
|
<p>模块导入: {testResult.module_import ? '✅ 成功' : '❌ 失败'}</p>
|
||||||
|
{testResult.module_error && (
|
||||||
|
<p className="text-red-600">模块错误: {testResult.module_error}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-red-600">{testResult.error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<AIVideoGenerator className="max-w-4xl mx-auto" />
|
<AIVideoGenerator className="max-w-4xl mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -382,10 +382,21 @@ export class AIVideoService {
|
|||||||
*/
|
*/
|
||||||
static async generateVideo(request: AIVideoRequest): Promise<any> {
|
static async generateVideo(request: AIVideoRequest): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
console.log('Sending AI video request:', request)
|
||||||
const result = await invoke('generate_ai_video', { request })
|
const result = await invoke('generate_ai_video', { request })
|
||||||
return JSON.parse(result as string)
|
console.log('Raw result from Tauri:', result)
|
||||||
|
|
||||||
|
const parsedResult = JSON.parse(result as string)
|
||||||
|
console.log('Parsed result:', parsedResult)
|
||||||
|
|
||||||
|
return parsedResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate AI video:', error)
|
console.error('Failed to generate AI video:', error)
|
||||||
|
console.error('Error details:', {
|
||||||
|
name: error instanceof Error ? error.name : 'Unknown',
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
})
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,4 +413,17 @@ export class AIVideoService {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test AI video environment
|
||||||
|
*/
|
||||||
|
static async testEnvironment(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const result = await invoke('test_ai_video_environment')
|
||||||
|
return JSON.parse(result as string)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to test AI video environment:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user