This commit is contained in:
root
2025-07-12 14:38:15 +08:00
parent 92db62869a
commit 493e347b03
14 changed files with 3267 additions and 114 deletions

View File

@@ -114,115 +114,17 @@ class SceneDetector:
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 batch_detect_and_split(self, video_paths: List[Path], output_base_dir: Optional[Path] = None,
detector_type: DetectorType = DetectorType.CONTENT, threshold: float = 30.0,
min_scene_length: float = 1.0, output_format: OutputFormat = OutputFormat.JSON,
enable_ai_analysis: bool = False, enable_video_splitting: bool = True,
max_concurrent: int = 2, continue_on_error: bool = True,
use_advanced_split: bool = True, split_quality: int = 23, split_preset: str = "fast",
request_id: Optional[str] = None) -> Dict[str, Any]:
"""批量场景检测和视频切分"""
return self.workflow_manager.batch_detect_and_split(
video_paths, output_base_dir, detector_type, threshold, min_scene_length,
output_format, enable_ai_analysis, enable_video_splitting,
max_concurrent, continue_on_error, use_advanced_split, split_quality, split_preset, request_id
)
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

@@ -9,9 +9,11 @@ Scene Detection Services
from .detector_service import SceneDetectorService
from .ai_analysis_service import AIAnalysisService
from .video_info_service import VideoInfoService
from .video_splitter_service import VideoSplitterService
__all__ = [
"SceneDetectorService",
"AIAnalysisService",
"VideoInfoService"
"AIAnalysisService",
"VideoInfoService",
"VideoSplitterService"
]

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
Batch Workflow State
批量工作流状态定义
"""
from dataclasses import dataclass, field
from typing import Dict, Any, List, Optional
from pathlib import Path
from .models import SceneInfo, DetectionResult
from python_core.utils.jsonrpc_enhanced import EnhancedJSONRPCResponse, ProgressLevel
@dataclass
class BatchVideoTask:
"""批量视频处理任务"""
video_path: Path
output_dir: Optional[Path] = None
status: str = "pending" # pending, processing, completed, failed
detection_result: Optional[DetectionResult] = None
split_results: List[Dict[str, Any]] = field(default_factory=list)
error: Optional[str] = None
start_time: Optional[float] = None
end_time: Optional[float] = None
@dataclass
class BatchSceneDetectionWorkflowState:
"""批量场景检测工作流状态"""
# 输入参数
video_paths: List[Path] = field(default_factory=list)
output_base_dir: Optional[Path] = None
detector_type: str = "content"
threshold: float = 30.0
min_scene_length: float = 1.0
output_format: str = "json"
enable_ai_analysis: bool = False
enable_video_splitting: bool = True
# 视频切分配置
use_advanced_split: bool = True
split_quality: int = 23
split_preset: str = "fast"
# 批量处理配置
max_concurrent: int = 2
continue_on_error: bool = True
# 工作流状态
current_stage: str = "init"
current_task_index: int = 0
total_tasks: int = 0
completed_tasks: int = 0
failed_tasks: int = 0
# JSON-RPC支持
request_id: Optional[str] = None
enable_jsonrpc: bool = False
# 任务列表
tasks: List[BatchVideoTask] = field(default_factory=list)
# 全局结果
batch_results: Dict[str, Any] = field(default_factory=dict)
global_errors: List[str] = field(default_factory=list)
def __post_init__(self):
if not self.tasks and self.video_paths:
# 从视频路径创建任务
self.tasks = [
BatchVideoTask(
video_path=video_path,
output_dir=self.output_base_dir / video_path.stem if self.output_base_dir else None
)
for video_path in self.video_paths
]
self.total_tasks = len(self.tasks)
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:
# 计算总体进度
if self.total_tasks > 0:
progress_percent = int((self.completed_tasks / self.total_tasks * 100))
else:
progress_percent = -1
# 添加批量处理特定数据
progress_data = {
"current_task": self.current_task_index + 1,
"total_tasks": self.total_tasks,
"completed_tasks": self.completed_tasks,
"failed_tasks": self.failed_tasks,
"current_stage": self.current_stage
}
if data:
progress_data.update(data)
handler.progress(step, progress_percent, message, level, progress_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)
def get_current_task(self) -> Optional[BatchVideoTask]:
"""获取当前任务"""
if 0 <= self.current_task_index < len(self.tasks):
return self.tasks[self.current_task_index]
return None
def mark_task_completed(self, task_index: int, result: DetectionResult, split_results: List[Dict[str, Any]] = None):
"""标记任务完成"""
if 0 <= task_index < len(self.tasks):
task = self.tasks[task_index]
task.status = "completed"
task.detection_result = result
task.split_results = split_results or []
self.completed_tasks += 1
def mark_task_failed(self, task_index: int, error: str):
"""标记任务失败"""
if 0 <= task_index < len(self.tasks):
task = self.tasks[task_index]
task.status = "failed"
task.error = error
self.failed_tasks += 1
def get_summary(self) -> Dict[str, Any]:
"""获取批量处理摘要"""
return {
"total_tasks": self.total_tasks,
"completed_tasks": self.completed_tasks,
"failed_tasks": self.failed_tasks,
"success_rate": (self.completed_tasks / self.total_tasks * 100) if self.total_tasks > 0 else 0,
"current_stage": self.current_stage,
"tasks": [
{
"video_path": str(task.video_path),
"status": task.status,
"total_scenes": task.detection_result.total_scenes if task.detection_result else 0,
"split_count": len(task.split_results),
"error": task.error
}
for task in self.tasks
]
}

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
Batch Workflow Nodes
批量工作流节点
"""
import time
import asyncio
from pathlib import Path
from typing import Dict, Any, List
from concurrent.futures import ThreadPoolExecutor, as_completed
from python_core.utils.logger import logger
from python_core.utils.jsonrpc_enhanced import ProgressLevel
from ..types.batch_workflow_state import BatchSceneDetectionWorkflowState, BatchVideoTask
from ..types.enums import DetectorType, OutputFormat
from ..services.detector_service import SceneDetectorService
from ..services.video_info_service import VideoInfoService
from ..services.ai_analysis_service import AIAnalysisService
from python_core.services.ffmpeg_slice_service import FfmpegSliceService, SliceOptions
from ..utils.result_saver import ResultSaver
class BatchWorkflowNodes:
"""批量工作流节点"""
def __init__(self):
# 初始化服务
self.detector_service = SceneDetectorService()
self.video_info_service = VideoInfoService()
self.ai_analysis_service = AIAnalysisService()
self.splitter_service = FfmpegSliceService()
self.result_saver = ResultSaver()
def validate_batch_input(self, state: BatchSceneDetectionWorkflowState) -> Dict[str, Any]:
"""验证批量输入"""
state.current_stage = "validation"
state.send_progress("validate_input", "验证批量输入参数", ProgressLevel.INFO)
errors = []
# 检查视频文件
valid_videos = []
for video_path in state.video_paths:
if not video_path.exists():
errors.append(f"视频文件不存在: {video_path}")
continue
if video_path.suffix.lower() not in {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}:
errors.append(f"不支持的视频格式: {video_path}")
continue
valid_videos.append(video_path)
if not valid_videos:
errors.append("没有有效的视频文件")
# 检查输出目录
if state.output_base_dir:
try:
state.output_base_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
errors.append(f"无法创建输出目录: {e}")
# 检查FFmpeg如果需要切分
if state.enable_video_splitting:
if not self.splitter_service.check_ffmpeg_available():
errors.append("FFmpeg不可用无法进行视频切分")
if errors:
state.global_errors.extend(errors)
return {
"workflow_state": "failed",
"errors": errors,
"valid_videos": valid_videos
}
# 更新有效视频列表
state.video_paths = valid_videos
state.tasks = [
BatchVideoTask(
video_path=video_path,
output_dir=state.output_base_dir / video_path.stem if state.output_base_dir else None
)
for video_path in valid_videos
]
state.total_tasks = len(state.tasks)
state.send_progress("validate_input", f"验证完成,找到 {len(valid_videos)} 个有效视频", ProgressLevel.SUCCESS)
return {
"workflow_state": "validated",
"valid_videos": valid_videos,
"total_tasks": state.total_tasks
}
def process_videos_batch(self, state: BatchSceneDetectionWorkflowState) -> Dict[str, Any]:
"""批量处理视频"""
state.current_stage = "batch_processing"
state.send_progress("batch_process", "开始批量处理视频", ProgressLevel.INFO)
# 使用线程池进行并发处理
with ThreadPoolExecutor(max_workers=state.max_concurrent) as executor:
# 提交所有任务
future_to_index = {}
for i, task in enumerate(state.tasks):
future = executor.submit(self._process_single_video, state, i, task)
future_to_index[future] = i
# 处理完成的任务
for future in as_completed(future_to_index):
task_index = future_to_index[future]
try:
result = future.result()
if result["success"]:
state.mark_task_completed(
task_index,
result["detection_result"],
result.get("split_results", [])
)
state.send_progress(
"task_completed",
f"任务 {task_index + 1}/{state.total_tasks} 完成: {state.tasks[task_index].video_path.name}",
ProgressLevel.SUCCESS,
{"task_index": task_index + 1, "video_name": state.tasks[task_index].video_path.name}
)
else:
state.mark_task_failed(task_index, result["error"])
state.send_progress(
"task_failed",
f"任务 {task_index + 1}/{state.total_tasks} 失败: {result['error']}",
ProgressLevel.ERROR,
{"task_index": task_index + 1, "error": result["error"]}
)
except Exception as e:
state.mark_task_failed(task_index, str(e))
state.send_progress(
"task_error",
f"任务 {task_index + 1}/{state.total_tasks} 异常: {str(e)}",
ProgressLevel.ERROR,
{"task_index": task_index + 1, "error": str(e)}
)
# 生成批量处理结果
summary = state.get_summary()
state.send_progress(
"batch_complete",
f"批量处理完成: {state.completed_tasks}/{state.total_tasks} 成功",
ProgressLevel.SUCCESS,
summary
)
return {
"workflow_state": "completed",
"summary": summary,
"completed_tasks": state.completed_tasks,
"failed_tasks": state.failed_tasks,
"total_tasks": state.total_tasks
}
def _process_single_video(self, state: BatchSceneDetectionWorkflowState,
task_index: int, task: BatchVideoTask) -> Dict[str, Any]:
"""处理单个视频"""
try:
task.status = "processing"
task.start_time = time.time()
logger.info(f"🎬 处理视频 {task_index + 1}/{state.total_tasks}: {task.video_path.name}")
# 1. 场景检测
detection_result = self.detector_service.detect_scenes(
task.video_path,
DetectorType(state.detector_type),
state.threshold,
state.min_scene_length
)
if not detection_result.success:
return {
"success": False,
"error": f"场景检测失败: {detection_result.error}"
}
# 2. 保存检测结果
if task.output_dir:
task.output_dir.mkdir(parents=True, exist_ok=True)
result_file = task.output_dir / f"scenes.{state.output_format}"
self.result_saver.save_results(
detection_result,
result_file,
OutputFormat(state.output_format)
)
# 3. 视频切分(如果启用)
split_results = []
if state.enable_video_splitting and task.output_dir:
try:
# 创建切分选项
slice_options = SliceOptions(
crf=state.split_quality,
preset=state.split_preset
)
split_results_raw = self.splitter_service.split_video_by_scenes(
task.video_path,
detection_result.scenes,
task.output_dir / "scenes",
use_advanced=state.use_advanced_split,
options=slice_options
)
# 转换为字典格式
split_results = [
{
"scene_index": r.scene_index + 1,
"output_path": str(r.output_path),
"start_time": r.start_time,
"end_time": r.end_time,
"duration": r.duration,
"file_size": r.file_size,
"success": r.success,
"error": r.error
}
for r in split_results_raw
]
# 保存切分摘要
split_summary = self.splitter_service.create_split_summary(
task.video_path, split_results_raw
)
summary_file = task.output_dir / "split_summary.json"
import json
with open(summary_file, 'w', encoding='utf-8') as f:
json.dump(split_summary, f, indent=2, ensure_ascii=False)
except Exception as e:
logger.error(f"视频切分失败: {e}")
# 切分失败不影响整体任务成功
task.end_time = time.time()
return {
"success": True,
"detection_result": detection_result,
"split_results": split_results
}
except Exception as e:
task.end_time = time.time()
return {
"success": False,
"error": str(e)
}

View File

@@ -6,12 +6,14 @@ Workflow Manager
import time
from pathlib import Path
from typing import Dict, Any, Optional, Literal
from typing import Dict, Any, Optional, List, 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
from .batch_workflow_nodes import BatchWorkflowNodes
from ..types.batch_workflow_state import BatchSceneDetectionWorkflowState
class SceneDetectionWorkflowManager:
@@ -19,6 +21,7 @@ class SceneDetectionWorkflowManager:
def __init__(self):
self.nodes = WorkflowNodes()
self.batch_nodes = BatchWorkflowNodes()
self.workflow = None
def create_detection_workflow(self):
@@ -174,3 +177,111 @@ class SceneDetectionWorkflowManager:
# 非JSON-RPC模式抛出异常
logger.error(f"{error_msg}")
raise
def batch_detect_and_split(self, video_paths: List[Path], output_base_dir: Optional[Path] = None,
detector_type: DetectorType = DetectorType.CONTENT, threshold: float = 30.0,
min_scene_length: float = 1.0, output_format: OutputFormat = OutputFormat.JSON,
enable_ai_analysis: bool = False, enable_video_splitting: bool = True,
max_concurrent: int = 2, continue_on_error: bool = True,
use_advanced_split: bool = True, split_quality: int = 23, split_preset: str = "fast",
request_id: Optional[str] = None) -> Dict[str, Any]:
"""批量场景检测和视频切分"""
# 创建批量工作流状态
state = BatchSceneDetectionWorkflowState(
video_paths=video_paths,
output_base_dir=output_base_dir,
detector_type=detector_type.value,
threshold=threshold,
min_scene_length=min_scene_length,
output_format=output_format.value,
enable_ai_analysis=enable_ai_analysis,
enable_video_splitting=enable_video_splitting,
use_advanced_split=use_advanced_split,
split_quality=split_quality,
split_preset=split_preset,
max_concurrent=max_concurrent,
continue_on_error=continue_on_error,
request_id=request_id,
enable_jsonrpc=request_id is not None
)
try:
logger.info(f"🚀 开始批量场景检测和切分")
logger.info(f"📁 视频数量: {len(video_paths)}")
logger.info(f"📂 输出目录: {output_base_dir}")
logger.info(f"🎯 检测器: {detector_type.value}, 阈值: {threshold}")
logger.info(f"✂️ 视频切分: {'启用' if enable_video_splitting else '禁用'}")
logger.info(f"🔄 并发数: {max_concurrent}")
# 1. 验证输入
validation_result = self.batch_nodes.validate_batch_input(state)
if validation_result["workflow_state"] == "failed":
error_msg = f"批量输入验证失败: {'; '.join(validation_result['errors'])}"
if request_id:
state.send_error_result(-32602, error_msg, validation_result["errors"])
return {
"jsonrpc_mode": True,
"request_id": request_id,
"error_sent": True,
"error": error_msg
}
else:
raise ValueError(error_msg)
# 2. 批量处理
processing_result = self.batch_nodes.process_videos_batch(state)
# 3. 构建最终结果
final_result = {
"workflow_state": processing_result["workflow_state"],
"summary": processing_result["summary"],
"batch_results": {
"total_videos": state.total_tasks,
"completed_videos": state.completed_tasks,
"failed_videos": state.failed_tasks,
"success_rate": (state.completed_tasks / state.total_tasks * 100) if state.total_tasks > 0 else 0,
"output_base_dir": str(output_base_dir) if output_base_dir else None,
"enable_video_splitting": enable_video_splitting,
"tasks": [
{
"video_path": str(task.video_path),
"status": task.status,
"output_dir": str(task.output_dir) if task.output_dir else None,
"total_scenes": task.detection_result.total_scenes if task.detection_result else 0,
"detection_time": task.detection_result.detection_time if task.detection_result else 0,
"split_count": len(task.split_results) if task.split_results else 0,
"error": task.error,
"processing_time": (task.end_time - task.start_time) if task.start_time and task.end_time else 0
}
for task in state.tasks
]
}
}
if request_id:
state.send_final_result(final_result)
return {
"jsonrpc_mode": True,
"request_id": request_id,
"result_sent": True,
"result": final_result
}
else:
return final_result
except Exception as e:
error_msg = f"批量工作流执行失败: {str(e)}"
logger.error(error_msg)
if request_id:
state.send_error_result(-32603, error_msg, str(e))
return {
"jsonrpc_mode": True,
"request_id": request_id,
"error_sent": True,
"error": error_msg
}
else:
logger.error(f"{error_msg}")
raise