Files
mxivideo/python_core/cli/commands/scene.py
2025-07-12 11:31:03 +08:00

345 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
场景检测命令模块
"""
from pathlib import Path
from typing import Optional
import typer
from python_core.cli.const import progress_reporter, console
from json import dumps
# 创建场景检测命令组
scene_app = typer.Typer(help="🎯 场景检测工具")
@scene_app.command()
def detect(
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
detector: str = typer.Option("content", help="🔧 检测器类型 (content/threshold/adaptive)"),
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值 (0-100)"),
min_scene_length: float = typer.Option(1.0, help="⏱️ 最小场景长度(秒)"),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="📄 输出文件路径"),
format: str = typer.Option("json", help="📋 输出格式 (json/csv/txt)")
):
"""🎯 检测单个视频的场景"""
try:
from python_core.cli.scene_detect import detector as scene_detector, DetectorType, OutputFormat
# 验证参数
try:
detector_type = DetectorType(detector)
except ValueError:
progress_reporter.error(f"❌ 无效的检测器类型: {detector}")
progress_reporter.info("💡 可用类型: content, threshold, adaptive")
raise typer.Exit(1)
try:
output_format = OutputFormat(format)
except ValueError:
progress_reporter.error(f"❌ 无效的输出格式: {format}")
progress_reporter.info("💡 可用格式: json, csv, txt")
raise typer.Exit(1)
# 执行检测
result = scene_detector.detect_scenes(
video_path, detector_type, threshold, min_scene_length
)
if not result.success:
progress_reporter.error(f"❌ 检测失败: {result.error}")
raise typer.Exit(1)
# 显示结果摘要
console.print(f"📊 检测结果摘要:")
console.print(f" 文件: {result.filename}")
console.print(f" 检测器: {result.detector_type}")
console.print(f" 阈值: {result.threshold}")
console.print(f" 场景数: {result.total_scenes}")
console.print(f" 总时长: {result.total_duration:.2f}")
console.print(f" 检测时间: {result.detection_time:.2f}")
# 显示场景详情
if result.scenes:
console.print(f"\n🎬 场景列表:")
for scene in result.scenes[:10]: # 只显示前10个场景
console.print(f" 场景 {scene.index}: {scene.start_time:.2f}s - {scene.end_time:.2f}s ({scene.duration:.2f}s)")
if len(result.scenes) > 10:
console.print(f" ... 还有 {len(result.scenes) - 10} 个场景")
# 保存结果
if output:
scene_detector.save_results(result, output, output_format)
progress_reporter.success(f"📄 结果已保存到: {output}")
return result
except Exception as e:
progress_reporter.error(f"❌ 命令执行失败: {e}")
raise typer.Exit(1)
@scene_app.command()
def batch_detect(
input_directory: Path = typer.Argument(..., help="📁 输入目录路径", exists=True),
detector: str = typer.Option("content", help="🔧 检测器类型 (content/threshold/adaptive)"),
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值 (0-100)"),
recursive: bool = typer.Option(False, "--recursive", "-r", help="🔄 递归扫描子目录"),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="📄 输出文件路径"),
format: str = typer.Option("json", help="📋 输出格式 (json/csv/txt)")
):
"""📦 批量检测目录中的所有视频"""
try:
from python_core.cli.scene_detect import detector as scene_detector, DetectorType, OutputFormat
# 验证参数
try:
detector_type = DetectorType(detector)
output_format = OutputFormat(format)
except ValueError as e:
progress_reporter.error(f"❌ 参数错误: {e}")
raise typer.Exit(1)
# 执行批量检测
results = scene_detector.batch_detect(
input_directory, detector_type, threshold, recursive
)
if not results:
progress_reporter.warning("⚠️ 没有检测到任何视频文件")
return
# 统计结果
successful = len([r for r in results if r.success])
failed = len(results) - successful
total_scenes = sum(r.total_scenes for r in results if r.success)
total_duration = sum(r.total_duration for r in results if r.success)
console.print(f"📊 批量检测结果:")
console.print(f" 总文件数: {len(results)}")
console.print(f" 成功: {successful}")
console.print(f" 失败: {failed}")
console.print(f" 总场景数: {total_scenes}")
console.print(f" 总时长: {total_duration:.2f}")
# 显示详细的场景信息
console.print(f"\n🎬 详细场景信息:")
for result in results:
if result.success and result.scenes:
console.print(f"\n📹 {result.filename} ({result.total_scenes} 个场景):")
for scene in result.scenes:
console.print(f" 场景 {scene.index}: {scene.start_time:.2f}s - {scene.end_time:.2f}s (时长: {scene.duration:.2f}s)")
elif result.success:
console.print(f"\n📹 {result.filename}: 无场景数据")
# 显示失败的文件
failed_files = [r for r in results if not r.success]
if failed_files:
console.print(f"\n❌ 失败的文件:")
for result in failed_files[:5]: # 只显示前5个失败文件
console.print(f" {result.filename}: {result.error}")
if len(failed_files) > 5:
console.print(f" ... 还有 {len(failed_files) - 5} 个失败文件")
# 保存结果
if output:
scene_detector.save_results(results, output, output_format)
progress_reporter.success(f"📄 结果已保存到: {output}")
return results
except Exception as e:
progress_reporter.error(f"❌ 批量检测失败: {e}")
raise typer.Exit(1)
@scene_app.command()
def compare(
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
thresholds: str = typer.Option("20,30,40", help="🎚️ 测试阈值列表(逗号分隔)"),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="📄 输出文件路径")
):
"""🔬 比较不同检测器的效果"""
try:
from python_core.cli.scene_detect import detector as scene_detector
# 解析阈值列表
try:
threshold_list = [float(t.strip()) for t in thresholds.split(",")]
except ValueError:
progress_reporter.error("❌ 无效的阈值格式,请使用逗号分隔的数字")
raise typer.Exit(1)
# 执行比较
result = scene_detector.compare_detectors(video_path, threshold_list)
# 显示分析结果
analysis = result["analysis"]
console.print(f"🔬 检测器比较结果:")
console.print(f" 视频: {Path(result['video_path']).name}")
console.print(f" 总测试数: {result['total_tests']}")
console.print(f" 成功测试数: {analysis['total_successful']}")
console.print(f" 推荐检测器: {analysis['best_detector']}")
console.print(f" 建议: {analysis['recommendation']}")
# 显示详细分析
console.print(f"\n📊 各检测器表现:")
for detector_name, stats in analysis["detector_analysis"].items():
console.print(f" 🔧 {detector_name}:")
console.print(f" 平均场景数: {stats['average_scenes']:.1f}")
console.print(f" 平均检测时间: {stats['average_detection_time']:.2f}")
console.print(f" 测试次数: {stats['test_count']}")
# 显示详细测试结果
console.print(f"\n🧪 详细测试结果:")
for test_result in result["results"]:
if test_result["success"]:
console.print(f" {test_result['detector']} (阈值: {test_result['threshold']}): "
f"{test_result['scenes']} 场景, {test_result['detection_time']:.2f}s")
else:
console.print(f" {test_result['detector']} (阈值: {test_result['threshold']}): "
f"{test_result['error']}")
# 保存结果
if output:
scene_detector.save_results(result, output)
progress_reporter.success(f"📄 结果已保存到: {output}")
return result
except Exception as e:
progress_reporter.error(f"❌ 比较测试失败: {e}")
raise typer.Exit(1)
@scene_app.command()
def split(
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
detector: str = typer.Option("content", help="🔧 检测器类型 (content/threshold/adaptive)"),
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值 (0-100)"),
output_dir: Optional[Path] = typer.Option(None, "--output-dir", "-d", help="📁 输出目录"),
filename_template: str = typer.Option("scene_{:03d}.mp4", help="📝 文件名模板")
):
"""✂️ 根据场景检测结果分割视频"""
try:
from python_core.cli.scene_detect import detector as scene_detector, DetectorType
from scenedetect.video_splitter import split_video_ffmpeg
# 验证参数
try:
detector_type = DetectorType(detector)
except ValueError:
progress_reporter.error(f"❌ 无效的检测器类型: {detector}")
raise typer.Exit(1)
# 设置输出目录
if output_dir is None:
output_dir = video_path.parent / f"{video_path.stem}_scenes"
output_dir.mkdir(parents=True, exist_ok=True)
# 先检测场景
progress_reporter.info("🎯 正在检测场景...")
result = scene_detector.detect_scenes(video_path, detector_type, threshold)
if not result.success:
progress_reporter.error(f"❌ 场景检测失败: {result.error}")
raise typer.Exit(1)
if not result.scenes:
progress_reporter.warning("⚠️ 未检测到任何场景")
return
# 构建场景列表PySceneDetect格式
from scenedetect import FrameTimecode
scene_list = []
# 假设视频帧率(实际应该从视频中获取)
fps = 25.0 # 默认帧率,实际使用时应该从视频文件中获取
for scene in result.scenes:
start_tc = FrameTimecode(timecode=scene.start_time, fps=fps)
end_tc = FrameTimecode(timecode=scene.end_time, fps=fps)
scene_list.append((start_tc, end_tc))
# 分割视频
progress_reporter.info(f"✂️ 正在分割视频到 {len(scene_list)} 个场景...")
try:
split_video_ffmpeg(
input_video_path=str(video_path),
scene_list=scene_list,
output_file_template=str(output_dir / filename_template),
video_name=video_path.stem,
arg_override=None,
hide_progress=False
)
progress_reporter.success(f"✅ 视频分割完成,输出到: {output_dir}")
console.print(f"📁 输出目录: {output_dir}")
console.print(f"🎬 场景数量: {len(scene_list)}")
# 列出生成的文件
output_files = list(output_dir.glob("*.mp4"))
if output_files:
console.print(f"\n📄 生成的文件:")
for file_path in sorted(output_files)[:10]:
console.print(f" {file_path.name}")
if len(output_files) > 10:
console.print(f" ... 还有 {len(output_files) - 10} 个文件")
except Exception as e:
progress_reporter.error(f"❌ 视频分割失败: {e}")
raise typer.Exit(1)
except Exception as e:
progress_reporter.error(f"❌ 分割命令执行失败: {e}")
raise typer.Exit(1)
@scene_app.command()
def info(
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True)
):
"""📋 显示视频基本信息"""
try:
import cv2
progress_reporter.info(f"📋 获取视频信息: {video_path.name}")
# 使用OpenCV获取视频信息
cap = cv2.VideoCapture(str(video_path))
if not cap.isOpened():
raise Exception("无法打开视频文件")
# 获取视频信息
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration = frame_count / fps if fps > 0 else 0
resolution = (width, height)
cap.release()
# 显示信息
console.print(f"📹 视频信息:")
console.print(f" 文件名: {video_path.name}")
console.print(f" 文件大小: {video_path.stat().st_size / (1024*1024):.2f} MB")
console.print(f" 分辨率: {resolution[0]}x{resolution[1]}")
console.print(f" 帧率: {fps:.2f} fps")
console.print(f" 总帧数: {frame_count}")
console.print(f" 时长: {duration:.2f}秒 ({duration//60:.0f}{duration%60:.0f}秒)")
progress_reporter.success(dumps({
"filename": video_path.name,
"file_size_mb": video_path.stat().st_size / (1024*1024),
"resolution": resolution,
"fps": fps,
"frame_count": frame_count,
"duration": duration
}))
except Exception as e:
progress_reporter.error(f"❌ 获取视频信息失败: {e}")
raise typer.Exit(1)