This commit is contained in:
root
2025-07-12 16:12:06 +08:00
parent 808c143bf8
commit 2b5867342f
22 changed files with 1785 additions and 2 deletions

View File

@@ -0,0 +1,238 @@
"""
单个文件场景检测工作流管理器
"""
from pathlib import Path
from typing import Dict, Any, Optional, List
from python_core.utils.logger import logger
from .single_workflow_nodes import SingleWorkflowNodes
from ..types.single_workflow_state import SingleSceneDetectionWorkflowState
from ..types.enums import DetectorType, OutputFormat
class SingleWorkflowManager:
"""单个文件场景检测工作流管理器"""
def __init__(self):
self.workflow_nodes = SingleWorkflowNodes()
def detect_and_split_single_video(
self,
video_path: Path,
output_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,
use_advanced_split: bool = True,
split_quality: int = 23,
split_preset: str = "fast",
max_video_duration: float = 60.0,
project_id: Optional[str] = None,
project_directory: Optional[Path] = None,
material_tags: Optional[List[str]] = None,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
单个视频文件的场景检测和切分
Args:
video_path: 视频文件路径
output_dir: 输出目录
detector_type: 检测器类型
threshold: 检测阈值
min_scene_length: 最小场景长度
output_format: 输出格式
enable_ai_analysis: 是否启用AI分析
enable_video_splitting: 是否启用视频切分
use_advanced_split: 是否使用高级切分
split_quality: 切分质量
split_preset: 切分预设
max_video_duration: 最大视频时长
project_id: 项目ID用于项目素材导入
project_directory: 项目目录(用于项目素材导入)
material_tags: 素材标签
request_id: 请求ID用于JSON-RPC进度报告
Returns:
Dict: 处理结果
"""
# 验证输入
if not video_path.exists():
raise FileNotFoundError(f"视频文件不存在: {video_path}")
if not video_path.suffix.lower() in {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}:
raise ValueError(f"不支持的视频格式: {video_path.suffix}")
# 设置默认输出目录
if output_dir is None:
output_dir = video_path.parent / f"{video_path.stem}_scenes"
# 确保输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"🚀 开始单个视频场景检测和切分")
logger.info(f"📁 视频文件: {video_path}")
logger.info(f"📂 输出目录: {output_dir}")
logger.info(f"🎯 检测器: {detector_type.value}, 阈值: {threshold}")
logger.info(f"✂️ 视频切分: {'启用' if enable_video_splitting else '禁用'}")
if project_id:
logger.info(f"📦 项目导入: {project_id}")
# 创建工作流状态
state = SingleSceneDetectionWorkflowState(
video_path=video_path,
output_dir=output_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_video_duration=max_video_duration,
project_id=project_id,
project_directory=project_directory,
material_tags=material_tags or [],
request_id=request_id,
enable_jsonrpc=request_id is not None
)
try:
# 执行工作流
result = self.workflow_nodes.process_single_video(state)
# 构建返回结果
response = {
"success": result["success"],
"video_path": str(video_path),
"output_dir": str(output_dir),
"processing_time": result.get("processing_time", 0),
"total_scenes": result.get("total_scenes", 0),
"total_segments": result.get("total_segments", 0),
"state": state.to_dict()
}
if result["success"]:
# 转换检测结果为字典
detection_result_dict = None
if result.get("detection_result"):
from dataclasses import asdict
detection_result_dict = asdict(result["detection_result"])
response.update({
"detection_result": detection_result_dict,
"split_results": [
{
"scene_index": sr.scene_index,
"output_path": str(sr.output_path),
"start_time": sr.start_time,
"end_time": sr.end_time,
"duration": sr.duration,
"file_size": sr.file_size,
"success": sr.success,
"error": sr.error
}
for sr in result.get("split_results", [])
]
})
logger.info(f"✅ 单个视频处理成功!")
logger.info(f"📊 处理统计:")
logger.info(f" 视频文件: {video_path.name}")
logger.info(f" 处理时间: {result.get('processing_time', 0):.1f}s")
logger.info(f" 场景数量: {result.get('total_scenes', 0)}")
logger.info(f" 切分片段: {result.get('total_segments', 0)}")
else:
response["error"] = result.get("error", "未知错误")
logger.error(f"❌ 单个视频处理失败: {response['error']}")
return response
except Exception as e:
error_msg = f"工作流执行失败: {str(e)}"
logger.error(f"{error_msg}")
return {
"success": False,
"error": error_msg,
"video_path": str(video_path),
"output_dir": str(output_dir),
"processing_time": state.processing_time or 0,
"state": state.to_dict()
}
def detect_and_split_for_project(
self,
video_path: Path,
project_id: str,
project_directory: Path,
material_tags: Optional[List[str]] = None,
detector_type: DetectorType = DetectorType.CONTENT,
threshold: float = 30.0,
min_scene_length: float = 1.0,
split_quality: int = 23,
split_preset: str = "fast",
max_video_duration: float = 2.0,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
为项目导入单个视频素材
Args:
video_path: 视频文件路径
project_id: 项目ID
project_directory: 项目目录
material_tags: 素材标签
其他参数: 检测和切分配置
Returns:
Dict: 处理结果
"""
# 创建临时输出目录
temp_output_dir = project_directory / "temp" / f"{video_path.stem}_processing"
return self.detect_and_split_single_video(
video_path=video_path,
output_dir=temp_output_dir,
detector_type=detector_type,
threshold=threshold,
min_scene_length=min_scene_length,
output_format=OutputFormat.JSON,
enable_ai_analysis=False,
enable_video_splitting=True,
use_advanced_split=True,
split_quality=split_quality,
split_preset=split_preset,
max_video_duration=max_video_duration,
project_id=project_id,
project_directory=project_directory,
material_tags=material_tags,
request_id=request_id
)
def get_supported_formats(self) -> List[str]:
"""获取支持的视频格式"""
return ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v']
def validate_video_file(self, video_path: Path) -> bool:
"""验证视频文件"""
if not video_path.exists():
return False
if not video_path.is_file():
return False
if video_path.suffix.lower() not in self.get_supported_formats():
return False
return True

View File

@@ -0,0 +1,315 @@
"""
单个文件场景检测工作流节点
"""
import os
import json
import hashlib
from pathlib import Path
from typing import Dict, Any
from datetime import datetime
from python_core.utils.logger import logger
from python_core.utils.jsonrpc_enhanced import ProgressLevel
from ..services.detector_service import SceneDetectorService
from ..services.ai_analysis_service import AIAnalysisService
from ..utils.result_saver import ResultSaver
from ..types.single_workflow_state import SingleSceneDetectionWorkflowState, SingleVideoTask
from python_core.services.ffmpeg_slice_service_sync import FfmpegSliceService, SliceOptions, SliceSegment
from python_core.services.project_material_service import ProjectMaterialService
class SingleWorkflowNodes:
"""单个文件场景检测工作流节点"""
def __init__(self):
self.detector_service = SceneDetectorService()
self.ai_service = AIAnalysisService()
self.result_saver = ResultSaver()
self.splitter_service = FfmpegSliceService()
self.material_service = ProjectMaterialService()
def process_single_video(self, state: SingleSceneDetectionWorkflowState) -> Dict[str, Any]:
"""处理单个视频文件"""
try:
state.mark_task_started()
state.update_progress("started", 0.0, f"开始处理视频: {state.video_path.name}")
logger.info(f"🎬 开始处理单个视频: {state.video_path.name}")
# 1. 场景检测
state.update_progress("detecting", 20.0, "正在进行场景检测...")
# 转换检测器类型
from ..types.enums import DetectorType
detector_type_enum = DetectorType(state.detector_type)
detection_result = self.detector_service.detect_scenes(
video_path=state.video_path,
detector_type=detector_type_enum,
threshold=state.threshold,
min_scene_length=state.min_scene_length
)
logger.info(f"✅ 场景检测完成: 检测到 {detection_result.total_scenes} 个场景")
# 2. 保存检测结果
if state.output_dir:
state.update_progress("saving", 40.0, "保存检测结果...")
# 转换输出格式
from ..types.enums import OutputFormat
output_format_enum = OutputFormat(state.output_format)
# 创建结果文件路径
result_filename = f"{state.video_path.stem}_scenes.{output_format_enum.value}"
result_path = state.output_dir / result_filename
self.result_saver.save_results(
detection_result,
result_path,
output_format_enum
)
# 3. AI分析如果启用
if state.enable_ai_analysis:
state.update_progress("ai_analysis", 60.0, "正在进行AI分析...")
try:
ai_result = self.ai_service.analyze_scenes(detection_result)
detection_result.ai_analysis = ai_result
logger.info("✅ AI分析完成")
except Exception as e:
logger.warning(f"⚠️ AI分析失败: {e}")
# 4. 视频切分(如果启用)
split_results = []
if state.enable_video_splitting and state.output_dir:
state.update_progress("splitting", 70.0, "正在进行视频切分...")
split_results = self._handle_video_splitting(state, detection_result)
# 5. 项目素材管理(如果是项目导入)
if state.project_id and state.project_directory:
state.update_progress("project_import", 90.0, "导入到项目素材库...")
self._handle_project_material_import(state, detection_result, split_results)
# 标记完成
state.mark_task_completed(detection_result, split_results)
state.update_progress("completed", 100.0, "处理完成")
logger.info(f"🎉 视频处理完成: {state.video_path.name}")
return {
"success": True,
"detection_result": detection_result,
"split_results": split_results,
"processing_time": state.processing_time,
"total_scenes": state.total_scenes,
"total_segments": state.total_split_segments
}
except Exception as e:
error_msg = f"处理视频失败: {str(e)}"
logger.error(f"{error_msg}")
state.mark_task_failed(error_msg)
state.update_progress("failed", 0.0, error_msg)
return {
"success": False,
"error": error_msg,
"processing_time": state.processing_time
}
def _handle_video_splitting(self, state: SingleSceneDetectionWorkflowState, detection_result) -> list:
"""处理视频切分"""
try:
# 创建切分选项
slice_options = SliceOptions(
crf=state.split_quality,
preset=state.split_preset
)
# 转换场景为SliceSegment
segments = [
SliceSegment(start=scene.start_time, end=scene.end_time)
for scene in detection_result.scenes
]
# 创建输出目录
scenes_dir = state.output_dir / "scenes"
scenes_dir.mkdir(parents=True, exist_ok=True)
# 检查是否无场景,如果是则跳过切分
if len(segments) <= 0:
logger.info(f"🔄 检测到 0 个场景切换,跳过切分,直接使用源文件")
# 直接复制源文件作为结果
import shutil
source_file = state.video_path
target_file = scenes_dir / f"{state.video_path.stem}_scene_001.mp4"
try:
shutil.copy2(source_file, target_file)
logger.info(f"✅ 源文件已复制到: {target_file}")
# 获取源文件元数据
metadata = self.splitter_service.get_video_metadata(str(source_file))
# 创建模拟的切分结果
slice_results = [(str(target_file), metadata)]
# 修正场景统计:无场景切换 = 1个场景整个视频
detection_result.total_scenes = 1
except Exception as e:
logger.error(f"❌ 复制源文件失败: {e}")
slice_results = []
else:
logger.info(f"🎬 检测到 {len(segments)} 个场景,开始切分")
# 生成输出路径
base_output_path = str(scenes_dir / f"{state.video_path.stem}_scene")
# 使用同步方法进行切分
slice_results = self.splitter_service.slice_video(
media_path=str(state.video_path),
segments=segments,
options=slice_options,
output_path=base_output_path
)
# 转换为兼容格式
split_results_raw = []
# 创建一个简单的结果对象
class SplitResult:
def __init__(self, scene_index, output_path, start_time, end_time, duration, file_size, success, error=None):
self.scene_index = scene_index
self.output_path = Path(output_path)
self.start_time = start_time
self.end_time = end_time
self.duration = duration
self.file_size = file_size
self.success = success
self.error = error
for i, (output_path, metadata) in enumerate(slice_results):
if len(detection_result.scenes) == 0:
# 无场景切换的情况:整个视频作为一个场景
split_result = SplitResult(
scene_index=0,
output_path=output_path,
start_time=0.0,
end_time=metadata.duration,
duration=metadata.duration,
file_size=metadata.size,
success=Path(output_path).exists(),
error=None if Path(output_path).exists() else "文件不存在"
)
split_results_raw.append(split_result)
else:
# 有场景切换的情况
scene = detection_result.scenes[i] if i < len(detection_result.scenes) else None
if scene:
split_result = SplitResult(
scene_index=i,
output_path=output_path,
start_time=scene.start_time,
end_time=scene.end_time,
duration=scene.duration,
file_size=metadata.size,
success=Path(output_path).exists(),
error=None if Path(output_path).exists() else "文件不存在"
)
split_results_raw.append(split_result)
# 4. 添加视频时长检查,如果时长大于最大视频时长那么就要进行二次切分
final_split_results = []
for split_result in split_results_raw:
try:
# 检查切分后的视频时长
video_path = str(split_result.output_path)
if split_result.duration > state.max_video_duration:
logger.info(f"⚠️ 片段 {split_result.scene_index + 1} 时长 {split_result.duration:.2f}s 超过限制 {state.max_video_duration:.2f}s进行二次切分")
# 进行二次切分
secondary_results = self.splitter_service.check_and_split_by_duration(
video_path=video_path,
max_duration=state.max_video_duration,
options=slice_options,
output_dir=str(split_result.output_path.parent)
)
# 处理二次切分结果
for i, (secondary_path, secondary_metadata) in enumerate(secondary_results):
if len(secondary_results) > 1:
# 如果进行了二次切分,创建新的结果对象
secondary_split_result = SplitResult(
scene_index=split_result.scene_index,
output_path=secondary_path,
start_time=split_result.start_time + (i * state.max_video_duration),
end_time=min(split_result.start_time + ((i + 1) * state.max_video_duration), split_result.end_time),
duration=secondary_metadata.duration,
file_size=secondary_metadata.size,
success=Path(secondary_path).exists(),
error=None if Path(secondary_path).exists() else "二次切分文件不存在"
)
final_split_results.append(secondary_split_result)
# 删除原始的过长片段(如果二次切分成功)
if Path(video_path).exists() and len(secondary_results) > 1:
try:
Path(video_path).unlink()
logger.info(f"🗑️ 已删除过长的原始片段: {Path(video_path).name}")
except Exception as e:
logger.warning(f"⚠️ 删除原始片段失败: {e}")
else:
# 如果没有进行二次切分(时长检查通过),保持原结果
final_split_results.append(split_result)
else:
# 时长未超过限制,直接添加到最终结果
final_split_results.append(split_result)
except Exception as e:
logger.error(f"❌ 处理片段 {split_result.scene_index + 1} 的时长检查时出错: {e}")
# 出错时保持原结果
final_split_results.append(split_result)
# 使用最终的切分结果
split_results_raw = final_split_results
# 更新统计信息
logger.info(f"📊 最终生成 {len(split_results_raw)} 个视频片段(包含二次切分)")
return split_results_raw
except Exception as e:
logger.error(f"❌ 视频切分失败: {e}")
return []
def _handle_project_material_import(self, state: SingleSceneDetectionWorkflowState, detection_result, split_results):
"""处理项目素材导入"""
try:
if not state.project_directory:
logger.warning("⚠️ 项目目录未设置,跳过素材导入")
return
# 使用项目素材管理服务
result = self.material_service.import_video_materials(
video_segments=split_results,
project_id=state.project_id,
project_directory=state.project_directory,
source_video_path=str(state.video_path),
material_tags=state.material_tags
)
if result["success"]:
logger.info(f"📝 项目素材导入完成: 新增 {result['imported_count']} 个素材,跳过 {result['skipped_count']} 个重复素材")
else:
logger.error(f"❌ 项目素材导入失败: {result.get('error', '未知错误')}")
except Exception as e:
logger.error(f"❌ 项目素材导入失败: {e}")