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

@@ -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