fix
This commit is contained in:
212
python_core/scene_detection/single_scene_detector.py
Normal file
212
python_core/scene_detection/single_scene_detector.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
单个文件场景检测器
|
||||
提供单个视频文件的场景检测和切分功能
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from python_core.utils.logger import logger
|
||||
from .workflows.single_workflow_manager import SingleWorkflowManager
|
||||
from .types.enums import DetectorType, OutputFormat
|
||||
|
||||
|
||||
class SingleSceneDetector:
|
||||
"""单个文件场景检测器"""
|
||||
|
||||
def __init__(self):
|
||||
self.workflow_manager = SingleWorkflowManager()
|
||||
logger.info("SingleSceneDetector 初始化完成")
|
||||
|
||||
def detect_and_split(
|
||||
self,
|
||||
video_path: str | Path,
|
||||
output_dir: Optional[str | 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,
|
||||
request_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
单个视频文件的场景检测和切分
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
output_dir: 输出目录,如果为None则使用视频文件同目录
|
||||
detector_type: 检测器类型 (content/threshold/adaptive)
|
||||
threshold: 检测阈值 (0-100)
|
||||
min_scene_length: 最小场景长度(秒)
|
||||
output_format: 输出格式 (json/csv/txt)
|
||||
enable_ai_analysis: 是否启用AI分析
|
||||
enable_video_splitting: 是否启用视频切分
|
||||
use_advanced_split: 是否使用高级切分
|
||||
split_quality: 切分质量 (CRF值, 18-28)
|
||||
split_preset: 编码预设 (ultrafast/fast/medium/slow)
|
||||
max_video_duration: 最大视频时长限制(秒)
|
||||
request_id: 请求ID(用于JSON-RPC进度报告)
|
||||
|
||||
Returns:
|
||||
Dict: 处理结果
|
||||
"""
|
||||
|
||||
# 转换路径
|
||||
video_path = Path(video_path)
|
||||
output_dir = Path(output_dir) if output_dir else None
|
||||
|
||||
return self.workflow_manager.detect_and_split_single_video(
|
||||
video_path=video_path,
|
||||
output_dir=output_dir,
|
||||
detector_type=detector_type,
|
||||
threshold=threshold,
|
||||
min_scene_length=min_scene_length,
|
||||
output_format=output_format,
|
||||
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,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
def import_to_project(
|
||||
self,
|
||||
video_path: str | Path,
|
||||
project_id: str,
|
||||
project_directory: str | 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: 素材标签列表
|
||||
detector_type: 检测器类型
|
||||
threshold: 检测阈值
|
||||
min_scene_length: 最小场景长度
|
||||
split_quality: 切分质量
|
||||
split_preset: 编码预设
|
||||
max_video_duration: 最大视频时长限制
|
||||
request_id: 请求ID
|
||||
|
||||
Returns:
|
||||
Dict: 导入结果
|
||||
"""
|
||||
|
||||
# 转换路径
|
||||
video_path = Path(video_path)
|
||||
project_directory = Path(project_directory)
|
||||
|
||||
return self.workflow_manager.detect_and_split_for_project(
|
||||
video_path=video_path,
|
||||
project_id=project_id,
|
||||
project_directory=project_directory,
|
||||
material_tags=material_tags,
|
||||
detector_type=detector_type,
|
||||
threshold=threshold,
|
||||
min_scene_length=min_scene_length,
|
||||
split_quality=split_quality,
|
||||
split_preset=split_preset,
|
||||
max_video_duration=max_video_duration,
|
||||
request_id=request_id
|
||||
)
|
||||
|
||||
def validate_video(self, video_path: str | Path) -> Dict[str, Any]:
|
||||
"""
|
||||
验证视频文件
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
|
||||
Returns:
|
||||
Dict: 验证结果
|
||||
"""
|
||||
video_path = Path(video_path)
|
||||
|
||||
result = {
|
||||
"valid": False,
|
||||
"path": str(video_path),
|
||||
"exists": False,
|
||||
"is_file": False,
|
||||
"supported_format": False,
|
||||
"file_size": 0,
|
||||
"error": None
|
||||
}
|
||||
|
||||
try:
|
||||
# 检查文件是否存在
|
||||
result["exists"] = video_path.exists()
|
||||
if not result["exists"]:
|
||||
result["error"] = "文件不存在"
|
||||
return result
|
||||
|
||||
# 检查是否为文件
|
||||
result["is_file"] = video_path.is_file()
|
||||
if not result["is_file"]:
|
||||
result["error"] = "路径不是文件"
|
||||
return result
|
||||
|
||||
# 检查文件格式
|
||||
supported_formats = self.workflow_manager.get_supported_formats()
|
||||
result["supported_format"] = video_path.suffix.lower() in supported_formats
|
||||
if not result["supported_format"]:
|
||||
result["error"] = f"不支持的格式: {video_path.suffix}"
|
||||
return result
|
||||
|
||||
# 获取文件大小
|
||||
result["file_size"] = video_path.stat().st_size
|
||||
|
||||
# 验证通过
|
||||
result["valid"] = True
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
|
||||
return result
|
||||
|
||||
def get_supported_formats(self) -> List[str]:
|
||||
"""获取支持的视频格式"""
|
||||
return self.workflow_manager.get_supported_formats()
|
||||
|
||||
def get_detector_types(self) -> List[str]:
|
||||
"""获取支持的检测器类型"""
|
||||
return [dt.value for dt in DetectorType]
|
||||
|
||||
def get_output_formats(self) -> List[str]:
|
||||
"""获取支持的输出格式"""
|
||||
return [of.value for of in OutputFormat]
|
||||
|
||||
def get_default_config(self) -> Dict[str, Any]:
|
||||
"""获取默认配置"""
|
||||
return {
|
||||
"detector_type": DetectorType.CONTENT.value,
|
||||
"threshold": 30.0,
|
||||
"min_scene_length": 1.0,
|
||||
"output_format": OutputFormat.JSON.value,
|
||||
"enable_ai_analysis": False,
|
||||
"enable_video_splitting": True,
|
||||
"use_advanced_split": True,
|
||||
"split_quality": 23,
|
||||
"split_preset": "fast",
|
||||
"max_video_duration": 60.0,
|
||||
"supported_formats": self.get_supported_formats(),
|
||||
"detector_types": self.get_detector_types(),
|
||||
"output_formats": self.get_output_formats()
|
||||
}
|
||||
185
python_core/scene_detection/types/single_workflow_state.py
Normal file
185
python_core/scene_detection/types/single_workflow_state.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
单个文件场景检测工作流状态
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from .models import DetectionResult
|
||||
from python_core.utils.jsonrpc_enhanced import ProgressLevel
|
||||
|
||||
|
||||
@dataclass
|
||||
class SingleVideoTask:
|
||||
"""单个视频处理任务"""
|
||||
video_path: Path
|
||||
output_dir: Optional[Path] = None
|
||||
|
||||
# 任务状态
|
||||
status: str = "pending" # pending, processing, completed, failed
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
# 处理结果
|
||||
detection_result: Optional[DetectionResult] = None
|
||||
split_results: List[Any] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SingleSceneDetectionWorkflowState:
|
||||
"""单个文件场景检测工作流状态"""
|
||||
|
||||
# 基础配置
|
||||
video_path: Path
|
||||
output_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_video_duration: float = 60.0 # 最大视频时长(秒)
|
||||
|
||||
# 项目配置(用于项目素材导入)
|
||||
project_id: Optional[str] = None
|
||||
project_directory: Optional[Path] = None
|
||||
material_tags: List[str] = field(default_factory=list)
|
||||
|
||||
# 工作流状态
|
||||
task: Optional[SingleVideoTask] = None
|
||||
current_step: str = "initialized"
|
||||
progress: float = 0.0
|
||||
|
||||
# JSON-RPC 支持
|
||||
request_id: Optional[str] = None
|
||||
enable_jsonrpc: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
"""初始化后处理"""
|
||||
if self.task is None:
|
||||
self.task = SingleVideoTask(
|
||||
video_path=self.video_path,
|
||||
output_dir=self.output_dir
|
||||
)
|
||||
|
||||
def update_progress(self, step: str, progress: float, message: str = ""):
|
||||
"""更新进度"""
|
||||
self.current_step = step
|
||||
self.progress = progress
|
||||
|
||||
if self.enable_jsonrpc and self.request_id:
|
||||
self.send_progress(step, message, ProgressLevel.INFO, {
|
||||
"progress": progress,
|
||||
"step": step
|
||||
})
|
||||
|
||||
def send_progress(self, event_type: str, message: str, level: ProgressLevel, data: Dict[str, Any] = None):
|
||||
"""发送进度消息(JSON-RPC)"""
|
||||
if not self.enable_jsonrpc or not self.request_id:
|
||||
return
|
||||
|
||||
try:
|
||||
from python_core.utils.jsonrpc_enhanced import create_response_handler
|
||||
handler = create_response_handler(self.request_id)
|
||||
handler.progress(
|
||||
step=event_type,
|
||||
progress=-1, # 不确定进度时使用-1
|
||||
message=message,
|
||||
level=level,
|
||||
data=data or {}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send progress: {e}")
|
||||
|
||||
def mark_task_started(self):
|
||||
"""标记任务开始"""
|
||||
if self.task:
|
||||
self.task.status = "processing"
|
||||
self.task.start_time = datetime.now()
|
||||
|
||||
def mark_task_completed(self, detection_result: DetectionResult, split_results: List[Any] = None):
|
||||
"""标记任务完成"""
|
||||
if self.task:
|
||||
self.task.status = "completed"
|
||||
self.task.end_time = datetime.now()
|
||||
self.task.detection_result = detection_result
|
||||
self.task.split_results = split_results or []
|
||||
|
||||
def mark_task_failed(self, error: str):
|
||||
"""标记任务失败"""
|
||||
if self.task:
|
||||
self.task.status = "failed"
|
||||
self.task.end_time = datetime.now()
|
||||
self.task.error = error
|
||||
|
||||
@property
|
||||
def is_completed(self) -> bool:
|
||||
"""是否已完成"""
|
||||
return self.task and self.task.status == "completed"
|
||||
|
||||
@property
|
||||
def is_failed(self) -> bool:
|
||||
"""是否失败"""
|
||||
return self.task and self.task.status == "failed"
|
||||
|
||||
@property
|
||||
def processing_time(self) -> Optional[float]:
|
||||
"""处理时间(秒)"""
|
||||
if not self.task or not self.task.start_time:
|
||||
return None
|
||||
|
||||
end_time = self.task.end_time or datetime.now()
|
||||
return (end_time - self.task.start_time).total_seconds()
|
||||
|
||||
@property
|
||||
def total_scenes(self) -> int:
|
||||
"""总场景数"""
|
||||
if self.task and self.task.detection_result:
|
||||
return self.task.detection_result.total_scenes
|
||||
return 0
|
||||
|
||||
@property
|
||||
def total_split_segments(self) -> int:
|
||||
"""总切分片段数"""
|
||||
if self.task and self.task.split_results:
|
||||
return len(self.task.split_results)
|
||||
return 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"video_path": str(self.video_path),
|
||||
"output_dir": str(self.output_dir) if self.output_dir else None,
|
||||
"detector_type": self.detector_type,
|
||||
"threshold": self.threshold,
|
||||
"min_scene_length": self.min_scene_length,
|
||||
"output_format": self.output_format,
|
||||
"enable_ai_analysis": self.enable_ai_analysis,
|
||||
"enable_video_splitting": self.enable_video_splitting,
|
||||
"use_advanced_split": self.use_advanced_split,
|
||||
"split_quality": self.split_quality,
|
||||
"split_preset": self.split_preset,
|
||||
"max_video_duration": self.max_video_duration,
|
||||
"project_id": self.project_id,
|
||||
"project_directory": str(self.project_directory) if self.project_directory else None,
|
||||
"material_tags": self.material_tags,
|
||||
"current_step": self.current_step,
|
||||
"progress": self.progress,
|
||||
"task_status": self.task.status if self.task else "pending",
|
||||
"processing_time": self.processing_time,
|
||||
"total_scenes": self.total_scenes,
|
||||
"total_split_segments": self.total_split_segments,
|
||||
"request_id": self.request_id
|
||||
}
|
||||
238
python_core/scene_detection/workflows/single_workflow_manager.py
Normal file
238
python_core/scene_detection/workflows/single_workflow_manager.py
Normal 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
|
||||
315
python_core/scene_detection/workflows/single_workflow_nodes.py
Normal file
315
python_core/scene_detection/workflows/single_workflow_nodes.py
Normal 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}")
|
||||
|
||||
Reference in New Issue
Block a user