143 lines
5.5 KiB
Python
Executable File
143 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
import json
|
|
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 get_video_info(input_path: Path) -> tuple:
|
|
"""Uses ffprobe to instantly read input video dimensions and frame rate."""
|
|
cmd = f"ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate -of json {input_path}"
|
|
# Run command and capture output (assumes linux.run_command prints or you use subprocess)
|
|
import subprocess
|
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
#linux.run_command(cmd)
|
|
try:
|
|
data = json.loads(result.stdout)
|
|
stream = data['streams'][0]
|
|
w = int(stream['width'])
|
|
h = int(stream['height'])
|
|
# Convert fractional FPS string (e.g. "60/1" or "30000/1001") to float
|
|
fps_parts = stream['r_frame_rate'].split('/')
|
|
fps = float(fps_parts[0]) / float(fps_parts[1]) if len(fps_parts) > 1 else float(fps_parts[0])
|
|
return w, h, fps
|
|
except Exception:
|
|
return 1920, 1080, 60.0 # Safe defaults if probe fails
|
|
|
|
def fit_to_9_16_letterbox(input_path: str, top_text: str = "TOP TEXT", bottom_text: str = "BOTTOM TEXT", use_blur: bool = True, force: bool = False):
|
|
threads = "8"
|
|
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}"
|
|
|
|
if os.path.exists(output_path):
|
|
if not force:
|
|
print(f"❌ Error: Short video already exists: {output_path}")
|
|
return
|
|
else:
|
|
os.remove(output_path)
|
|
|
|
# 1. Probe input metadata instantly
|
|
orig_w, orig_h, fps = get_video_info(input_file)
|
|
canvas_w = 1080
|
|
canvas_h = 1920
|
|
|
|
print("✍️ Generating text overlay graphics via MoviePy...")
|
|
# Render static images for text instead of running a video context
|
|
title_clip = TextClip(
|
|
text=top_text, font_size=55, color="white", font="DejaVuSans-Bold",
|
|
text_align="center", size=(canvas_w - 100, 300), method="caption"
|
|
)
|
|
bottom_clip = TextClip(
|
|
text=bottom_text, font_size=55, color="white", font="DejaVuSans-Bold",
|
|
text_align="center", size=(canvas_w - 100, 300), method="caption"
|
|
)
|
|
|
|
# Save text layers to temporary PNGs
|
|
top_png = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
|
|
bottom_png = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
|
|
title_clip.save_frame(top_png)
|
|
bottom_clip.save_frame(bottom_png)
|
|
|
|
title_clip.close()
|
|
bottom_clip.close()
|
|
|
|
print("🎬 Dispatching compilation workload to FFmpeg filtergraph...")
|
|
|
|
# 2. Build the complex FFmpeg filtergraph
|
|
# [0:v] is the raw input video stream
|
|
filter_complex = []
|
|
|
|
if use_blur:
|
|
# Scale height to 1920, crop center 1080x1920, apply fast boxblur (power of 3 approximates Gaussian)
|
|
filter_complex.append(
|
|
f"[0:v]scale=-1:{canvas_h},crop={canvas_w}:{canvas_h}:(iw-{canvas_w})/2:0,boxblur=luma_radius=35:luma_power=3[bg];"
|
|
)
|
|
else:
|
|
# Generate a pure black background canvas matching video frame specs
|
|
filter_complex.append(
|
|
f"color=c=black:s={canvas_w}x{canvas_h}:r={fps}[bg];"
|
|
)
|
|
|
|
# Scale the foreground video to a clean 1080 width, keeping aspect ratio
|
|
filter_complex.append(
|
|
f"[0:v]scale={canvas_w}:-1[fg];"
|
|
)
|
|
|
|
# Layer composition chain:
|
|
# Overlay 1: Put scaled foreground onto background (centered vertically)
|
|
filter_complex.append(
|
|
f"[bg][fg]overlay=0:(H-h)/2[tmp1];"
|
|
)
|
|
# Overlay 2: Drop top text asset onto position Y=180
|
|
filter_complex.append(
|
|
f"[tmp1][1:v]overlay=(W-w)/2:180[tmp2];"
|
|
)
|
|
# Overlay 3: Drop bottom text asset onto position Y=1430
|
|
filter_complex.append(
|
|
f"[tmp2][2:v]overlay=(W-w)/2:1430[finalv]"
|
|
)
|
|
|
|
filter_graph = "".join(filter_complex)
|
|
|
|
# 3. Execute the native assembly command
|
|
# -map_chapters -1 -sn: Strips unnecessary metadata chunks instantly
|
|
# -c:a copy: Safely pulls original digital audio directly without decompression cycles
|
|
# -threads 0: Forces FFmpeg to auto-consume all available processing cores
|
|
ffmpeg_cmd = (
|
|
f'ffmpeg -y -v error -i "{input_file}" -i "{top_png}" -i "{bottom_png}" '
|
|
f'-filter_complex "{filter_graph}" '
|
|
f'-map "[finalv]" -map 0:a? -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p '
|
|
f'-c:a copy -map_chapters -1 -sn -threads {threads} "{output_path}"'
|
|
)
|
|
|
|
try:
|
|
linux.run_command(ffmpeg_cmd)
|
|
print(f"🎉 High-speed processing complete! Video saved to: {output_path}")
|
|
finally:
|
|
# Clean up temporary PNG picture files safely
|
|
for path in (top_png, bottom_png):
|
|
if os.path.exists(path):
|
|
os.unlink(path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import time
|
|
|
|
start_time = time.perf_counter()
|
|
|
|
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, True, True)
|
|
|
|
end_time = time.perf_counter()
|
|
execution_time = end_time - start_time
|
|
|
|
print(f"The function took {execution_time:.6f} seconds to complete.") |