Files
autoclip/shared/utils/video_processor.py
T

299 lines
11 KiB
Python

"""
视频处理工具
"""
import subprocess
import json
import logging
import re
from typing import List, Dict, Optional
from pathlib import Path
# 修复导入问题
try:
from config import CLIPS_DIR, COLLECTIONS_DIR
except ImportError:
# 如果相对导入失败,尝试绝对导入
import sys
from pathlib import Path
shared_path = Path(__file__).parent.parent
if str(shared_path) not in sys.path:
sys.path.insert(0, str(shared_path))
from config import CLIPS_DIR, COLLECTIONS_DIR
logger = logging.getLogger(__name__)
class VideoProcessor:
"""视频处理工具类"""
def __init__(self, clips_dir: Optional[str] = None, collections_dir: Optional[str] = None):
self.clips_dir = Path(clips_dir) if clips_dir else CLIPS_DIR
self.collections_dir = Path(collections_dir) if collections_dir else COLLECTIONS_DIR
@staticmethod
def sanitize_filename(filename: str) -> str:
"""
清理文件名,移除或替换不合法的字符
Args:
filename: 原始文件名
Returns:
清理后的文件名
"""
# 移除或替换不合法的字符
# Windows和Unix系统都不允许的字符: < > : " | ? * \ /
# 替换为下划线
sanitized = re.sub(r'[<>:"|?*\\/]', '_', filename)
# 移除前后空格和点
sanitized = sanitized.strip(' .')
# 限制长度,避免文件名过长
if len(sanitized) > 100:
sanitized = sanitized[:100]
# 确保文件名不为空
if not sanitized:
sanitized = "untitled"
return sanitized
@staticmethod
def convert_srt_time_to_ffmpeg_time(srt_time: str) -> str:
"""
将SRT时间格式转换为FFmpeg时间格式
Args:
srt_time: SRT时间格式 (如 "00:00:06,140")
Returns:
FFmpeg时间格式 (如 "00:00:06.140")
"""
# 将逗号替换为点
return srt_time.replace(',', '.')
@staticmethod
def extract_clip(input_video: Path, output_path: Path,
start_time: str, end_time: str) -> bool:
"""
从视频中提取指定时间段的片段
Args:
input_video: 输入视频路径
output_path: 输出视频路径
start_time: 开始时间 (格式: "00:01:25,140")
end_time: 结束时间 (格式: "00:02:53,500")
Returns:
是否成功
"""
try:
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 转换时间格式:从SRT格式转换为FFmpeg格式
ffmpeg_start_time = VideoProcessor.convert_srt_time_to_ffmpeg_time(start_time)
ffmpeg_end_time = VideoProcessor.convert_srt_time_to_ffmpeg_time(end_time)
# 计算持续时间
def time_to_seconds(time_str: str) -> float:
"""将时间字符串转换为秒数"""
h, m, s = time_str.split(':')
s, ms = s.split('.')
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000
start_seconds = time_to_seconds(ffmpeg_start_time)
end_seconds = time_to_seconds(ffmpeg_end_time)
duration = end_seconds - start_seconds
# 构建优化的FFmpeg命令
# 使用 -ss 在输入前进行精确定位,使用 -t 指定持续时间
cmd = [
'ffmpeg',
'-ss', ffmpeg_start_time, # 在输入前定位,更精确
'-i', str(input_video),
'-t', str(duration), # 使用持续时间而不是绝对结束时间
'-c:v', 'copy', # 复制视频流
'-c:a', 'copy', # 复制音频流
'-avoid_negative_ts', 'make_zero',
'-y', # 覆盖输出文件
str(output_path)
]
# 执行命令
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
if result.returncode == 0:
logger.info(f"成功提取视频片段: {output_path} ({ffmpeg_start_time} -> {ffmpeg_end_time}, 时长: {duration:.2f}秒)")
return True
else:
logger.error(f"提取视频片段失败: {result.stderr}")
return False
except Exception as e:
logger.error(f"视频处理异常: {str(e)}")
return False
@staticmethod
def create_collection(clips_list: List[Path], output_path: Path) -> bool:
"""
将多个视频片段拼接成合集
Args:
clips_list: 视频片段路径列表
output_path: 输出合集路径
Returns:
是否成功
"""
try:
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 创建concat文件
concat_file = output_path.parent / "concat_list.txt"
with open(concat_file, 'w', encoding='utf-8') as f:
for clip_path in clips_list:
f.write(f"file '{clip_path.absolute()}'\n")
# 构建FFmpeg命令
cmd = [
'ffmpeg',
'-f', 'concat',
'-safe', '0',
'-i', str(concat_file),
'-c', 'copy',
'-y',
str(output_path)
]
# 执行命令
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
# 清理临时文件
concat_file.unlink(missing_ok=True)
if result.returncode == 0:
logger.info(f"成功创建合集: {output_path}")
return True
else:
logger.error(f"创建合集失败: {result.stderr}")
return False
except Exception as e:
logger.error(f"视频拼接异常: {str(e)}")
return False
@staticmethod
def get_video_info(video_path: Path) -> Dict:
"""
获取视频信息
Args:
video_path: 视频文件路径
Returns:
视频信息字典
"""
try:
cmd = [
'ffprobe',
'-v', 'quiet',
'-print_format', 'json',
'-show_format',
'-show_streams',
str(video_path)
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8', errors='ignore')
if result.returncode == 0:
info = json.loads(result.stdout)
return {
'duration': float(info['format']['duration']),
'size': int(info['format']['size']),
'bitrate': int(info['format']['bit_rate']),
'streams': info['streams']
}
else:
logger.error(f"获取视频信息失败: {result.stderr}")
return {}
except Exception as e:
logger.error(f"获取视频信息异常: {str(e)}")
return {}
def batch_extract_clips(self, input_video: Path, clips_data: List[Dict]) -> List[Path]:
"""
批量提取视频片段
Args:
input_video: 输入视频路径
clips_data: 片段数据列表,每个元素包含id、title、start_time、end_time
Returns:
成功提取的片段路径列表
"""
successful_clips = []
for clip_data in clips_data:
clip_id = clip_data['id']
title = clip_data.get('title', f"片段_{clip_id}")
start_time = clip_data['start_time']
end_time = clip_data['end_time']
# 使用标题作为文件名,并清理不合法的字符
# 在文件名中包含clip_id,便于后续合集拼接时查找
safe_title = VideoProcessor.sanitize_filename(title)
output_path = self.clips_dir / f"{clip_id}_{safe_title}.mp4"
if VideoProcessor.extract_clip(input_video, output_path, start_time, end_time):
successful_clips.append(output_path)
return successful_clips
def create_collections_from_metadata(self, collections_data: List[Dict]) -> List[Path]:
"""
根据元数据创建合集
Args:
collections_data: 合集数据列表
Returns:
成功创建的合集路径列表
"""
successful_collections = []
for collection_data in collections_data:
collection_id = collection_data['id']
collection_title = collection_data.get('collection_title', f'合集_{collection_id}')
clip_ids = collection_data['clip_ids']
# 构建片段路径列表
clips_list = []
for clip_id in clip_ids:
# 查找对应的切片文件
# 新的文件名格式是: {clip_id}_{title}.mp4
clip_path = self.clips_dir / f"{clip_id}_*.mp4"
found_clips = list(self.clips_dir.glob(f"{clip_id}_*.mp4"))
if found_clips:
found_clip = found_clips[0] # 取第一个匹配的文件
clips_list.append(found_clip)
logger.info(f"找到合集 {collection_id} 的切片: {found_clip.name}")
else:
logger.warning(f"未找到合集 {collection_id} 的切片 {clip_id}")
if clips_list:
# 使用collection_title作为文件名,并清理不合法的字符
safe_title = VideoProcessor.sanitize_filename(collection_title)
output_path = self.collections_dir / f"{safe_title}.mp4"
if VideoProcessor.create_collection(clips_list, output_path):
successful_collections.append(output_path)
logger.info(f"成功创建合集 {collection_id}: {output_path}")
else:
logger.warning(f"合集 {collection_id} 没有找到任何有效的切片文件")
return successful_collections