| 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: |
| 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) |
|
|
| |
| |
| 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 |