#!/usr/bin/env python3 import argparse import sys import subprocess import tempfile from pathlib import Path from moviepy import VideoFileClip, ColorClip, CompositeVideoClip from moviepy.video.VideoClip import TextClip from pathlib import Path import numpy as np from PIL import Image, ImageFilter def apply_gaussian_blur(frame, radius: int = 30): """ Transforms a single NumPy array frame using PIL's true GaussianBlur filter. """ # Convert numpy array to PIL Image image = Image.fromarray(frame) # Apply high-quality true Gaussian Blur blurred_image = image.filter(ImageFilter.GaussianBlur(radius=radius)) # Return back as a numpy array for MoviePy return np.array(blurred_image) def fit_to_9_16_letterbox(input_path: str, top_text: str = "TOP TEXT", bottom_text: str = "BOTTOM TEXT", use_blur: bool = True): input_file = Path(input_path) output_suffix = "gaussian_9_16" if use_blur else "black_9_16" output_path = input_file.parent / f"{input_file.stem}_{output_suffix}{input_file.suffix}" print("๐Ÿงผ Sanitizing video metadata streams inside an automated safe context...") with tempfile.NamedTemporaryFile(suffix=input_file.suffix, delete=False) as temp_file: temp_path = temp_file.name bg_scaled = None bg_cropped = None background_layer = None try: cleanup_cmd = [ "ffmpeg", "-y", "-i", str(input_file), "-map_chapters", "-1", "-sn", "-c", "copy", temp_path ] subprocess.run(cleanup_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # Load video clip = VideoFileClip(temp_path) # Standard vertical 9:16 canvas sizes canvas_w = 1080 canvas_h = 1920 # Background logic if use_blur: print("๐Ÿ“ Scaling, cropping, and blurring background layer...") bg_scaled = clip.resized(height=canvas_h) bg_cropped = bg_scaled.cropped(width=canvas_w, x_center=bg_scaled.w / 2) background_layer = bg_cropped.transform(lambda gf, t: apply_gaussian_blur(gf(t), radius=35)) else: print("โšซ Creating solid black background canvas...") background_layer = ColorClip(size=(canvas_w, canvas_h), color=(0, 0, 0), duration=clip.duration) print("๐Ÿ“ Shrinking foreground video width to fit the 1080 wide canvas...") foreground_clip = clip.resized(width=canvas_w) print("โœ๏ธ Creating multi-line top title text clip...") # FIXED: Added 'method="caption"' and increased vertical size to 300 title_clip = TextClip( text=top_text, font_size=55, # Slightly smaller to accommodate paragraphs comfortably color="white", font="DejaVuSans-Bold", text_align="center", size=(canvas_w - 100, 300), # Subtracted 100px for safety margins on left/right edges method="caption", # Forces text to wrap cleanly onto a new line duration=clip.duration ) # Position adjusted to center the taller 300px box in the upper section positioned_top_text = title_clip.with_position(("center", 180)) print("โœ๏ธ Creating multi-line bottom text clip...") # FIXED: Added 'method="caption"' and increased vertical size to 300 bottom_clip = TextClip( text=bottom_text, font_size=55, color="white", font="DejaVuSans-Bold", text_align="center", size=(canvas_w - 100, 300), # Left/right margins included method="caption", # Forces text to wrap cleanly onto a new line duration=clip.duration ) # Position adjusted to center the taller 300px box in the lower section positioned_bottom_text = bottom_clip.with_position(("center", 1430)) # Composite layers final_clip = CompositeVideoClip( [ background_layer, foreground_clip.with_position("center"), positioned_top_text, positioned_bottom_text ] ).with_audio(clip.audio) print("๐ŸŽฌ Rendering final vertical composition...") final_clip.write_videofile( str(output_path), codec="libx264", audio_codec="aac", fps=clip.fps ) # Clean up file locks safely clip.close() if bg_scaled: bg_scaled.close() if bg_cropped: bg_cropped.close() background_layer.close() foreground_clip.close() title_clip.close() bottom_clip.close() final_clip.close() finally: temp_file_path = Path(temp_path) if temp_file_path.exists(): temp_file_path.unlink() print(f"๐ŸŽ‰ Text overlay video saved to: {output_path}") def main(): # Set up the command-line argument parser parser = argparse.ArgumentParser(description="TeamPGP Clip Processing and Upload Pipeline") # Add optional arguments parser.add_argument('--convert', type=str, metavar='CLIP_PATH', help='Path to a video file to convert to a 9:16 Short') parser.add_argument('--upload', action='store_true', help='Process and upload pending Shorts in the database to YouTube') args = parser.parse_args() # If no flags are provided, show help text and exit if not args.convert and not args.upload: parser.print_help() sys.exit("\nโŒ Error: You must provide at least one action flag (--convert or --upload).") # Execute conversion step if path is provided if args.convert: clip_by = "joelmckinney" #convert_to_short(args.convert) fit_to_9_16_letterbox(args.convert, "TeamPGP Live on Twitch...\n Every Friday and Sunday @7:30 ET.\n twitch.tv/teampgp", f"Clipped By: {clip_by}.", True) # Execute database upload step if flag is provided if args.upload: upload_shorts() if __name__ == "__main__": main()