152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import sys
|
|
import linux
|
|
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:
|
|
linux.run_command(f"ffmpeg -y -i {input_file} -map_chapters -1 -sn -c copy {temp_path}")
|
|
|
|
# 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() |