Upload training/scripts/normalize_videos.py with huggingface_hub
Browse files
training/scripts/normalize_videos.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Normalize mega_curated videos: 24 fps CFR, square pixels (SAR 1:1), copy captions.
|
| 3 |
+
|
| 4 |
+
Non-destructive: writes derived files to OUT_DIR, sources untouched.
|
| 5 |
+
Skips clips whose 24fps-converted frame count would fall below 22 (17n+5 grid minimum for video).
|
| 6 |
+
Usage: normalize_videos.py SRC_DIR OUT_DIR [--jobs N]
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import fractions
|
| 10 |
+
import json
|
| 11 |
+
import shutil
|
| 12 |
+
import subprocess
|
| 13 |
+
import sys
|
| 14 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
MIN_FRAMES = 22 # smallest useful 17n+5 video length
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def probe(path: Path):
|
| 21 |
+
out = subprocess.run(
|
| 22 |
+
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
|
| 23 |
+
"stream=width,height,sample_aspect_ratio,avg_frame_rate,nb_frames,duration",
|
| 24 |
+
"-of", "json", str(path)],
|
| 25 |
+
capture_output=True, text=True, timeout=120)
|
| 26 |
+
if out.returncode != 0:
|
| 27 |
+
return None
|
| 28 |
+
st = json.loads(out.stdout)["streams"][0]
|
| 29 |
+
w, h = int(st["width"]), int(st["height"])
|
| 30 |
+
sar = st.get("sample_aspect_ratio") or "1:1"
|
| 31 |
+
if sar in ("0:1", "N/A"):
|
| 32 |
+
sar = "1:1"
|
| 33 |
+
fps = float(fractions.Fraction(st["avg_frame_rate"]))
|
| 34 |
+
nb = st.get("nb_frames")
|
| 35 |
+
if nb is None or nb == "N/A":
|
| 36 |
+
dur = float(st.get("duration") or 0)
|
| 37 |
+
nb = int(dur * fps)
|
| 38 |
+
return w, h, sar, fps, int(nb)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def process(src: Path, dst_dir: Path):
|
| 42 |
+
info = probe(src)
|
| 43 |
+
if info is None:
|
| 44 |
+
return ("probe_fail", src.name, None)
|
| 45 |
+
w, h, sar, fps, nb = info
|
| 46 |
+
out_frames = int(nb * 24.0 / fps) if fps > 0 else 0
|
| 47 |
+
if out_frames < MIN_FRAMES:
|
| 48 |
+
return ("too_short", src.name, out_frames)
|
| 49 |
+
sn, sd = (int(x) for x in sar.split(":"))
|
| 50 |
+
vf = ["fps=24"]
|
| 51 |
+
if sn != sd: # anamorphic: resample to square pixels, keep height
|
| 52 |
+
out_w = round(w * sn / sd / 2) * 2
|
| 53 |
+
vf.insert(0, f"scale={out_w}:{h}:flags=lanczos")
|
| 54 |
+
final_w = out_w
|
| 55 |
+
else:
|
| 56 |
+
final_w = w
|
| 57 |
+
vf.append("setsar=1")
|
| 58 |
+
dst = dst_dir / src.name
|
| 59 |
+
if dst.exists():
|
| 60 |
+
return ("exists", src.name, (final_w, h))
|
| 61 |
+
r = subprocess.run(
|
| 62 |
+
["ffmpeg", "-y", "-v", "error", "-i", str(src),
|
| 63 |
+
"-vf", ",".join(vf), "-c:v", "libx264", "-crf", "15",
|
| 64 |
+
"-preset", "fast", "-pix_fmt", "yuv420p", "-an", "-map_metadata", "-1",
|
| 65 |
+
str(dst) + ".tmp.mp4"],
|
| 66 |
+
capture_output=True, text=True, timeout=600)
|
| 67 |
+
if r.returncode != 0:
|
| 68 |
+
Path(str(dst) + ".tmp.mp4").unlink(missing_ok=True)
|
| 69 |
+
return ("encode_fail", src.name, r.stderr[-200:])
|
| 70 |
+
Path(str(dst) + ".tmp.mp4").rename(dst)
|
| 71 |
+
txt = src.with_suffix(".txt")
|
| 72 |
+
if txt.exists():
|
| 73 |
+
shutil.copy2(txt, dst_dir / txt.name)
|
| 74 |
+
return ("ok", src.name, (final_w, h))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def main():
|
| 78 |
+
ap = argparse.ArgumentParser()
|
| 79 |
+
ap.add_argument("src")
|
| 80 |
+
ap.add_argument("out")
|
| 81 |
+
ap.add_argument("--jobs", type=int, default=30)
|
| 82 |
+
args = ap.parse_args()
|
| 83 |
+
src_dir, out_dir = Path(args.src), Path(args.out)
|
| 84 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 85 |
+
videos = sorted(src_dir.glob("*.mp4"))
|
| 86 |
+
print(f"{len(videos)} source videos", flush=True)
|
| 87 |
+
results = {"ok": 0, "exists": 0, "too_short": 0, "probe_fail": 0, "encode_fail": 0}
|
| 88 |
+
failures = []
|
| 89 |
+
with ProcessPoolExecutor(max_workers=args.jobs) as ex:
|
| 90 |
+
futs = {ex.submit(process, v, out_dir): v for v in videos}
|
| 91 |
+
for i, fut in enumerate(as_completed(futs)):
|
| 92 |
+
status, name, extra = fut.result()
|
| 93 |
+
results[status] += 1
|
| 94 |
+
if status in ("too_short", "probe_fail", "encode_fail"):
|
| 95 |
+
failures.append((status, name, extra))
|
| 96 |
+
if (i + 1) % 500 == 0:
|
| 97 |
+
print(f"{i+1}/{len(videos)} {results}", flush=True)
|
| 98 |
+
print("FINAL:", results, flush=True)
|
| 99 |
+
with open(out_dir.parent / "normalize_failures.log", "w") as f:
|
| 100 |
+
for status, name, extra in failures:
|
| 101 |
+
f.write(f"{status}\t{name}\t{extra}\n")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
main()
|