import os import logging import warnings # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Suppress MoviePy warnings about frame reading warnings.filterwarnings('ignore', category=UserWarning, module='moviepy') try: from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip, CompositeVideoClip, concatenate_audioclips from moviepy.audio.AudioClip import CompositeAudioClip from moviepy.audio.fx.volumex import volumex MOVIEPY_AVAILABLE = True logger.info("✅ MoviePy loaded successfully") except ImportError as e: logger.error(f"❌ MoviePy not available: {e}") MOVIEPY_AVAILABLE = False try: import yt_dlp YT_DLP_AVAILABLE = True logger.info("✅ yt-dlp loaded successfully") except ImportError as e: logger.error(f"❌ yt-dlp not available: {e}") YT_DLP_AVAILABLE = False try: from PIL import Image, ImageDraw, ImageFont import numpy as np PIL_AVAILABLE = True logger.info("✅ PIL (Pillow) loaded successfully") except ImportError as e: logger.error(f"❌ PIL not available: {e}") PIL_AVAILABLE = False class VideoEditor: def __init__(self, temp_folder='temp_audio'): if not MOVIEPY_AVAILABLE: raise ImportError("MoviePy is required for video editing. Install with: pip install moviepy") self.temp_folder = temp_folder os.makedirs(temp_folder, exist_ok=True) logger.info(f"✅ VideoEditor initialized with temp folder: {temp_folder}") def download_youtube_audio(self, youtube_url): """Download audio from YouTube video""" if not YT_DLP_AVAILABLE: raise ImportError("yt-dlp is required. Install with: pip install yt-dlp") try: audio_path = os.path.join(self.temp_folder, 'background_music.m4a') # Remove old file if exists if os.path.exists(audio_path): os.remove(audio_path) # Also check for mp3 version mp3_path = os.path.join(self.temp_folder, 'background_music.mp3') if os.path.exists(mp3_path): os.remove(mp3_path) ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': os.path.join(self.temp_folder, 'background_music.%(ext)s'), 'quiet': True, 'no_warnings': True } logger.info(f"📥 Downloading audio from: {youtube_url}") with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore info = ydl.extract_info(youtube_url, download=True) ext = info.get('ext', 'm4a') actual_path = os.path.join(self.temp_folder, f'background_music.{ext}') # Check which file was created if os.path.exists(actual_path): logger.info(f"✅ Downloaded audio from YouTube: {actual_path}") return actual_path elif os.path.exists(audio_path): logger.info(f"✅ Downloaded audio from YouTube: {audio_path}") return audio_path elif os.path.exists(mp3_path): logger.info(f"✅ Downloaded audio from YouTube: {mp3_path}") return mp3_path else: raise Exception("Audio file was not created") except Exception as e: logger.error(f"❌ Failed to download YouTube audio: {str(e)}") raise Exception(f"Failed to download music: {str(e)}") def add_background_music(self, video, audio_path, volume=0.3): """Add background music to video with volume control""" background_audio = None try: logger.info(f"🎵 Adding background music to video from: {audio_path}") # Check if audio_path is a local file or needs to be downloaded if not os.path.exists(audio_path): raise Exception(f"Audio file not found: {audio_path}") background_audio = AudioFileClip(audio_path) # Adjust background music duration to match video if background_audio.duration > video.duration: background_audio = background_audio.subclip(0, video.duration) else: # Loop if music is shorter than video loops_needed = int(video.duration / background_audio.duration) + 1 background_audio = concatenate_audioclips([background_audio] * loops_needed).subclip(0, video.duration) # Set background music volume background_audio = volumex(background_audio, volume) # Replace original audio with background music (mute original) video = video.set_audio(background_audio) logger.info("✅ Added background music to video (original audio muted)") return video except Exception as e: logger.error(f"❌ Failed to add background music: {str(e)}") raise Exception(f"Failed to add music: {str(e)}") def add_text_overlay(self, video, text, position='top', fontsize=50, color='white', bg_color='black', duration=None): """Add text overlay to video using PIL (no ImageMagick needed)""" try: if not PIL_AVAILABLE: raise ImportError("PIL (Pillow) is required. Install with: pip install Pillow") if duration is None: duration = video.duration logger.info(f"📝 Adding text overlay: {text[:30]}...") # Create an image with text using PIL img_width, img_height = video.size img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Try to use a default font, fall back to default if not available try: font = ImageFont.truetype("arial.ttf", fontsize) except: try: font = ImageFont.truetype("Arial.ttf", fontsize) except: try: # For Windows font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", fontsize) except: logger.warning("⚠️ Could not load Arial font, using default") font = ImageFont.load_default() # Get text bounding box bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # Position mapping if position == 'top': x = (img_width - text_width) // 2 y = 50 elif position == 'bottom': x = (img_width - text_width) // 2 y = img_height - text_height - 50 elif position == 'center': x = (img_width - text_width) // 2 y = (img_height - text_height) // 2 elif position == 'top-left': x = 50 y = 50 elif position == 'top-right': x = img_width - text_width - 50 y = 50 else: x = (img_width - text_width) // 2 y = 50 # Parse colors color_map = { 'white': (255, 255, 255), 'black': (0, 0, 0), 'red': (255, 0, 0), 'blue': (0, 0, 255), 'green': (0, 255, 0), 'yellow': (255, 255, 0) } text_color = color_map.get(color.lower(), (255, 255, 255)) stroke_color = color_map.get(bg_color.lower(), (0, 0, 0)) # Draw text with stroke (outline) stroke_width = 3 for adj_x in range(-stroke_width, stroke_width + 1): for adj_y in range(-stroke_width, stroke_width + 1): draw.text((x + adj_x, y + adj_y), text, font=font, fill=stroke_color) # Draw main text draw.text((x, y), text, font=font, fill=text_color) # Convert PIL image to numpy array img_array = np.array(img) # Create ImageClip from the array txt_clip = ImageClip(img_array, transparent=True) txt_clip = txt_clip.set_duration(duration) # Add fade in/out effects txt_clip = txt_clip.fadein(0.5).fadeout(0.5) # Composite video with text video = CompositeVideoClip([video, txt_clip]) logger.info(f"✅ Added text overlay: {text}") return video except Exception as e: logger.error(f"❌ Failed to add text overlay: {str(e)}") raise Exception(f"Failed to add text: {str(e)}") def edit_video(self, video_path, output_path, music_url=None, music_volume=0.3, text_overlays=None): """ Complete video editing with music and text overlays Args: video_path: Path to input video output_path: Path for output video music_url: YouTube URL OR local file path for background music music_volume: Volume of background music (0.0 to 1.0) text_overlays: List of dict with keys: text, position, duration """ if not os.path.exists(video_path): raise FileNotFoundError(f"Video file not found: {video_path}") # Ensure output path is a file, not a directory if os.path.isdir(output_path): video_filename = os.path.basename(video_path) output_path = os.path.join(output_path, os.path.splitext(video_filename)[0] + "_edited.mp4") logger.info(f"Output is a directory, saving to: {output_path}") video = None audio_path = None try: logger.info(f"🎬 Starting video editing for: {video_path}") with warnings.catch_warnings(): warnings.simplefilter("ignore") video = VideoFileClip(video_path) logger.info(f"📊 Video info: {video.duration:.2f}s, {video.size[0]}x{video.size[1]}") # ✅ NEW: Handle both YouTube URLs and local files if music_url: # Check if it's a local file or YouTube URL if os.path.exists(music_url): # It's a local file logger.info(f"🎵 Using local music file: {music_url}") audio_path = music_url elif music_url.startswith('http'): # It's a YouTube URL logger.info(f"🎵 Downloading music from YouTube: {music_url}") audio_path = self.download_youtube_audio(music_url) else: raise Exception(f"Invalid music source: {music_url}") video = self.add_background_music(video, audio_path, music_volume) # Add text overlays if provided if text_overlays and len(text_overlays) > 0: logger.info(f"📝 Adding {len(text_overlays)} text overlays") for i, overlay in enumerate(text_overlays): logger.info(f"Adding overlay {i+1}/{len(text_overlays)}: {overlay.get('text', 'N/A')[:30]}") video = self.add_text_overlay( video, text=overlay.get('text', 'Subscribe!'), position=overlay.get('position', 'top'), fontsize=overlay.get('fontsize', 50), color=overlay.get('color', 'white'), duration=overlay.get('duration', min(5, video.duration)) ) # Write output video logger.info(f"💾 Writing edited video to: {output_path}") logger.info(f"⏳ This may take a few minutes... Please wait.") # Suppress warnings during video writing with warnings.catch_warnings(): warnings.simplefilter("ignore") video.write_videofile( output_path, codec='libx264', audio_codec='aac', temp_audiofile='temp-audio.m4a', remove_temp=True, fps=30, preset='medium', threads=4, logger=None # Suppress moviepy's verbose logging ) # Verify output file was created if not os.path.exists(output_path): raise Exception(f"Output video file was not created: {output_path}") file_size = os.path.getsize(output_path) if file_size == 0: raise Exception(f"Output video file is empty (0 bytes): {output_path}") file_size_mb = file_size / (1024 * 1024) logger.info(f"✅ Video editing completed: {output_path}") logger.info(f"📊 Output file size: {file_size_mb:.2f} MB") return output_path except Exception as e: logger.error(f"❌ Video editing failed: {str(e)}") import traceback logger.error(traceback.format_exc()) raise Exception(f"Video editing failed: {str(e)}") finally: # Cleanup - close video and wait a bit for file handles to release if video: try: video.close() import time time.sleep(0.5) # Give time for file handles to release except: pass # Clean up temp files self.cleanup_temp_files() def cleanup_temp_files(self): """Clean up temporary files""" try: import time import gc # Force garbage collection to release file handles gc.collect() time.sleep(0.5) if os.path.exists(self.temp_folder): for file in os.listdir(self.temp_folder): file_path = os.path.join(self.temp_folder, file) try: if os.path.isfile(file_path): os.remove(file_path) logger.debug(f"🧹 Removed temp file: {file_path}") except PermissionError: # File still in use, skip it logger.debug(f"⏭️ Skipping temp file (still in use): {file_path}") except Exception as e: logger.warning(f"⚠️ Failed to remove {file_path}: {e}") except Exception as e: logger.warning(f"⚠️ Failed to cleanup temp files: {str(e)}") def main(): """Main function to run video editor with user input""" import argparse parser = argparse.ArgumentParser(description='Edit videos with background music and text overlays') parser.add_argument('--video', type=str, help='Path to input video file') parser.add_argument('--music', type=str, help='YouTube URL for background music') parser.add_argument('--output', type=str, help='Path for output video file') parser.add_argument('--volume', type=float, default=0.3, help='Music volume (0.0 to 1.0, default: 0.3)') parser.add_argument('--text', type=str, help='Text overlay (optional)') parser.add_argument('--text-position', type=str, default='top', choices=['top', 'bottom', 'center', 'top-left', 'top-right'], help='Text position (default: top)') args = parser.parse_args() print("\n" + "="*60) print("🎬 VIDEO EDITOR - Add Background Music & Text") print("="*60 + "\n") # Get video path if args.video: video_path = args.video else: video_path = input("📹 Enter path to your video file: ").strip().strip('"') # Validate video path if not os.path.exists(video_path): print(f"❌ Error: Video file not found: {video_path}") return # Get music URL if args.music: music_url = args.music else: music_url = input("🎵 Enter YouTube URL for background music (or press Enter to skip): ").strip() if not music_url: music_url = None # Get output path if args.output: output_path = args.output else: default_output = os.path.splitext(video_path)[0] + "_edited.mp4" output_path = input(f"💾 Enter output path (press Enter for '{default_output}'): ").strip().strip('"') if not output_path: output_path = default_output # Get volume music_volume = args.volume # Get text overlay text_overlays = None if args.text: text_overlays = [{ 'text': args.text, 'position': args.text_position, 'duration': None }] else: add_text = input("📝 Add text overlay? (y/n, press Enter to skip): ").strip().lower() if add_text == 'y': text = input("Enter text: ").strip() position = input("Position (top/bottom/center, default: top): ").strip() or 'top' text_overlays = [{ 'text': text, 'position': position, 'duration': None }] # Process video try: print("\n" + "="*60) print("🚀 Starting video editing...") print("="*60 + "\n") editor = VideoEditor() result = editor.edit_video( video_path=video_path, output_path=output_path, music_url=music_url, music_volume=music_volume, text_overlays=text_overlays ) print("\n" + "="*60) print(f"✅ SUCCESS! Video saved to: {result}") # Verify and display file info if os.path.exists(result): file_size_mb = os.path.getsize(result) / (1024 * 1024) print(f"📁 File size: {file_size_mb:.2f} MB") print(f"📂 Open folder: {os.path.dirname(result)}") else: print("⚠️ Warning: Output file path shown but file not found!") print("="*60 + "\n") except Exception as e: print(f"\n❌ Error: {str(e)}\n") import traceback traceback.print_exc() if __name__ == "__main__": main()