mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-01-27 23:08:14 +00:00
refactor(app): 优化视频剪辑函数(毫秒+缓存目录)
- 优化时间格式处理,支持更灵活的时间输入- 改进视频缓存目录结构,基于原视频生成哈希值 - 优化日志输出和错误处理 - 调整合并视频功能,移除未使用的示例代码 - 修复了一些与时间戳相关的小问题
This commit is contained in:
parent
974a219dd3
commit
9efccea97f
@ -254,10 +254,14 @@ def download_videos(
|
||||
|
||||
def time_to_seconds(time_str: str) -> float:
|
||||
"""
|
||||
将时间字符串转换为秒数,支持多种格式:
|
||||
1. 'HH:MM:SS,mmm' (时:分:秒,毫秒)
|
||||
2. 'MM:SS' (分:秒)
|
||||
3. 'SS' (秒)
|
||||
将时间字符串转换为秒数
|
||||
支持格式: 'HH:MM:SS,mmm' (时:分:秒,毫秒)
|
||||
|
||||
Args:
|
||||
time_str: 时间字符串,如 "00:00:20,100"
|
||||
|
||||
Returns:
|
||||
float: 转换后的秒数(包含毫秒)
|
||||
"""
|
||||
try:
|
||||
# 处理毫秒部分
|
||||
@ -268,26 +272,30 @@ def time_to_seconds(time_str: str) -> float:
|
||||
time_part = time_str
|
||||
ms = 0
|
||||
|
||||
# 根据格式分别处理
|
||||
# 处理时分秒
|
||||
parts = time_part.split(':')
|
||||
if len(parts) == 3: # HH:MM:SS
|
||||
time_obj = datetime.strptime(time_part, "%H:%M:%S")
|
||||
seconds = time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
|
||||
elif len(parts) == 2: # MM:SS
|
||||
time_obj = datetime.strptime(time_part, "%M:%S")
|
||||
seconds = time_obj.minute * 60 + time_obj.second
|
||||
else: # SS
|
||||
seconds = float(time_part)
|
||||
h, m, s = map(int, parts)
|
||||
seconds = h * 3600 + m * 60 + s
|
||||
else:
|
||||
raise ValueError("时间格式必须为 HH:MM:SS,mmm")
|
||||
|
||||
return seconds + ms
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"时间格式错误: {time_str}")
|
||||
raise ValueError(f"时间格式错误,支持的格式:HH:MM:SS,mmm 或 MM:SS 或 SS") from e
|
||||
raise ValueError(f"时间格式错误: 必须为 HH:MM:SS,mmm 格式") from e
|
||||
|
||||
|
||||
def format_timestamp(seconds: float) -> str:
|
||||
"""
|
||||
将秒数转换为可读的时间格式 (HH:MM:SS,mmm)
|
||||
|
||||
Args:
|
||||
seconds: 秒数(可包含毫秒)
|
||||
|
||||
Returns:
|
||||
str: 格式化的时间字符串,如 "00:00:20,100"
|
||||
"""
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
@ -301,41 +309,44 @@ def format_timestamp(seconds: float) -> str:
|
||||
def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> dict:
|
||||
"""
|
||||
保存剪辑后的视频
|
||||
|
||||
Args:
|
||||
timestamp: 需要裁剪的单个时间戳,支持格式:
|
||||
1. 'HH:MM:SS,mmm-HH:MM:SS,mmm' (时:分:秒,毫秒)
|
||||
2. 'MM:SS-MM:SS' (分:秒-分:秒)
|
||||
3. 'SS-SS' (秒-秒)
|
||||
timestamp: 需要裁剪的时间戳,格式为 'HH:MM:SS,mmm-HH:MM:SS,mmm'
|
||||
例如: '00:00:00,000-00:00:20,100'
|
||||
origin_video: 原视频路径
|
||||
save_dir: 存储目录
|
||||
|
||||
Returns:
|
||||
裁剪后的视频路径,格式为 {timestamp: video_path}
|
||||
dict: 裁剪后的视频路径,格式为 {timestamp: video_path}
|
||||
"""
|
||||
# 使用新的路径结构
|
||||
if not save_dir:
|
||||
save_dir = utils.storage_dir("cache_videos")
|
||||
base_dir = os.path.join(utils.temp_dir(), "clip_video")
|
||||
video_hash = utils.md5(origin_video)
|
||||
save_dir = os.path.join(base_dir, video_hash)
|
||||
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir)
|
||||
|
||||
video_id = f"vid-{timestamp.replace(':', '_').replace(',', '-')}"
|
||||
video_path = f"{save_dir}/{video_id}.mp4"
|
||||
# 生成更规范的视频文件名
|
||||
video_id = f"vid-{timestamp.replace(':', '-').replace(',', '_')}"
|
||||
video_path = os.path.join(save_dir, f"{video_id}.mp4")
|
||||
|
||||
if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
|
||||
logger.info(f"video already exists: {video_path}")
|
||||
return {timestamp: video_path}
|
||||
|
||||
try:
|
||||
# 先加载视频获取总时长
|
||||
# 加载视频获取总时长
|
||||
video = VideoFileClip(origin_video)
|
||||
total_duration = video.duration
|
||||
|
||||
# 获取目标时间段
|
||||
# 解析时间戳
|
||||
start_str, end_str = timestamp.split('-')
|
||||
start = time_to_seconds(start_str)
|
||||
end = time_to_seconds(end_str)
|
||||
|
||||
# 验证时间段是否有效
|
||||
# 验证时间段
|
||||
if start >= total_duration:
|
||||
logger.warning(f"起始时间 {format_timestamp(start)} ({start:.3f}秒) 超出视频总时长 {format_timestamp(total_duration)} ({total_duration:.3f}秒)")
|
||||
video.close()
|
||||
@ -353,6 +364,8 @@ def save_clip_video(timestamp: str, origin_video: str, save_dir: str = "") -> di
|
||||
# 剪辑视频
|
||||
duration = end - start
|
||||
logger.info(f"开始剪辑视频: {format_timestamp(start)} - {format_timestamp(end)},时长 {format_timestamp(duration)}")
|
||||
|
||||
# 剪辑视频
|
||||
subclip = video.subclip(start, end)
|
||||
|
||||
try:
|
||||
@ -401,13 +414,21 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, pro
|
||||
剪辑视频
|
||||
Args:
|
||||
task_id: 任务id
|
||||
timestamp_terms: 需要剪辑的时间戳列表,如:['00:00-00:20', '00:36-00:40', '07:07-07:22']
|
||||
timestamp_terms: 需要剪辑的时间戳列表,如:['00:00:00,000-00:00:20,100', '00:00:43,039-00:00:46,959']
|
||||
origin_video: 原视频路径
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
剪辑后的视频路径
|
||||
"""
|
||||
# 创建基于原视频的缓存目录
|
||||
video_cache_dir = os.path.join(utils.temp_dir(), "video")
|
||||
video_hash = utils.md5(origin_video + str(os.path.getmtime(origin_video)))
|
||||
video_clips_dir = os.path.join(video_cache_dir, video_hash)
|
||||
|
||||
if not os.path.exists(video_clips_dir):
|
||||
os.makedirs(video_clips_dir)
|
||||
|
||||
video_paths = {}
|
||||
total_items = len(timestamp_terms)
|
||||
for index, item in enumerate(timestamp_terms):
|
||||
@ -415,8 +436,9 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, pro
|
||||
if material_directory == "task":
|
||||
material_directory = utils.task_dir(task_id)
|
||||
elif material_directory and not os.path.isdir(material_directory):
|
||||
material_directory = ""
|
||||
material_directory = video_clips_dir # 如果没有指定material_directory,使用缓存目录
|
||||
|
||||
logger.debug("material_directory:",material_directory)
|
||||
try:
|
||||
saved_video_path = save_clip_video(timestamp=item, origin_video=origin_video, save_dir=material_directory)
|
||||
if saved_video_path:
|
||||
@ -429,6 +451,7 @@ def clip_videos(task_id: str, timestamp_terms: List[str], origin_video: str, pro
|
||||
except Exception as e:
|
||||
logger.error(f"视频裁剪失败: {utils.to_json(item)} =>\n{str(traceback.format_exc())}")
|
||||
return {}
|
||||
|
||||
logger.success(f"裁剪 {len(video_paths)} videos")
|
||||
return video_paths
|
||||
|
||||
@ -490,27 +513,5 @@ def merge_videos(video_paths, ost_list):
|
||||
return output_file
|
||||
|
||||
|
||||
# 使用示例
|
||||
# if __name__ == "__main__":
|
||||
# video_paths = ['/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_17-01_37.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_00-00_06.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_06-00_09.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_03-01_10.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_10-01_17.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_24-00_27.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_28-01_36.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_32-00_41.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_36-01_58.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_12-00_15.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-00_09-00_12.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-02_12-02_25.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-02_03-02_12.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-01_58-02_03.mp4',
|
||||
# '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-03_14-03_18.mp4', '/Users/apple/Desktop/home/NarratoAI/storage/cache_videos/vid-03_18-03_20.mp4']
|
||||
#
|
||||
# ost_list = [True, False, False, False, False, False, False, False, True, False, False, False, False, False, False,
|
||||
# False]
|
||||
#
|
||||
# result = merge_videos(video_paths, ost_list)
|
||||
# if result:
|
||||
# print(f"合并后的视频文件:{result}")
|
||||
# else:
|
||||
# print("视频合并失败")
|
||||
#
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
save_clip_video('00:50-01:41', 'E:\\projects\\NarratoAI\\resource\\videos\\WeChat_20241110144511.mp4')
|
||||
|
||||
@ -68,8 +68,9 @@ def get_batch_timestamps(batch_files, prev_batch_files=None):
|
||||
将时间字符串转换为 HH:MM:SS,mmm 格式
|
||||
|
||||
Args:
|
||||
time_str: 9位数字字符串,格式为 MMSSSMMM
|
||||
例如: 000050100 表示 00分50秒100毫秒
|
||||
time_str: 9位数字字符串,格式为 HHMMSSMMM
|
||||
例如: 000010000 表示 00时00分10秒000毫秒
|
||||
000043039 表示 00时00分43秒039毫秒
|
||||
|
||||
Returns:
|
||||
str: HH:MM:SS,mmm 格式的时间戳
|
||||
@ -79,22 +80,12 @@ def get_batch_timestamps(batch_files, prev_batch_files=None):
|
||||
logger.warning(f"Invalid timestamp format: {time_str}")
|
||||
return "00:00:00,000"
|
||||
|
||||
# 提取分钟、秒和毫秒
|
||||
minutes = int(time_str[-9:-6]) # 取后9位的前3位作为分钟
|
||||
seconds = int(time_str[-6:-3]) # 取中间3位作为秒数
|
||||
milliseconds = int(time_str[-3:]) # 取最后3位作为毫秒
|
||||
# 从时间戳中提取时、分、秒和毫秒
|
||||
hours = int(time_str[0:2]) # 前2位作为小时
|
||||
minutes = int(time_str[2:4]) # 第3-4位作为分钟
|
||||
seconds = int(time_str[4:6]) # 第5-6位作为秒数
|
||||
milliseconds = int(time_str[6:]) # 最后3位作为毫秒
|
||||
|
||||
# 处理进位
|
||||
if seconds >= 60:
|
||||
minutes += seconds // 60
|
||||
seconds = seconds % 60
|
||||
|
||||
if minutes >= 60:
|
||||
hours = minutes // 60
|
||||
minutes = minutes % 60
|
||||
else:
|
||||
hours = 0
|
||||
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"
|
||||
|
||||
except ValueError as e:
|
||||
@ -113,7 +104,7 @@ def get_batch_timestamps(batch_files, prev_batch_files=None):
|
||||
last_timestamp = format_timestamp(last_time)
|
||||
timestamp_range = f"{first_timestamp}-{last_timestamp}"
|
||||
|
||||
logger.debug(f"解析时间戳: {first_frame} -> {first_timestamp}, {last_frame} -> {last_timestamp}")
|
||||
# logger.debug(f"解析时间戳: {first_frame} -> {first_timestamp}, {last_frame} -> {last_timestamp}")
|
||||
return first_timestamp, last_timestamp, timestamp_range
|
||||
|
||||
def get_batch_files(keyframe_files, result, batch_size=5):
|
||||
@ -410,7 +401,7 @@ def generate_script(tr, params):
|
||||
skip_seconds=0
|
||||
)
|
||||
|
||||
# 获取所有关键帧文件路径
|
||||
# 获取所有关键文件路径
|
||||
for filename in sorted(os.listdir(video_keyframes_dir)):
|
||||
if filename.endswith('.jpg'):
|
||||
keyframe_files.append(os.path.join(video_keyframes_dir, filename))
|
||||
@ -446,7 +437,7 @@ def generate_script(tr, params):
|
||||
vision_base_url = st.session_state.get('vision_gemini_base_url')
|
||||
|
||||
if not vision_api_key or not vision_model:
|
||||
raise ValueError("未配置 Gemini API Key 或者 模型,请在基础设置中配置")
|
||||
raise ValueError("未配置 Gemini API Key 或者 型,请在基础设置配置")
|
||||
|
||||
analyzer = vision_analyzer.VisionAnalyzer(
|
||||
model_name=vision_model,
|
||||
@ -484,7 +475,7 @@ def generate_script(tr, params):
|
||||
# 获取当前批次的文件列表 keyframe_001136_000045.jpg 将 000045 精度提升到 毫秒
|
||||
batch_files = get_batch_files(keyframe_files, result, vision_batch_size)
|
||||
logger.debug(f"批次 {result['batch_index']} 处理完成,共 {len(batch_files)} 张图片")
|
||||
logger.debug(batch_files)
|
||||
# logger.debug(batch_files)
|
||||
|
||||
first_timestamp, last_timestamp, _ = get_batch_timestamps(batch_files, prev_batch_files)
|
||||
logger.debug(f"处理时间戳: {first_timestamp}-{last_timestamp}")
|
||||
@ -767,7 +758,7 @@ def crop_video(tr, params):
|
||||
utils.cut_video(params, update_progress)
|
||||
time.sleep(0.5)
|
||||
progress_bar.progress(100)
|
||||
status_text.text("剪辑完成!")
|
||||
status_text.text("剪完成!")
|
||||
st.success("视频剪辑成功完成!")
|
||||
except Exception as e:
|
||||
st.error(f"剪辑过程中发生错误: {str(e)}")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user