Files
agent_test/video_cutter.py

329 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
视频切割导出模块
功能:根据图片名找到原始视频,按时间范围切割并导出
依赖:系统需要安装 ffmpeg
"""
import os
import re
import subprocess
from pathlib import Path
from typing import Optional
# ========== 配置 ==========
VIDEO_DIR = "./shipin" # 原始视频目录
EXPORT_DIR = "./export_videos" # 导出视频目录
VIDEO_EXTENSIONS = {'.avi', '.mp4', '.mkv', '.mov', '.flv', '.wmv', '.webm', '.ts', '.m4v'}
# ==========================
def parse_image_info(image_name: str) -> Optional[dict]:
"""
从图片名解析视频名和时间点
Args:
image_name: 图片文件名,例如 '001 Puss Gets the Boot [1940]_00m05s_213.jpg'
Returns:
{
'video_name': '001 Puss Gets the Boot [1940]',
'timestamp_seconds': 5.213
}
解析失败返回 None
"""
# 移除扩展名
name_without_ext = Path(image_name).stem
# 匹配时间戳格式_XXmYYs_ZZZ
match = re.search(r'_(\d{2})m(\d{2})s_(\d{3})$', name_without_ext)
if not match:
return None
minutes = int(match.group(1))
seconds = int(match.group(2))
milliseconds = int(match.group(3))
timestamp_seconds = minutes * 60 + seconds + milliseconds / 1000
# 提取视频名(去掉时间戳部分)
video_name = name_without_ext[:match.start()]
return {
'video_name': video_name,
'timestamp_seconds': timestamp_seconds
}
def find_video_file(video_name: str) -> Optional[str]:
"""
根据视频名找到对应的视频文件
Args:
video_name: 视频名,例如 '001 Puss Gets the Boot [1940]'
Returns:
视频文件路径,未找到返回 None
"""
video_dir = Path(VIDEO_DIR)
for ext in VIDEO_EXTENSIONS:
video_path = video_dir / f"{video_name}{ext}"
if video_path.exists():
return str(video_path)
return None
def get_video_duration(video_path: str) -> float:
"""
获取视频总时长(秒)
Args:
video_path: 视频文件路径
Returns:
视频时长(秒),失败返回 0.0
"""
try:
cmd = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
return float(result.stdout.strip())
except (subprocess.TimeoutExpired, ValueError):
pass
return 0.0
def format_time(seconds: float) -> str:
"""
格式化时间:秒 -> '01m23s'
Args:
seconds: 秒数
Returns:
格式化的时间字符串
"""
mins = int(seconds // 60)
secs = int(seconds % 60)
return f"{mins:02d}m{secs:02d}s"
def cut_video(
video_path: str,
start_time: float,
end_time: float,
output_path: str
) -> dict:
"""
使用 ffmpeg 切割视频
Args:
video_path: 原始视频路径
start_time: 开始时间(秒)
end_time: 结束时间(秒)
output_path: 输出路径
Returns:
{
'success': bool,
'output_path': str,
'start_time': float,
'end_time': float,
'duration': float,
'error': str
}
"""
# 确保输出目录存在
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# 计算持续时间
duration = end_time - start_time
cmd = [
'ffmpeg',
'-i', video_path,
'-ss', str(start_time),
'-t', str(duration),
'-c', 'copy', # 直接复制,不重新编码(速度快)
'-y', # 覆盖已存在文件
'-avoid_negative_ts', 'make_zero', # 避免负时间戳
output_path
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600 # 10分钟超时
)
if result.returncode == 0:
return {
'success': True,
'output_path': output_path,
'start_time': start_time,
'end_time': end_time,
'duration': duration,
'error': None
}
else:
return {
'success': False,
'output_path': None,
'start_time': start_time,
'end_time': end_time,
'duration': duration,
'error': f"ffmpeg 错误: {result.stderr[-300:]}"
}
except subprocess.TimeoutExpired:
return {
'success': False,
'output_path': None,
'start_time': start_time,
'end_time': end_time,
'duration': duration,
'error': "视频切割超时超过10分钟"
}
def export_video_clip(
image_name: str,
mode: str = "around",
seconds: int = 300
) -> dict:
"""
根据图片导出视频片段
Args:
image_name: 图片文件名
mode: 导出模式
- "around": 前后各 N 秒
- "after": 后 N 秒
- "before": 前 N 秒
- "total": 整个视频
seconds: 秒数total 模式下忽略)
Returns:
{
'success': bool,
'video_path': str, # 导出的视频路径
'video_name': str, # 视频名
'start_time': str, # 开始时间(格式化)
'end_time': str, # 结束时间(格式化)
'duration': str, # 时长(格式化)
'original_image': str, # 原始图片名
'original_timestamp': str, # 图片在视频中的时间点
'error': str # 错误信息
}
"""
# 1. 解析图片信息
info = parse_image_info(image_name)
if not info:
return {
'success': False,
'error': f"无法解析图片名: {image_name}格式应为视频名_XXmYYs_ZZZ.jpg"
}
video_name = info['video_name']
timestamp = info['timestamp_seconds']
# 2. 找到原始视频
video_path = find_video_file(video_name)
if not video_path:
return {
'success': False,
'error': f"未找到视频: {video_name}(请确认视频文件在 {VIDEO_DIR} 目录下)"
}
# 3. 获取视频总时长
video_duration = get_video_duration(video_path)
if video_duration <= 0:
return {
'success': False,
'error': "无法获取视频时长,请确认视频文件格式正确"
}
# 4. 计算切割范围
if mode == "total":
start_time = 0
end_time = video_duration
elif mode == "around":
start_time = max(0, timestamp - seconds)
end_time = min(video_duration, timestamp + seconds)
elif mode == "after":
start_time = timestamp
end_time = min(video_duration, timestamp + seconds)
elif mode == "before":
start_time = max(0, timestamp - seconds)
end_time = timestamp
else:
return {
'success': False,
'error': f"未知模式: {mode}支持around/after/before/total"
}
# 5. 生成输出路径
start_str = format_time(start_time)
end_str = format_time(end_time)
output_filename = f"{video_name}_{start_str}-{end_str}.mp4"
output_path = os.path.join(EXPORT_DIR, output_filename)
# 6. 执行切割
print(f"开始切割视频: {video_name}")
print(f" 时间范围: {start_str} -> {end_str}")
print(f" 输出路径: {output_path}")
result = cut_video(video_path, start_time, end_time, output_path)
if result['success']:
print(f"✅ 视频导出成功: {output_path}")
return {
'success': True,
'video_path': output_path,
'video_name': video_name,
'start_time': start_str,
'end_time': end_str,
'duration': format_time(result['duration']),
'original_image': image_name,
'original_timestamp': format_time(timestamp),
'error': None
}
else:
print(f"❌ 视频导出失败: {result['error']}")
return {
'success': False,
'error': result['error']
}
# ========== 测试 ==========
if __name__ == "__main__":
import sys
# 测试解析功能
test_images = [
"001 Puss Gets the Boot [1940]_05m23s_456.jpg",
"002 The Midnight Snack [1941]_02m15s_123.jpg",
"invalid_image_name.jpg"
]
print("=== 测试图片名解析 ===")
for img in test_images:
info = parse_image_info(img)
print(f" {img}")
print(f" -> {info}")
# 测试导出功能(如果有命令行参数)
if len(sys.argv) > 1:
print("\n=== 测试视频导出 ===")
test_image = sys.argv[1]
test_mode = sys.argv[2] if len(sys.argv) > 2 else "around"
test_seconds = int(sys.argv[3]) if len(sys.argv) > 3 else 120
result = export_video_clip(test_image, mode=test_mode, seconds=test_seconds)
print(f"\n导出结果: {result}")