From 4a84f34b31d34df2eabcd693ed5a8adc1dc127ec Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Wed, 4 Feb 2026 21:54:54 -0600 Subject: [PATCH] fix(blocks): add proper resource cleanup to video blocks Add try/finally blocks to close MoviePy clips in loop.py, duration.py, and add_audio.py. Without this, file handles and ffmpeg subprocesses leak on each block execution. Co-Authored-By: Claude Opus 4.5 --- .../backend/backend/blocks/video/add_audio.py | 37 ++++++++++------ .../backend/backend/blocks/video/duration.py | 17 +++++--- .../backend/backend/blocks/video/loop.py | 43 +++++++++++-------- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/autogpt_platform/backend/backend/blocks/video/add_audio.py b/autogpt_platform/backend/backend/blocks/video/add_audio.py index 4f0eadffcb..6f327e278d 100644 --- a/autogpt_platform/backend/backend/blocks/video/add_audio.py +++ b/autogpt_platform/backend/backend/blocks/video/add_audio.py @@ -78,20 +78,33 @@ class AddAudioToVideoBlock(Block): # 2) Load video + audio with moviepy strip_chapters_inplace(video_abspath) - video_clip = VideoFileClip(video_abspath) - audio_clip = AudioFileClip(audio_abspath) - # Optionally scale volume - if input_data.volume != 1.0: - audio_clip = audio_clip.with_volume_scaled(input_data.volume) + video_clip = None + audio_clip = None + final_clip = None + try: + video_clip = VideoFileClip(video_abspath) + audio_clip = AudioFileClip(audio_abspath) + # Optionally scale volume + if input_data.volume != 1.0: + audio_clip = audio_clip.with_volume_scaled(input_data.volume) - # 3) Attach the new audio track - final_clip = video_clip.with_audio(audio_clip) + # 3) Attach the new audio track + final_clip = video_clip.with_audio(audio_clip) - # 4) Write to output file - source = extract_source_name(local_video_path) - output_filename = MediaFileType(f"{node_exec_id}_with_audio_{source}.mp4") - output_abspath = os.path.join(abs_temp_dir, output_filename) - final_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac") + # 4) Write to output file + source = extract_source_name(local_video_path) + output_filename = MediaFileType(f"{node_exec_id}_with_audio_{source}.mp4") + output_abspath = os.path.join(abs_temp_dir, output_filename) + final_clip.write_videofile( + output_abspath, codec="libx264", audio_codec="aac" + ) + finally: + if final_clip: + final_clip.close() + if audio_clip: + audio_clip.close() + if video_clip: + video_clip.close() # 5) Return output - for_block_output returns workspace:// if available, else data URI video_out = await store_media_file( diff --git a/autogpt_platform/backend/backend/blocks/video/duration.py b/autogpt_platform/backend/backend/blocks/video/duration.py index 9b5d7d8206..9e05d35b00 100644 --- a/autogpt_platform/backend/backend/blocks/video/duration.py +++ b/autogpt_platform/backend/backend/blocks/video/duration.py @@ -62,9 +62,16 @@ class MediaDurationBlock(Block): # 2) Strip chapters to avoid MoviePy crash, then load the clip strip_chapters_inplace(media_abspath) - if input_data.is_video: - clip = VideoFileClip(media_abspath) - else: - clip = AudioFileClip(media_abspath) + clip = None + try: + if input_data.is_video: + clip = VideoFileClip(media_abspath) + else: + clip = AudioFileClip(media_abspath) - yield "duration", clip.duration + duration = clip.duration + finally: + if clip: + clip.close() + + yield "duration", duration diff --git a/autogpt_platform/backend/backend/blocks/video/loop.py b/autogpt_platform/backend/backend/blocks/video/loop.py index 272a3f497c..57859d09da 100644 --- a/autogpt_platform/backend/backend/blocks/video/loop.py +++ b/autogpt_platform/backend/backend/blocks/video/loop.py @@ -72,27 +72,36 @@ class LoopVideoBlock(Block): # 2) Load the clip strip_chapters_inplace(input_abspath) - clip = VideoFileClip(input_abspath) + clip = None + looped_clip = None + try: + clip = VideoFileClip(input_abspath) - # 3) Apply the loop effect - looped_clip = clip - if input_data.duration: - # Loop until we reach the specified duration - looped_clip = looped_clip.with_effects([Loop(duration=input_data.duration)]) - elif input_data.n_loops: - looped_clip = looped_clip.with_effects([Loop(n=input_data.n_loops)]) - else: - raise ValueError("Either 'duration' or 'n_loops' must be provided.") + # 3) Apply the loop effect + if input_data.duration: + # Loop until we reach the specified duration + looped_clip = clip.with_effects([Loop(duration=input_data.duration)]) + elif input_data.n_loops: + looped_clip = clip.with_effects([Loop(n=input_data.n_loops)]) + else: + raise ValueError("Either 'duration' or 'n_loops' must be provided.") - assert isinstance(looped_clip, VideoFileClip) + assert isinstance(looped_clip, VideoFileClip) - # 4) Save the looped output - source = extract_source_name(local_video_path) - output_filename = MediaFileType(f"{node_exec_id}_looped_{source}.mp4") - output_abspath = get_exec_file_path(graph_exec_id, output_filename) + # 4) Save the looped output + source = extract_source_name(local_video_path) + output_filename = MediaFileType(f"{node_exec_id}_looped_{source}.mp4") + output_abspath = get_exec_file_path(graph_exec_id, output_filename) - looped_clip = looped_clip.with_audio(clip.audio) - looped_clip.write_videofile(output_abspath, codec="libx264", audio_codec="aac") + looped_clip = looped_clip.with_audio(clip.audio) + looped_clip.write_videofile( + output_abspath, codec="libx264", audio_codec="aac" + ) + finally: + if looped_clip: + looped_clip.close() + if clip: + clip.close() # Return output - for_block_output returns workspace:// if available, else data URI video_out = await store_media_file(