fix: 修复命令行工具
This commit is contained in:
67
python_core/scene_detection/__init__.py
Normal file
67
python_core/scene_detection/__init__.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Module
|
||||
场景检测模块
|
||||
|
||||
重构后的场景检测功能,按照功能模块化组织:
|
||||
- types: 类型定义和数据模型
|
||||
- services: 核心业务逻辑服务
|
||||
- workflows: LangGraph工作流
|
||||
- utils: 工具类和辅助函数
|
||||
"""
|
||||
|
||||
# 导出主要类型
|
||||
from .types import (
|
||||
DetectorType,
|
||||
OutputFormat,
|
||||
SceneInfo,
|
||||
DetectionResult,
|
||||
SceneDetectionWorkflowState
|
||||
)
|
||||
|
||||
# 导出主要服务
|
||||
from .services import (
|
||||
SceneDetectorService,
|
||||
AIAnalysisService,
|
||||
VideoInfoService
|
||||
)
|
||||
|
||||
# 导出工作流
|
||||
from .workflows import (
|
||||
SceneDetectionWorkflowManager,
|
||||
WorkflowNodes
|
||||
)
|
||||
|
||||
# 导出工具类
|
||||
from .utils import (
|
||||
ResultSaver,
|
||||
InputValidator
|
||||
)
|
||||
|
||||
# 导出主要接口类
|
||||
from .scene_detector import SceneDetector
|
||||
|
||||
__all__ = [
|
||||
# Types
|
||||
"DetectorType",
|
||||
"OutputFormat",
|
||||
"SceneInfo",
|
||||
"DetectionResult",
|
||||
"SceneDetectionWorkflowState",
|
||||
|
||||
# Services
|
||||
"SceneDetectorService",
|
||||
"AIAnalysisService",
|
||||
"VideoInfoService",
|
||||
|
||||
# Workflows
|
||||
"SceneDetectionWorkflowManager",
|
||||
"WorkflowNodes",
|
||||
|
||||
# Utils
|
||||
"ResultSaver",
|
||||
"InputValidator",
|
||||
|
||||
# Main Interface
|
||||
"SceneDetector"
|
||||
]
|
||||
228
python_core/scene_detection/scene_detector.py
Normal file
228
python_core/scene_detection/scene_detector.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detector Main Interface
|
||||
场景检测主接口类
|
||||
|
||||
整合所有功能的主要接口类,提供简洁的API
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import asdict
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from .types import DetectorType, OutputFormat, DetectionResult
|
||||
from .services import SceneDetectorService, VideoInfoService, AIAnalysisService
|
||||
from .workflows import SceneDetectionWorkflowManager
|
||||
from .utils import ResultSaver, InputValidator
|
||||
|
||||
|
||||
class SceneDetector:
|
||||
"""场景检测器主类"""
|
||||
|
||||
def __init__(self):
|
||||
self.supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
|
||||
|
||||
# 初始化服务
|
||||
self.detector_service = SceneDetectorService()
|
||||
self.video_info_service = VideoInfoService()
|
||||
self.ai_analysis_service = AIAnalysisService()
|
||||
|
||||
# 初始化工作流管理器
|
||||
self.workflow_manager = SceneDetectionWorkflowManager()
|
||||
|
||||
# 初始化工具类
|
||||
self.result_saver = ResultSaver()
|
||||
self.input_validator = InputValidator(self.supported_formats)
|
||||
|
||||
def detect_scenes(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0) -> DetectionResult:
|
||||
"""基础场景检测方法"""
|
||||
return self.detector_service.detect_scenes(video_path, detector_type, threshold, min_scene_length)
|
||||
|
||||
def get_video_info(self, video_path: Path) -> Dict[str, Any]:
|
||||
"""获取视频信息"""
|
||||
try:
|
||||
# 验证文件
|
||||
self.video_info_service.validate_video_file(video_path, self.supported_formats)
|
||||
|
||||
# 提取信息
|
||||
video_info = self.video_info_service.extract_video_info(video_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"info": video_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def detect_with_workflow(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0,
|
||||
output_path: Optional[Path] = None, output_format: OutputFormat = OutputFormat.JSON,
|
||||
enable_ai_analysis: bool = True, request_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""使用LangGraph工作流进行场景检测"""
|
||||
return self.workflow_manager.detect_with_workflow(
|
||||
video_path, detector_type, threshold, min_scene_length,
|
||||
output_path, output_format, enable_ai_analysis, request_id
|
||||
)
|
||||
|
||||
def save_results(self, result: DetectionResult, output_path: Path,
|
||||
output_format: OutputFormat) -> None:
|
||||
"""保存检测结果"""
|
||||
self.result_saver.save_results(result, output_path, output_format)
|
||||
|
||||
def batch_detect(self, video_paths: List[Path], detector_type: DetectorType = DetectorType.CONTENT,
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0,
|
||||
output_dir: Optional[Path] = None, output_format: OutputFormat = OutputFormat.JSON) -> List[Dict[str, Any]]:
|
||||
"""批量检测场景"""
|
||||
results = []
|
||||
|
||||
for video_path in video_paths:
|
||||
try:
|
||||
logger.info(f"🎬 处理视频: {video_path.name}")
|
||||
|
||||
# 检测场景
|
||||
result = self.detect_scenes(video_path, detector_type, threshold, min_scene_length)
|
||||
|
||||
# 保存结果(如果指定了输出目录)
|
||||
if output_dir and result.success:
|
||||
output_file = output_dir / f"{video_path.stem}_scenes.{output_format.value}"
|
||||
self.save_results(result, output_file, output_format)
|
||||
|
||||
# 序列化结果
|
||||
serialized_result = {
|
||||
"video_path": str(video_path),
|
||||
"success": result.success,
|
||||
"result": asdict(result) if result.success else None,
|
||||
"error": result.error
|
||||
}
|
||||
|
||||
results.append(serialized_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 处理视频失败 {video_path.name}: {e}")
|
||||
results.append({
|
||||
"video_path": str(video_path),
|
||||
"success": False,
|
||||
"result": None,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
# JSON-RPC方法
|
||||
def jsonrpc_detect_scenes(self, video_path: str, detector_type: str = "content",
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0) -> Dict[str, Any]:
|
||||
"""JSON-RPC方法:基础场景检测"""
|
||||
try:
|
||||
result = self.detect_scenes(
|
||||
Path(video_path),
|
||||
DetectorType(detector_type),
|
||||
threshold,
|
||||
min_scene_length
|
||||
)
|
||||
|
||||
return asdict(result)
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def jsonrpc_get_video_info(self, video_path: str) -> Dict[str, Any]:
|
||||
"""JSON-RPC方法:获取视频信息"""
|
||||
try:
|
||||
return self.get_video_info(Path(video_path))
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def jsonrpc_detect_with_workflow(self, video_path: str, detector_type: str = "content",
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0,
|
||||
output_path: Optional[str] = None, output_format: str = "json",
|
||||
enable_ai_analysis: bool = True, request_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""JSON-RPC方法:工作流场景检测"""
|
||||
try:
|
||||
output_path_obj = Path(output_path) if output_path else None
|
||||
|
||||
result = self.detect_with_workflow(
|
||||
Path(video_path),
|
||||
DetectorType(detector_type),
|
||||
threshold,
|
||||
min_scene_length,
|
||||
output_path_obj,
|
||||
OutputFormat(output_format),
|
||||
enable_ai_analysis,
|
||||
request_id
|
||||
)
|
||||
|
||||
# 检查是否是JSON-RPC模式
|
||||
if result.get("jsonrpc_mode"):
|
||||
# JSON-RPC模式:结果已经通过工作流发送,返回None
|
||||
return None
|
||||
else:
|
||||
# 非JSON-RPC模式:序列化并返回结果
|
||||
serialized_result = {}
|
||||
for key, value in result.items():
|
||||
if key == "detection_result" and value:
|
||||
serialized_result[key] = asdict(value)
|
||||
else:
|
||||
serialized_result[key] = value
|
||||
return serialized_result
|
||||
|
||||
except Exception as e:
|
||||
# 如果有request_id,错误已经在detect_with_workflow中发送
|
||||
if request_id:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def jsonrpc_batch_detect(self, video_paths: List[str], detector_type: str = "content",
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0,
|
||||
output_dir: Optional[str] = None, output_format: str = "json") -> Dict[str, Any]:
|
||||
"""JSON-RPC方法:批量检测"""
|
||||
try:
|
||||
paths = [Path(p) for p in video_paths]
|
||||
output_dir_obj = Path(output_dir) if output_dir else None
|
||||
|
||||
results = self.batch_detect(
|
||||
paths,
|
||||
DetectorType(detector_type),
|
||||
threshold,
|
||||
min_scene_length,
|
||||
output_dir_obj,
|
||||
OutputFormat(output_format)
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"results": results,
|
||||
"total_videos": len(video_paths),
|
||||
"successful_detections": sum(1 for r in results if r["success"])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def register_jsonrpc_methods(self):
|
||||
"""注册JSON-RPC方法到全局注册器"""
|
||||
from python_core.utils.jsonrpc_enhanced import method_registry
|
||||
|
||||
# 直接注册方法
|
||||
method_registry.register_function(self.jsonrpc_detect_scenes, "scene.detect")
|
||||
method_registry.register_function(self.jsonrpc_detect_with_workflow, "scene.detect_workflow")
|
||||
method_registry.register_function(self.jsonrpc_get_video_info, "scene.get_video_info")
|
||||
method_registry.register_function(self.jsonrpc_batch_detect, "scene.batch_detect")
|
||||
17
python_core/scene_detection/services/__init__.py
Normal file
17
python_core/scene_detection/services/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Services
|
||||
场景检测服务层
|
||||
|
||||
导出所有服务类
|
||||
"""
|
||||
|
||||
from .detector_service import SceneDetectorService
|
||||
from .ai_analysis_service import AIAnalysisService
|
||||
from .video_info_service import VideoInfoService
|
||||
|
||||
__all__ = [
|
||||
"SceneDetectorService",
|
||||
"AIAnalysisService",
|
||||
"VideoInfoService"
|
||||
]
|
||||
100
python_core/scene_detection/services/ai_analysis_service.py
Normal file
100
python_core/scene_detection/services/ai_analysis_service.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Analysis Service
|
||||
AI分析服务
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from ..types import SceneInfo, DetectionResult
|
||||
|
||||
|
||||
class AIAnalysisService:
|
||||
"""AI分析服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.ai_enabled = False
|
||||
self.llm = None
|
||||
|
||||
# 尝试初始化AI分析器
|
||||
try:
|
||||
import os
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
# 检查是否有API密钥
|
||||
api_key = os.getenv('ANTHROPIC_API_KEY')
|
||||
if not api_key:
|
||||
logger.info("ℹ️ 未设置ANTHROPIC_API_KEY环境变量,AI分析功能已禁用")
|
||||
self.ai_enabled = False
|
||||
return
|
||||
|
||||
self.llm = ChatAnthropic(model="claude-3-5-sonnet-20241022", api_key=api_key)
|
||||
self.ai_enabled = True
|
||||
logger.info("✅ AI分析器初始化成功")
|
||||
except ImportError:
|
||||
logger.info("ℹ️ langchain_anthropic未安装,AI分析功能已禁用")
|
||||
self.ai_enabled = False
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ AI分析器初始化失败: {e}")
|
||||
self.ai_enabled = False
|
||||
|
||||
def analyze_detection_result(self, detection_result: DetectionResult,
|
||||
video_info: Dict[str, Any]) -> str:
|
||||
"""分析检测结果"""
|
||||
if not self.ai_enabled:
|
||||
return "AI分析器未启用"
|
||||
|
||||
try:
|
||||
analysis_prompt = self._create_analysis_prompt(detection_result, video_info)
|
||||
response = self.llm.invoke([{"role": "user", "content": analysis_prompt}])
|
||||
return response.content
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"AI分析失败: {e}"
|
||||
logger.warning(error_msg)
|
||||
return error_msg
|
||||
|
||||
def _create_analysis_prompt(self, result: DetectionResult, video_info: Dict[str, Any]) -> str:
|
||||
"""创建分析提示词"""
|
||||
return f"""
|
||||
请分析以下视频场景检测结果:
|
||||
|
||||
视频信息:
|
||||
- 文件: {result.filename}
|
||||
- 分辨率: {video_info.get('resolution', 'Unknown')}
|
||||
- 时长: {result.total_duration:.2f}秒
|
||||
- 帧率: {video_info.get('fps', 0):.2f}fps
|
||||
|
||||
检测结果:
|
||||
- 检测器: {result.detector_type}
|
||||
- 阈值: {result.threshold}
|
||||
- 场景数: {result.total_scenes}
|
||||
- 检测时间: {result.detection_time:.2f}秒
|
||||
|
||||
场景详情:
|
||||
{self._format_scenes_for_ai(result.scenes)}
|
||||
|
||||
请提供:
|
||||
1. 场景分布分析
|
||||
2. 检测质量评估
|
||||
3. 参数优化建议
|
||||
4. 潜在问题识别
|
||||
"""
|
||||
|
||||
def _format_scenes_for_ai(self, scenes: List[SceneInfo]) -> str:
|
||||
"""格式化场景信息供AI分析"""
|
||||
if not scenes:
|
||||
return "无场景数据"
|
||||
|
||||
formatted = []
|
||||
for i, scene in enumerate(scenes[:10]): # 只显示前10个场景
|
||||
formatted.append(
|
||||
f"场景 {i+1}: {scene.start_time:.2f}s - {scene.end_time:.2f}s "
|
||||
f"(时长: {scene.duration:.2f}s)"
|
||||
)
|
||||
|
||||
if len(scenes) > 10:
|
||||
formatted.append(f"... 还有 {len(scenes) - 10} 个场景")
|
||||
|
||||
return "\n".join(formatted)
|
||||
158
python_core/scene_detection/services/detector_service.py
Normal file
158
python_core/scene_detection/services/detector_service.py
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detector Service
|
||||
场景检测服务
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Set
|
||||
|
||||
from scenedetect import open_video, SceneManager
|
||||
from scenedetect.detectors import ContentDetector, ThresholdDetector, AdaptiveDetector
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from ..types import DetectorType, SceneInfo, DetectionResult
|
||||
|
||||
|
||||
def _timecode_to_seconds(timecode) -> float:
|
||||
"""将FrameTimecode对象转换为秒数"""
|
||||
try:
|
||||
# 新版本PySceneDetect
|
||||
if hasattr(timecode, 'total_seconds'):
|
||||
return timecode.total_seconds()
|
||||
# 旧版本PySceneDetect
|
||||
elif hasattr(timecode, 'get_seconds'):
|
||||
return timecode.get_seconds()
|
||||
# 如果是数字,直接返回
|
||||
elif isinstance(timecode, (int, float)):
|
||||
return float(timecode)
|
||||
# 尝试直接转换
|
||||
else:
|
||||
return float(timecode)
|
||||
except Exception as e:
|
||||
logger.warning(f"时间码转换失败: {e}, 使用默认值0.0")
|
||||
return 0.0
|
||||
|
||||
|
||||
class SceneDetectorService:
|
||||
"""场景检测服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.supported_formats: Set[str] = {
|
||||
'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'
|
||||
}
|
||||
|
||||
def detect_scenes(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0) -> DetectionResult:
|
||||
"""检测视频场景"""
|
||||
detection_start_time = time.time()
|
||||
|
||||
try:
|
||||
logger.info(f"🎬 开始检测: {video_path.name}")
|
||||
|
||||
# 打开视频
|
||||
video = open_video(str(video_path))
|
||||
scene_manager = SceneManager()
|
||||
|
||||
# 选择检测器
|
||||
detector = self._create_detector(detector_type, threshold)
|
||||
scene_manager.add_detector(detector)
|
||||
|
||||
logger.info(f"📊 使用{detector_type.value}检测器,阈值: {threshold}")
|
||||
|
||||
# 执行检测
|
||||
logger.info("🔍 正在分析视频帧...")
|
||||
scene_manager.detect_scenes(video, show_progress=False)
|
||||
|
||||
# 获取场景列表
|
||||
scene_list = scene_manager.get_scene_list()
|
||||
|
||||
# 过滤短场景
|
||||
if min_scene_length > 0:
|
||||
filtered_scenes = []
|
||||
for scene in scene_list:
|
||||
start_time, end_time = scene
|
||||
start_seconds = _timecode_to_seconds(start_time)
|
||||
end_seconds = _timecode_to_seconds(end_time)
|
||||
duration = end_seconds - start_seconds
|
||||
|
||||
if duration >= min_scene_length:
|
||||
filtered_scenes.append(scene)
|
||||
scene_list = filtered_scenes
|
||||
|
||||
# 转换为SceneInfo对象
|
||||
scenes = self._convert_scenes(scene_list, video.frame_rate)
|
||||
|
||||
detection_time = time.time() - detection_start_time
|
||||
total_duration = _timecode_to_seconds(video.duration)
|
||||
|
||||
logger.info(f"✅ 检测到 {len(scenes)} 个场景")
|
||||
logger.info(f"📊 视频信息: {video.frame_rate:.2f}fps, {total_duration:.2f}秒")
|
||||
logger.info(f"🎯 检测完成: {len(scenes)} 个场景,耗时 {detection_time:.2f}秒")
|
||||
|
||||
return DetectionResult(
|
||||
success=True,
|
||||
filename=video_path.name,
|
||||
detector_type=detector_type,
|
||||
threshold=threshold,
|
||||
total_scenes=len(scenes),
|
||||
total_duration=total_duration,
|
||||
detection_time=detection_time,
|
||||
scenes=scenes
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
detection_time = time.time() - detection_start_time
|
||||
error_msg = f"场景检测失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
logger.error(f"详细错误信息: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
logger.error(f"错误堆栈: {traceback.format_exc()}")
|
||||
|
||||
return DetectionResult(
|
||||
success=False,
|
||||
filename=video_path.name,
|
||||
detector_type=detector_type,
|
||||
threshold=threshold,
|
||||
total_scenes=0,
|
||||
total_duration=0,
|
||||
detection_time=detection_time,
|
||||
scenes=[],
|
||||
error=error_msg
|
||||
)
|
||||
|
||||
def _create_detector(self, detector_type: DetectorType, threshold: float):
|
||||
"""创建检测器"""
|
||||
if detector_type == DetectorType.CONTENT:
|
||||
return ContentDetector(threshold=threshold)
|
||||
elif detector_type == DetectorType.THRESHOLD:
|
||||
return ThresholdDetector(threshold=threshold)
|
||||
elif detector_type == DetectorType.ADAPTIVE:
|
||||
return AdaptiveDetector()
|
||||
else:
|
||||
raise ValueError(f"不支持的检测器类型: {detector_type}")
|
||||
|
||||
def _convert_scenes(self, scene_list, frame_rate: float) -> List[SceneInfo]:
|
||||
"""转换场景列表为SceneInfo对象"""
|
||||
scenes = []
|
||||
|
||||
for i, (start_time, end_time) in enumerate(scene_list):
|
||||
start_seconds = _timecode_to_seconds(start_time)
|
||||
end_seconds = _timecode_to_seconds(end_time)
|
||||
duration = end_seconds - start_seconds
|
||||
|
||||
start_frame = int(start_seconds * frame_rate)
|
||||
end_frame = int(end_seconds * frame_rate)
|
||||
|
||||
scene_info = SceneInfo(
|
||||
index=i,
|
||||
start_time=start_seconds,
|
||||
end_time=end_seconds,
|
||||
duration=duration,
|
||||
start_frame=start_frame,
|
||||
end_frame=end_frame
|
||||
)
|
||||
scenes.append(scene_info)
|
||||
|
||||
return scenes
|
||||
65
python_core/scene_detection/services/video_info_service.py
Normal file
65
python_core/scene_detection/services/video_info_service.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Video Info Service
|
||||
视频信息服务
|
||||
"""
|
||||
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
|
||||
|
||||
class VideoInfoService:
|
||||
"""视频信息服务"""
|
||||
|
||||
def extract_video_info(self, video_path: Path) -> Dict[str, Any]:
|
||||
"""提取视频信息"""
|
||||
try:
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
|
||||
if not cap.isOpened():
|
||||
raise Exception("无法打开视频文件")
|
||||
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
duration = frame_count / fps if fps > 0 else 0
|
||||
|
||||
cap.release()
|
||||
|
||||
# 获取文件大小
|
||||
file_size = video_path.stat().st_size
|
||||
|
||||
video_info = {
|
||||
"filename": video_path.name,
|
||||
"file_path": str(video_path),
|
||||
"file_size": file_size,
|
||||
"fps": fps,
|
||||
"frame_count": frame_count,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"duration": duration,
|
||||
"resolution": f"{width}x{height}",
|
||||
"format": video_path.suffix.lower()
|
||||
}
|
||||
|
||||
logger.info(f"📹 视频信息: {video_info['resolution']}, {fps:.2f}fps, {duration:.2f}s")
|
||||
|
||||
return video_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取视频信息失败: {e}")
|
||||
raise
|
||||
|
||||
def validate_video_file(self, video_path: Path, supported_formats: set) -> bool:
|
||||
"""验证视频文件"""
|
||||
if not video_path.exists():
|
||||
raise FileNotFoundError(f"视频文件不存在: {video_path}")
|
||||
|
||||
if video_path.suffix.lower() not in supported_formats:
|
||||
raise ValueError(f"不支持的文件格式: {video_path.suffix}")
|
||||
|
||||
return True
|
||||
19
python_core/scene_detection/types/__init__.py
Normal file
19
python_core/scene_detection/types/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Types
|
||||
场景检测类型定义
|
||||
|
||||
导出所有类型定义
|
||||
"""
|
||||
|
||||
from .enums import DetectorType, OutputFormat
|
||||
from .models import SceneInfo, DetectionResult
|
||||
from .workflow_state import SceneDetectionWorkflowState
|
||||
|
||||
__all__ = [
|
||||
"DetectorType",
|
||||
"OutputFormat",
|
||||
"SceneInfo",
|
||||
"DetectionResult",
|
||||
"SceneDetectionWorkflowState"
|
||||
]
|
||||
21
python_core/scene_detection/types/enums.py
Normal file
21
python_core/scene_detection/types/enums.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Enums
|
||||
场景检测枚举类型
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DetectorType(str, Enum):
|
||||
"""检测器类型"""
|
||||
CONTENT = "content"
|
||||
THRESHOLD = "threshold"
|
||||
ADAPTIVE = "adaptive"
|
||||
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
"""输出格式"""
|
||||
JSON = "json"
|
||||
CSV = "csv"
|
||||
TXT = "txt"
|
||||
34
python_core/scene_detection/types/models.py
Normal file
34
python_core/scene_detection/types/models.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Models
|
||||
场景检测数据模型
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from .enums import DetectorType
|
||||
|
||||
|
||||
@dataclass
|
||||
class SceneInfo:
|
||||
"""场景信息"""
|
||||
index: int
|
||||
start_time: float
|
||||
end_time: float
|
||||
duration: float
|
||||
start_frame: int = 0
|
||||
end_frame: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectionResult:
|
||||
"""检测结果"""
|
||||
success: bool
|
||||
filename: str
|
||||
detector_type: DetectorType
|
||||
threshold: float
|
||||
total_scenes: int
|
||||
total_duration: float
|
||||
detection_time: float
|
||||
scenes: List[SceneInfo]
|
||||
error: Optional[str] = None
|
||||
82
python_core/scene_detection/types/workflow_state.py
Normal file
82
python_core/scene_detection/types/workflow_state.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow State
|
||||
工作流状态定义
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .models import SceneInfo, DetectionResult
|
||||
from python_core.utils.jsonrpc_enhanced import EnhancedJSONRPCResponse, ProgressLevel
|
||||
|
||||
|
||||
@dataclass
|
||||
class SceneDetectionWorkflowState:
|
||||
"""场景检测工作流状态"""
|
||||
# 输入参数
|
||||
video_path: str = ""
|
||||
detector_type: str = "content"
|
||||
threshold: float = 30.0
|
||||
min_scene_length: float = 1.0
|
||||
output_path: Optional[str] = None
|
||||
output_format: str = "json"
|
||||
enable_ai_analysis: bool = True
|
||||
|
||||
# 工作流状态
|
||||
current_stage: str = "init"
|
||||
progress: int = 0
|
||||
total_steps: int = 5
|
||||
|
||||
# JSON-RPC支持
|
||||
request_id: Optional[str] = None
|
||||
enable_jsonrpc: bool = False
|
||||
|
||||
# 中间结果
|
||||
video_info: Dict[str, Any] = None
|
||||
raw_scenes: List[Any] = None
|
||||
processed_scenes: List[SceneInfo] = None
|
||||
|
||||
# 最终结果
|
||||
detection_result: Optional[DetectionResult] = None
|
||||
ai_analysis: Optional[str] = None
|
||||
|
||||
# 错误处理
|
||||
errors: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.video_info is None:
|
||||
self.video_info = {}
|
||||
if self.raw_scenes is None:
|
||||
self.raw_scenes = []
|
||||
if self.processed_scenes is None:
|
||||
self.processed_scenes = []
|
||||
if self.errors is None:
|
||||
self.errors = []
|
||||
|
||||
def get_jsonrpc_handler(self) -> Optional[EnhancedJSONRPCResponse]:
|
||||
"""获取JSON-RPC响应处理器"""
|
||||
if self.enable_jsonrpc and self.request_id:
|
||||
return EnhancedJSONRPCResponse(self.request_id)
|
||||
return None
|
||||
|
||||
def send_progress(self, step: str, message: str, level: ProgressLevel = ProgressLevel.INFO,
|
||||
data: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""发送进度更新"""
|
||||
handler = self.get_jsonrpc_handler()
|
||||
if handler:
|
||||
progress_percent = int((self.progress / self.total_steps * 100)) if self.total_steps > 0 else -1
|
||||
handler.progress(step, progress_percent, message, level, data)
|
||||
|
||||
def send_final_result(self, result: Dict[str, Any]) -> None:
|
||||
"""发送最终结果"""
|
||||
handler = self.get_jsonrpc_handler()
|
||||
if handler:
|
||||
# 发送最终结果作为成功响应
|
||||
handler.success(result)
|
||||
|
||||
def send_error_result(self, error_code: int, error_message: str, error_data: Any = None) -> None:
|
||||
"""发送错误结果"""
|
||||
handler = self.get_jsonrpc_handler()
|
||||
if handler:
|
||||
# 发送错误响应
|
||||
handler.error(error_code, error_message, error_data)
|
||||
15
python_core/scene_detection/utils/__init__.py
Normal file
15
python_core/scene_detection/utils/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Utils
|
||||
场景检测工具类
|
||||
|
||||
导出所有工具类
|
||||
"""
|
||||
|
||||
from .result_saver import ResultSaver
|
||||
from .validators import InputValidator
|
||||
|
||||
__all__ = [
|
||||
"ResultSaver",
|
||||
"InputValidator"
|
||||
]
|
||||
110
python_core/scene_detection/utils/result_saver.py
Normal file
110
python_core/scene_detection/utils/result_saver.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Result Saver
|
||||
结果保存工具
|
||||
"""
|
||||
|
||||
import json
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from ..types import DetectionResult, OutputFormat
|
||||
|
||||
|
||||
class ResultSaver:
|
||||
"""结果保存器"""
|
||||
|
||||
def save_results(self, result: DetectionResult, output_path: Path,
|
||||
output_format: OutputFormat) -> None:
|
||||
"""保存检测结果"""
|
||||
try:
|
||||
if output_format == OutputFormat.JSON:
|
||||
self._save_json(result, output_path)
|
||||
elif output_format == OutputFormat.CSV:
|
||||
self._save_csv(result, output_path)
|
||||
elif output_format == OutputFormat.TXT:
|
||||
self._save_txt(result, output_path)
|
||||
else:
|
||||
raise ValueError(f"不支持的输出格式: {output_format}")
|
||||
|
||||
logger.info(f"💾 结果已保存到: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存结果失败: {e}")
|
||||
raise
|
||||
|
||||
def _save_json(self, result: DetectionResult, output_path: Path) -> None:
|
||||
"""保存为JSON格式"""
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 转换为字典
|
||||
result_dict = asdict(result)
|
||||
|
||||
# 保存JSON文件
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result_dict, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def _save_csv(self, result: DetectionResult, output_path: Path) -> None:
|
||||
"""保存为CSV格式"""
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# 写入头部信息
|
||||
writer.writerow(['# 场景检测结果'])
|
||||
writer.writerow(['# 文件名', result.filename])
|
||||
writer.writerow(['# 检测器', result.detector_type])
|
||||
writer.writerow(['# 阈值', result.threshold])
|
||||
writer.writerow(['# 总场景数', result.total_scenes])
|
||||
writer.writerow(['# 总时长', f"{result.total_duration:.2f}秒"])
|
||||
writer.writerow(['# 检测时间', f"{result.detection_time:.2f}秒"])
|
||||
writer.writerow([])
|
||||
|
||||
# 写入场景数据
|
||||
writer.writerow(['场景索引', '开始时间(秒)', '结束时间(秒)', '时长(秒)', '开始帧', '结束帧'])
|
||||
|
||||
for scene in result.scenes:
|
||||
writer.writerow([
|
||||
scene.index,
|
||||
f"{scene.start_time:.2f}",
|
||||
f"{scene.end_time:.2f}",
|
||||
f"{scene.duration:.2f}",
|
||||
scene.start_frame,
|
||||
scene.end_frame
|
||||
])
|
||||
|
||||
def _save_txt(self, result: DetectionResult, output_path: Path) -> None:
|
||||
"""保存为TXT格式"""
|
||||
# 确保输出目录存在
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write("场景检测结果\n")
|
||||
f.write("=" * 50 + "\n\n")
|
||||
|
||||
# 基本信息
|
||||
f.write(f"文件名: {result.filename}\n")
|
||||
f.write(f"检测器: {result.detector_type}\n")
|
||||
f.write(f"阈值: {result.threshold}\n")
|
||||
f.write(f"总场景数: {result.total_scenes}\n")
|
||||
f.write(f"总时长: {result.total_duration:.2f}秒\n")
|
||||
f.write(f"检测时间: {result.detection_time:.2f}秒\n")
|
||||
f.write(f"检测状态: {'成功' if result.success else '失败'}\n")
|
||||
|
||||
if result.error:
|
||||
f.write(f"错误信息: {result.error}\n")
|
||||
|
||||
f.write("\n场景详情:\n")
|
||||
f.write("-" * 50 + "\n")
|
||||
|
||||
# 场景列表
|
||||
for scene in result.scenes:
|
||||
f.write(f"场景 {scene.index + 1:2d}: ")
|
||||
f.write(f"{scene.start_time:7.2f}s - {scene.end_time:7.2f}s ")
|
||||
f.write(f"(时长: {scene.duration:6.2f}s, ")
|
||||
f.write(f"帧: {scene.start_frame:5d} - {scene.end_frame:5d})\n")
|
||||
84
python_core/scene_detection/utils/validators.py
Normal file
84
python_core/scene_detection/utils/validators.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Input Validators
|
||||
输入验证器
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Set
|
||||
|
||||
from ..types import DetectorType, OutputFormat
|
||||
|
||||
|
||||
class InputValidator:
|
||||
"""输入验证器"""
|
||||
|
||||
def __init__(self, supported_formats: Set[str]):
|
||||
self.supported_formats = supported_formats
|
||||
|
||||
def validate_video_path(self, video_path: Path) -> List[str]:
|
||||
"""验证视频路径"""
|
||||
errors = []
|
||||
|
||||
if not video_path.exists():
|
||||
errors.append(f"视频文件不存在: {video_path}")
|
||||
|
||||
if video_path.suffix.lower() not in self.supported_formats:
|
||||
errors.append(f"不支持的文件格式: {video_path.suffix}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_detector_type(self, detector_type: str) -> List[str]:
|
||||
"""验证检测器类型"""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
DetectorType(detector_type)
|
||||
except ValueError:
|
||||
valid_types = [dt.value for dt in DetectorType]
|
||||
errors.append(f"无效的检测器类型: {detector_type},支持的类型: {valid_types}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_threshold(self, threshold: float) -> List[str]:
|
||||
"""验证阈值"""
|
||||
errors = []
|
||||
|
||||
if not (0 <= threshold <= 100):
|
||||
errors.append(f"阈值超出范围 (0-100): {threshold}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_min_scene_length(self, min_scene_length: float) -> List[str]:
|
||||
"""验证最小场景长度"""
|
||||
errors = []
|
||||
|
||||
if min_scene_length < 0:
|
||||
errors.append(f"最小场景长度不能为负数: {min_scene_length}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_output_format(self, output_format: str) -> List[str]:
|
||||
"""验证输出格式"""
|
||||
errors = []
|
||||
|
||||
try:
|
||||
OutputFormat(output_format)
|
||||
except ValueError:
|
||||
valid_formats = [of.value for of in OutputFormat]
|
||||
errors.append(f"无效的输出格式: {output_format},支持的格式: {valid_formats}")
|
||||
|
||||
return errors
|
||||
|
||||
def validate_all(self, video_path: Path, detector_type: str, threshold: float,
|
||||
min_scene_length: float, output_format: str) -> List[str]:
|
||||
"""验证所有输入参数"""
|
||||
errors = []
|
||||
|
||||
errors.extend(self.validate_video_path(video_path))
|
||||
errors.extend(self.validate_detector_type(detector_type))
|
||||
errors.extend(self.validate_threshold(threshold))
|
||||
errors.extend(self.validate_min_scene_length(min_scene_length))
|
||||
errors.extend(self.validate_output_format(output_format))
|
||||
|
||||
return errors
|
||||
15
python_core/scene_detection/workflows/__init__.py
Normal file
15
python_core/scene_detection/workflows/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scene Detection Workflows
|
||||
场景检测工作流
|
||||
|
||||
导出所有工作流类
|
||||
"""
|
||||
|
||||
from .workflow_manager import SceneDetectionWorkflowManager
|
||||
from .workflow_nodes import WorkflowNodes
|
||||
|
||||
__all__ = [
|
||||
"SceneDetectionWorkflowManager",
|
||||
"WorkflowNodes"
|
||||
]
|
||||
176
python_core/scene_detection/workflows/workflow_manager.py
Normal file
176
python_core/scene_detection/workflows/workflow_manager.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow Manager
|
||||
工作流管理器
|
||||
"""
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, Literal
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from python_core.utils.jsonrpc_enhanced import EnhancedJSONRPCResponse
|
||||
from ..types import SceneDetectionWorkflowState, DetectorType, OutputFormat
|
||||
from .workflow_nodes import WorkflowNodes
|
||||
|
||||
|
||||
class SceneDetectionWorkflowManager:
|
||||
"""场景检测工作流管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.nodes = WorkflowNodes()
|
||||
self.workflow = None
|
||||
|
||||
def create_detection_workflow(self):
|
||||
"""创建检测工作流"""
|
||||
try:
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
# 创建状态图
|
||||
workflow = StateGraph(SceneDetectionWorkflowState)
|
||||
|
||||
# 添加节点
|
||||
workflow.add_node("validate", self.nodes.validate_input)
|
||||
workflow.add_node("extract_info", self.nodes.extract_video_info)
|
||||
workflow.add_node("detect", self.nodes.detect_scenes)
|
||||
workflow.add_node("analyze", self.nodes.analyze_with_ai)
|
||||
workflow.add_node("finalize", self.nodes.finalize_results)
|
||||
workflow.add_node("error", self.nodes.handle_error)
|
||||
|
||||
# 设置入口点
|
||||
workflow.set_entry_point("validate")
|
||||
|
||||
# 添加条件边
|
||||
workflow.add_conditional_edges(
|
||||
"validate",
|
||||
self._route_next_step,
|
||||
{
|
||||
"extract_info": "extract_info",
|
||||
"error": "error"
|
||||
}
|
||||
)
|
||||
|
||||
workflow.add_conditional_edges(
|
||||
"extract_info",
|
||||
self._route_next_step,
|
||||
{
|
||||
"detect": "detect",
|
||||
"error": "error"
|
||||
}
|
||||
)
|
||||
|
||||
workflow.add_conditional_edges(
|
||||
"detect",
|
||||
self._route_next_step,
|
||||
{
|
||||
"analyze": "analyze",
|
||||
"error": "error"
|
||||
}
|
||||
)
|
||||
|
||||
workflow.add_conditional_edges(
|
||||
"analyze",
|
||||
self._route_next_step,
|
||||
{
|
||||
"finalize": "finalize",
|
||||
"error": "error"
|
||||
}
|
||||
)
|
||||
|
||||
# 结束节点
|
||||
workflow.add_edge("finalize", END)
|
||||
workflow.add_edge("error", END)
|
||||
|
||||
# 编译工作流
|
||||
self.workflow = workflow.compile()
|
||||
return self.workflow
|
||||
|
||||
except ImportError:
|
||||
logger.error("❌ LangGraph未安装,无法创建工作流")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 创建工作流失败: {e}")
|
||||
return None
|
||||
|
||||
def _route_next_step(self, state: SceneDetectionWorkflowState) -> Literal["extract_info", "detect", "analyze", "finalize", "error"]:
|
||||
"""路由下一步"""
|
||||
if state.errors:
|
||||
return "error"
|
||||
elif state.current_stage == "validated":
|
||||
return "extract_info"
|
||||
elif state.current_stage == "info_extracted":
|
||||
return "detect"
|
||||
elif state.current_stage == "scenes_detected":
|
||||
return "analyze"
|
||||
elif state.current_stage in ["ai_analyzed", "analysis_skipped", "analysis_failed"]:
|
||||
return "finalize"
|
||||
else:
|
||||
return "error"
|
||||
|
||||
def detect_with_workflow(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
|
||||
threshold: float = 30.0, min_scene_length: float = 1.0,
|
||||
output_path: Optional[Path] = None, output_format: OutputFormat = OutputFormat.JSON,
|
||||
enable_ai_analysis: bool = True, request_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""使用LangGraph工作流进行场景检测"""
|
||||
|
||||
# 创建工作流
|
||||
workflow = self.create_detection_workflow()
|
||||
if not workflow:
|
||||
raise Exception("无法创建工作流")
|
||||
|
||||
# 初始化状态
|
||||
initial_state = SceneDetectionWorkflowState(
|
||||
video_path=str(video_path),
|
||||
detector_type=detector_type.value,
|
||||
threshold=threshold,
|
||||
min_scene_length=min_scene_length,
|
||||
output_path=str(output_path) if output_path else None,
|
||||
output_format=output_format.value,
|
||||
enable_ai_analysis=enable_ai_analysis,
|
||||
request_id=request_id,
|
||||
enable_jsonrpc=request_id is not None # 如果有request_id就启用JSON-RPC
|
||||
)
|
||||
|
||||
# 执行工作流
|
||||
config = {"configurable": {"thread_id": f"detection_{int(time.time())}"}}
|
||||
|
||||
try:
|
||||
final_state = workflow.invoke(initial_state, config)
|
||||
|
||||
# 如果是JSON-RPC模式,结果已经通过JSON-RPC发送了
|
||||
if request_id is not None:
|
||||
# JSON-RPC模式:结果已经在工作流节点中发送
|
||||
# 这里返回简化的状态信息供内部使用
|
||||
return {
|
||||
"jsonrpc_mode": True,
|
||||
"request_id": request_id,
|
||||
"workflow_state": final_state.get("current_stage"),
|
||||
"final_result_sent": True
|
||||
}
|
||||
else:
|
||||
# 非JSON-RPC模式:返回完整结果
|
||||
return {
|
||||
"detection_result": final_state.get("detection_result"),
|
||||
"ai_analysis": final_state.get("ai_analysis"),
|
||||
"video_info": final_state.get("video_info"),
|
||||
"workflow_state": final_state.get("current_stage"),
|
||||
"errors": final_state.get("errors", [])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"工作流执行失败: {e}"
|
||||
|
||||
# 如果是JSON-RPC模式,发送错误响应
|
||||
if request_id is not None:
|
||||
handler = EnhancedJSONRPCResponse(request_id)
|
||||
handler.error(-32603, error_msg, {"exception": str(e)})
|
||||
return {
|
||||
"jsonrpc_mode": True,
|
||||
"request_id": request_id,
|
||||
"error_sent": True,
|
||||
"error": error_msg
|
||||
}
|
||||
else:
|
||||
# 非JSON-RPC模式:抛出异常
|
||||
logger.error(f"❌ {error_msg}")
|
||||
raise
|
||||
242
python_core/scene_detection/workflows/workflow_nodes.py
Normal file
242
python_core/scene_detection/workflows/workflow_nodes.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Workflow Nodes
|
||||
工作流节点定义
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from dataclasses import asdict
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from python_core.utils.jsonrpc_enhanced import ProgressLevel
|
||||
from ..types import SceneDetectionWorkflowState, DetectorType
|
||||
from ..services import SceneDetectorService, VideoInfoService, AIAnalysisService
|
||||
|
||||
|
||||
class WorkflowNodes:
|
||||
"""工作流节点集合"""
|
||||
|
||||
def __init__(self):
|
||||
self.detector_service = SceneDetectorService()
|
||||
self.video_info_service = VideoInfoService()
|
||||
self.ai_analysis_service = AIAnalysisService()
|
||||
|
||||
def validate_input(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""验证输入参数"""
|
||||
state.send_progress("validate", "🔍 验证输入参数...", ProgressLevel.INFO)
|
||||
|
||||
video_path = Path(state.video_path)
|
||||
errors = []
|
||||
|
||||
# 验证文件存在
|
||||
if not video_path.exists():
|
||||
errors.append(f"视频文件不存在: {video_path}")
|
||||
state.send_progress("validate", f"❌ 文件不存在: {video_path}", ProgressLevel.ERROR)
|
||||
|
||||
# 验证文件格式
|
||||
if video_path.suffix.lower() not in self.detector_service.supported_formats:
|
||||
errors.append(f"不支持的文件格式: {video_path.suffix}")
|
||||
state.send_progress("validate", f"❌ 不支持的格式: {video_path.suffix}", ProgressLevel.ERROR)
|
||||
|
||||
# 验证参数范围
|
||||
if not (0 <= state.threshold <= 100):
|
||||
errors.append(f"阈值超出范围 (0-100): {state.threshold}")
|
||||
state.send_progress("validate", f"❌ 阈值超出范围: {state.threshold}", ProgressLevel.ERROR)
|
||||
|
||||
if state.min_scene_length < 0:
|
||||
errors.append(f"最小场景长度不能为负数: {state.min_scene_length}")
|
||||
state.send_progress("validate", f"❌ 最小场景长度无效: {state.min_scene_length}", ProgressLevel.ERROR)
|
||||
|
||||
if not errors:
|
||||
state.send_progress("validate", "✅ 输入参数验证通过", ProgressLevel.SUCCESS)
|
||||
|
||||
return {
|
||||
"current_stage": "validated" if not errors else "error",
|
||||
"progress": 1,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
def extract_video_info(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""提取视频信息"""
|
||||
state.send_progress("extract_info", "📊 提取视频信息...", ProgressLevel.INFO)
|
||||
|
||||
try:
|
||||
video_info = self.video_info_service.extract_video_info(Path(state.video_path))
|
||||
|
||||
state.send_progress("extract_info",
|
||||
f"📹 视频信息: {video_info['resolution']}, {video_info['fps']:.2f}fps, {video_info['duration']:.2f}s",
|
||||
ProgressLevel.SUCCESS,
|
||||
{"video_info": video_info}
|
||||
)
|
||||
|
||||
return {
|
||||
"current_stage": "info_extracted",
|
||||
"progress": 2,
|
||||
"video_info": video_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"提取视频信息失败: {e}"
|
||||
state.send_progress("extract_info", f"❌ {error_msg}", ProgressLevel.ERROR)
|
||||
return {
|
||||
"current_stage": "error",
|
||||
"errors": state.errors + [error_msg]
|
||||
}
|
||||
|
||||
def detect_scenes(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""执行场景检测"""
|
||||
state.send_progress("detect", "🎯 执行场景检测...", ProgressLevel.INFO)
|
||||
|
||||
try:
|
||||
result = self.detector_service.detect_scenes(
|
||||
Path(state.video_path),
|
||||
DetectorType(state.detector_type),
|
||||
state.threshold,
|
||||
state.min_scene_length
|
||||
)
|
||||
|
||||
state.send_progress("detect",
|
||||
f"✅ 检测完成: {result.total_scenes} 个场景,耗时 {result.detection_time:.2f}秒",
|
||||
ProgressLevel.SUCCESS,
|
||||
{
|
||||
"total_scenes": result.total_scenes,
|
||||
"detection_time": result.detection_time,
|
||||
"total_duration": result.total_duration
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"current_stage": "scenes_detected",
|
||||
"progress": 3,
|
||||
"detection_result": result,
|
||||
"processed_scenes": result.scenes
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"场景检测失败: {e}"
|
||||
state.send_progress("detect", f"❌ {error_msg}", ProgressLevel.ERROR)
|
||||
return {
|
||||
"current_stage": "error",
|
||||
"errors": state.errors + [error_msg]
|
||||
}
|
||||
|
||||
def analyze_with_ai(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""AI分析场景结果"""
|
||||
if not self.ai_analysis_service.ai_enabled or not state.enable_ai_analysis:
|
||||
state.send_progress("analyze", "⚠️ AI分析已禁用,跳过此步骤", ProgressLevel.WARNING)
|
||||
return {
|
||||
"current_stage": "analysis_skipped",
|
||||
"progress": 4,
|
||||
"ai_analysis": "AI分析已禁用"
|
||||
}
|
||||
|
||||
state.send_progress("analyze", "🧠 AI分析场景结果...", ProgressLevel.INFO)
|
||||
|
||||
try:
|
||||
state.send_progress("analyze", "🤖 正在调用AI分析服务...", ProgressLevel.INFO)
|
||||
analysis = self.ai_analysis_service.analyze_detection_result(
|
||||
state.detection_result,
|
||||
state.video_info
|
||||
)
|
||||
|
||||
state.send_progress("analyze", "✅ AI分析完成", ProgressLevel.SUCCESS,
|
||||
{"analysis_length": len(analysis)})
|
||||
|
||||
return {
|
||||
"current_stage": "ai_analyzed",
|
||||
"progress": 4,
|
||||
"ai_analysis": analysis
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"AI分析失败: {e}"
|
||||
state.send_progress("analyze", f"⚠️ {error_msg}", ProgressLevel.WARNING)
|
||||
return {
|
||||
"current_stage": "analysis_failed",
|
||||
"progress": 4,
|
||||
"ai_analysis": error_msg
|
||||
}
|
||||
|
||||
def finalize_results(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""整理最终结果"""
|
||||
state.send_progress("finalize", "📋 整理最终结果...", ProgressLevel.INFO)
|
||||
|
||||
# 保存结果(如果指定了输出路径)
|
||||
if state.output_path and state.detection_result:
|
||||
try:
|
||||
from ..utils import ResultSaver
|
||||
from ..types import OutputFormat
|
||||
|
||||
output_path = Path(state.output_path)
|
||||
output_format = OutputFormat(state.output_format)
|
||||
|
||||
saver = ResultSaver()
|
||||
saver.save_results(state.detection_result, output_path, output_format)
|
||||
state.send_progress("finalize", f"💾 结果已保存到: {output_path}", ProgressLevel.SUCCESS)
|
||||
except Exception as e:
|
||||
state.send_progress("finalize", f"⚠️ 保存结果失败: {e}", ProgressLevel.WARNING)
|
||||
|
||||
# 准备最终结果
|
||||
final_result = {
|
||||
"success": True,
|
||||
"workflow_state": "completed",
|
||||
"detection_result": None,
|
||||
"ai_analysis": state.ai_analysis,
|
||||
"video_info": state.video_info,
|
||||
"errors": state.errors or []
|
||||
}
|
||||
|
||||
# 序列化检测结果
|
||||
if state.detection_result:
|
||||
final_result["detection_result"] = {
|
||||
"success": state.detection_result.success,
|
||||
"filename": state.detection_result.filename,
|
||||
"detector_type": state.detection_result.detector_type,
|
||||
"threshold": state.detection_result.threshold,
|
||||
"total_scenes": state.detection_result.total_scenes,
|
||||
"total_duration": state.detection_result.total_duration,
|
||||
"detection_time": state.detection_result.detection_time,
|
||||
"scenes": [asdict(scene) for scene in state.detection_result.scenes],
|
||||
"error": state.detection_result.error
|
||||
}
|
||||
|
||||
state.send_progress("finalize", "🎉 工作流执行完成", ProgressLevel.SUCCESS, {
|
||||
"total_scenes": state.detection_result.total_scenes if state.detection_result else 0,
|
||||
"workflow_stage": "completed"
|
||||
})
|
||||
|
||||
# 发送最终结果到JSON-RPC客户端
|
||||
state.send_final_result(final_result)
|
||||
|
||||
return {
|
||||
"current_stage": "completed",
|
||||
"progress": 5,
|
||||
"final_result": final_result
|
||||
}
|
||||
|
||||
def handle_error(self, state: SceneDetectionWorkflowState) -> Dict[str, Any]:
|
||||
"""处理错误"""
|
||||
error_msg = "; ".join(state.errors) if state.errors else "未知错误"
|
||||
|
||||
# 发送错误进度
|
||||
state.send_progress("error", f"❌ 工作流错误: {error_msg}", ProgressLevel.ERROR)
|
||||
|
||||
# 准备错误结果
|
||||
error_result = {
|
||||
"success": False,
|
||||
"workflow_state": "failed",
|
||||
"error": error_msg,
|
||||
"errors": state.errors or [],
|
||||
"detection_result": None,
|
||||
"ai_analysis": None,
|
||||
"video_info": state.video_info
|
||||
}
|
||||
|
||||
# 发送错误结果到JSON-RPC客户端
|
||||
state.send_error_result(-32603, f"工作流执行失败: {error_msg}", error_result)
|
||||
|
||||
return {
|
||||
"current_stage": "failed",
|
||||
"error_result": error_result
|
||||
}
|
||||
Reference in New Issue
Block a user