AdwolfCzar commited on
Commit
07d944b
·
verified ·
1 Parent(s): bc6a20f

Upload training/scripts/normalize_videos.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. training/scripts/normalize_videos.py +105 -0
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()