creatorplus / video_editor.py
nitubhai's picture
Upload 13 files
0a512d5 verified
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()