add new video edit scripts for combine video and spliting video

This commit is contained in:
SumGuyV5 committed 2026-09-22 22:12:44 +00:00
1 parent ee93c06f7d
commit 856ab439f7
10 files changed
+199 -32

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
# package/video_editing/__init__.py
# Expose components from individual modules using relative imports
from .combine_videos import merge_videos
from .split_video_by_timestamps import split_video
from .build_shorts import fit_to_9_16_letterbox
# Explicitly define what is exposed when importing the subpackage
__all__ = [
"merge_videos",
"split_video",
"fit_to_9_16_letterbox"
]
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
import os
import json
import package.utils as utils
import tempfile
from pathlib import Path
from moviepy.video.VideoClip import TextClip
from pathlib import Path
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:
utils.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.")
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Combine multiple video files into one file using FFmpeg.
Example:
python helper_files/combine_videos.py output.mp4 video1.mp4 video2.mp4 video3.mp4
python helper_files/combine_videos.py --output merged.mp4 --inputs video1.mp4 video2.mp4
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
def build_concat_list(input_files, list_path):
with open(list_path, "w", encoding="utf-8") as f:
for path in input_files:
f.write(f"file '{os.path.abspath(path)}'\n")
def run_ffmpeg(cmd):
print("Running:", " ".join(cmd))
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(result.stdout)
print(result.stderr)
raise RuntimeError(f"FFmpeg failed with exit code {result.returncode}")
print("✅ Finished successfully.")
def concat_videos_copy(input_files, output_path):
list_path = Path(output_path).with_suffix(".concat.txt")
build_concat_list(input_files, list_path)
cmd = [
"ffmpeg",
"-y",
"-f", "concat",
"-safe", "0",
"-i", str(list_path),
"-c", "copy",
str(output_path),
]
run_ffmpeg(cmd)
list_path.unlink(missing_ok=True)
def concat_videos_reencode(input_files, output_path):
inputs = []
filter_parts = []
audio_parts = []
for i, video_path in enumerate(input_files):
inputs.extend(["-i", str(video_path)])
# Use semicolons to separate different input streams in the filter graph
filter_parts.append(f"[{i}:v]scale=trunc(iw/2)*2:trunc(ih/2)*2,setpts=PTS-STARTPTS[v{i}]")
audio_parts.append(f"[{i}:a]aresample=async=1[a{i}]")
# Correctly separate individual stream filters with semicolons
filter_complex = f"{';'.join(filter_parts)};{';'.join(audio_parts)};"
filter_complex += "".join(f"[v{i}][a{i}]" for i in range(len(input_files)))
filter_complex += f"concat=n={len(input_files)}:v=1:a=1[outv][outa]"
cmd = [
"ffmpeg",
"-y",
*inputs,
"-filter_complex", filter_complex,
"-map", "[outv]",
"-map", "[outa]",
"-c:v", "libx264",
"-crf", "18", # High quality video
"-preset", "slow", # High efficiency
"-c:a", "aac",
"-b:a", "256k", # High quality audio
"-movflags", "+faststart",
str(output_path),
]
run_ffmpeg(cmd)
def merge_videos(input_files, output_path):
input_files = [str(Path(p).expanduser().resolve()) for p in input_files]
output_path = str(Path(output_path).expanduser().resolve())
if len(input_files) < 2:
raise ValueError("You need at least 2 video files to merge.")
for path in input_files:
if not os.path.exists(path):
raise FileNotFoundError(f"File not found: {path}")
output_dir = Path(output_path).parent
output_dir.mkdir(parents=True, exist_ok=True)
try:
concat_videos_copy(input_files, output_path)
except Exception as exc:
print(f"Fast copy method failed: {exc}")
print("Retrying with re-encode fallback...")
concat_videos_reencode(input_files, output_path)
def main():
parser = argparse.ArgumentParser(description="Join two or more video files together with FFmpeg.")
parser.add_argument("output_file", help="Path to the merged output video")
parser.add_argument("input_files", nargs="+", help="Two or more videos to concatenate")
args = parser.parse_args()
try:
merge_videos(args.input_files, args.output_file)
print(f"Merged video saved to: {args.output_file}")
except Exception as exc:
print(f"Error: {exc}")
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,112 @@
import subprocess
import os
from typing import List, Tuple
def split_video(input_file: str, output_dir: str, timestamps: List[Tuple[float, float]], frame_perfect: bool = False) -> None:
"""
Splits a video file into multiple segments based on specified time ranges.
Args:
input_file (str): The path to the video file to be split.
output_dir (str): The directory where the split segments will be saved.
timestamps (List[Tuple[float, float]]): A list of tuples representing (start_time, end_time).
frame_perfect (bool): If True, skips fast-copy and forces high-quality re-encoding
to ensure cuts happen exactly on the requested frame.
"""
if not os.path.exists(input_file):
print(f"Error: Input file not found at {input_file}")
return
os.makedirs(output_dir, exist_ok=True)
print(f"Starting video split process for {input_file}...")
for i, (start, end) in enumerate(timestamps):
segment_name = f"segment_{i+1:02d}_{start:.2f}s_to_{end:.2f}s.mp4"
output_file = os.path.join(output_dir, segment_name)
print(f"\n-> Processing segment {i+1}/{len(timestamps)}: {segment_name}")
# Method 1: Fast Copy (Lossless, no re-encoding)
# Moving -ss BEFORE -i fixes the frozen/corrupted initial frames.
fast_command = [
"ffmpeg", "-y",
"-ss", str(start),
"-i", input_file,
"-to", str(end),
"-c", "copy",
output_file
]
# Method 2: High-Quality Re-encode (Frame-perfect fallback)
# Used if fast copy fails or if frame-accuracy is explicitly requested.
reencode_command = [
"ffmpeg", "-y",
"-ss", str(start),
"-i", input_file,
"-to", str(end),
"-c:v", "libx264",
"-crf", "18", # High visual quality
"-preset", "slow", # Better compression efficiency
"-c:a", "aac",
"-b:a", "256k", # High-fidelity audio
output_file
]
try:
if frame_perfect:
print("Running in frame-perfect mode (re-encoding)...")
subprocess.run(reencode_command, check=True, capture_output=True, text=True)
else:
try:
print("Attempting lightning-fast stream copy...")
subprocess.run(fast_command, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError:
print("⚠️ Fast copy failed. Falling back to high-quality re-encode...")
subprocess.run(reencode_command, check=True, capture_output=True, text=True)
print(f"Successfully created segment: {output_file}")
except subprocess.CalledProcessError as e:
print(f"❌ Error splitting video segment {i+1}:")
print(f"Command failed with return code {e.returncode}")
print(f"STDERR: {e.stderr}")
except FileNotFoundError:
print("-" * 50)
print("CRITICAL ERROR: FFmpeg executable not found.")
print("Please ensure 'ffmpeg' is installed and accessible in your system's PATH.")
print("-" * 50)
return
def main():
"""
Example usage:
Assumes 'input.mp4' exists and a 'split_videos' directory is ready.
"""
# --- CONFIGURATION ---
INPUT_VIDEO_PATH = "path/to/your/input_video.mp4" # <<< CHANGE THIS
OUTPUT_SEGMENTS_DIR = "output_splits" # <<< CHANGE THIS
# Define time segments: [(start_time, end_time), ...]
# Times should be floats representing seconds.
# Example 1: 0s to 10s, then 10s to 20s
TIMESTAMPS: List[Tuple[float, float]] = [
(0.0, 10.0),
(15.0, 25.0),
(30.0, 35.0)
]
# ---------------------
if not os.path.exists(INPUT_VIDEO_PATH):
print("!!! IMPORTANT !!!")
print(f"Please update the 'INPUT_VIDEO_PATH' variable in the main() function with the correct path to your video.")
print("!!! Script execution aborted. !!!")
return
split_video(
input_file=INPUT_VIDEO_PATH,
output_dir=OUTPUT_SEGMENTS_DIR,
timestamps=TIMESTAMPS
)
if __name__ == "__main__":
main()