Short-Faceless / utility /render_engine.py
codewithdark's picture
Upload 9 files
9b70717 verified
raw
history blame
7.43 kB
import time
import os
import tempfile
import zipfile
import platform
import subprocess
import logging
from pathlib import Path
from moviepy.editor import (AudioFileClip, CompositeVideoClip, CompositeAudioClip, ImageClip,
TextClip, VideoFileClip)
from moviepy.audio.fx.audio_loop import audio_loop
from moviepy.audio.fx.audio_normalize import audio_normalize
import requests
logger = logging.getLogger(__name__)
def download_file(url, filename):
with open(filename, 'wb') as f:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
response = requests.get(url, headers=headers)
f.write(response.content)
def search_program(program_name):
try:
search_cmd = "where" if platform.system() == "Windows" else "which"
return subprocess.check_output([search_cmd, program_name]).decode().strip()
except subprocess.CalledProcessError:
return None
def get_program_path(program_name):
program_path = search_program(program_name)
return program_path
def get_output_media(audio_file_path, timed_captions, background_video_data, video_server):
"""Generate final video with audio and captions
Args:
audio_file_path (str): Path to audio file
timed_captions (list): List of timed captions
background_video_data (list): List of background video data
video_server (str): Video server URL
Returns:
str: Path to output video file
Raises:
Exception: If video rendering fails
"""
OUTPUT_FILE_NAME = "rendered_video.mp4"
from utility.conf import IMAGEMAGICK_BINARY
from moviepy.config import change_settings
try:
# Validate input files
if not Path(audio_file_path).exists():
raise FileNotFoundError(f"Audio file not found at {audio_file_path}")
try:
change_settings({"IMAGEMAGICK_BINARY": IMAGEMAGICK_BINARY})
logger.info(f"Using ImageMagick from: {IMAGEMAGICK_BINARY}")
except Exception as e:
logger.error(f"Error configuring ImageMagick: {str(e)}")
raise Exception(f"ImageMagick configuration failed: {str(e)}")
except Exception as e:
logger.error(f"Error in initial setup: {str(e)}")
raise Exception(f"Initial setup failed: {str(e)}")
visual_clips = []
for (t1, t2), video_url in background_video_data:
try:
# Download the video file
video_filename = tempfile.NamedTemporaryFile(delete=False).name
logger.info(f"Downloading video from {video_url}")
download_file(video_url, video_filename)
if not Path(video_filename).exists():
raise FileNotFoundError(f"Failed to download video from {video_url}")
# Create VideoFileClip from the downloaded file
video_clip = VideoFileClip(video_filename)
if video_clip is None:
raise ValueError(f"Failed to create video clip from {video_filename}")
video_clip = video_clip.set_start(t1)
video_clip = video_clip.set_end(t2)
visual_clips.append(video_clip)
logger.info(f"Added video clip from {video_url} ({t1}-{t2}s)")
except Exception as e:
logger.error(f"Error processing video {video_url}: {str(e)}")
raise Exception(f"Failed to process video {video_url}: {str(e)}")
audio_clips = []
try:
# Verify audio file exists and is valid
if not os.path.exists(audio_file_path):
raise FileNotFoundError(f"Audio file not found: {audio_file_path}")
audio_file_clip = AudioFileClip(audio_file_path)
if audio_file_clip is None:
raise ValueError(f"Failed to create audio clip from {audio_file_path}")
# Normalize audio volume
audio_file_clip = audio_normalize(audio_file_clip)
# Verify audio duration
if audio_file_clip.duration <= 0:
raise ValueError("Audio file has zero or negative duration")
audio_clips.append(audio_file_clip)
logger.info(f"Added audio clip from {audio_file_path} (duration: {audio_file_clip.duration:.2f}s)")
except Exception as e:
logger.error(f"Error processing audio: {str(e)}")
raise Exception(f"Failed to process audio: {str(e)}")
for (t1, t2), text in timed_captions:
try:
# Updated caption style: changed font, fontsize, and position.
text_clip = TextClip(
txt=text,
fontsize=70,
font="Arial-Bold",
color="white",
stroke_width=2,
stroke_color="black",
method="label"
)
# Set the text to appear at the bottom-center
text_clip = text_clip.set_start(t1).set_end(t2).set_position(('center','bottom'))
visual_clips.append(text_clip)
logger.info(f"Added text clip: {text} ({t1}-{t2}s)")
except Exception as e:
logger.error(f"Error creating text clip: {str(e)}")
raise Exception(f"Failed to create text clip: {str(e)}")
try:
if not visual_clips:
raise ValueError("No visual clips available for rendering")
video = CompositeVideoClip(visual_clips)
if audio_clips:
audio = CompositeAudioClip(audio_clips)
# Ensure video duration matches audio and update video with audio properly
if video.duration < audio.duration:
last_clip = visual_clips[-1]
extended_clip = last_clip.set_end(audio.duration)
visual_clips[-1] = extended_clip
video = CompositeVideoClip(visual_clips)
video = video.set_duration(audio.duration)
# Updated audio application using set_audio
video = video.set_audio(audio)
logger.info(f"Audio synchronized with video (duration: {video.duration:.2f}s)")
logger.info(f"Rendering final video to {OUTPUT_FILE_NAME}")
video.write_videofile(OUTPUT_FILE_NAME, codec='libx264', audio_codec='aac', fps=25, preset='veryfast')
# Clean up downloaded files
for (t1, t2), video_url in background_video_data:
video_filename = tempfile.NamedTemporaryFile(delete=False).name
if Path(video_filename).exists():
os.remove(video_filename)
logger.info(f"Cleaned up temporary file: {video_filename}")
if not Path(OUTPUT_FILE_NAME).exists():
raise FileNotFoundError(f"Failed to create output video at {OUTPUT_FILE_NAME}")
logger.info(f"Successfully rendered video at {OUTPUT_FILE_NAME}")
return OUTPUT_FILE_NAME
except Exception as e:
logger.error(f"Error rendering video: {str(e)}")
raise Exception(f"Video rendering failed: {str(e)}")