|
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:
|
|
|
|
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:
|
|
|
|
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}")
|
|
|
|
|
|
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:
|
|
|
|
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}")
|
|
|
|
|
|
audio_file_clip = audio_normalize(audio_file_clip)
|
|
|
|
|
|
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:
|
|
|
|
text_clip = TextClip(
|
|
txt=text,
|
|
fontsize=70,
|
|
font="Arial-Bold",
|
|
color="white",
|
|
stroke_width=2,
|
|
stroke_color="black",
|
|
method="label"
|
|
)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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')
|
|
|
|
|
|
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)}")
|
|
|