871 lines
33 KiB
Python
871 lines
33 KiB
Python
#!/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() |