fix: 修复命令行工具

This commit is contained in:
root
2025-07-12 13:44:57 +08:00
parent 81035caf0e
commit 31de1e5a4d
30 changed files with 2728 additions and 1755 deletions

View 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"
]

View 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")

View 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"
]

View 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)

View 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

View 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

View 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"
]

View 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"

View 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

View 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)

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
Scene Detection Utils
场景检测工具类
导出所有工具类
"""
from .result_saver import ResultSaver
from .validators import InputValidator
__all__ = [
"ResultSaver",
"InputValidator"
]

View 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")

View 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

View 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"
]

View 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

View 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
}