import json import subprocess import tempfile SHORT_CAP = 720 LONG_CAP = 1280 def _probe(video_path: str) -> dict: out = subprocess.run( ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", video_path], capture_output=True, text=True, check=True, ) return json.loads(out.stdout) def get_duration(video_path: str) -> float: return float(_probe(video_path)["format"]["duration"]) def get_display_dims(video_path: str) -> tuple[int, int]: """Return (width, height) as DISPLAYED — i.e. after rotation metadata is applied. Phone portrait clips are often stored landscape + a rotate flag; if we ignored it the target size (and detection) would be sideways. """ info = _probe(video_path) v = next(s for s in info["streams"] if s["codec_type"] == "video") w, h = int(v["width"]), int(v["height"]) rot = 0 if "tags" in v and "rotate" in v["tags"]: rot = int(v["tags"]["rotate"]) for sd in v.get("side_data_list", []): if "rotation" in sd: rot = int(sd["rotation"]) if abs(rot) % 180 == 90: # 90 or 270 -> swap w, h = h, w return w, h def compute_target_size(w: int, h: int) -> tuple[int, int]: """Scale = min(720/short, 1280/long, 1.0); dims forced even; no upscale.""" short, long = min(w, h), max(w, h) s = min(SHORT_CAP / short, LONG_CAP / long, 1.0) out_w = max(2, int(round(w * s / 2)) * 2) out_h = max(2, int(round(h * s / 2)) * 2) return out_w, out_h def normalize_video(video_path: str) -> tuple[str, int, int]: """ffmpeg: autorotate + scale to target + re-encode H.264 (+ keep audio). Returns (normalized_path, out_w, out_h). Everything downstream uses this. """ w, h = get_display_dims(video_path) out_w, out_h = compute_target_size(w, h) # NamedTemporaryFile gives us a safe unique path (mktemp is deprecated / # racy); close it and let ffmpeg overwrite via -y. tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") out_path = tmp.name tmp.close() subprocess.run( ["ffmpeg", "-y", "-i", video_path, "-vf", f"scale={out_w}:{out_h}", "-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", out_path], check=True, capture_output=True, ) return out_path, out_w, out_h