fix
This commit is contained in:
559
docs/multi-command-integration.md
Normal file
559
docs/multi-command-integration.md
Normal file
@@ -0,0 +1,559 @@
|
||||
# 多命令集成设计方案
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
将所有服务的命令集成到一个统一的CLI中,提供一致的用户体验和管理界面。
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### **方案1: 主CLI + 子命令组**
|
||||
```
|
||||
mixvideo
|
||||
├── media # 媒体管理命令组
|
||||
│ ├── upload
|
||||
│ ├── batch-upload
|
||||
│ ├── list
|
||||
│ └── status
|
||||
├── scene # 场景检测命令组
|
||||
│ ├── detect
|
||||
│ ├── batch-detect
|
||||
│ ├── compare
|
||||
│ └── analyze
|
||||
├── template # 模板管理命令组
|
||||
│ ├── import
|
||||
│ ├── export
|
||||
│ ├── list
|
||||
│ └── validate
|
||||
└── system # 系统管理命令组
|
||||
├── status
|
||||
├── config
|
||||
├── storage
|
||||
└── logs
|
||||
```
|
||||
|
||||
### **方案2: 扁平化命令**
|
||||
```
|
||||
mixvideo
|
||||
├── media-upload
|
||||
├── media-batch-upload
|
||||
├── scene-detect
|
||||
├── scene-batch-detect
|
||||
├── template-import
|
||||
├── template-export
|
||||
├── system-status
|
||||
└── system-config
|
||||
```
|
||||
|
||||
### **方案3: 混合方案(推荐)**
|
||||
```
|
||||
mixvideo
|
||||
├── upload # 常用命令提升到顶层
|
||||
├── batch-upload
|
||||
├── detect
|
||||
├── batch-detect
|
||||
├── media # 完整的媒体管理命令组
|
||||
├── scene # 完整的场景检测命令组
|
||||
├── template # 模板管理命令组
|
||||
└── system # 系统管理命令组
|
||||
```
|
||||
|
||||
## 📁 文件结构设计
|
||||
|
||||
```
|
||||
python_core/
|
||||
├── cli/ # 统一CLI模块
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # 主CLI入口
|
||||
│ ├── commands/ # 命令组织
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── media.py # 媒体管理命令组
|
||||
│ │ ├── scene.py # 场景检测命令组
|
||||
│ │ ├── template.py # 模板管理命令组
|
||||
│ │ └── system.py # 系统管理命令组
|
||||
│ └── utils/ # CLI工具
|
||||
│ ├── __init__.py
|
||||
│ ├── common.py # 通用CLI工具
|
||||
│ └── decorators.py # CLI装饰器
|
||||
├── services/ # 现有服务保持不变
|
||||
└── __main__.py # 项目入口
|
||||
```
|
||||
|
||||
## 🔧 实现方案
|
||||
|
||||
### **1. 主CLI框架**
|
||||
```python
|
||||
# python_core/cli/main.py
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from typing import Optional
|
||||
|
||||
from .commands import media, scene, template, system
|
||||
from python_core.utils.progress import ProgressJSONRPCCommander
|
||||
|
||||
console = Console()
|
||||
|
||||
class MixVideoCommander(ProgressJSONRPCCommander):
|
||||
"""MixVideo 统一命令行接口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("mixvideo")
|
||||
self.app = typer.Typer(
|
||||
name="mixvideo",
|
||||
help="""
|
||||
🎬 MixVideo - 智能视频处理平台
|
||||
|
||||
功能完整的视频处理和管理工具套件:
|
||||
• 📤 媒体管理 - 上传、处理、组织视频文件
|
||||
• 🎯 场景检测 - 智能识别视频场景变化
|
||||
• 📋 模板管理 - 视频模板导入导出
|
||||
• ⚙️ 系统管理 - 配置、状态、存储管理
|
||||
|
||||
快速开始:
|
||||
mixvideo upload video.mp4 # 上传视频
|
||||
mixvideo detect video.mp4 # 检测场景
|
||||
mixvideo system status # 查看状态
|
||||
""",
|
||||
rich_markup_mode="rich",
|
||||
no_args_is_help=True
|
||||
)
|
||||
self._setup_commands()
|
||||
|
||||
def _register_commands(self):
|
||||
"""注册命令(继承要求)"""
|
||||
pass
|
||||
|
||||
def _is_progressive_command(self, command: str) -> bool:
|
||||
"""判断是否需要进度报告"""
|
||||
return command.startswith(("batch-", "import-", "export-", "analyze-"))
|
||||
|
||||
def _execute_with_progress(self, command: str, args: dict):
|
||||
"""执行带进度的命令"""
|
||||
pass
|
||||
|
||||
def _execute_simple_command(self, command: str, args: dict):
|
||||
"""执行简单命令"""
|
||||
pass
|
||||
|
||||
def _setup_commands(self):
|
||||
"""设置命令结构"""
|
||||
# 添加子命令组
|
||||
self.app.add_typer(media.app, name="media", help="📤 媒体管理")
|
||||
self.app.add_typer(scene.app, name="scene", help="🎯 场景检测")
|
||||
self.app.add_typer(template.app, name="template", help="📋 模板管理")
|
||||
self.app.add_typer(system.app, name="system", help="⚙️ 系统管理")
|
||||
|
||||
# 添加常用命令到顶层(快捷方式)
|
||||
self._add_shortcuts()
|
||||
|
||||
def _add_shortcuts(self):
|
||||
"""添加常用命令的快捷方式"""
|
||||
|
||||
@self.app.command()
|
||||
def upload(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径"),
|
||||
tags: Optional[str] = typer.Option(None, "--tags", "-t", help="🏷️ 标签")
|
||||
):
|
||||
"""📤 快速上传视频(media upload 的快捷方式)"""
|
||||
# 调用媒体管理的上传命令
|
||||
return media.upload_video(video_path, tags)
|
||||
|
||||
@self.app.command()
|
||||
def detect(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径"),
|
||||
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值")
|
||||
):
|
||||
"""🎯 快速场景检测(scene detect 的快捷方式)"""
|
||||
# 调用场景检测的检测命令
|
||||
return scene.detect_scenes(video_path, threshold)
|
||||
|
||||
@self.app.command()
|
||||
def status():
|
||||
"""📊 快速状态查看(system status 的快捷方式)"""
|
||||
# 调用系统状态命令
|
||||
return system.show_status()
|
||||
|
||||
def run(self):
|
||||
"""运行CLI"""
|
||||
self.app()
|
||||
|
||||
def main():
|
||||
"""主入口函数"""
|
||||
commander = MixVideoCommander()
|
||||
commander.run()
|
||||
```
|
||||
|
||||
### **2. 媒体管理命令组**
|
||||
```python
|
||||
# python_core/cli/commands/media.py
|
||||
import typer
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from rich.console import Console
|
||||
|
||||
from python_core.services.media_manager import MediaManagerService
|
||||
from ..utils.common import create_progress_task, parse_tags
|
||||
|
||||
console = Console()
|
||||
app = typer.Typer(help="📤 媒体管理命令")
|
||||
|
||||
# 全局服务实例
|
||||
media_service = MediaManagerService()
|
||||
|
||||
@app.command()
|
||||
def upload(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
|
||||
tags: Optional[str] = typer.Option(None, "--tags", "-t", help="🏷️ 标签列表"),
|
||||
filename: Optional[str] = typer.Option(None, "--filename", "-f", help="📝 自定义文件名")
|
||||
):
|
||||
"""📤 上传单个视频文件"""
|
||||
return upload_video(video_path, tags, filename)
|
||||
|
||||
@app.command()
|
||||
def batch_upload(
|
||||
directory: Path = typer.Argument(..., help="📁 视频目录", exists=True),
|
||||
tags: Optional[str] = typer.Option(None, "--tags", "-t", help="🏷️ 标签列表"),
|
||||
recursive: bool = typer.Option(False, "--recursive", "-r", help="🔄 递归扫描")
|
||||
):
|
||||
"""📦 批量上传视频文件"""
|
||||
tag_list = parse_tags(tags)
|
||||
|
||||
with create_progress_task("批量上传") as task:
|
||||
result = media_service.batch_upload(directory, tag_list, recursive)
|
||||
task.finish(f"上传完成: {result['successful']}/{result['total']} 成功")
|
||||
|
||||
return result
|
||||
|
||||
@app.command()
|
||||
def list_videos(
|
||||
limit: int = typer.Option(10, "--limit", "-l", help="📊 显示数量"),
|
||||
tags: Optional[str] = typer.Option(None, "--tags", "-t", help="🏷️ 标签过滤")
|
||||
):
|
||||
"""📋 列出视频文件"""
|
||||
# 实现列表逻辑
|
||||
pass
|
||||
|
||||
@app.command()
|
||||
def delete(
|
||||
video_id: str = typer.Argument(..., help="🗑️ 视频ID"),
|
||||
confirm: bool = typer.Option(False, "--yes", "-y", help="✅ 确认删除")
|
||||
):
|
||||
"""🗑️ 删除视频文件"""
|
||||
if not confirm:
|
||||
confirm = typer.confirm("确定要删除这个视频吗?")
|
||||
|
||||
if confirm:
|
||||
# 实现删除逻辑
|
||||
console.print("✅ 视频已删除")
|
||||
else:
|
||||
console.print("❌ 取消删除")
|
||||
|
||||
# 共享函数
|
||||
def upload_video(video_path: Path, tags: str = None, filename: str = None):
|
||||
"""上传视频的共享实现"""
|
||||
tag_list = parse_tags(tags)
|
||||
|
||||
console.print(f"🚀 开始上传: [bold blue]{video_path}[/bold blue]")
|
||||
|
||||
result = media_service.upload_video(video_path, tag_list, filename)
|
||||
|
||||
console.print("✅ [bold green]上传完成[/bold green]")
|
||||
return result
|
||||
```
|
||||
|
||||
### **3. 场景检测命令组**
|
||||
```python
|
||||
# python_core/cli/commands/scene.py
|
||||
import typer
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from rich.console import Console
|
||||
|
||||
from python_core.services.scene_detection import SceneDetectionService
|
||||
from ..utils.common import create_progress_task
|
||||
|
||||
console = Console()
|
||||
app = typer.Typer(help="🎯 场景检测命令")
|
||||
|
||||
# 全局服务实例
|
||||
scene_service = SceneDetectionService()
|
||||
|
||||
@app.command()
|
||||
def detect(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
|
||||
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值"),
|
||||
output: Optional[Path] = typer.Option(None, "--output", "-o", help="📄 输出文件")
|
||||
):
|
||||
"""🎯 检测单个视频的场景"""
|
||||
return detect_scenes(video_path, threshold, output)
|
||||
|
||||
@app.command()
|
||||
def batch_detect(
|
||||
directory: Path = typer.Argument(..., help="📁 视频目录", exists=True),
|
||||
threshold: float = typer.Option(30.0, help="🎚️ 检测阈值"),
|
||||
output: Optional[Path] = typer.Option(None, "--output", "-o", help="📄 输出文件")
|
||||
):
|
||||
"""📦 批量检测场景"""
|
||||
with create_progress_task("批量检测") as task:
|
||||
result = scene_service.batch_detect(directory, threshold)
|
||||
task.finish(f"检测完成: {result['processed']} 个文件")
|
||||
|
||||
return result
|
||||
|
||||
@app.command()
|
||||
def compare(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径", exists=True),
|
||||
thresholds: str = typer.Option("20,30,40", help="🎚️ 阈值列表")
|
||||
):
|
||||
"""🔬 比较不同检测器效果"""
|
||||
with create_progress_task("检测器比较") as task:
|
||||
result = scene_service.compare_detectors(video_path, thresholds)
|
||||
task.finish("比较完成")
|
||||
|
||||
return result
|
||||
|
||||
# 共享函数
|
||||
def detect_scenes(video_path: Path, threshold: float = 30.0, output: Path = None):
|
||||
"""场景检测的共享实现"""
|
||||
console.print(f"🎯 开始检测: [bold blue]{video_path}[/bold blue]")
|
||||
|
||||
result = scene_service.detect_single(video_path, threshold)
|
||||
|
||||
if output:
|
||||
scene_service.save_result(result, output)
|
||||
console.print(f"📄 结果已保存: {output}")
|
||||
|
||||
console.print("✅ [bold green]检测完成[/bold green]")
|
||||
return result
|
||||
```
|
||||
|
||||
### **4. 系统管理命令组**
|
||||
```python
|
||||
# python_core/cli/commands/system.py
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
|
||||
from python_core.storage import get_storage
|
||||
from python_core.config import settings
|
||||
|
||||
console = Console()
|
||||
app = typer.Typer(help="⚙️ 系统管理命令")
|
||||
|
||||
@app.command()
|
||||
def status():
|
||||
"""📊 显示系统状态"""
|
||||
return show_status()
|
||||
|
||||
@app.command()
|
||||
def config(
|
||||
show: bool = typer.Option(False, "--show", help="📋 显示配置"),
|
||||
set_key: Optional[str] = typer.Option(None, "--set", help="⚙️ 设置配置项"),
|
||||
value: Optional[str] = typer.Option(None, "--value", help="💾 配置值")
|
||||
):
|
||||
"""⚙️ 配置管理"""
|
||||
if show:
|
||||
show_config()
|
||||
elif set_key and value:
|
||||
set_config(set_key, value)
|
||||
else:
|
||||
console.print("❌ 请指定 --show 或 --set 和 --value")
|
||||
|
||||
@app.command()
|
||||
def storage(
|
||||
action: str = typer.Argument(..., help="📦 操作类型 (info/clean/migrate)"),
|
||||
target: Optional[str] = typer.Option(None, help="🎯 目标存储类型")
|
||||
):
|
||||
"""📦 存储管理"""
|
||||
if action == "info":
|
||||
show_storage_info()
|
||||
elif action == "clean":
|
||||
clean_storage()
|
||||
elif action == "migrate":
|
||||
migrate_storage(target)
|
||||
else:
|
||||
console.print("❌ 未知操作类型")
|
||||
|
||||
@app.command()
|
||||
def logs(
|
||||
lines: int = typer.Option(50, "--lines", "-n", help="📄 显示行数"),
|
||||
follow: bool = typer.Option(False, "--follow", "-f", help="🔄 实时跟踪")
|
||||
):
|
||||
"""📄 查看日志"""
|
||||
# 实现日志查看逻辑
|
||||
pass
|
||||
|
||||
# 共享函数
|
||||
def show_status():
|
||||
"""显示系统状态的共享实现"""
|
||||
console.print("📊 MixVideo 系统状态")
|
||||
|
||||
# 创建状态表格
|
||||
table = Table(title="系统组件状态")
|
||||
table.add_column("组件", style="cyan")
|
||||
table.add_column("状态", style="green")
|
||||
table.add_column("详情", style="yellow")
|
||||
|
||||
# 检查各组件状态
|
||||
storage = get_storage()
|
||||
collections = storage.get_collections()
|
||||
|
||||
table.add_row("存储系统", "✅ 正常", f"{len(collections)} 个集合")
|
||||
table.add_row("媒体管理", "✅ 就绪", "服务正常")
|
||||
table.add_row("场景检测", "✅ 就绪", "服务正常")
|
||||
table.add_row("模板管理", "✅ 就绪", "服务正常")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# 系统信息面板
|
||||
info_panel = Panel(
|
||||
"[bold blue]MixVideo 运行正常[/bold blue]\n"
|
||||
"所有核心组件状态良好,系统准备就绪。",
|
||||
title="📊 系统摘要",
|
||||
border_style="green"
|
||||
)
|
||||
console.print(info_panel)
|
||||
|
||||
def show_config():
|
||||
"""显示配置信息"""
|
||||
# 实现配置显示逻辑
|
||||
pass
|
||||
|
||||
def set_config(key: str, value: str):
|
||||
"""设置配置项"""
|
||||
# 实现配置设置逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
### **5. 通用工具**
|
||||
```python
|
||||
# python_core/cli/utils/common.py
|
||||
from contextlib import contextmanager
|
||||
from typing import List, Optional
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
def parse_tags(tags_str: Optional[str]) -> List[str]:
|
||||
"""解析标签字符串"""
|
||||
if not tags_str:
|
||||
return []
|
||||
return [tag.strip() for tag in tags_str.split(",") if tag.strip()]
|
||||
|
||||
@contextmanager
|
||||
def create_progress_task(name: str):
|
||||
"""创建进度任务的上下文管理器"""
|
||||
class MockTask:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
console.print(f"🚀 开始{name}...")
|
||||
|
||||
def update(self, message: str):
|
||||
console.print(f"📊 {message}")
|
||||
|
||||
def finish(self, message: str):
|
||||
console.print(f"✅ {message}")
|
||||
|
||||
yield MockTask(name)
|
||||
|
||||
def confirm_action(message: str, default: bool = False) -> bool:
|
||||
"""确认操作"""
|
||||
import typer
|
||||
return typer.confirm(message, default=default)
|
||||
|
||||
def show_error(message: str):
|
||||
"""显示错误信息"""
|
||||
console.print(f"❌ [red]{message}[/red]")
|
||||
|
||||
def show_success(message: str):
|
||||
"""显示成功信息"""
|
||||
console.print(f"✅ [green]{message}[/green]")
|
||||
|
||||
def show_warning(message: str):
|
||||
"""显示警告信息"""
|
||||
console.print(f"⚠️ [yellow]{message}[/yellow]")
|
||||
```
|
||||
|
||||
### **6. 项目入口**
|
||||
```python
|
||||
# python_core/__main__.py
|
||||
"""
|
||||
MixVideo 项目主入口
|
||||
支持: python -m python_core
|
||||
"""
|
||||
|
||||
from .cli.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## 🎯 使用示例
|
||||
|
||||
### **命令组方式**
|
||||
```bash
|
||||
# 媒体管理
|
||||
mixvideo media upload video.mp4 --tags "demo,test"
|
||||
mixvideo media batch-upload /videos --recursive
|
||||
mixvideo media list --limit 20
|
||||
|
||||
# 场景检测
|
||||
mixvideo scene detect video.mp4 --threshold 25
|
||||
mixvideo scene batch-detect /videos --output results.json
|
||||
mixvideo scene compare video.mp4 --thresholds "20,30,40"
|
||||
|
||||
# 模板管理
|
||||
mixvideo template import /templates --batch
|
||||
mixvideo template export template_id --output template.json
|
||||
|
||||
# 系统管理
|
||||
mixvideo system status
|
||||
mixvideo system config --show
|
||||
mixvideo system storage info
|
||||
```
|
||||
|
||||
### **快捷命令方式**
|
||||
```bash
|
||||
# 常用命令的快捷方式
|
||||
mixvideo upload video.mp4 --tags "demo"
|
||||
mixvideo detect video.mp4 --threshold 30
|
||||
mixvideo status
|
||||
```
|
||||
|
||||
### **帮助系统**
|
||||
```bash
|
||||
# 查看主帮助
|
||||
mixvideo --help
|
||||
|
||||
# 查看命令组帮助
|
||||
mixvideo media --help
|
||||
mixvideo scene --help
|
||||
|
||||
# 查看具体命令帮助
|
||||
mixvideo media upload --help
|
||||
mixvideo scene detect --help
|
||||
```
|
||||
|
||||
## 🎉 方案优势
|
||||
|
||||
### **1. 统一体验**
|
||||
- 🎯 一个入口点,所有功能
|
||||
- 📋 一致的命令风格和参数
|
||||
- 🎨 统一的输出格式和样式
|
||||
|
||||
### **2. 灵活使用**
|
||||
- ⚡ 快捷命令满足日常需求
|
||||
- 🔧 完整命令组满足专业需求
|
||||
- 📚 清晰的帮助系统
|
||||
|
||||
### **3. 易于扩展**
|
||||
- 🔌 新服务易于集成
|
||||
- 📦 模块化的命令组织
|
||||
- 🔄 向后兼容的设计
|
||||
|
||||
### **4. 开发友好**
|
||||
- 🧩 清晰的代码组织
|
||||
- 🔧 共享的工具函数
|
||||
- 📝 统一的开发规范
|
||||
|
||||
这个设计方案提供了完整的多命令集成解决方案,既保持了各服务的独立性,又提供了统一的用户体验!
|
||||
485
docs/typer-development-standards.md
Normal file
485
docs/typer-development-standards.md
Normal file
@@ -0,0 +1,485 @@
|
||||
# Typer 开发规范
|
||||
|
||||
## 🎯 核心原则
|
||||
|
||||
基于您选择的**方案2: 纯Typer实现**,制定以下开发规范,确保与现有JSON-RPC进度条架构完美集成。
|
||||
|
||||
## 📋 开发规范总览
|
||||
|
||||
### **1. 项目结构规范**
|
||||
```
|
||||
python_core/services/{service_name}/
|
||||
├── __init__.py # 统一导入
|
||||
├── types.py # 数据类型定义(dataclass + enum)
|
||||
├── service.py # 业务逻辑服务
|
||||
├── cli.py # Typer命令行接口
|
||||
└── __main__.py # 模块入口
|
||||
```
|
||||
|
||||
### **2. 命名规范**
|
||||
- **服务名**: `snake_case` (如: `media_manager`, `scene_detection`)
|
||||
- **类名**: `PascalCase` (如: `MediaManagerCommander`, `SceneDetectionService`)
|
||||
- **函数名**: `snake_case` (如: `upload_video`, `batch_detect`)
|
||||
- **常量**: `UPPER_SNAKE_CASE` (如: `DEFAULT_THRESHOLD`, `MAX_FILE_SIZE`)
|
||||
|
||||
## 🔧 Typer CLI 开发规范
|
||||
|
||||
### **1. 基础结构模板**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
{服务名}命令行接口
|
||||
"""
|
||||
|
||||
import typer
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from rich.console import Console
|
||||
|
||||
from .types import {相关类型}
|
||||
from .service import {服务类}
|
||||
from python_core.utils.progress import ProgressJSONRPCCommander
|
||||
|
||||
console = Console()
|
||||
|
||||
class {服务名}Commander(ProgressJSONRPCCommander):
|
||||
"""基于Typer的{服务名}命令行接口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("{service_name}")
|
||||
self.app = typer.Typer(
|
||||
name="{service_name}",
|
||||
help="🎯 {服务描述}",
|
||||
rich_markup_mode="rich",
|
||||
no_args_is_help=True
|
||||
)
|
||||
self.service = {服务类}()
|
||||
self._setup_commands()
|
||||
|
||||
def _register_commands(self):
|
||||
"""注册命令(继承要求)"""
|
||||
pass # Typer通过装饰器自动注册
|
||||
|
||||
def _is_progressive_command(self, command: str) -> bool:
|
||||
"""判断是否需要进度报告"""
|
||||
return command in ["batch_*", "compare_*", "analyze_*"]
|
||||
|
||||
def _execute_with_progress(self, command: str, args: dict):
|
||||
"""执行带进度的命令"""
|
||||
# 通过Typer命令直接处理
|
||||
pass
|
||||
|
||||
def _execute_simple_command(self, command: str, args: dict):
|
||||
"""执行简单命令"""
|
||||
# 通过Typer命令直接处理
|
||||
pass
|
||||
|
||||
def _setup_commands(self):
|
||||
"""设置Typer命令"""
|
||||
# 在这里定义所有命令
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""运行CLI"""
|
||||
self.app()
|
||||
|
||||
def main():
|
||||
"""主入口函数"""
|
||||
commander = {服务名}Commander()
|
||||
commander.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### **2. 类型定义规范**
|
||||
|
||||
#### **枚举类型**
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
"""输出格式枚举"""
|
||||
JSON = "json"
|
||||
CSV = "csv"
|
||||
TXT = "txt"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
class ProcessingMode(str, Enum):
|
||||
"""处理模式枚举"""
|
||||
FAST = "fast"
|
||||
BALANCED = "balanced"
|
||||
QUALITY = "quality"
|
||||
```
|
||||
|
||||
#### **数据类型**
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
from pathlib import Path
|
||||
|
||||
@dataclass
|
||||
class ProcessConfig:
|
||||
"""处理配置"""
|
||||
input_path: Path
|
||||
output_path: Optional[Path] = None
|
||||
format: OutputFormat = OutputFormat.JSON
|
||||
mode: ProcessingMode = ProcessingMode.BALANCED
|
||||
tags: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.tags is None:
|
||||
self.tags = []
|
||||
```
|
||||
|
||||
### **3. 命令定义规范**
|
||||
|
||||
#### **基础命令模板**
|
||||
```python
|
||||
@self.app.command()
|
||||
def {command_name}(
|
||||
# 必需参数 - 使用Argument
|
||||
input_path: Path = typer.Argument(
|
||||
...,
|
||||
help="📁 输入文件/目录路径",
|
||||
exists=True
|
||||
),
|
||||
|
||||
# 可选参数 - 使用Option
|
||||
output_format: OutputFormat = typer.Option(
|
||||
OutputFormat.JSON,
|
||||
"--format", "-f",
|
||||
help="📄 输出格式"
|
||||
),
|
||||
|
||||
tags: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--tags", "-t",
|
||||
help="🏷️ 标签列表(逗号分隔)"
|
||||
),
|
||||
|
||||
# 布尔选项
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose", "-v",
|
||||
help="📝 详细输出"
|
||||
),
|
||||
|
||||
# 数值选项
|
||||
threshold: float = typer.Option(
|
||||
30.0,
|
||||
"--threshold",
|
||||
min=0.0,
|
||||
max=100.0,
|
||||
help="🎚️ 处理阈值 (0-100)"
|
||||
)
|
||||
):
|
||||
"""
|
||||
📋 命令描述
|
||||
|
||||
详细说明命令的功能和用法。
|
||||
"""
|
||||
# 参数验证
|
||||
if not input_path.exists():
|
||||
console.print("❌ [red]输入路径不存在[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# 解析标签
|
||||
tag_list = []
|
||||
if tags:
|
||||
tag_list = [tag.strip() for tag in tags.split(",")]
|
||||
|
||||
# 显示开始信息
|
||||
console.print(f"🚀 开始处理: [bold blue]{input_path}[/bold blue]")
|
||||
|
||||
try:
|
||||
# 调用服务逻辑
|
||||
result = self.service.process(
|
||||
input_path=input_path,
|
||||
output_format=output_format,
|
||||
tags=tag_list,
|
||||
verbose=verbose,
|
||||
threshold=threshold
|
||||
)
|
||||
|
||||
# 显示结果
|
||||
console.print("✅ [bold green]处理完成[/bold green]")
|
||||
if verbose:
|
||||
console.print(f"📊 结果: {result}")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]处理失败: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
```
|
||||
|
||||
#### **进度命令模板**
|
||||
```python
|
||||
@self.app.command()
|
||||
def batch_process(
|
||||
input_directory: Path = typer.Argument(..., help="📁 输入目录"),
|
||||
output_path: Optional[Path] = typer.Option(None, help="📄 输出文件路径"),
|
||||
recursive: bool = typer.Option(False, "--recursive", "-r", help="🔄 递归处理子目录")
|
||||
):
|
||||
"""
|
||||
📦 批量处理命令(带进度条)
|
||||
"""
|
||||
console.print(f"📦 批量处理目录: [bold blue]{input_directory}[/bold blue]")
|
||||
|
||||
# 扫描文件
|
||||
files = self._scan_files(input_directory, recursive)
|
||||
|
||||
if not files:
|
||||
console.print("⚠️ [yellow]未找到可处理的文件[/yellow]")
|
||||
return
|
||||
|
||||
# 使用进度任务
|
||||
with self.create_task("批量处理", len(files)) as task:
|
||||
results = []
|
||||
|
||||
for i, file_path in enumerate(files):
|
||||
task.update(i, f"处理文件: {file_path.name} ({i+1}/{len(files)})")
|
||||
|
||||
try:
|
||||
result = self.service.process_single(file_path)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
console.print(f"❌ 处理失败 {file_path.name}: {e}")
|
||||
|
||||
task.finish(f"批量处理完成: {len(results)}/{len(files)} 成功")
|
||||
|
||||
# 保存结果
|
||||
if output_path:
|
||||
self._save_results(results, output_path)
|
||||
console.print(f"📄 结果已保存到: {output_path}")
|
||||
```
|
||||
|
||||
### **4. 错误处理规范**
|
||||
|
||||
```python
|
||||
# 输入验证
|
||||
def validate_input(self, input_path: Path) -> None:
|
||||
"""验证输入参数"""
|
||||
if not input_path.exists():
|
||||
console.print(f"❌ [red]文件不存在: {input_path}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if input_path.is_dir() and not any(input_path.iterdir()):
|
||||
console.print(f"⚠️ [yellow]目录为空: {input_path}[/yellow]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# 异常处理
|
||||
try:
|
||||
result = self.service.process(input_path)
|
||||
except FileNotFoundError:
|
||||
console.print("❌ [red]文件未找到[/red]")
|
||||
raise typer.Exit(1)
|
||||
except PermissionError:
|
||||
console.print("❌ [red]权限不足[/red]")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]处理失败: {e}[/red]")
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
raise typer.Exit(1)
|
||||
```
|
||||
|
||||
### **5. 输出规范**
|
||||
|
||||
#### **Rich输出样式**
|
||||
```python
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
|
||||
# 状态表格
|
||||
def show_status(self, data: dict):
|
||||
"""显示状态表格"""
|
||||
table = Table(title="📊 处理状态")
|
||||
table.add_column("项目", style="cyan")
|
||||
table.add_column("状态", style="green")
|
||||
table.add_column("详情", style="yellow")
|
||||
|
||||
for key, value in data.items():
|
||||
table.add_row(key, "✅ 完成", str(value))
|
||||
|
||||
console.print(table)
|
||||
|
||||
# 信息面板
|
||||
def show_summary(self, message: str, title: str = "📋 摘要"):
|
||||
"""显示信息面板"""
|
||||
panel = Panel(
|
||||
f"[bold blue]{message}[/bold blue]",
|
||||
title=title,
|
||||
border_style="green"
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
# 进度条(非JSON-RPC场景)
|
||||
def show_local_progress(self, items: list, description: str):
|
||||
"""显示本地进度条"""
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
console=console
|
||||
) as progress:
|
||||
task = progress.add_task(description, total=len(items))
|
||||
|
||||
for item in items:
|
||||
# 处理逻辑
|
||||
progress.advance(task)
|
||||
```
|
||||
|
||||
## 📦 服务集成规范
|
||||
|
||||
### **1. 服务基类继承**
|
||||
```python
|
||||
from python_core.services.base import ProgressServiceBase
|
||||
|
||||
class MediaManagerService(ProgressServiceBase):
|
||||
"""媒体管理服务"""
|
||||
|
||||
def get_service_name(self) -> str:
|
||||
return "media_manager"
|
||||
|
||||
def process_with_progress(self, items: list, operation_name: str):
|
||||
"""带进度的处理"""
|
||||
self.report_progress(f"开始{operation_name}")
|
||||
|
||||
for i, item in enumerate(items):
|
||||
self.report_progress(f"{operation_name}: {item} ({i+1}/{len(items)})")
|
||||
# 处理逻辑
|
||||
|
||||
self.report_progress(f"完成{operation_name}")
|
||||
```
|
||||
|
||||
### **2. 存储集成**
|
||||
```python
|
||||
def save_results(self, results: list, collection_type: str = "results"):
|
||||
"""保存结果到存储"""
|
||||
for i, result in enumerate(results):
|
||||
key = f"result_{i}_{int(time.time())}"
|
||||
self.save_data(collection_type, key, result)
|
||||
|
||||
return len(results)
|
||||
|
||||
def load_recent_results(self, collection_type: str = "results", limit: int = 10):
|
||||
"""加载最近的结果"""
|
||||
keys = self.list_keys(collection_type)
|
||||
recent_keys = sorted(keys)[-limit:]
|
||||
|
||||
return self.load_batch_data(collection_type, recent_keys)
|
||||
```
|
||||
|
||||
## 🧪 测试规范
|
||||
|
||||
### **1. 命令测试**
|
||||
```python
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
def test_upload_command():
|
||||
"""测试上传命令"""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["upload", "test.mp4", "--tags", "test"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "上传完成" in result.stdout
|
||||
|
||||
def test_batch_command():
|
||||
"""测试批量命令"""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(app, ["batch-upload", "/test/dir"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "批量处理完成" in result.stdout
|
||||
```
|
||||
|
||||
### **2. 进度测试**
|
||||
```python
|
||||
def test_progress_integration():
|
||||
"""测试进度集成"""
|
||||
commander = MediaManagerCommander()
|
||||
|
||||
# 模拟进度回调
|
||||
progress_messages = []
|
||||
def mock_callback(message):
|
||||
progress_messages.append(message)
|
||||
|
||||
commander.set_progress_callback(mock_callback)
|
||||
|
||||
# 执行带进度的操作
|
||||
commander.service.process_with_progress(["item1", "item2"], "测试")
|
||||
|
||||
assert len(progress_messages) > 0
|
||||
assert "开始测试" in progress_messages[0]
|
||||
```
|
||||
|
||||
## 📚 文档规范
|
||||
|
||||
### **1. 命令文档**
|
||||
```python
|
||||
@app.command()
|
||||
def upload(
|
||||
video_path: Path = typer.Argument(..., help="📹 视频文件路径")
|
||||
):
|
||||
"""
|
||||
📤 上传视频文件
|
||||
|
||||
上传单个视频文件到媒体库,自动进行场景检测和元数据提取。
|
||||
|
||||
示例:
|
||||
python -m media_manager upload video.mp4 --tags "demo,test"
|
||||
python -m media_manager upload /path/to/video.mp4 --format json
|
||||
|
||||
注意:
|
||||
- 支持的格式: MP4, AVI, MOV, MKV
|
||||
- 文件大小限制: 2GB
|
||||
- 自动检测重复文件
|
||||
"""
|
||||
```
|
||||
|
||||
### **2. 帮助信息**
|
||||
```python
|
||||
# 应用级帮助
|
||||
app = typer.Typer(
|
||||
name="media_manager",
|
||||
help="""
|
||||
🎬 媒体管理器
|
||||
|
||||
功能强大的视频处理和管理工具,支持:
|
||||
• 视频上传和元数据提取
|
||||
• 自动场景检测和分割
|
||||
• 批量处理和进度跟踪
|
||||
• 多种输出格式
|
||||
|
||||
使用 --help 查看具体命令帮助。
|
||||
""",
|
||||
rich_markup_mode="rich"
|
||||
)
|
||||
```
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### **1. 性能优化**
|
||||
- 使用类型提示提高IDE支持
|
||||
- 合理使用Rich组件,避免过度渲染
|
||||
- 大文件处理时显示进度条
|
||||
- 异步操作使用适当的进度反馈
|
||||
|
||||
### **2. 用户体验**
|
||||
- 提供清晰的错误信息
|
||||
- 使用emoji和颜色增强可读性
|
||||
- 重要操作前进行确认
|
||||
- 提供详细的帮助文档
|
||||
|
||||
### **3. 代码质量**
|
||||
- 所有函数添加类型提示
|
||||
- 使用dataclass定义数据结构
|
||||
- 遵循PEP 8代码风格
|
||||
- 编写完整的单元测试
|
||||
|
||||
这套规范确保了Typer实现与您现有架构的完美集成,同时提供了现代化、类型安全的开发体验!
|
||||
504
examples/typer_media_manager.py
Normal file
504
examples/typer_media_manager.py
Normal file
@@ -0,0 +1,504 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
基于Typer开发规范的媒体管理器实现示例
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
try:
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
except ImportError:
|
||||
print("❌ 需要安装 typer 和 rich: pip install typer rich")
|
||||
sys.exit(1)
|
||||
|
||||
from python_core.utils.progress import ProgressJSONRPCCommander
|
||||
from python_core.services.base import ProgressServiceBase
|
||||
|
||||
console = Console()
|
||||
|
||||
# 1. 类型定义(遵循规范)
|
||||
class OutputFormat(str, Enum):
|
||||
"""输出格式枚举"""
|
||||
JSON = "json"
|
||||
CSV = "csv"
|
||||
TXT = "txt"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
class ProcessingMode(str, Enum):
|
||||
"""处理模式枚举"""
|
||||
FAST = "fast"
|
||||
BALANCED = "balanced"
|
||||
QUALITY = "quality"
|
||||
|
||||
# 2. 服务类(遵循规范)
|
||||
class MediaManagerService(ProgressServiceBase):
|
||||
"""媒体管理服务"""
|
||||
|
||||
def get_service_name(self) -> str:
|
||||
return "media_manager"
|
||||
|
||||
def upload_video(self, video_path: Path, tags: List[str] = None, filename: str = None) -> dict:
|
||||
"""上传单个视频"""
|
||||
if tags is None:
|
||||
tags = []
|
||||
|
||||
# 模拟上传过程
|
||||
steps = [
|
||||
"计算文件哈希...",
|
||||
"检查重复文件...",
|
||||
"复制文件到存储...",
|
||||
"提取视频信息...",
|
||||
"检测场景变化..."
|
||||
]
|
||||
|
||||
for step in steps:
|
||||
self.report_progress(step)
|
||||
time.sleep(0.3) # 模拟处理时间
|
||||
|
||||
result = {
|
||||
"video_path": str(video_path),
|
||||
"filename": filename or video_path.name,
|
||||
"tags": tags,
|
||||
"success": True,
|
||||
"scenes_detected": 3,
|
||||
"duration": 120.5
|
||||
}
|
||||
|
||||
# 保存结果到存储
|
||||
self.save_data("uploads", f"upload_{int(time.time())}", result)
|
||||
|
||||
return result
|
||||
|
||||
def batch_upload(self, directory: Path, tags: List[str] = None, recursive: bool = False) -> dict:
|
||||
"""批量上传视频"""
|
||||
if tags is None:
|
||||
tags = []
|
||||
|
||||
# 扫描视频文件
|
||||
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv'}
|
||||
video_files = []
|
||||
|
||||
if recursive:
|
||||
for ext in video_extensions:
|
||||
video_files.extend(directory.rglob(f"*{ext}"))
|
||||
else:
|
||||
for ext in video_extensions:
|
||||
video_files.extend(directory.glob(f"*{ext}"))
|
||||
|
||||
results = []
|
||||
for i, video_file in enumerate(video_files):
|
||||
self.report_progress(f"处理文件: {video_file.name} ({i+1}/{len(video_files)})")
|
||||
|
||||
try:
|
||||
result = self.upload_video(video_file, tags)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
results.append({
|
||||
"video_path": str(video_file),
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
batch_result = {
|
||||
"total_files": len(video_files),
|
||||
"successful": len([r for r in results if r.get("success")]),
|
||||
"failed": len([r for r in results if not r.get("success")]),
|
||||
"results": results
|
||||
}
|
||||
|
||||
# 保存批量结果
|
||||
self.save_data("batch_uploads", f"batch_{int(time.time())}", batch_result)
|
||||
|
||||
return batch_result
|
||||
|
||||
def get_recent_uploads(self, limit: int = 10) -> List[dict]:
|
||||
"""获取最近的上传记录"""
|
||||
keys = self.list_keys("uploads")
|
||||
recent_keys = sorted(keys)[-limit:]
|
||||
return list(self.load_batch_data("uploads", recent_keys).values())
|
||||
|
||||
# 3. Typer命令行接口(遵循规范)
|
||||
class MediaManagerCommander(ProgressJSONRPCCommander):
|
||||
"""基于Typer的媒体管理器命令行接口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("media_manager")
|
||||
self.app = typer.Typer(
|
||||
name="media_manager",
|
||||
help="""
|
||||
🎬 媒体管理器
|
||||
|
||||
功能强大的视频处理和管理工具,支持:
|
||||
• 视频上传和元数据提取
|
||||
• 自动场景检测和分割
|
||||
• 批量处理和进度跟踪
|
||||
• 多种输出格式
|
||||
|
||||
使用 --help 查看具体命令帮助。
|
||||
""",
|
||||
rich_markup_mode="rich",
|
||||
no_args_is_help=True
|
||||
)
|
||||
self.service = MediaManagerService()
|
||||
self._setup_commands()
|
||||
|
||||
def _register_commands(self):
|
||||
"""注册命令(继承要求)"""
|
||||
pass # Typer通过装饰器自动注册
|
||||
|
||||
def _is_progressive_command(self, command: str) -> bool:
|
||||
"""判断是否需要进度报告"""
|
||||
return command in ["batch_upload", "analyze"]
|
||||
|
||||
def _execute_with_progress(self, command: str, args: dict):
|
||||
"""执行带进度的命令"""
|
||||
pass # 通过Typer命令直接处理
|
||||
|
||||
def _execute_simple_command(self, command: str, args: dict):
|
||||
"""执行简单命令"""
|
||||
pass # 通过Typer命令直接处理
|
||||
|
||||
def _setup_commands(self):
|
||||
"""设置Typer命令"""
|
||||
|
||||
@self.app.command()
|
||||
def upload(
|
||||
video_path: Path = typer.Argument(
|
||||
...,
|
||||
help="📹 视频文件路径",
|
||||
exists=True
|
||||
),
|
||||
tags: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--tags", "-t",
|
||||
help="🏷️ 标签列表(逗号分隔)"
|
||||
),
|
||||
filename: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--filename", "-f",
|
||||
help="📝 自定义文件名"
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose", "-v",
|
||||
help="📝 详细输出"
|
||||
)
|
||||
):
|
||||
"""
|
||||
📤 上传视频文件
|
||||
|
||||
上传单个视频文件到媒体库,自动进行场景检测和元数据提取。
|
||||
|
||||
示例:
|
||||
media_manager upload video.mp4 --tags "demo,test"
|
||||
media_manager upload /path/to/video.mp4 --filename "my_video"
|
||||
|
||||
注意:
|
||||
- 支持的格式: MP4, AVI, MOV, MKV, WMV
|
||||
- 自动检测重复文件
|
||||
- 自动提取视频元数据
|
||||
"""
|
||||
# 参数验证
|
||||
if not video_path.exists():
|
||||
console.print("❌ [red]视频文件不存在[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# 解析标签
|
||||
tag_list = []
|
||||
if tags:
|
||||
tag_list = [tag.strip() for tag in tags.split(",")]
|
||||
|
||||
# 显示开始信息
|
||||
console.print(f"🚀 开始上传: [bold blue]{video_path}[/bold blue]")
|
||||
|
||||
try:
|
||||
# 设置进度回调
|
||||
def progress_callback(message: str):
|
||||
console.print(f"📊 {message}")
|
||||
|
||||
self.service.set_progress_callback(progress_callback)
|
||||
|
||||
# 执行上传
|
||||
result = self.service.upload_video(video_path, tag_list, filename)
|
||||
|
||||
# 显示结果
|
||||
console.print("✅ [bold green]上传完成[/bold green]")
|
||||
|
||||
if verbose or True: # 总是显示基本信息
|
||||
self._show_upload_result(result)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]上传失败: {e}[/red]")
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
raise typer.Exit(1)
|
||||
|
||||
@self.app.command()
|
||||
def batch_upload(
|
||||
input_directory: Path = typer.Argument(
|
||||
...,
|
||||
help="📁 输入目录路径",
|
||||
exists=True,
|
||||
file_okay=False
|
||||
),
|
||||
output_path: Optional[Path] = typer.Option(
|
||||
None,
|
||||
"--output", "-o",
|
||||
help="📄 结果输出文件路径"
|
||||
),
|
||||
tags: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--tags", "-t",
|
||||
help="🏷️ 标签列表(逗号分隔)"
|
||||
),
|
||||
recursive: bool = typer.Option(
|
||||
False,
|
||||
"--recursive", "-r",
|
||||
help="🔄 递归处理子目录"
|
||||
),
|
||||
output_format: OutputFormat = typer.Option(
|
||||
OutputFormat.JSON,
|
||||
"--format", "-f",
|
||||
help="📄 输出格式"
|
||||
)
|
||||
):
|
||||
"""
|
||||
📦 批量上传视频文件(带进度条)
|
||||
|
||||
批量处理目录中的所有视频文件,支持递归扫描和进度跟踪。
|
||||
|
||||
示例:
|
||||
media_manager batch-upload /videos --tags "batch,demo"
|
||||
media_manager batch-upload /videos -r --output results.json
|
||||
"""
|
||||
console.print(f"📦 批量上传目录: [bold blue]{input_directory}[/bold blue]")
|
||||
|
||||
# 解析标签
|
||||
tag_list = []
|
||||
if tags:
|
||||
tag_list = [tag.strip() for tag in tags.split(",")]
|
||||
|
||||
# 扫描文件数量
|
||||
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv'}
|
||||
video_files = []
|
||||
|
||||
if recursive:
|
||||
for ext in video_extensions:
|
||||
video_files.extend(input_directory.rglob(f"*{ext}"))
|
||||
else:
|
||||
for ext in video_extensions:
|
||||
video_files.extend(input_directory.glob(f"*{ext}"))
|
||||
|
||||
if not video_files:
|
||||
console.print("⚠️ [yellow]未找到可处理的视频文件[/yellow]")
|
||||
return
|
||||
|
||||
console.print(f"📋 找到 {len(video_files)} 个视频文件")
|
||||
|
||||
# 使用进度任务
|
||||
with self.create_task("批量上传", len(video_files)) as task:
|
||||
def progress_callback(message: str):
|
||||
# 从消息中提取文件信息更新任务
|
||||
if "处理文件:" in message:
|
||||
task.update(message=message)
|
||||
|
||||
self.service.set_progress_callback(progress_callback)
|
||||
|
||||
# 执行批量上传
|
||||
result = self.service.batch_upload(input_directory, tag_list, recursive)
|
||||
|
||||
task.finish(f"批量上传完成: {result['successful']}/{result['total_files']} 成功")
|
||||
|
||||
# 显示结果摘要
|
||||
self._show_batch_result(result)
|
||||
|
||||
# 保存结果文件
|
||||
if output_path:
|
||||
self._save_batch_results(result, output_path, output_format)
|
||||
console.print(f"📄 结果已保存到: {output_path}")
|
||||
|
||||
@self.app.command()
|
||||
def list_uploads(
|
||||
limit: int = typer.Option(
|
||||
10,
|
||||
"--limit", "-l",
|
||||
min=1,
|
||||
max=100,
|
||||
help="📊 显示数量限制"
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose", "-v",
|
||||
help="📝 详细信息"
|
||||
)
|
||||
):
|
||||
"""
|
||||
📋 列出最近的上传记录
|
||||
|
||||
显示最近上传的视频文件信息和统计数据。
|
||||
"""
|
||||
console.print("📋 最近的上传记录")
|
||||
|
||||
try:
|
||||
uploads = self.service.get_recent_uploads(limit)
|
||||
|
||||
if not uploads:
|
||||
console.print("⚠️ [yellow]暂无上传记录[/yellow]")
|
||||
return
|
||||
|
||||
self._show_uploads_table(uploads, verbose)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]获取记录失败: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@self.app.command()
|
||||
def status():
|
||||
"""
|
||||
📊 显示系统状态
|
||||
|
||||
显示媒体管理器的当前状态和统计信息。
|
||||
"""
|
||||
console.print("📊 媒体管理器状态")
|
||||
|
||||
try:
|
||||
# 获取统计信息
|
||||
uploads_stats = self.service.get_collection_stats("uploads")
|
||||
batch_stats = self.service.get_collection_stats("batch_uploads")
|
||||
|
||||
# 创建状态表格
|
||||
table = Table(title="系统状态")
|
||||
table.add_column("组件", style="cyan")
|
||||
table.add_column("状态", style="green")
|
||||
table.add_column("详情", style="yellow")
|
||||
|
||||
table.add_row("存储", "✅ 正常", f"JSON文件存储")
|
||||
table.add_row("上传记录", "✅ 活跃", f"{uploads_stats.get('file_count', 0)} 条记录")
|
||||
table.add_row("批量任务", "✅ 就绪", f"{batch_stats.get('file_count', 0)} 个任务")
|
||||
table.add_row("进度系统", "✅ 就绪", "JSON-RPC协议")
|
||||
|
||||
console.print(table)
|
||||
|
||||
# 创建信息面板
|
||||
info_panel = Panel(
|
||||
"[bold blue]媒体管理器运行正常[/bold blue]\n"
|
||||
"所有组件状态良好,可以开始处理视频文件。",
|
||||
title="📊 状态摘要",
|
||||
border_style="green"
|
||||
)
|
||||
console.print(info_panel)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"❌ [red]获取状态失败: {e}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
def _show_upload_result(self, result: dict):
|
||||
"""显示上传结果"""
|
||||
table = Table(title="📤 上传结果")
|
||||
table.add_column("项目", style="cyan")
|
||||
table.add_column("值", style="green")
|
||||
|
||||
table.add_row("文件名", result["filename"])
|
||||
table.add_row("标签", ", ".join(result["tags"]) if result["tags"] else "无")
|
||||
table.add_row("检测场景", str(result["scenes_detected"]))
|
||||
table.add_row("视频时长", f"{result['duration']:.1f}秒")
|
||||
|
||||
console.print(table)
|
||||
|
||||
def _show_batch_result(self, result: dict):
|
||||
"""显示批量结果摘要"""
|
||||
panel = Panel(
|
||||
f"[bold green]总文件: {result['total_files']}[/bold green]\n"
|
||||
f"[bold blue]成功: {result['successful']}[/bold blue]\n"
|
||||
f"[bold red]失败: {result['failed']}[/bold red]",
|
||||
title="📦 批量上传摘要",
|
||||
border_style="green"
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
def _show_uploads_table(self, uploads: List[dict], verbose: bool):
|
||||
"""显示上传记录表格"""
|
||||
table = Table(title="📋 上传记录")
|
||||
table.add_column("文件名", style="cyan")
|
||||
table.add_column("场景数", style="green")
|
||||
table.add_column("时长", style="yellow")
|
||||
|
||||
if verbose:
|
||||
table.add_column("标签", style="magenta")
|
||||
|
||||
for upload in uploads:
|
||||
row = [
|
||||
upload["filename"],
|
||||
str(upload.get("scenes_detected", "N/A")),
|
||||
f"{upload.get('duration', 0):.1f}s"
|
||||
]
|
||||
|
||||
if verbose:
|
||||
tags = ", ".join(upload.get("tags", [])) or "无"
|
||||
row.append(tags)
|
||||
|
||||
table.add_row(*row)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def _save_batch_results(self, result: dict, output_path: Path, format: OutputFormat):
|
||||
"""保存批量结果"""
|
||||
if format == OutputFormat.JSON:
|
||||
import json
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, indent=2, ensure_ascii=False)
|
||||
elif format == OutputFormat.CSV:
|
||||
import csv
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['filename', 'success', 'scenes', 'duration', 'error'])
|
||||
for item in result['results']:
|
||||
writer.writerow([
|
||||
item.get('filename', ''),
|
||||
item.get('success', False),
|
||||
item.get('scenes_detected', ''),
|
||||
item.get('duration', ''),
|
||||
item.get('error', '')
|
||||
])
|
||||
else: # TXT
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"批量上传结果\n")
|
||||
f.write(f"总文件数: {result['total_files']}\n")
|
||||
f.write(f"成功: {result['successful']}\n")
|
||||
f.write(f"失败: {result['failed']}\n\n")
|
||||
|
||||
for item in result['results']:
|
||||
f.write(f"文件: {item.get('filename', '')}\n")
|
||||
f.write(f" 状态: {'成功' if item.get('success') else '失败'}\n")
|
||||
if item.get('success'):
|
||||
f.write(f" 场景数: {item.get('scenes_detected', 'N/A')}\n")
|
||||
f.write(f" 时长: {item.get('duration', 0):.1f}秒\n")
|
||||
else:
|
||||
f.write(f" 错误: {item.get('error', '')}\n")
|
||||
f.write("\n")
|
||||
|
||||
def run(self):
|
||||
"""运行CLI"""
|
||||
self.app()
|
||||
|
||||
def main():
|
||||
"""主入口函数"""
|
||||
commander = MediaManagerCommander()
|
||||
commander.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
prompt.md
36
prompt.md
@@ -5,6 +5,42 @@
|
||||
* 核心层Python 视频处理核心逻辑
|
||||
* 服务层Python 后台服务和文件管理
|
||||
|
||||
|
||||
- 命令行工具
|
||||
|
||||
### **方案2: 纯Typer实现**
|
||||
|
||||
#### **特点**
|
||||
- 🎯 类型提示驱动
|
||||
- ✨ 现代化Python风格
|
||||
- 🤖 自动补全和验证
|
||||
|
||||
#### **代码示例**
|
||||
```python
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
class DetectorType(str, Enum):
|
||||
content = "content"
|
||||
threshold = "threshold"
|
||||
adaptive = "adaptive"
|
||||
|
||||
@app.command()
|
||||
def detect(
|
||||
video_path: Path = typer.Argument(..., help="视频文件路径"),
|
||||
detector: DetectorType = typer.Option(DetectorType.content, help="检测器类型"),
|
||||
threshold: float = typer.Option(30.0, help="检测阈值"),
|
||||
output: Optional[Path] = typer.Option(None, help="输出文件路径")
|
||||
):
|
||||
"""检测单个视频的场景"""
|
||||
# 类型安全的处理逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
## 多命令集成方案
|
||||
|
||||
|
||||
|
||||
- MoviePy
|
||||
|
||||
```
|
||||
|
||||
8
python_core/cli/__init__.py
Normal file
8
python_core/cli/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MixVideo 统一命令行接口
|
||||
"""
|
||||
|
||||
from .cli import main
|
||||
|
||||
__all__ = ["main"]
|
||||
5
python_core/cli/__main__.py
Normal file
5
python_core/cli/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
58
python_core/cli/cli.py
Normal file
58
python_core/cli/cli.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MixVideo 主命令行接口
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from python_core.cli.const import progress_reporter, console, project_root
|
||||
import typer
|
||||
|
||||
# 导入命令模块
|
||||
from python_core.cli.commands import scene_app
|
||||
|
||||
app = typer.Typer(
|
||||
name="mixvideo",
|
||||
help="""
|
||||
🎬 MixVideo - 智能视频处理平台
|
||||
|
||||
功能完整的视频处理和管理工具套件:
|
||||
• 🎯 场景检测 - 智能识别视频场景变化
|
||||
• 📤 媒体管理 - 上传、处理、组织视频文件
|
||||
• 📋 模板管理 - 视频模板导入导出
|
||||
• ⚙️ 系统管理 - 配置、状态、存储管理
|
||||
|
||||
快速开始:
|
||||
mixvideo scene detect video.mp4 # 检测场景
|
||||
mixvideo scene batch-detect /videos # 批量检测
|
||||
mixvideo scene split video.mp4 # 分割视频
|
||||
mixvideo scene info video.mp4 # 视频信息
|
||||
""",
|
||||
rich_markup_mode="rich",
|
||||
no_args_is_help=True
|
||||
)
|
||||
|
||||
# 添加场景检测命令组到主应用
|
||||
app.add_typer(scene_app, name="scene")
|
||||
|
||||
@app.command()
|
||||
def init():
|
||||
"""🚀 初始化MixVideo工作环境"""
|
||||
progress_reporter.info("🚀 初始化MixVideo环境...")
|
||||
# TODO: 实现初始化逻辑
|
||||
progress_reporter.success("✅ 初始化完成")
|
||||
|
||||
def main():
|
||||
"""主入口函数"""
|
||||
try:
|
||||
app()
|
||||
except KeyboardInterrupt:
|
||||
progress_reporter.error("\n👋 用户取消操作")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
progress_reporter.error(f"\n❌ [red]程序异常: {e}[/red]")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
python_core/cli/commands/__init__.py
Normal file
8
python_core/cli/commands/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI 命令模块
|
||||
"""
|
||||
|
||||
from .scene import scene_app
|
||||
|
||||
__all__ = ["scene_app"]
|
||||
344
python_core/cli/commands/scene.py
Normal file
344
python_core/cli/commands/scene.py
Normal file
@@ -0,0 +1,344 @@
|
||||
#!/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)
|
||||
6
python_core/cli/const.py
Normal file
6
python_core/cli/const.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from python_core.utils.jsonrpc 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
|
||||
365
python_core/cli/scene_detect.py
Normal file
365
python_core/cli/scene_detect.py
Normal file
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PySceneDetect 场景检测命令行工具
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
import typer
|
||||
from python_core.cli.const import progress_reporter, console, project_root
|
||||
|
||||
# 检查 PySceneDetect 依赖
|
||||
from scenedetect import open_video, SceneManager
|
||||
from scenedetect.detectors import ContentDetector, ThresholdDetector
|
||||
from scenedetect.video_splitter import split_video_ffmpeg
|
||||
|
||||
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
|
||||
|
||||
class SceneDetector:
|
||||
"""场景检测器"""
|
||||
|
||||
def __init__(self):
|
||||
self.supported_formats = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
|
||||
|
||||
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")
|
||||
|
||||
# 创建全局检测器实例
|
||||
detector = SceneDetector()
|
||||
@@ -25,3 +25,5 @@ pydantic
|
||||
pyyaml
|
||||
pydantic_settings
|
||||
scenedetect[opencv]
|
||||
typer
|
||||
rich
|
||||
@@ -144,6 +144,18 @@ class ProgressReporter:
|
||||
"""Report error"""
|
||||
self.report("error", -1, message, error_details)
|
||||
|
||||
def info(self, message: str) -> None:
|
||||
"""Report info message"""
|
||||
self.report("info", -1, message)
|
||||
|
||||
def success(self, message: str) -> None:
|
||||
"""Report success message"""
|
||||
self.report("success", -1, message)
|
||||
|
||||
def warning(self, message: str) -> None:
|
||||
"""Report warning message"""
|
||||
self.report("warning", -1, message)
|
||||
|
||||
|
||||
# Error codes following JSON-RPC 2.0 specification
|
||||
class JSONRPCError:
|
||||
|
||||
Reference in New Issue
Block a user