130 lines
5.0 KiB
Python
Executable File
130 lines
5.0 KiB
Python
Executable File
#!/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}")
|
|
|
|
if __name__ == "__main__":
|
|
creator_name = "greenskiesbluegrass"
|
|
top_txt = "TeamPGP Live on Twitch...\n Every Friday and Sunday @7:30 ET.\n twitch.tv/teampgp"
|
|
bottom_txt = f"Clipped By: {creator_name}."
|
|
fit_to_9_16_letterbox("download/clips/AbnegateAgitatedGrassPJSalt/AbnegateAgitatedGrassPJSalt.mp4", top_txt, bottom_txt) |