Animetrix_AI / backend /narrator.py
SayedZahur786's picture
Fix visual clutter and audio sync: enhanced layout zones, audio concatenation, proper timing
ee6d55e
import os
from gtts import gTTS
from moviepy import VideoFileClip, AudioFileClip, CompositeAudioClip, concatenate_audioclips
import uuid
# Get the directory of the current script (backend/)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEDIA_DIR = os.path.join(BASE_DIR, "media")
AUDIO_DIR = os.path.join(MEDIA_DIR, "audio")
# Ensure audio directory exists
os.makedirs(AUDIO_DIR, exist_ok=True)
def get_audio_duration(audio_path: str) -> float:
"""Get the duration of an audio file in seconds"""
try:
if not audio_path or not os.path.exists(audio_path):
return 0.0
audio = AudioFileClip(audio_path)
duration = audio.duration
audio.close()
return duration
except Exception as e:
print(f"Error getting audio duration: {e}")
return 0.0
def concatenate_audio_files(audio_paths: list, output_filename: str = None) -> str:
"""
Concatenates multiple audio files into one.
Returns the path to the combined audio file.
"""
try:
# Filter out None values and non-existent files
valid_paths = [p for p in audio_paths if p and os.path.exists(p)]
if not valid_paths:
print("No valid audio files to concatenate")
return None
if len(valid_paths) == 1:
# Only one file, just return it
return valid_paths[0]
# Load all audio clips
clips = [AudioFileClip(path) for path in valid_paths]
# Concatenate
final_audio = concatenate_audioclips(clips)
# Generate output path
if not output_filename:
run_id = str(uuid.uuid4())
output_filename = f"combined_narration_{run_id}.mp3"
output_path = os.path.join(BASE_DIR, output_filename)
final_audio.write_audiofile(output_path, logger=None)
# Cleanup
for clip in clips:
clip.close()
final_audio.close()
return output_path
except Exception as e:
print(f"Error concatenating audio files: {e}")
return None
def generate_narration_audio(text: str, filename: str = None) -> str:
"""
Generates an MP3 audio file from the given text using gTTS.
Returns the absolute path to the generated audio file.
"""
try:
if not filename:
run_id = str(uuid.uuid4())
filename = f"narration_{run_id}.mp3"
# Save in BASE_DIR (backend/) so it's accessible to the script
filepath = os.path.join(BASE_DIR, filename)
tts = gTTS(text=text, lang='en', slow=False)
tts.save(filepath)
return filepath
except Exception as e:
print(f"Error generating audio: {e}")
return None
def merge_audio_video(video_path: str, audio_path: str) -> str:
"""
Merges the given audio file into the video file.
Returns the path to the new video file with audio.
If merging fails, returns the original video path.
"""
try:
if not audio_path or not os.path.exists(audio_path):
print("Audio file not found, skipping merge.")
return video_path
if not video_path or not os.path.exists(video_path):
print("Video file not found, skipping merge.")
return video_path
# Generate output path
video_dir = os.path.dirname(video_path)
video_filename = os.path.basename(video_path)
output_filename = f"narrated_{video_filename}"
output_path = os.path.join(video_dir, output_filename)
# Load clips
video_clip = VideoFileClip(video_path)
audio_clip = AudioFileClip(audio_path)
# Handle duration mismatch
# If audio is longer, we might need to loop video or cut audio.
# For simplicity, we'll let the video dictate duration, but if audio is longer,
# we might lose the end. Ideally, we'd extend the last frame of video.
# Here, we'll just set audio to video duration (cut off) or loop video?
# Let's just set the audio to the video.
# Better approach: If audio is longer, extend video? No, that's hard with compiled video.
# Let's just set the audio. If it's too long, it gets cut.
final_audio = audio_clip
# Set audio to video
final_video = video_clip.with_audio(final_audio)
# Write output
# codec='libx264' is standard. audio_codec='aac'
final_video.write_videofile(output_path, codec='libx264', audio_codec='aac', logger=None)
# Cleanup
video_clip.close()
audio_clip.close()
return output_path
except Exception as e:
print(f"Error merging audio and video: {e}")
with open("merge_error.log", "w") as f:
f.write(str(e))
import traceback
traceback.print_exc(file=f)
# Return original video on failure
return video_path