#!/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}")