#!/usr/bin/env python3 """ PySceneDetect 场景检测命令行工具 - LangGraph增强版 """ import json import time from pathlib import Path from typing import Optional, List, Literal, Dict, Any from enum import Enum from dataclasses import dataclass, asdict from python_core.cli.const import progress_reporter # PySceneDetect 依赖 from scenedetect import open_video, SceneManager from scenedetect.detectors import ContentDetector, ThresholdDetector # LangGraph 依赖 from langgraph.graph import StateGraph, START, END from langgraph.graph.state import CompiledStateGraph from langgraph.checkpoint.memory import MemorySaver from langchain_anthropic import ChatAnthropic class DetectorType(str, Enum): """检测器类型""" CONTENT = "content" THRESHOLD = "threshold" ADAPTIVE = "adaptive" class OutputFormat(str, Enum): """输出格式""" JSON = "json" CSV = "csv" TXT = "txt" @dataclass class SceneInfo: """场景信息""" index: int start_time: float end_time: float duration: float start_frame: int end_frame: int @dataclass class DetectionResult: """检测结果""" video_path: str filename: str detector_type: str threshold: float total_scenes: int total_duration: float detection_time: float scenes: List[SceneInfo] success: bool error: Optional[str] = None # LangGraph 工作流状态 @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 # 中间结果 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 = [] class SceneDetector: """场景检测器""" def __init__(self): self.supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'} # 初始化AI分析器(如果可用) try: self.llm = ChatAnthropic(model="claude-3-5-sonnet-20241022") self.ai_enabled = True except Exception as e: progress_reporter.warning(f"⚠️ AI分析器初始化失败: {e}") self.ai_enabled = False def detect_scenes(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT, threshold: float = 30.0, min_scene_length: float = 1.0) -> DetectionResult: """检测单个视频的场景""" start_time = time.time() filename = video_path.name try: progress_reporter.info(f"🎬 开始检测: {filename}") # 打开视频文件 video = open_video(str(video_path)) scene_manager = SceneManager() # 根据类型添加检测器 if detector_type == DetectorType.CONTENT: scene_manager.add_detector(ContentDetector(threshold=threshold)) progress_reporter.info(f"📊 使用内容检测器,阈值: {threshold}") elif detector_type == DetectorType.THRESHOLD: scene_manager.add_detector(ThresholdDetector(threshold=threshold)) progress_reporter.info(f"📊 使用阈值检测器,阈值: {threshold}") elif detector_type == DetectorType.ADAPTIVE: # 自适应:同时使用两种检测器 scene_manager.add_detector(ContentDetector(threshold=threshold)) scene_manager.add_detector(ThresholdDetector(threshold=threshold * 0.8)) progress_reporter.info(f"📊 使用自适应检测器,阈值: {threshold}") # 开始检测 progress_reporter.info("🔍 正在分析视频帧...") scene_manager.detect_scenes(video) scene_list = scene_manager.get_scene_list() progress_reporter.info(f"✅ 检测到 {len(scene_list)} 个场景") # 获取视频信息 fps = video.frame_rate total_duration = video.duration.get_seconds() progress_reporter.info(f"📊 视频信息: {fps:.2f}fps, {total_duration:.2f}秒") # 构建场景信息 scenes = [] if not scene_list: # 如果没有检测到场景变化,整个视频就是一个场景 scene_info = SceneInfo( index=0, start_time=0.0, end_time=total_duration, duration=total_duration, start_frame=0, end_frame=int(total_duration * fps) if fps > 0 else 0 ) scenes.append(scene_info) progress_reporter.info(f"📝 无场景变化,整个视频作为单一场景: {total_duration:.2f}秒") else: # 处理检测到的场景 for start_time_scene, end_time_scene in scene_list: start_seconds = start_time_scene.get_seconds() end_seconds = end_time_scene.get_seconds() duration = end_seconds - start_seconds # 跳过太短的场景 if duration < min_scene_length: continue scene_info = SceneInfo( index=len(scenes), start_time=start_seconds, end_time=end_seconds, duration=duration, start_frame=start_time_scene.get_frames(), end_frame=end_time_scene.get_frames() ) scenes.append(scene_info) detection_time = time.time() - start_time result = DetectionResult( video_path=str(video_path), filename=filename, detector_type=detector_type.value, threshold=threshold, total_scenes=len(scenes), total_duration=total_duration, detection_time=detection_time, scenes=scenes, success=True ) progress_reporter.success(f"🎯 检测完成: {len(scenes)} 个场景,耗时 {detection_time:.2f}秒") return result except Exception as e: detection_time = time.time() - start_time error_msg = str(e) progress_reporter.error(f"❌ 检测失败: {error_msg}") return DetectionResult( video_path=str(video_path), filename=filename, detector_type=detector_type.value, threshold=threshold, total_scenes=0, total_duration=0.0, detection_time=detection_time, scenes=[], success=False, error=error_msg ) def batch_detect(self, input_directory: Path, detector_type: DetectorType = DetectorType.CONTENT, threshold: float = 30.0, recursive: bool = False) -> List[DetectionResult]: """批量检测场景""" progress_reporter.info(f"📦 开始批量检测: {input_directory}") # 扫描视频文件 video_files = self._scan_video_files(input_directory, recursive) if not video_files: progress_reporter.warning("⚠️ 未找到视频文件") return [] progress_reporter.info(f"📋 找到 {len(video_files)} 个视频文件") results = [] for i, video_file in enumerate(video_files): progress_reporter.info(f"📊 处理进度: {i+1}/{len(video_files)} - {video_file.name}") result = self.detect_scenes(video_file, detector_type, threshold) results.append(result) successful = len([r for r in results if r.success]) progress_reporter.success(f"🎉 批量检测完成: {successful}/{len(results)} 成功") return results def compare_detectors(self, video_path: Path, thresholds: List[float] = None) -> dict: """比较不同检测器效果""" if thresholds is None: thresholds = [20.0, 30.0, 40.0] progress_reporter.info(f"🔬 开始检测器比较: {video_path.name}") detectors = [DetectorType.CONTENT, DetectorType.THRESHOLD, DetectorType.ADAPTIVE] results = [] total_tests = len(detectors) * len(thresholds) current_test = 0 for detector in detectors: for threshold in thresholds: current_test += 1 progress_reporter.info(f"🧪 测试 {current_test}/{total_tests}: {detector.value} (阈值: {threshold})") result = self.detect_scenes(video_path, detector, threshold) results.append({ "detector": detector.value, "threshold": threshold, "scenes": result.total_scenes, "duration": result.total_duration, "detection_time": result.detection_time, "success": result.success, "error": result.error }) # 分析结果 analysis = self._analyze_comparison(results) comparison_result = { "video_path": str(video_path), "total_tests": total_tests, "results": results, "analysis": analysis } progress_reporter.success("🔬 检测器比较完成") return comparison_result def _scan_video_files(self, directory: Path, recursive: bool = False) -> List[Path]: """扫描视频文件""" video_files = [] if recursive: for ext in self.supported_formats: video_files.extend(directory.rglob(f"*{ext}")) else: for ext in self.supported_formats: video_files.extend(directory.glob(f"*{ext}")) return sorted(video_files) def _analyze_comparison(self, results: List[dict]) -> dict: """分析比较结果""" successful_results = [r for r in results if r["success"]] if not successful_results: return {"message": "所有测试都失败了"} # 按检测器分组 by_detector = {} for result in successful_results: detector = result["detector"] if detector not in by_detector: by_detector[detector] = [] by_detector[detector].append(result) # 分析每个检测器 detector_analysis = {} for detector, detector_results in by_detector.items(): avg_scenes = sum(r["scenes"] for r in detector_results) / len(detector_results) avg_time = sum(r["detection_time"] for r in detector_results) / len(detector_results) detector_analysis[detector] = { "average_scenes": avg_scenes, "average_detection_time": avg_time, "test_count": len(detector_results) } # 推荐最佳检测器 best_detector = max(detector_analysis.keys(), key=lambda d: detector_analysis[d]["average_scenes"]) return { "total_successful": len(successful_results), "detector_analysis": detector_analysis, "best_detector": best_detector, "recommendation": f"推荐使用 {best_detector} 检测器" } def save_results(self, results, output_path: Path, format: OutputFormat = OutputFormat.JSON): """保存检测结果""" output_path.parent.mkdir(parents=True, exist_ok=True) if format == OutputFormat.JSON: self._save_json(results, output_path) elif format == OutputFormat.CSV: self._save_csv(results, output_path) elif format == OutputFormat.TXT: self._save_txt(results, output_path) progress_reporter.success(f"📄 结果已保存: {output_path}") def _save_json(self, results, output_path: Path): """保存JSON格式""" if isinstance(results, list): # 批量结果 data = [asdict(result) for result in results] else: # 单个结果或比较结果 if hasattr(results, '__dict__'): data = asdict(results) else: data = results with open(output_path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) def _save_csv(self, results, output_path: Path): """保存CSV格式""" import csv with open(output_path, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) if isinstance(results, list) and results: # 批量结果 writer.writerow(['filename', 'detector', 'threshold', 'scenes', 'duration', 'detection_time', 'success']) for result in results: writer.writerow([ result.filename, result.detector_type, result.threshold, result.total_scenes, result.total_duration, result.detection_time, result.success ]) def _save_txt(self, results, output_path: Path): """保存文本格式""" with open(output_path, 'w', encoding='utf-8') as f: f.write("PySceneDetect 场景检测结果\n") f.write("=" * 50 + "\n\n") if isinstance(results, list): # 批量结果 for result in results: 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.scenes: f.write(" 场景列表:\n") for scene in result.scenes: f.write(f" 场景 {scene.index}: {scene.start_time:.2f}s - {scene.end_time:.2f}s ({scene.duration:.2f}s)\n") f.write("\n") # ==================== LangGraph 工作流方法 ==================== def create_detection_workflow(self) -> Optional[CompiledStateGraph]: """创建场景检测工作流""" # 定义工作流节点 def validate_input(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """验证输入参数""" progress_reporter.info("🔍 验证输入参数...") video_path = Path(state.video_path) 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}") # 验证参数范围 if not (0 <= state.threshold <= 100): errors.append(f"阈值超出范围 (0-100): {state.threshold}") if state.min_scene_length < 0: errors.append(f"最小场景长度不能为负数: {state.min_scene_length}") return { "current_stage": "validated" if not errors else "error", "progress": 1, "errors": errors } def extract_video_info(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """提取视频信息""" progress_reporter.info("📊 提取视频信息...") try: import cv2 cap = cv2.VideoCapture(state.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() video_info = { "fps": fps, "frame_count": frame_count, "width": width, "height": height, "duration": duration, "resolution": f"{width}x{height}" } progress_reporter.info(f"📹 视频信息: {video_info['resolution']}, {fps:.2f}fps, {duration:.2f}s") return { "current_stage": "info_extracted", "progress": 2, "video_info": video_info } except Exception as e: return { "current_stage": "error", "errors": state.errors + [f"提取视频信息失败: {e}"] } def detect_scenes(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """执行场景检测""" progress_reporter.info("🎯 执行场景检测...") try: # 使用现有的检测逻辑 result = self.detect_scenes( Path(state.video_path), DetectorType(state.detector_type), state.threshold, state.min_scene_length ) return { "current_stage": "scenes_detected", "progress": 3, "detection_result": result, "processed_scenes": result.scenes } except Exception as e: return { "current_stage": "error", "errors": state.errors + [f"场景检测失败: {e}"] } def analyze_with_ai(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """AI分析场景结果""" if not self.ai_enabled or not state.enable_ai_analysis: progress_reporter.info("⚠️ AI分析已禁用,跳过此步骤") return { "current_stage": "analysis_skipped", "progress": 4, "ai_analysis": "AI分析已禁用" } progress_reporter.info("🧠 AI分析场景结果...") try: result = state.detection_result video_info = state.video_info analysis_prompt = 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. 潜在问题识别 """ response = self.llm.invoke([{"role": "user", "content": analysis_prompt}]) return { "current_stage": "ai_analyzed", "progress": 4, "ai_analysis": response.content } except Exception as e: progress_reporter.warning(f"⚠️ AI分析失败: {e}") return { "current_stage": "analysis_failed", "progress": 4, "ai_analysis": f"AI分析失败: {e}" } def finalize_results(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """整理最终结果""" progress_reporter.info("📋 整理最终结果...") # 保存结果(如果指定了输出路径) if state.output_path and state.detection_result: try: output_path = Path(state.output_path) output_format = OutputFormat(state.output_format) self.save_results(state.detection_result, output_path, output_format) except Exception as e: progress_reporter.warning(f"⚠️ 保存结果失败: {e}") return { "current_stage": "completed", "progress": 5 } # 路由函数 def route_next_step(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 handle_error(state: SceneDetectionWorkflowState) -> Dict[str, Any]: """处理错误""" error_msg = "; ".join(state.errors) progress_reporter.error(f"❌ 工作流错误: {error_msg}") return {"current_stage": "failed"} # 构建工作流图 workflow = StateGraph(SceneDetectionWorkflowState) # 添加节点 workflow.add_node("validate", validate_input) workflow.add_node("extract_info", extract_video_info) workflow.add_node("detect", detect_scenes) workflow.add_node("analyze", analyze_with_ai) workflow.add_node("finalize", finalize_results) workflow.add_node("error", handle_error) # 添加边 workflow.add_edge(START, "validate") workflow.add_conditional_edges("validate", route_next_step) workflow.add_conditional_edges("extract_info", route_next_step) workflow.add_conditional_edges("detect", route_next_step) workflow.add_conditional_edges("analyze", route_next_step) workflow.add_edge("finalize", END) workflow.add_edge("error", END) # 编译工作流 memory = MemorySaver() return workflow.compile(checkpointer=memory) def _format_scenes_for_ai(self, scenes: List[SceneInfo]) -> str: """格式化场景信息供AI分析""" if not scenes: return "无场景数据" formatted = [] for scene in scenes[:10]: # 只显示前10个场景 formatted.append( f"场景 {scene.index}: {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) 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) -> 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 # 使用参数 ) # 执行工作流 config = {"configurable": {"thread_id": f"detection_{int(time.time())}"}} try: final_state = workflow.invoke(initial_state, config) 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: progress_reporter.error(f"❌ 工作流执行失败: {e}") raise # ==================== JSON-RPC 方法注册 ==================== 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") 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 { "success": result.success, "filename": result.filename, "detector_type": result.detector_type, "threshold": result.threshold, "total_scenes": result.total_scenes, "total_duration": result.total_duration, "detection_time": result.detection_time, "scenes": [asdict(scene) for scene in result.scenes], "error": result.error } 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) -> 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 ) # 序列化结果 serialized_result = {} for key, value in result.items(): if key == "detection_result" and value: serialized_result[key] = { "success": value.success, "filename": value.filename, "detector_type": value.detector_type, "threshold": value.threshold, "total_scenes": value.total_scenes, "total_duration": value.total_duration, "detection_time": value.detection_time, "scenes": [asdict(scene) for scene in value.scenes], "error": value.error } else: serialized_result[key] = value return serialized_result except Exception as e: return { "success": False, "error": str(e) } def get_video_info(self, video_path: Path) -> Dict[str, Any]: """获取视频信息""" try: import cv2 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() return { "filename": video_path.name, "fps": fps, "frame_count": frame_count, "width": width, "height": height, "duration": duration, "resolution": f"{width}x{height}", "file_size": video_path.stat().st_size if video_path.exists() else 0 } except Exception as e: raise Exception(f"获取视频信息失败: {e}") def jsonrpc_get_video_info(self, video_path: str) -> Dict[str, Any]: """JSON-RPC方法:获取视频信息""" try: info = self.get_video_info(Path(video_path)) return { "success": True, "info": info } except Exception as e: return { "success": False, "error": str(e) } def jsonrpc_batch_detect(self, directory: 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: output_dir_obj = Path(output_dir) if output_dir else None results = self.batch_detect( Path(directory), DetectorType(detector_type), threshold, min_scene_length, output_dir_obj, OutputFormat(output_format) ) # 序列化结果 serialized_results = [] for result in results: serialized_results.append({ "success": result.success, "filename": result.filename, "detector_type": result.detector_type, "threshold": result.threshold, "total_scenes": result.total_scenes, "total_duration": result.total_duration, "detection_time": result.detection_time, "scenes": [asdict(scene) for scene in result.scenes], "error": result.error }) return { "success": True, "results": serialized_results, "total_processed": len(results) } except Exception as e: return { "success": False, "error": str(e) } # 创建全局检测器实例 detector = SceneDetector() # 注册JSON-RPC方法 detector.register_jsonrpc_methods()