first commit

This commit is contained in:
2026-10-03 14:54:07 -04:00
commit 3a0691c2ad
19 changed files with 2332 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
import json
import os
import subprocess
import re
# --- CONFIGURATION ---
VIDS_DIR = "./vids" # Subfolder containing video and .llc files
XFADE_DUR = 1.5 # Duration of crossfades between segments/videos (seconds)
FADE_IN_OUT_DUR = 1.5 # Duration of global Fade In (start) and Fade Out (end)
OUTPUT_FILE = "final_edited_video.mp4"
# Video Encoder Options: "h264_nvenc" (NVIDIA GPU), "h264_qsv" (Intel), "h264_amf" (AMD), "libx264" (CPU)
ENCODER = "hevc_nvenc"
# Explicit input pairs (Targeting ./vids)
INPUT_FILES = [
(
os.path.join(VIDS_DIR, "PXL_20260201_163136402.TS.mp4"),
os.path.join(VIDS_DIR, "PXL_20260201_163136402.TS-proj.llc")
),
(
os.path.join(VIDS_DIR, "PXL_20260201_163651197.TS.mp4"),
os.path.join(VIDS_DIR, "PXL_20260201_163651197.TS-proj.llc")
),
(
os.path.join(VIDS_DIR, "PXL_20260201_163930934.TS.mp4"),
os.path.join(VIDS_DIR, "PXL_20260201_163930934.TS-proj.llc")
),
]
def get_video_duration(video_path):
"""Uses ffprobe to retrieve the exact duration of an input video file."""
cmd = [
"ffprobe", "-v", "quiet",
"-print_format", "json",
"-show_format", video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
return float(data["format"]["duration"])
def parse_llc_content(raw_text):
"""Converts a JavaScript object literal into valid JSON syntax."""
# 1. Convert single-quoted strings to double-quoted strings
cleaned = re.sub(r"'([^'\\]*(?:\\.[^'\\]*)*)'", r'"\1"', raw_text)
# 2. Enclose unquoted keys in double quotes (e.g., version: -> "version":)
cleaned = re.sub(r'([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', cleaned)
# 3. Remove trailing commas before closing braces or brackets
cleaned = re.sub(r',\s*([}\]])', r'\1', cleaned)
return json.loads(cleaned)
def get_kept_segments(llc_path, total_duration):
"""
Parses LosslessCut .llc project files (supports strict JSON and JS object literals).
Assumes cutSegments represent discarded regions (cutouts to remove).
Returns a list of kept tuples: [(kept_start, kept_end), ...]
"""
if not os.path.exists(llc_path):
print(f"Warning: {llc_path} not found. Keeping full video.")
return [(0.0, total_duration)]
with open(llc_path, "r", encoding="utf-8") as f:
content = f.read().strip()
if not content:
print(f"Warning: {llc_path} is empty. Keeping full video.")
return [(0.0, total_duration)]
try:
data = parse_llc_content(content)
except Exception as e:
print(f"⚠️ Could not parse '{llc_path}': {e}. Keeping full video.")
return [(0.0, total_duration)]
cut_segments = data.get("cutSegments", []) if isinstance(data, dict) else []
if not cut_segments:
return [(0.0, total_duration)]
# Sort discarded cutouts by start time
cut_segments = sorted(cut_segments, key=lambda x: float(x["start"]))
kept = []
current_time = 0.0
for seg in cut_segments:
cut_start = float(seg["start"])
cut_end = float(seg["end"])
# If there is video content before the cut, keep it
if cut_start > current_time:
kept.append((current_time, cut_start))
current_time = max(current_time, cut_end)
# Keep remaining video content after the final cut
if current_time < total_duration:
kept.append((current_time, total_duration))
return kept
def run_pipeline():
cmd = ["ffmpeg", "-y"]
# Verify input files exist
for video_path, llc_path in INPUT_FILES:
if not os.path.exists(video_path):
print(f"Error: Could not find video file at '{video_path}'")
return
cmd.extend(["-i", video_path])
# Extract and collect kept segments across all videos
all_segments = []
for file_idx, (video_path, llc_path) in enumerate(INPUT_FILES):
duration = get_video_duration(video_path)
kept = get_kept_segments(llc_path, duration)
for start, end in kept:
seg_dur = end - start
if seg_dur > XFADE_DUR: # Segment must be longer than xfade duration
all_segments.append({
"file_idx": file_idx,
"start": start,
"end": end,
"duration": seg_dur
})
if not all_segments:
print("Error: No valid kept segments found.")
return
print(f"Extracted {len(all_segments)} kept segment(s) from input files in '{VIDS_DIR}'.")
filter_chains = []
# 1. Create trim filters for each kept segment
for i, seg in enumerate(all_segments):
f_idx = seg["file_idx"]
s, e = seg["start"], seg["end"]
filter_chains.append(f"[{f_idx}:v]trim=start={s:.3f}:end={e:.3f},setpts=PTS-STARTPTS[v{i}];")
filter_chains.append(f"[{f_idx}:a]atrim=start={s:.3f}:end={e:.3f},asetpts=PTS-STARTPTS[a{i}];")
# 2. Chain crossfades sequentially across all segments
current_v = "v0"
current_a = "a0"
cumulative_dur = all_segments[0]["duration"]
for i in range(1, len(all_segments)):
next_v, next_a = f"v{i}", f"a{i}"
offset = cumulative_dur - XFADE_DUR
out_v, out_a = f"vc{i}", f"ac{i}"
filter_chains.append(
f"[{current_v}][{next_v}]xfade=transition=fade:duration={XFADE_DUR}:offset={offset:.3f}[{out_v}];"
)
filter_chains.append(
f"[{current_a}][{next_a}]acrossfade=d={XFADE_DUR}[{out_a}];"
)
current_v, current_a = out_v, out_a
cumulative_dur += all_segments[i]["duration"] - XFADE_DUR
# 3. Apply global Fade In at start and Fade Out at end
final_v, final_a = "outv", "outa"
fade_out_start = max(0.0, cumulative_dur - FADE_IN_OUT_DUR)
filter_chains.append(
f"[{current_v}]fade=t=in:st=0:d={FADE_IN_OUT_DUR},fade=t=out:st={fade_out_start:.3f}:d={FADE_IN_OUT_DUR}[{final_v}];"
)
filter_chains.append(
f"[{current_a}]afade=t=in:st=0:d={FADE_IN_OUT_DUR},afade=t=out:st={fade_out_start:.3f}:d={FADE_IN_OUT_DUR}[{final_a}]"
)
# 4. Execute FFmpeg Command
filtergraph = " ".join(filter_chains)
cmd.extend([
"-filter_complex", filtergraph,
"-map", f"[{final_v}]",
"-map", f"[{final_a}]",
# --- Video Settings ---
"-c:v", ENCODER,
"-tag:v", "hvc1", # Ensures standard MP4 HEVC metadata playback
"-b:v", "20M", # Targets Pixel 7a ~20 Mbps bitrate
"-maxrate", "25M",
"-bufsize", "30M",
"-pix_fmt", "yuv420p",
"-color_primaries", "bt709", # Maintains Pixel 7a BT.709 color accuracy
"-color_trc", "bt709",
"-colorspace", "bt709",
"-preset", "p4" if "nvenc" in ENCODER else "fast",
# --- Audio Settings ---
"-c:a", "aac",
"-ar", "48000", # Matches 48 kHz source audio sampling
"-b:a", "192k", # Matches 192 kbps audio bitrate
OUTPUT_FILE
])
print(f"Estimated final output duration: {cumulative_dur:.2f} seconds.")
print("Running FFmpeg pipeline...")
subprocess.run(cmd, check=True)
print(f"Done! Saved output to: {OUTPUT_FILE}")
if __name__ == "__main__":
run_pipeline()