clipviral / scripts /generate_clip.py
baimkita's picture
Deploy ClipViral - AI-powered YouTube Shorts generator
79d35a4
#!/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()