fix
This commit is contained in:
254
python_core/scene_detection/workflows/batch_workflow_nodes.py
Normal file
254
python_core/scene_detection/workflows/batch_workflow_nodes.py
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user