feat: loop video clips to match audio duration
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import glob
|
import glob
|
||||||
|
import itertools
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import gc
|
import gc
|
||||||
@@ -31,15 +32,19 @@ from app.services.utils import video_effects
|
|||||||
from app.utils import utils
|
from app.utils import utils
|
||||||
|
|
||||||
class SubClippedVideoClip:
|
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.file_path = file_path
|
||||||
self.start_time = start_time
|
self.start_time = start_time
|
||||||
self.end_time = end_time
|
self.end_time = end_time
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
|
if duration is None:
|
||||||
|
self.duration = end_time - start_time
|
||||||
|
else:
|
||||||
|
self.duration = duration
|
||||||
|
|
||||||
def __str__(self):
|
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"
|
audio_codec = "aac"
|
||||||
@@ -131,7 +136,7 @@ def combine_videos(
|
|||||||
aspect = VideoAspect(video_aspect)
|
aspect = VideoAspect(video_aspect)
|
||||||
video_width, video_height = aspect.to_resolution()
|
video_width, video_height = aspect.to_resolution()
|
||||||
|
|
||||||
clip_files = []
|
processed_clips = []
|
||||||
subclipped_items = []
|
subclipped_items = []
|
||||||
video_duration = 0
|
video_duration = 0
|
||||||
for video_path in video_paths:
|
for video_path in video_paths:
|
||||||
@@ -221,28 +226,39 @@ def combine_videos(
|
|||||||
|
|
||||||
close_clip(clip)
|
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
|
video_duration += clip.duration
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"failed to process clip: {str(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
|
# merge video clips progressively, avoid loading all videos at once to avoid memory overflow
|
||||||
logger.info("starting clip merging process")
|
logger.info("starting clip merging process")
|
||||||
if not clip_files:
|
if not processed_clips:
|
||||||
logger.warning("no clips available for merging")
|
logger.warning("no clips available for merging")
|
||||||
return combined_video_path
|
return combined_video_path
|
||||||
|
|
||||||
# if there is only one clip, use it directly
|
# 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")
|
logger.info("using single clip directly")
|
||||||
shutil.copy(clip_files[0], combined_video_path)
|
shutil.copy(processed_clips[0].file_path, combined_video_path)
|
||||||
delete_files(clip_files)
|
delete_files(processed_clips)
|
||||||
logger.info("video combining completed")
|
logger.info("video combining completed")
|
||||||
return combined_video_path
|
return combined_video_path
|
||||||
|
|
||||||
# create initial video file as base
|
# 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_video = f"{output_dir}/temp-merged-video.mp4"
|
||||||
temp_merged_next = f"{output_dir}/temp-merged-next.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)
|
shutil.copy(base_clip_path, temp_merged_video)
|
||||||
|
|
||||||
# merge remaining video clips one by one
|
# merge remaining video clips one by one
|
||||||
for i, clip_path in enumerate(clip_files[1:], 1):
|
for i, clip in enumerate(processed_clips[1:], 1):
|
||||||
logger.info(f"merging clip {i}/{len(clip_files)-1}")
|
logger.info(f"merging clip {i}/{len(processed_clips)-1}, duration: {clip.duration:.2f}s")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# load current base video and next clip to merge
|
# load current base video and next clip to merge
|
||||||
base_clip = VideoFileClip(temp_merged_video)
|
base_clip = VideoFileClip(temp_merged_video)
|
||||||
next_clip = VideoFileClip(clip_path)
|
next_clip = VideoFileClip(clip.file_path)
|
||||||
|
|
||||||
# merge these two clips
|
# merge these two clips
|
||||||
merged_clip = concatenate_videoclips([base_clip, next_clip])
|
merged_clip = concatenate_videoclips([base_clip, next_clip])
|
||||||
@@ -286,6 +302,7 @@ def combine_videos(
|
|||||||
os.rename(temp_merged_video, combined_video_path)
|
os.rename(temp_merged_video, combined_video_path)
|
||||||
|
|
||||||
# clean temp files
|
# clean temp files
|
||||||
|
clip_files = [clip.file_path for clip in processed_clips]
|
||||||
delete_files(clip_files)
|
delete_files(clip_files)
|
||||||
|
|
||||||
logger.info("video combining completed")
|
logger.info("video combining completed")
|
||||||
|
|||||||
Reference in New Issue
Block a user