diff --git a/app/services/video.py b/app/services/video.py index ea0344f..011f8f2 100644 --- a/app/services/video.py +++ b/app/services/video.py @@ -1,4 +1,5 @@ import glob +import itertools import os import random import gc @@ -31,15 +32,19 @@ from app.services.utils import video_effects from app.utils import utils class SubClippedVideoClip: - def __init__(self, file_path, start_time, end_time, width=None, height=None): + def __init__(self, file_path, start_time=None, end_time=None, width=None, height=None, duration=None): self.file_path = file_path self.start_time = start_time self.end_time = end_time self.width = width self.height = height + if duration is None: + self.duration = end_time - start_time + else: + self.duration = duration def __str__(self): - return f"SubClippedVideoClip(file_path={self.file_path}, start_time={self.start_time}, end_time={self.end_time}, width={self.width}, height={self.height})" + return f"SubClippedVideoClip(file_path={self.file_path}, start_time={self.start_time}, end_time={self.end_time}, duration={self.duration}, width={self.width}, height={self.height})" audio_codec = "aac" @@ -131,7 +136,7 @@ def combine_videos( aspect = VideoAspect(video_aspect) video_width, video_height = aspect.to_resolution() - clip_files = [] + processed_clips = [] subclipped_items = [] video_duration = 0 for video_path in video_paths: @@ -221,28 +226,39 @@ def combine_videos( close_clip(clip) - clip_files.append(clip_file) + processed_clips.append(SubClippedVideoClip(file_path=clip_file, duration=clip.duration, width=clip_w, height=clip_h)) video_duration += clip.duration except Exception as e: logger.error(f"failed to process clip: {str(e)}") - + + # loop processed clips until the video duration matches or exceeds the audio duration. + if video_duration < audio_duration: + logger.warning(f"video duration ({video_duration:.2f}s) is shorter than audio duration ({audio_duration:.2f}s), looping clips to match audio length.") + base_clips = processed_clips.copy() + for clip in itertools.cycle(base_clips): + if video_duration >= audio_duration: + break + processed_clips.append(clip) + video_duration += clip.duration + logger.info(f"video duration: {video_duration:.2f}s, audio duration: {audio_duration:.2f}s, looped {len(processed_clips)-len(base_clips)} clips") + # merge video clips progressively, avoid loading all videos at once to avoid memory overflow logger.info("starting clip merging process") - if not clip_files: + if not processed_clips: logger.warning("no clips available for merging") return combined_video_path # if there is only one clip, use it directly - if len(clip_files) == 1: + if len(processed_clips) == 1: logger.info("using single clip directly") - shutil.copy(clip_files[0], combined_video_path) - delete_files(clip_files) + shutil.copy(processed_clips[0].file_path, combined_video_path) + delete_files(processed_clips) logger.info("video combining completed") return combined_video_path # create initial video file as base - base_clip_path = clip_files[0] + base_clip_path = processed_clips[0].file_path temp_merged_video = f"{output_dir}/temp-merged-video.mp4" temp_merged_next = f"{output_dir}/temp-merged-next.mp4" @@ -250,13 +266,13 @@ def combine_videos( shutil.copy(base_clip_path, temp_merged_video) # merge remaining video clips one by one - for i, clip_path in enumerate(clip_files[1:], 1): - logger.info(f"merging clip {i}/{len(clip_files)-1}") + for i, clip in enumerate(processed_clips[1:], 1): + logger.info(f"merging clip {i}/{len(processed_clips)-1}, duration: {clip.duration:.2f}s") try: # load current base video and next clip to merge base_clip = VideoFileClip(temp_merged_video) - next_clip = VideoFileClip(clip_path) + next_clip = VideoFileClip(clip.file_path) # merge these two clips merged_clip = concatenate_videoclips([base_clip, next_clip]) @@ -286,6 +302,7 @@ def combine_videos( os.rename(temp_merged_video, combined_video_path) # clean temp files + clip_files = [clip.file_path for clip in processed_clips] delete_files(clip_files) logger.info("video combining completed")