Initial commit: WeChat bot project with AI chat, video export, and image upload scripts

This commit is contained in:
anqiang12
2026-06-18 15:53:24 +08:00
commit 3b1e106cb0
16 changed files with 2541 additions and 0 deletions

329
video_cutter.py Normal file
View 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}")