fix: 修复命令行工具

This commit is contained in:
root
2025-07-12 13:44:57 +08:00
parent 81035caf0e
commit 31de1e5a4d
30 changed files with 2728 additions and 1755 deletions

View File

@@ -4,14 +4,11 @@ MixVideo 主命令行接口
"""
import sys
from pathlib import Path
from typing import Optional
from python_core.cli.const import progress_reporter, console, project_root
from python_core.utils.logger import logger
import typer
# 导入命令模块
from python_core.cli.commands import scene_app
from python_core.cli.commands.jsonrpc_server import jsonrpc_app
from python_core.cli.commands import scene_detect
app = typer.Typer(
name="mixvideo",
@@ -23,38 +20,30 @@ app = typer.Typer(
• 📤 媒体管理 - 上传、处理、组织视频文件
• 📋 模板管理 - 视频模板导入导出
• ⚙️ 系统管理 - 配置、状态、存储管理
快速开始:
mixvideo scene detect video.mp4 # 检测场景
mixvideo scene batch-detect /videos # 批量检测
mixvideo scene split video.mp4 # 分割视频
mixvideo scene info video.mp4 # 视频信息
mixvideo jsonrpc start # 启动JSON-RPC服务器
""",
rich_markup_mode="rich",
no_args_is_help=True
)
# 添加命令组到主应用
app.add_typer(scene_app, name="scene")
app.add_typer(jsonrpc_app, name="jsonrpc")
app.add_typer(scene_detect, name="scene")
@app.command()
def init():
"""🚀 初始化MixVideo工作环境"""
progress_reporter.info("🚀 初始化MixVideo环境...")
logger.info("🚀 初始化MixVideo环境...")
# TODO: 实现初始化逻辑
progress_reporter.success("✅ 初始化完成")
logger.success("✅ 初始化完成")
def main():
"""主入口函数"""
try:
app()
except KeyboardInterrupt:
progress_reporter.error("\n👋 用户取消操作")
logger.error("\n👋 用户取消操作")
sys.exit(0)
except Exception as e:
progress_reporter.error(f"\n❌ [red]程序异常: {e}[/red]")
logger.error(f"\n❌ [red]程序异常: {e}[/red]")
sys.exit(1)
if __name__ == "__main__":

View File

@@ -3,6 +3,6 @@
CLI 命令模块
"""
from .scene import scene_app
from .scene_detect import scene_detect
__all__ = ["scene_app"]
__all__ = ["scene_detect"]

View File

@@ -1,303 +0,0 @@
#!/usr/bin/env python3
"""
JSON-RPC Server Command
JSON-RPC 服务器命令
Provides HTTP and WebSocket JSON-RPC server for scene detection services.
"""
from pathlib import Path
from typing import Optional
import signal
import sys
import typer
from python_core.cli.const import console
from python_core.utils.jsonrpc_server import JSONRPCServer, JSONRPCWebSocketServer, ServerConfig
# 创建子应用
jsonrpc_app = typer.Typer(help="🌐 JSON-RPC 服务器")
@jsonrpc_app.command()
def start(
host: str = typer.Option("localhost", help="🌐 服务器主机地址"),
port: int = typer.Option(8080, help="🔌 服务器端口"),
debug: bool = typer.Option(False, help="🐛 启用调试模式"),
cors: bool = typer.Option(True, help="🔗 启用CORS支持"),
websocket: bool = typer.Option(False, help="🔌 启用WebSocket服务器"),
max_request_size: int = typer.Option(1024*1024, help="📦 最大请求大小(字节)")
):
"""🚀 启动JSON-RPC服务器"""
config = ServerConfig(
host=host,
port=port,
debug=debug,
cors_enabled=cors,
max_request_size=max_request_size
)
console.print(f"🚀 [bold blue]启动JSON-RPC服务器[/bold blue]")
console.print(f"📍 地址: {host}:{port}")
console.print(f"🔧 模式: {'WebSocket' if websocket else 'HTTP'}")
console.print(f"🐛 调试: {'启用' if debug else '禁用'}")
console.print(f"🔗 CORS: {'启用' if cors else '禁用'}")
# 导入并注册所有JSON-RPC方法
console.print("📋 注册JSON-RPC方法...")
try:
# 导入场景检测模块以注册方法
from python_core.cli.scene_detect import detector
console.print("✅ 场景检测方法已注册")
except Exception as e:
console.print(f"⚠️ 注册方法时出错: {e}")
try:
if websocket:
# WebSocket服务器
import asyncio
server = JSONRPCWebSocketServer(config)
# 注册信号处理
def signal_handler(sig, frame):
console.print("\n🛑 [yellow]收到停止信号,正在关闭服务器...[/yellow]")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 启动异步服务器
asyncio.run(server.start())
else:
# HTTP服务器
server = JSONRPCServer(config)
# 注册信号处理
def signal_handler(sig, frame):
console.print("\n🛑 [yellow]收到停止信号,正在关闭服务器...[/yellow]")
server.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 启动服务器
server.start(blocking=True)
except Exception as e:
console.print(f"❌ [red]服务器启动失败: {e}[/red]")
raise typer.Exit(1)
@jsonrpc_app.command()
def test(
host: str = typer.Option("localhost", help="🌐 服务器主机地址"),
port: int = typer.Option(8080, help="🔌 服务器端口"),
method: str = typer.Option("scene.detect", help="🎯 测试方法名"),
video_path: Optional[str] = typer.Option(None, help="📹 测试视频路径")
):
"""🧪 测试JSON-RPC服务器"""
import requests
import json
if not video_path:
video_path = "assets/1/1752032011698.mp4" # 默认测试视频
console.print(f"🧪 [bold blue]测试JSON-RPC服务器[/bold blue]")
console.print(f"📍 服务器: http://{host}:{port}")
console.print(f"🎯 方法: {method}")
console.print(f"📹 视频: {video_path}")
# 准备测试请求
test_requests = {
"scene.detect": {
"video_path": video_path,
"detector_type": "content",
"threshold": 30.0,
"min_scene_length": 1.0
},
"scene.get_video_info": {
"video_path": video_path
},
"scene.detect_workflow": {
"video_path": video_path,
"detector_type": "content",
"threshold": 15.0,
"enable_ai_analysis": False
}
}
params = test_requests.get(method, {"video_path": video_path})
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1
}
try:
console.print("📤 发送请求...")
response = requests.post(
f"http://{host}:{port}",
json=payload,
headers={'Content-Type': 'application/json'},
timeout=30
)
console.print(f"📥 响应状态: {response.status_code}")
if response.status_code == 200:
result = response.json()
console.print("✅ [green]请求成功[/green]")
console.print("📋 响应内容:")
console.print(json.dumps(result, indent=2, ensure_ascii=False))
else:
console.print(f"❌ [red]请求失败: {response.text}[/red]")
except requests.exceptions.ConnectionError:
console.print(f"❌ [red]无法连接到服务器 http://{host}:{port}[/red]")
console.print("💡 请确保服务器已启动")
raise typer.Exit(1)
except Exception as e:
console.print(f"❌ [red]测试失败: {e}[/red]")
raise typer.Exit(1)
@jsonrpc_app.command()
def methods():
"""📋 列出可用的JSON-RPC方法"""
console.print("📋 [bold blue]可用的JSON-RPC方法[/bold blue]")
console.print("=" * 60)
methods_info = [
{
"method": "scene.detect",
"description": "基础场景检测",
"params": ["video_path", "detector_type?", "threshold?", "min_scene_length?"]
},
{
"method": "scene.detect_workflow",
"description": "LangGraph工作流场景检测",
"params": ["video_path", "detector_type?", "threshold?", "min_scene_length?",
"output_path?", "output_format?", "enable_ai_analysis?"]
},
{
"method": "scene.get_video_info",
"description": "获取视频信息",
"params": ["video_path"]
},
{
"method": "scene.batch_detect",
"description": "批量场景检测",
"params": ["directory", "detector_type?", "threshold?", "min_scene_length?",
"output_dir?", "output_format?"]
}
]
for info in methods_info:
console.print(f"\n🎯 [bold]{info['method']}[/bold]")
console.print(f" 📝 {info['description']}")
console.print(f" 📋 参数: {', '.join(info['params'])}")
console.print("\n💡 [yellow]参数说明:[/yellow]")
console.print(" • ? 表示可选参数")
console.print(" • detector_type: content/threshold/adaptive")
console.print(" • output_format: json/csv/txt")
console.print(" • threshold: 0-100")
@jsonrpc_app.command()
def client_example():
"""📖 显示客户端调用示例"""
console.print("📖 [bold blue]JSON-RPC 客户端调用示例[/bold blue]")
console.print("=" * 60)
examples = [
{
"title": "Python requests 示例",
"code": '''import requests
import json
def call_scene_detect(video_path, threshold=30.0):
payload = {
"jsonrpc": "2.0",
"method": "scene.detect",
"params": {
"video_path": video_path,
"threshold": threshold
},
"id": 1
}
response = requests.post(
"http://localhost:8080",
json=payload,
headers={'Content-Type': 'application/json'}
)
return response.json()
# 调用示例
result = call_scene_detect("video.mp4", 15.0)
print(result)'''
},
{
"title": "curl 命令示例",
"code": '''curl -X POST http://localhost:8080 \\
-H "Content-Type: application/json" \\
-d '{
"jsonrpc": "2.0",
"method": "scene.detect",
"params": {
"video_path": "video.mp4",
"threshold": 15.0
},
"id": 1
}'
'''
},
{
"title": "JavaScript fetch 示例",
"code": '''async function detectScenes(videoPath, threshold = 30.0) {
const response = await fetch('http://localhost:8080', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'scene.detect',
params: {
video_path: videoPath,
threshold: threshold
},
id: 1
})
});
return await response.json();
}
// 调用示例
detectScenes('video.mp4', 15.0).then(result => {
console.log(result);
});'''
}
]
for example in examples:
console.print(f"\n📝 [bold]{example['title']}[/bold]")
console.print("```")
console.print(example['code'])
console.print("```")
if __name__ == "__main__":
jsonrpc_app()

View File

@@ -1,549 +0,0 @@
#!/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)"),
use_workflow: bool = typer.Option(False, "--workflow", help="🔄 使用LangGraph工作流"),
enable_ai: bool = typer.Option(True, "--ai/--no-ai", help="🧠 启用AI分析")
):
"""🎯 检测单个视频的场景"""
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)
# 选择执行方式
if use_workflow:
# 使用LangGraph工作流
progress_reporter.info("🔄 使用LangGraph工作流进行检测...")
workflow_result = scene_detector.detect_with_workflow(
video_path, detector_type, threshold, min_scene_length,
output, output_format, enable_ai
)
result = workflow_result.get("detection_result")
ai_analysis = workflow_result.get("ai_analysis")
video_info = workflow_result.get("video_info")
errors = workflow_result.get("errors", [])
if errors:
for error in errors:
progress_reporter.error(f"{error}")
raise typer.Exit(1)
if not result or not result.success:
progress_reporter.error(f"❌ 工作流检测失败: {result.error if result else '未知错误'}")
raise typer.Exit(1)
# 显示工作流结果
console.print(f"🔄 LangGraph工作流检测完成")
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 video_info:
console.print(f"\n📹 视频信息:")
console.print(f" 分辨率: {video_info.get('resolution', 'Unknown')}")
console.print(f" 帧率: {video_info.get('fps', 0):.2f} fps")
console.print(f" 总帧数: {video_info.get('frame_count', 0)}")
# 显示AI分析结果
if ai_analysis and enable_ai:
console.print(f"\n🧠 AI分析结果:")
console.print(f"{ai_analysis}")
# 显示场景详情
if result.scenes:
console.print(f"\n🎬 场景列表:")
for scene in result.scenes[: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} 个场景")
return workflow_result
else:
# 使用传统方法
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)
@scene_app.command()
def workflow(
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)"),
enable_ai: bool = typer.Option(True, "--ai/--no-ai", help="🧠 启用AI分析"),
interactive: bool = typer.Option(False, "--interactive", "-i", help="🔄 交互式工作流")
):
"""🔄 使用LangGraph工作流进行智能场景检测"""
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)
console.print("🔄 [bold blue]LangGraph智能场景检测工作流[/bold blue]")
console.print("=" * 60)
if interactive:
# 交互式模式
console.print("🎯 交互式模式启动...")
# 确认参数
console.print(f"\n📋 检测参数:")
console.print(f" 视频文件: {video_path}")
console.print(f" 检测器: {detector}")
console.print(f" 阈值: {threshold}")
console.print(f" 最小场景长度: {min_scene_length}")
console.print(f" AI分析: {'启用' if enable_ai else '禁用'}")
if not typer.confirm("\n是否继续执行?"):
console.print("❌ 用户取消操作")
return
# 执行工作流
progress_reporter.info("🚀 启动LangGraph工作流...")
workflow_result = scene_detector.detect_with_workflow(
video_path, detector_type, threshold, min_scene_length,
output, output_format, enable_ai
)
result = workflow_result.get("detection_result")
ai_analysis = workflow_result.get("ai_analysis")
video_info = workflow_result.get("video_info")
workflow_state = workflow_result.get("workflow_state")
errors = workflow_result.get("errors", [])
# 检查错误
if errors:
console.print("\n❌ [red]工作流执行中发现错误:[/red]")
for error in errors:
console.print(f"{error}")
raise typer.Exit(1)
if not result or not result.success:
progress_reporter.error(f"❌ 工作流检测失败: {result.error if result else '未知错误'}")
raise typer.Exit(1)
# 显示完整结果
console.print("\n" + "=" * 60)
console.print("🎉 [bold green]LangGraph工作流执行完成[/bold green]")
console.print("=" * 60)
# 工作流状态
console.print(f"\n🔄 工作流状态: [bold]{workflow_state}[/bold]")
# 视频信息
if video_info:
console.print(f"\n📹 [bold]视频信息[/bold]:")
console.print(f" 文件名: {result.filename}")
console.print(f" 分辨率: {video_info.get('resolution', 'Unknown')}")
console.print(f" 帧率: {video_info.get('fps', 0):.2f} fps")
console.print(f" 总帧数: {video_info.get('frame_count', 0):,}")
console.print(f" 时长: {result.total_duration:.2f}")
# 检测结果
console.print(f"\n🎯 [bold]检测结果[/bold]:")
console.print(f" 检测器类型: {result.detector_type}")
console.print(f" 检测阈值: {result.threshold}")
console.print(f" 场景数量: [bold green]{result.total_scenes}[/bold green]")
console.print(f" 检测耗时: {result.detection_time:.2f}")
# 场景详情
if result.scenes:
console.print(f"\n🎬 [bold]场景详情[/bold]:")
for scene in result.scenes:
duration_color = "green" if scene.duration >= 2.0 else "yellow" if scene.duration >= 1.0 else "red"
console.print(
f" 场景 {scene.index:2d}: "
f"{scene.start_time:6.2f}s - {scene.end_time:6.2f}s "
f"([{duration_color}]{scene.duration:5.2f}s[/{duration_color}])"
)
# AI分析结果
if ai_analysis and enable_ai:
console.print(f"\n🧠 [bold]AI智能分析[/bold]:")
console.print("-" * 50)
console.print(ai_analysis)
console.print("-" * 50)
elif enable_ai:
console.print(f"\n⚠️ AI分析不可用")
# 保存信息
if output:
console.print(f"\n💾 结果已保存到: [bold]{output}[/bold]")
# 交互式后续操作
if interactive:
console.print(f"\n🎯 [bold]后续操作选项[/bold]:")
console.print("1. 保存结果到文件")
console.print("2. 调整参数重新检测")
console.print("3. 分割视频")
console.print("4. 退出")
choice = typer.prompt("请选择操作 (1-4)", type=int, default=4)
if choice == 1 and not output:
output_path = typer.prompt("请输入输出文件路径", type=str)
scene_detector.save_results(result, Path(output_path), output_format)
console.print(f"✅ 结果已保存到: {output_path}")
elif choice == 2:
console.print("🔄 参数调整功能开发中...")
elif choice == 3:
console.print("✂️ 视频分割功能开发中...")
else:
console.print("👋 感谢使用LangGraph工作流")
return workflow_result
except Exception as e:
progress_reporter.error(f"❌ 工作流命令执行失败: {e}")
raise typer.Exit(1)

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Scene Detection CLI - Refactored
场景检测命令行工具 - 重构版
使用重构后的场景检测模块,代码更简洁、模块化更好。
"""
import typer
from pathlib import Path
from typing import Optional, List
from rich.console import Console
from rich.table import Table
from python_core.scene_detection import (
SceneDetector,
DetectorType,
OutputFormat
)
from python_core.utils.logger import logger
scene_detect = typer.Typer(help="场景检测工具 - 重构版")
console = Console()
@scene_detect.command("split")
def split(
video_path: Path = typer.Argument(..., help="视频文件路径"),
detector_type: DetectorType = typer.Option(DetectorType.CONTENT, "--detector", "-d", help="检测器类型"),
threshold: float = typer.Option(30.0, "--threshold", "-t", help="检测阈值"),
min_scene_length: float = typer.Option(1.0, "--min-length", "-m", help="最小场景长度(秒)"),
output: Optional[Path] = typer.Option(None, "--output", "-o", help="输出文件路径"),
output_format: OutputFormat = typer.Option(OutputFormat.JSON, "--format", "-f", help="输出格式"),
ai_analysis: bool = typer.Option(True, "--ai/--no-ai", help="启用/禁用AI分析"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="详细输出")
):
"""使用LangGraph工作流进行场景检测"""
console.print(f"🔄 使用工作流检测视频: [bold blue]{video_path}[/bold blue]")
try:
# 创建检测器
detector = SceneDetector()
# 执行工作流检测
result = detector.detect_with_workflow(
video_path, detector_type, threshold, min_scene_length,
output, output_format, ai_analysis
)
# 显示结果
if result.get("workflow_state") == "completed":
detection_result = result.get("detection_result")
if detection_result and detection_result.success:
console.print(f"✅ 工作流完成: [bold green]{detection_result.total_scenes}[/bold green] 个场景")
console.print(f"📊 检测时间: {detection_result.detection_time:.2f}")
# 显示AI分析结果
ai_analysis_result = result.get("ai_analysis")
if ai_analysis_result and ai_analysis_result != "AI分析已禁用":
console.print("\n🧠 AI分析结果:")
console.print(ai_analysis_result[:500] + "..." if len(ai_analysis_result) > 500 else ai_analysis_result)
# 显示场景列表
if verbose:
_display_scenes_table(detection_result.scenes)
else:
console.print(f"❌ 检测失败: [bold red]{detection_result.error if detection_result else '未知错误'}[/bold red]")
raise typer.Exit(1)
else:
errors = result.get("errors", [])
error_msg = "; ".join(errors) if errors else "工作流执行失败"
console.print(f"❌ 工作流失败: [bold red]{error_msg}[/bold red]")
raise typer.Exit(1)
except Exception as e:
console.print(f"❌ 执行失败: [bold red]{str(e)}[/bold red]")
raise typer.Exit(1)
def _display_scenes_table(scenes):
"""显示场景表格"""
table = Table(title="检测到的场景")
table.add_column("场景", style="cyan")
table.add_column("开始时间", style="green")
table.add_column("结束时间", style="green")
table.add_column("时长", style="yellow")
for scene in scenes:
table.add_row(
str(scene.index + 1),
f"{scene.start_time:.2f}s",
f"{scene.end_time:.2f}s",
f"{scene.duration:.2f}s"
)
console.print(table)
if __name__ == "__main__":
scene_detect()

View File

@@ -1,6 +0,0 @@
from python_core.utils.jsonrpc_enhanced import create_progress_reporter
from rich.console import Console
from python_core.config import settings
console = Console()
progress_reporter = create_progress_reporter()
project_root = settings.project_root

View File

@@ -1,871 +0,0 @@
#!/usr/bin/env python3
"""
PySceneDetect 场景检测命令行工具 - LangGraph增强版
"""
import json
import time
from pathlib import Path
from typing import Optional, List, Literal, Dict, Any
from enum import Enum
from dataclasses import dataclass, asdict
from python_core.cli.const import progress_reporter
# PySceneDetect 依赖
from scenedetect import open_video, SceneManager
from scenedetect.detectors import ContentDetector, ThresholdDetector
# LangGraph 依赖
from langgraph.graph import StateGraph, START, END
from langgraph.graph.state import CompiledStateGraph
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
class DetectorType(str, Enum):
"""检测器类型"""
CONTENT = "content"
THRESHOLD = "threshold"
ADAPTIVE = "adaptive"
class OutputFormat(str, Enum):
"""输出格式"""
JSON = "json"
CSV = "csv"
TXT = "txt"
@dataclass
class SceneInfo:
"""场景信息"""
index: int
start_time: float
end_time: float
duration: float
start_frame: int
end_frame: int
@dataclass
class DetectionResult:
"""检测结果"""
video_path: str
filename: str
detector_type: str
threshold: float
total_scenes: int
total_duration: float
detection_time: float
scenes: List[SceneInfo]
success: bool
error: Optional[str] = None
# LangGraph 工作流状态
@dataclass
class SceneDetectionWorkflowState:
"""场景检测工作流状态"""
# 输入参数
video_path: str = ""
detector_type: str = "content"
threshold: float = 30.0
min_scene_length: float = 1.0
output_path: Optional[str] = None
output_format: str = "json"
enable_ai_analysis: bool = True
# 工作流状态
current_stage: str = "init"
progress: int = 0
total_steps: int = 5
# 中间结果
video_info: Dict[str, Any] = None
raw_scenes: List[Any] = None
processed_scenes: List[SceneInfo] = None
# 最终结果
detection_result: Optional[DetectionResult] = None
ai_analysis: Optional[str] = None
# 错误处理
errors: List[str] = None
def __post_init__(self):
if self.video_info is None:
self.video_info = {}
if self.raw_scenes is None:
self.raw_scenes = []
if self.processed_scenes is None:
self.processed_scenes = []
if self.errors is None:
self.errors = []
class SceneDetector:
"""场景检测器"""
def __init__(self):
self.supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
# 初始化AI分析器如果可用
try:
self.llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
self.ai_enabled = True
except Exception as e:
progress_reporter.warning(f"⚠️ AI分析器初始化失败: {e}")
self.ai_enabled = False
def detect_scenes(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
threshold: float = 30.0, min_scene_length: float = 1.0) -> DetectionResult:
"""检测单个视频的场景"""
start_time = time.time()
filename = video_path.name
try:
progress_reporter.info(f"🎬 开始检测: {filename}")
# 打开视频文件
video = open_video(str(video_path))
scene_manager = SceneManager()
# 根据类型添加检测器
if detector_type == DetectorType.CONTENT:
scene_manager.add_detector(ContentDetector(threshold=threshold))
progress_reporter.info(f"📊 使用内容检测器,阈值: {threshold}")
elif detector_type == DetectorType.THRESHOLD:
scene_manager.add_detector(ThresholdDetector(threshold=threshold))
progress_reporter.info(f"📊 使用阈值检测器,阈值: {threshold}")
elif detector_type == DetectorType.ADAPTIVE:
# 自适应:同时使用两种检测器
scene_manager.add_detector(ContentDetector(threshold=threshold))
scene_manager.add_detector(ThresholdDetector(threshold=threshold * 0.8))
progress_reporter.info(f"📊 使用自适应检测器,阈值: {threshold}")
# 开始检测
progress_reporter.info("🔍 正在分析视频帧...")
scene_manager.detect_scenes(video)
scene_list = scene_manager.get_scene_list()
progress_reporter.info(f"✅ 检测到 {len(scene_list)} 个场景")
# 获取视频信息
fps = video.frame_rate
total_duration = video.duration.get_seconds()
progress_reporter.info(f"📊 视频信息: {fps:.2f}fps, {total_duration:.2f}")
# 构建场景信息
scenes = []
if not scene_list:
# 如果没有检测到场景变化,整个视频就是一个场景
scene_info = SceneInfo(
index=0,
start_time=0.0,
end_time=total_duration,
duration=total_duration,
start_frame=0,
end_frame=int(total_duration * fps) if fps > 0 else 0
)
scenes.append(scene_info)
progress_reporter.info(f"📝 无场景变化,整个视频作为单一场景: {total_duration:.2f}")
else:
# 处理检测到的场景
for start_time_scene, end_time_scene in scene_list:
start_seconds = start_time_scene.get_seconds()
end_seconds = end_time_scene.get_seconds()
duration = end_seconds - start_seconds
# 跳过太短的场景
if duration < min_scene_length:
continue
scene_info = SceneInfo(
index=len(scenes),
start_time=start_seconds,
end_time=end_seconds,
duration=duration,
start_frame=start_time_scene.get_frames(),
end_frame=end_time_scene.get_frames()
)
scenes.append(scene_info)
detection_time = time.time() - start_time
result = DetectionResult(
video_path=str(video_path),
filename=filename,
detector_type=detector_type.value,
threshold=threshold,
total_scenes=len(scenes),
total_duration=total_duration,
detection_time=detection_time,
scenes=scenes,
success=True
)
progress_reporter.success(f"🎯 检测完成: {len(scenes)} 个场景,耗时 {detection_time:.2f}")
return result
except Exception as e:
detection_time = time.time() - start_time
error_msg = str(e)
progress_reporter.error(f"❌ 检测失败: {error_msg}")
return DetectionResult(
video_path=str(video_path),
filename=filename,
detector_type=detector_type.value,
threshold=threshold,
total_scenes=0,
total_duration=0.0,
detection_time=detection_time,
scenes=[],
success=False,
error=error_msg
)
def batch_detect(self, input_directory: Path, detector_type: DetectorType = DetectorType.CONTENT,
threshold: float = 30.0, recursive: bool = False) -> List[DetectionResult]:
"""批量检测场景"""
progress_reporter.info(f"📦 开始批量检测: {input_directory}")
# 扫描视频文件
video_files = self._scan_video_files(input_directory, recursive)
if not video_files:
progress_reporter.warning("⚠️ 未找到视频文件")
return []
progress_reporter.info(f"📋 找到 {len(video_files)} 个视频文件")
results = []
for i, video_file in enumerate(video_files):
progress_reporter.info(f"📊 处理进度: {i+1}/{len(video_files)} - {video_file.name}")
result = self.detect_scenes(video_file, detector_type, threshold)
results.append(result)
successful = len([r for r in results if r.success])
progress_reporter.success(f"🎉 批量检测完成: {successful}/{len(results)} 成功")
return results
def compare_detectors(self, video_path: Path, thresholds: List[float] = None) -> dict:
"""比较不同检测器效果"""
if thresholds is None:
thresholds = [20.0, 30.0, 40.0]
progress_reporter.info(f"🔬 开始检测器比较: {video_path.name}")
detectors = [DetectorType.CONTENT, DetectorType.THRESHOLD, DetectorType.ADAPTIVE]
results = []
total_tests = len(detectors) * len(thresholds)
current_test = 0
for detector in detectors:
for threshold in thresholds:
current_test += 1
progress_reporter.info(f"🧪 测试 {current_test}/{total_tests}: {detector.value} (阈值: {threshold})")
result = self.detect_scenes(video_path, detector, threshold)
results.append({
"detector": detector.value,
"threshold": threshold,
"scenes": result.total_scenes,
"duration": result.total_duration,
"detection_time": result.detection_time,
"success": result.success,
"error": result.error
})
# 分析结果
analysis = self._analyze_comparison(results)
comparison_result = {
"video_path": str(video_path),
"total_tests": total_tests,
"results": results,
"analysis": analysis
}
progress_reporter.success("🔬 检测器比较完成")
return comparison_result
def _scan_video_files(self, directory: Path, recursive: bool = False) -> List[Path]:
"""扫描视频文件"""
video_files = []
if recursive:
for ext in self.supported_formats:
video_files.extend(directory.rglob(f"*{ext}"))
else:
for ext in self.supported_formats:
video_files.extend(directory.glob(f"*{ext}"))
return sorted(video_files)
def _analyze_comparison(self, results: List[dict]) -> dict:
"""分析比较结果"""
successful_results = [r for r in results if r["success"]]
if not successful_results:
return {"message": "所有测试都失败了"}
# 按检测器分组
by_detector = {}
for result in successful_results:
detector = result["detector"]
if detector not in by_detector:
by_detector[detector] = []
by_detector[detector].append(result)
# 分析每个检测器
detector_analysis = {}
for detector, detector_results in by_detector.items():
avg_scenes = sum(r["scenes"] for r in detector_results) / len(detector_results)
avg_time = sum(r["detection_time"] for r in detector_results) / len(detector_results)
detector_analysis[detector] = {
"average_scenes": avg_scenes,
"average_detection_time": avg_time,
"test_count": len(detector_results)
}
# 推荐最佳检测器
best_detector = max(detector_analysis.keys(),
key=lambda d: detector_analysis[d]["average_scenes"])
return {
"total_successful": len(successful_results),
"detector_analysis": detector_analysis,
"best_detector": best_detector,
"recommendation": f"推荐使用 {best_detector} 检测器"
}
def save_results(self, results, output_path: Path, format: OutputFormat = OutputFormat.JSON):
"""保存检测结果"""
output_path.parent.mkdir(parents=True, exist_ok=True)
if format == OutputFormat.JSON:
self._save_json(results, output_path)
elif format == OutputFormat.CSV:
self._save_csv(results, output_path)
elif format == OutputFormat.TXT:
self._save_txt(results, output_path)
progress_reporter.success(f"📄 结果已保存: {output_path}")
def _save_json(self, results, output_path: Path):
"""保存JSON格式"""
if isinstance(results, list):
# 批量结果
data = [asdict(result) for result in results]
else:
# 单个结果或比较结果
if hasattr(results, '__dict__'):
data = asdict(results)
else:
data = results
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def _save_csv(self, results, output_path: Path):
"""保存CSV格式"""
import csv
with open(output_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
if isinstance(results, list) and results:
# 批量结果
writer.writerow(['filename', 'detector', 'threshold', 'scenes', 'duration', 'detection_time', 'success'])
for result in results:
writer.writerow([
result.filename,
result.detector_type,
result.threshold,
result.total_scenes,
result.total_duration,
result.detection_time,
result.success
])
def _save_txt(self, results, output_path: Path):
"""保存文本格式"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write("PySceneDetect 场景检测结果\n")
f.write("=" * 50 + "\n\n")
if isinstance(results, list):
# 批量结果
for result in results:
f.write(f"文件: {result.filename}\n")
f.write(f" 检测器: {result.detector_type}\n")
f.write(f" 阈值: {result.threshold}\n")
f.write(f" 场景数: {result.total_scenes}\n")
f.write(f" 总时长: {result.total_duration:.2f}\n")
f.write(f" 检测时间: {result.detection_time:.2f}\n")
f.write(f" 状态: {'成功' if result.success else '失败'}\n")
if result.scenes:
f.write(" 场景列表:\n")
for scene in result.scenes:
f.write(f" 场景 {scene.index}: {scene.start_time:.2f}s - {scene.end_time:.2f}s ({scene.duration:.2f}s)\n")
f.write("\n")
# ==================== LangGraph 工作流方法 ====================
def create_detection_workflow(self) -> Optional[CompiledStateGraph]:
"""创建场景检测工作流"""
# 定义工作流节点
def validate_input(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""验证输入参数"""
progress_reporter.info("🔍 验证输入参数...")
video_path = Path(state.video_path)
errors = []
# 验证文件存在
if not video_path.exists():
errors.append(f"视频文件不存在: {video_path}")
# 验证文件格式
if video_path.suffix.lower() not in self.supported_formats:
errors.append(f"不支持的文件格式: {video_path.suffix}")
# 验证参数范围
if not (0 <= state.threshold <= 100):
errors.append(f"阈值超出范围 (0-100): {state.threshold}")
if state.min_scene_length < 0:
errors.append(f"最小场景长度不能为负数: {state.min_scene_length}")
return {
"current_stage": "validated" if not errors else "error",
"progress": 1,
"errors": errors
}
def extract_video_info(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""提取视频信息"""
progress_reporter.info("📊 提取视频信息...")
try:
import cv2
cap = cv2.VideoCapture(state.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
cap.release()
video_info = {
"fps": fps,
"frame_count": frame_count,
"width": width,
"height": height,
"duration": duration,
"resolution": f"{width}x{height}"
}
progress_reporter.info(f"📹 视频信息: {video_info['resolution']}, {fps:.2f}fps, {duration:.2f}s")
return {
"current_stage": "info_extracted",
"progress": 2,
"video_info": video_info
}
except Exception as e:
return {
"current_stage": "error",
"errors": state.errors + [f"提取视频信息失败: {e}"]
}
def detect_scenes(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""执行场景检测"""
progress_reporter.info("🎯 执行场景检测...")
try:
# 使用现有的检测逻辑
result = self.detect_scenes(
Path(state.video_path),
DetectorType(state.detector_type),
state.threshold,
state.min_scene_length
)
return {
"current_stage": "scenes_detected",
"progress": 3,
"detection_result": result,
"processed_scenes": result.scenes
}
except Exception as e:
return {
"current_stage": "error",
"errors": state.errors + [f"场景检测失败: {e}"]
}
def analyze_with_ai(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""AI分析场景结果"""
if not self.ai_enabled or not state.enable_ai_analysis:
progress_reporter.info("⚠️ AI分析已禁用跳过此步骤")
return {
"current_stage": "analysis_skipped",
"progress": 4,
"ai_analysis": "AI分析已禁用"
}
progress_reporter.info("🧠 AI分析场景结果...")
try:
result = state.detection_result
video_info = state.video_info
analysis_prompt = f"""
请分析以下视频场景检测结果:
视频信息:
- 文件: {result.filename}
- 分辨率: {video_info.get('resolution', 'Unknown')}
- 时长: {result.total_duration:.2f}
- 帧率: {video_info.get('fps', 0):.2f}fps
检测结果:
- 检测器: {result.detector_type}
- 阈值: {result.threshold}
- 场景数: {result.total_scenes}
- 检测时间: {result.detection_time:.2f}
场景详情:
{self._format_scenes_for_ai(result.scenes)}
请提供:
1. 场景分布分析
2. 检测质量评估
3. 参数优化建议
4. 潜在问题识别
"""
response = self.llm.invoke([{"role": "user", "content": analysis_prompt}])
return {
"current_stage": "ai_analyzed",
"progress": 4,
"ai_analysis": response.content
}
except Exception as e:
progress_reporter.warning(f"⚠️ AI分析失败: {e}")
return {
"current_stage": "analysis_failed",
"progress": 4,
"ai_analysis": f"AI分析失败: {e}"
}
def finalize_results(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""整理最终结果"""
progress_reporter.info("📋 整理最终结果...")
# 保存结果(如果指定了输出路径)
if state.output_path and state.detection_result:
try:
output_path = Path(state.output_path)
output_format = OutputFormat(state.output_format)
self.save_results(state.detection_result, output_path, output_format)
except Exception as e:
progress_reporter.warning(f"⚠️ 保存结果失败: {e}")
return {
"current_stage": "completed",
"progress": 5
}
# 路由函数
def route_next_step(state: SceneDetectionWorkflowState) -> Literal["extract_info", "detect", "analyze", "finalize", "error"]:
if state.errors:
return "error"
elif state.current_stage == "validated":
return "extract_info"
elif state.current_stage == "info_extracted":
return "detect"
elif state.current_stage == "scenes_detected":
return "analyze"
elif state.current_stage in ["ai_analyzed", "analysis_skipped", "analysis_failed"]:
return "finalize"
else:
return "error"
def handle_error(state: SceneDetectionWorkflowState) -> Dict[str, Any]:
"""处理错误"""
error_msg = "; ".join(state.errors)
progress_reporter.error(f"❌ 工作流错误: {error_msg}")
return {"current_stage": "failed"}
# 构建工作流图
workflow = StateGraph(SceneDetectionWorkflowState)
# 添加节点
workflow.add_node("validate", validate_input)
workflow.add_node("extract_info", extract_video_info)
workflow.add_node("detect", detect_scenes)
workflow.add_node("analyze", analyze_with_ai)
workflow.add_node("finalize", finalize_results)
workflow.add_node("error", handle_error)
# 添加边
workflow.add_edge(START, "validate")
workflow.add_conditional_edges("validate", route_next_step)
workflow.add_conditional_edges("extract_info", route_next_step)
workflow.add_conditional_edges("detect", route_next_step)
workflow.add_conditional_edges("analyze", route_next_step)
workflow.add_edge("finalize", END)
workflow.add_edge("error", END)
# 编译工作流
memory = MemorySaver()
return workflow.compile(checkpointer=memory)
def _format_scenes_for_ai(self, scenes: List[SceneInfo]) -> str:
"""格式化场景信息供AI分析"""
if not scenes:
return "无场景数据"
formatted = []
for scene in scenes[:10]: # 只显示前10个场景
formatted.append(
f"场景 {scene.index}: {scene.start_time:.2f}s - {scene.end_time:.2f}s "
f"(时长: {scene.duration:.2f}s)"
)
if len(scenes) > 10:
formatted.append(f"... 还有 {len(scenes) - 10} 个场景")
return "\n".join(formatted)
def detect_with_workflow(self, video_path: Path, detector_type: DetectorType = DetectorType.CONTENT,
threshold: float = 30.0, min_scene_length: float = 1.0,
output_path: Optional[Path] = None, output_format: OutputFormat = OutputFormat.JSON,
enable_ai_analysis: bool = True) -> Dict[str, Any]:
"""使用LangGraph工作流进行场景检测"""
# 创建工作流
workflow = self.create_detection_workflow()
if not workflow:
raise Exception("无法创建工作流")
# 初始化状态
initial_state = SceneDetectionWorkflowState(
video_path=str(video_path),
detector_type=detector_type.value,
threshold=threshold,
min_scene_length=min_scene_length,
output_path=str(output_path) if output_path else None,
output_format=output_format.value,
enable_ai_analysis=enable_ai_analysis # 使用参数
)
# 执行工作流
config = {"configurable": {"thread_id": f"detection_{int(time.time())}"}}
try:
final_state = workflow.invoke(initial_state, config)
return {
"detection_result": final_state.get("detection_result"),
"ai_analysis": final_state.get("ai_analysis"),
"video_info": final_state.get("video_info"),
"workflow_state": final_state.get("current_stage"),
"errors": final_state.get("errors", [])
}
except Exception as e:
progress_reporter.error(f"❌ 工作流执行失败: {e}")
raise
# ==================== JSON-RPC 方法注册 ====================
def register_jsonrpc_methods(self):
"""注册JSON-RPC方法到全局注册器"""
from python_core.utils.jsonrpc_enhanced import method_registry
# 注册方法到全局注册器
method_registry.register_function(self.jsonrpc_detect_scenes, "scene.detect")
method_registry.register_function(self.jsonrpc_detect_with_workflow, "scene.detect_workflow")
method_registry.register_function(self.jsonrpc_get_video_info, "scene.get_video_info")
method_registry.register_function(self.jsonrpc_batch_detect, "scene.batch_detect")
def jsonrpc_detect_scenes(self, video_path: str, detector_type: str = "content",
threshold: float = 30.0, min_scene_length: float = 1.0) -> Dict[str, Any]:
"""JSON-RPC方法场景检测"""
try:
result = self.detect_scenes(
Path(video_path),
DetectorType(detector_type),
threshold,
min_scene_length
)
return {
"success": result.success,
"filename": result.filename,
"detector_type": result.detector_type,
"threshold": result.threshold,
"total_scenes": result.total_scenes,
"total_duration": result.total_duration,
"detection_time": result.detection_time,
"scenes": [asdict(scene) for scene in result.scenes],
"error": result.error
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
def jsonrpc_detect_with_workflow(self, video_path: str, detector_type: str = "content",
threshold: float = 30.0, min_scene_length: float = 1.0,
output_path: Optional[str] = None, output_format: str = "json",
enable_ai_analysis: bool = True) -> Dict[str, Any]:
"""JSON-RPC方法工作流场景检测"""
try:
output_path_obj = Path(output_path) if output_path else None
result = self.detect_with_workflow(
Path(video_path),
DetectorType(detector_type),
threshold,
min_scene_length,
output_path_obj,
OutputFormat(output_format),
enable_ai_analysis
)
# 序列化结果
serialized_result = {}
for key, value in result.items():
if key == "detection_result" and value:
serialized_result[key] = {
"success": value.success,
"filename": value.filename,
"detector_type": value.detector_type,
"threshold": value.threshold,
"total_scenes": value.total_scenes,
"total_duration": value.total_duration,
"detection_time": value.detection_time,
"scenes": [asdict(scene) for scene in value.scenes],
"error": value.error
}
else:
serialized_result[key] = value
return serialized_result
except Exception as e:
return {
"success": False,
"error": str(e)
}
def get_video_info(self, video_path: Path) -> Dict[str, Any]:
"""获取视频信息"""
try:
import cv2
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
cap.release()
return {
"filename": video_path.name,
"fps": fps,
"frame_count": frame_count,
"width": width,
"height": height,
"duration": duration,
"resolution": f"{width}x{height}",
"file_size": video_path.stat().st_size if video_path.exists() else 0
}
except Exception as e:
raise Exception(f"获取视频信息失败: {e}")
def jsonrpc_get_video_info(self, video_path: str) -> Dict[str, Any]:
"""JSON-RPC方法获取视频信息"""
try:
info = self.get_video_info(Path(video_path))
return {
"success": True,
"info": info
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
def jsonrpc_batch_detect(self, directory: str, detector_type: str = "content",
threshold: float = 30.0, min_scene_length: float = 1.0,
output_dir: Optional[str] = None, output_format: str = "json") -> Dict[str, Any]:
"""JSON-RPC方法批量场景检测"""
try:
output_dir_obj = Path(output_dir) if output_dir else None
results = self.batch_detect(
Path(directory),
DetectorType(detector_type),
threshold,
min_scene_length,
output_dir_obj,
OutputFormat(output_format)
)
# 序列化结果
serialized_results = []
for result in results:
serialized_results.append({
"success": result.success,
"filename": result.filename,
"detector_type": result.detector_type,
"threshold": result.threshold,
"total_scenes": result.total_scenes,
"total_duration": result.total_duration,
"detection_time": result.detection_time,
"scenes": [asdict(scene) for scene in result.scenes],
"error": result.error
})
return {
"success": True,
"results": serialized_results,
"total_processed": len(results)
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
# 创建全局检测器实例
detector = SceneDetector()
# 注册JSON-RPC方法
detector.register_jsonrpc_methods()