Initial commit: WeChat bot project with AI chat, video export, and image upload scripts
This commit is contained in:
329
video_cutter.py
Normal file
329
video_cutter.py
Normal file
@@ -0,0 +1,329 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user