Files
playbook/antigravity-awesome-skills/skills/gemini-omni-flash-api/scripts/video/prep_video.py
T
2026-07-01 16:02:41 +00:00

256 lines
10 KiB
Python

#!/usr/bin/env python3
import argparse
import os
import subprocess
import sys
from inspect_video import inspect_video, format_size
def parse_timecode(time_str, total_duration=None):
"""Parses a time string (seconds, MM:SS, HH:MM:SS, or 'last') into float seconds."""
if not time_str:
return 0.0
time_str = time_str.strip().lower()
if time_str == "last":
if total_duration is None:
raise ValueError("Total duration is required to calculate 'last' starting point.")
target_dur = 10.0
if total_duration <= target_dur:
return 0.0
return total_duration - target_dur
if ":" in time_str:
parts = time_str.split(":")
if len(parts) == 2: # MM:SS
m, s = map(float, parts)
return m * 60.0 + s
elif len(parts) == 3: # HH:MM:SS
h, m, s = map(float, parts)
return h * 3600.0 + m * 60.0 + s
else:
raise ValueError(f"Invalid timecode format: '{time_str}'. Use HH:MM:SS or MM:SS.")
try:
return float(time_str)
except ValueError:
raise ValueError(f"Invalid timecode: '{time_str}'. Must be float seconds, HH:MM:SS, or 'last'.")
def prep_video(input_path, output_path, start_time_str=None, duration=10, fps=None, resolution=None, strip_audio=False):
"""Preps a video file by trimming, optionally re-encoding to target fps and resolution."""
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file not found: {input_path}")
# Check if ffmpeg is available
try:
subprocess.run(["ffmpeg", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
except (subprocess.SubprocessError, FileNotFoundError):
raise RuntimeError("ffmpeg is not installed or not found in system PATH.")
# Inspect input video first
print(f"Analyzing source video: {os.path.basename(input_path)}...")
source_info = inspect_video(input_path)
total_duration = source_info.get("duration_seconds", 0.0)
# Resolve start time
if start_time_str is None:
if total_duration and total_duration > 10.0 and sys.stdin.isatty():
print(f"\nThe input video is longer than 10s ({total_duration:.2f}s).")
print("Please choose a 10s segment to trim:")
print(" 1) First 10 seconds [default]")
print(" 2) Last 10 seconds")
print(" 3) Custom starting timecode (e.g., MM:SS, HH:MM:SS, or seconds)")
try:
choice = input("Your choice [1/2/3, default 1]: ").strip()
if choice == "2":
start_time_str = "last"
elif choice == "3":
custom_start = input("Enter starting timecode (e.g., 00:03 or 15): ").strip()
start_time_str = custom_start if custom_start else "0"
else:
start_time_str = "0"
except (KeyboardInterrupt, EOFError):
print("\nNo input received. Defaulting to first 10 seconds.")
start_time_str = "0"
else:
start_time_str = "0"
try:
start_seconds = parse_timecode(start_time_str, total_duration)
except Exception as e:
raise ValueError(f"Timecode parsing failed: {e}")
if start_seconds < 0 or (total_duration and start_seconds >= total_duration):
raise ValueError(f"Start time {start_seconds}s is out of bounds for video of length {total_duration}s.")
# Construct output path if not specified
if not output_path:
os.makedirs("media", exist_ok=True)
base_name = os.path.basename(input_path)
name, ext = os.path.splitext(base_name)
output_path = os.path.join("media", f"prepped_{name}.mp4")
# Check if the source video file is large (>25MB)
size_bytes_str = source_info.get("size_bytes")
is_large = False
try:
if size_bytes_str and int(size_bytes_str) > 25 * 1024 * 1024:
is_large = True
except (ValueError, TypeError):
pass
# Target resolution parsing
scale_filter = None
orig_width = None
orig_height = None
if "video" in source_info:
try:
orig_width = int(source_info["video"].get("width"))
orig_height = int(source_info["video"].get("height"))
except (ValueError, TypeError):
pass
if resolution:
try:
target_w, target_h = map(int, resolution.lower().split("x"))
if orig_width and orig_height:
# Scale to fit target_w and target_h while preserving aspect ratio
scale_factor = min(target_w / orig_width, target_h / orig_height)
width = int(orig_width * scale_factor)
height = int(orig_height * scale_factor)
else:
width, height = target_w, target_h
# Ensure divisible by 2 for standard decoders/encoders
width = (width // 2) * 2
height = (height // 2) * 2
scale_filter = f"scale={width}:{height}"
resolution = f"{width}x{height}"
except ValueError:
raise ValueError(f"Invalid resolution: '{resolution}'. Format must be WIDTHxHEIGHT (e.g. 1280x720).")
elif is_large:
if orig_width and orig_height:
# Scale down large videos proportionally (max 1280x720 for landscape, 720x1280 for portrait)
if orig_width >= orig_height:
max_w, max_h = 1280, 720
else:
max_w, max_h = 720, 1280
scale_factor = min(max_w / orig_width, max_h / orig_height)
if scale_factor < 1.0:
width = int(orig_width * scale_factor)
height = int(orig_height * scale_factor)
else:
width, height = orig_width, orig_height
else:
width, height = 1280, 720
# Ensure divisible by 2
width = (width // 2) * 2
height = (height // 2) * 2
resolution = f"{width}x{height}"
print(f"\nRecommendation: Source video is very large ({source_info.get('file_size')}).")
print(" Automatically scaling to optimize upload times for Gemini Omni Flash.")
scale_filter = f"scale={width}:{height}"
fps_spec = f"{fps} fps" if fps else "Original frame rate"
print(f"\nPreparing Video Processing:")
print(f" * Source Duration: {total_duration:.2f}s")
print(f" * Trim Range : Start at {start_seconds:.2f}s | Length {duration:.2f}s")
if resolution:
print(f" * Encoding Specs : {width}x{height} @ {fps_spec}")
else:
print(f" * Encoding Specs : Original Resolution @ {fps_spec}")
print(f" * Target Path : {output_path}")
print("=" * 50)
# ffmpeg command construction
cmd = [
"ffmpeg",
"-y", # Overwrite output
"-ss", str(start_seconds), # Seek start
"-i", input_path, # Input file
"-t", str(duration), # Duration to copy
]
if scale_filter:
cmd.extend(["-vf", scale_filter])
cmd.extend([
"-c:v", "libx264", # Standard H264 video codec
"-pix_fmt", "yuv420p", # Standard pixel format for web/Gemini compatibility
])
if fps:
cmd.extend(["-r", str(fps)]) # Output frame rate if requested
if strip_audio or not source_info.get("has_audio", False):
if not source_info.get("has_audio", False) and not strip_audio:
print("No audio stream detected in source video. Disabling audio output.")
else:
print("Stripping audio stream from video as requested.")
cmd.append("-an") # Disable audio streams completely
else:
cmd.extend([
"-c:a", "aac", # Convert audio to standard AAC
"-b:a", "128k", # Standard audio bitrate
"-ac", "2", # Convert to stereo
])
cmd.append(output_path)
print("Running ffmpeg encoding...")
process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if process.returncode != 0:
print("Error: ffmpeg failed. Stderr output follows:", file=sys.stderr)
print(process.stderr, file=sys.stderr)
raise RuntimeError("ffmpeg execution failed.")
print("Video preparation completed successfully!")
print("=" * 50)
# Call inspection tool on output to print clean specs
output_info = inspect_video(output_path)
return output_info
def main():
parser = argparse.ArgumentParser(description="Prep videos for editing (trimming, re-encoding to target fps and resolution).")
parser.add_argument("file", help="Path to the source video file to prep")
parser.add_argument("--start", "-s", default=None, help="Start timecode (seconds, MM:SS, HH:MM:SS, or 'last' for last 10s). Default: 0 (or prompted if > 10s)")
parser.add_argument("--duration", "-d", type=int, default=10, help="Duration of trimmed segment in seconds. Default: 10")
parser.add_argument("--fps", "-r", type=int, default=None, help="Target frame rate. Default: None (keep original frame rate)")
parser.add_argument("--resolution", "-g", default=None, help="Target resolution (e.g., 1280x720). Default: None (keep original resolution)")
parser.add_argument("--output", "-o", help="Custom output path. Defaults to media/prepped_<original_name>.mp4")
parser.add_argument("--strip-audio", "-a", action="store_true", help="Completely strip/disable audio stream so the model can generate new audio")
args = parser.parse_args()
try:
info = prep_video(
input_path=args.file,
output_path=args.output,
start_time_str=args.start,
duration=args.duration,
fps=args.fps,
resolution=args.resolution,
strip_audio=args.strip_audio
)
# Display final output report
print(f"\nPrepped Video Specifications: {info['file_name']}")
print("=" * 50)
print(f"File Size : {info['file_size']}")
print(f"Duration : {info['duration']}")
print(f"Bitrate : {info['bitrate']}")
print(f"Resolution : {info['video']['resolution']}")
print(f"Frame Rate : {info['video']['fps']}")
print(f"Video Codec : {info['video']['codec']}")
if info["has_audio"]:
print(f"Audio Spec : {info['audio']['codec']} | {info['audio']['channels']} ch | {info['audio']['sample_rate']}")
print()
except Exception as e:
print(f"Error prepping video: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()