Spaces:
Configuration error
Configuration error
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 | |