|
|
|
|
|
""" |
|
|
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, |
|
|
} |
|
|
|
|
|
|
|
|
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]) |
|
|
|
|
|
|
|
|
if os.path.exists(output_path + '.mp4'): |
|
|
return output_path + '.mp4' |
|
|
elif os.path.exists(output_path): |
|
|
return output_path |
|
|
else: |
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
video = VideoFileClip(input_path) |
|
|
|
|
|
|
|
|
target_width = 1080 |
|
|
target_height = 1920 |
|
|
target_aspect = target_height / target_width |
|
|
|
|
|
|
|
|
video_aspect = video.h / video.w |
|
|
|
|
|
if video_aspect < target_aspect: |
|
|
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
video = resize(video, newsize=(target_width, target_height)) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if add_fade and video.duration > 2: |
|
|
video = fadein(video, 0.5) |
|
|
video = fadeout(video, 0.5) |
|
|
|
|
|
|
|
|
video.write_videofile( |
|
|
output_path, |
|
|
codec='libx264', |
|
|
audio_codec='aac', |
|
|
fps=30, |
|
|
preset='fast', |
|
|
bitrate='5M', |
|
|
verbose=False, |
|
|
logger=None |
|
|
) |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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() |
|
|
|