feat: loop video clips to match audio duration

This commit is contained in:
harry
2025-05-10 13:26:24 +08:00
parent 4a64e211f9
commit 4c9ac5e6df

View File

@@ -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")