#!/usr/bin/env python3 """ ClipViral Clip Generator Script Generates vertical video clips from YouTube videos using MoviePy. """ import sys import json import argparse import tempfile import os try: import yt_dlp from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip from moviepy.video.fx.all import resize, fadein, fadeout except ImportError as e: print(json.dumps({"error": f"Missing dependency: {e}"})) sys.exit(1) def download_video(video_url: str, output_path: str, start_time: float = None, duration: float = None) -> str: """Download video segment from YouTube.""" ydl_opts = { 'format': 'bestvideo[height<=1080]+bestaudio/best[height<=1080]', 'outtmpl': output_path, 'merge_output_format': 'mp4', 'quiet': True, 'no_warnings': True, } # Add download sections if time range specified if start_time is not None and duration is not None: ydl_opts['download_sections'] = [f'*{start_time}-{start_time + duration}'] ydl_opts['force_keyframes_at_cuts'] = True with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([video_url]) # Find the output file if os.path.exists(output_path + '.mp4'): return output_path + '.mp4' elif os.path.exists(output_path): return output_path else: # Look for any video file in the directory directory = os.path.dirname(output_path) for f in os.listdir(directory): if f.endswith(('.mp4', '.webm', '.mkv')): return os.path.join(directory, f) raise FileNotFoundError("Downloaded video not found") def create_vertical_clip(input_path: str, output_path: str, text_overlay: str = None, add_fade: bool = True) -> dict: """ Create a vertical (1080x1920) clip from the input video. Suitable for YouTube Shorts, TikTok, Instagram Reels. """ # Load video video = VideoFileClip(input_path) # Target dimensions for Shorts target_width = 1080 target_height = 1920 target_aspect = target_height / target_width # 16:9 vertical # Calculate crop/resize video_aspect = video.h / video.w if video_aspect < target_aspect: # Video is wider - crop sides new_width = int(video.h / target_aspect) x_center = video.w // 2 x1 = x_center - new_width // 2 video = video.crop(x1=x1, x2=x1 + new_width) else: # Video is taller - crop top/bottom new_height = int(video.w * target_aspect) y_center = video.h // 2 y1 = y_center - new_height // 2 video = video.crop(y1=y1, y2=y1 + new_height) # Resize to target dimensions video = resize(video, newsize=(target_width, target_height)) # Add text overlay if provided if text_overlay: try: txt_clip = TextClip( text_overlay, fontsize=48, color='white', font='Arial-Bold', stroke_color='black', stroke_width=2, method='caption', size=(target_width - 80, None) ) txt_clip = txt_clip.set_position(('center', target_height - 200)) txt_clip = txt_clip.set_duration(min(5, video.duration)) video = CompositeVideoClip([video, txt_clip]) except Exception as e: print(f"Text overlay failed: {e}", file=sys.stderr) # Add fade effects if add_fade and video.duration > 2: video = fadein(video, 0.5) video = fadeout(video, 0.5) # Write output video.write_videofile( output_path, codec='libx264', audio_codec='aac', fps=30, preset='fast', bitrate='5M', verbose=False, logger=None ) # Get output info output_size = os.path.getsize(output_path) video.close() return { 'output_path': output_path, 'duration': video.duration, 'width': target_width, 'height': target_height, 'size_bytes': output_size } def main(): parser = argparse.ArgumentParser(description='Generate vertical clip from YouTube video') parser.add_argument('url', help='YouTube video URL') parser.add_argument('--start', '-s', type=float, required=True, help='Start time in seconds') parser.add_argument('--duration', '-d', type=float, default=30, help='Clip duration in seconds') parser.add_argument('--output', '-o', required=True, help='Output file path') parser.add_argument('--text', '-t', help='Text overlay for the clip') parser.add_argument('--no-fade', action='store_true', help='Disable fade effects') args = parser.parse_args() # Validate duration if args.duration < 15: print(json.dumps({'error': 'Duration must be at least 15 seconds'})) sys.exit(1) if args.duration > 60: print(json.dumps({'error': 'Duration cannot exceed 60 seconds for Shorts'})) sys.exit(1) try: with tempfile.TemporaryDirectory() as tmpdir: # Download video segment print("Downloading video segment...", file=sys.stderr) temp_video = os.path.join(tmpdir, 'input') downloaded = download_video(args.url, temp_video, args.start, args.duration) # Create vertical clip print("Creating vertical clip...", file=sys.stderr) result = create_vertical_clip( downloaded, args.output, text_overlay=args.text, add_fade=not args.no_fade ) print(json.dumps({ 'success': True, **result })) except Exception as e: print(json.dumps({'error': str(e)})) sys.exit(1) if __name__ == '__main__': main()