Upload training/scripts/assign_buckets.py with huggingface_hub
Browse files
training/scripts/assign_buckets.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Stratified multi-resolution bucket assignment for normalized clips.
|
| 3 |
+
|
| 4 |
+
Creates non-destructive symlink views (small/medium resolutions per user directive):
|
| 5 |
+
train_quick/v288 -> [512,288] ~55%
|
| 6 |
+
train_quick/v384 -> [640,384] ~28%
|
| 7 |
+
train_quick/v480 -> [832,480] ~17%
|
| 8 |
+
|
| 9 |
+
Constraint: a clip is only eligible for a bucket whose target area it can
|
| 10 |
+
cover without upscaling. Deterministic (seed 42). Writes a manifest CSV.
|
| 11 |
+
"""
|
| 12 |
+
import csv
|
| 13 |
+
import fractions
|
| 14 |
+
import json
|
| 15 |
+
import random
|
| 16 |
+
import subprocess
|
| 17 |
+
import sys
|
| 18 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
SRC = Path("/workspace/datasets/mega_24_ext")
|
| 22 |
+
DST = Path("/workspace/datasets/train_quick")
|
| 23 |
+
MANIFEST = Path("/workspace/projects/h3_loop/configs/bucket_manifest_quick.csv")
|
| 24 |
+
|
| 25 |
+
BUCKETS = { # name -> (w, h, share)
|
| 26 |
+
"v288": (512, 288, 0.55),
|
| 27 |
+
"v384": (640, 384, 0.28),
|
| 28 |
+
"v480": (832, 480, 0.17),
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def probe(path: Path):
|
| 33 |
+
out = subprocess.run(
|
| 34 |
+
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
|
| 35 |
+
"stream=width,height,nb_frames", "-of", "json", str(path)],
|
| 36 |
+
capture_output=True, text=True, timeout=120)
|
| 37 |
+
st = json.loads(out.stdout)["streams"][0]
|
| 38 |
+
return path.name, int(st["width"]), int(st["height"]), int(st.get("nb_frames") or 0)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def main():
|
| 42 |
+
videos = sorted(SRC.glob("*.mp4"))
|
| 43 |
+
print(f"{len(videos)} normalized videos")
|
| 44 |
+
with ProcessPoolExecutor(max_workers=30) as ex:
|
| 45 |
+
info = list(ex.map(probe, videos, chunksize=16))
|
| 46 |
+
|
| 47 |
+
rng = random.Random(42)
|
| 48 |
+
areas = {name: w * h for name, (w, h, _) in BUCKETS.items()}
|
| 49 |
+
total = len(info)
|
| 50 |
+
quota = {name: int(total * share) for name, (_, _, share) in BUCKETS.items()}
|
| 51 |
+
# fill rounding remainder into v384
|
| 52 |
+
quota["v288"] += total - sum(quota.values())
|
| 53 |
+
|
| 54 |
+
# group by source resolution
|
| 55 |
+
groups = {}
|
| 56 |
+
for name, w, h, nb in info:
|
| 57 |
+
groups.setdefault((w, h), []).append(name)
|
| 58 |
+
for g in groups.values():
|
| 59 |
+
rng.shuffle(g)
|
| 60 |
+
|
| 61 |
+
assign = {}
|
| 62 |
+
counts = {b: 0 for b in BUCKETS}
|
| 63 |
+
|
| 64 |
+
def eligible(w, h):
|
| 65 |
+
return [b for b in BUCKETS if w * h >= areas[b]]
|
| 66 |
+
|
| 67 |
+
# 1) clips eligible for only one bucket go there
|
| 68 |
+
flexible = []
|
| 69 |
+
for (w, h), names in sorted(groups.items()):
|
| 70 |
+
el = eligible(w, h)
|
| 71 |
+
if not el:
|
| 72 |
+
for n in names:
|
| 73 |
+
assign[n] = "v288" # tiny source: still train, bucket_no_upscale protects it
|
| 74 |
+
counts["v288"] += 1
|
| 75 |
+
elif len(el) == 1:
|
| 76 |
+
for n in names:
|
| 77 |
+
assign[n] = el[0]
|
| 78 |
+
counts[el[0]] += 1
|
| 79 |
+
else:
|
| 80 |
+
flexible.extend((n, w, h) for n in names)
|
| 81 |
+
|
| 82 |
+
# 2) flexible clips: fill scarcest-eligibility buckets first (v576 needs high-res)
|
| 83 |
+
rng.shuffle(flexible)
|
| 84 |
+
for bucket in ("v480", "v384", "v288"):
|
| 85 |
+
need = quota[bucket] - counts[bucket]
|
| 86 |
+
if need <= 0:
|
| 87 |
+
continue
|
| 88 |
+
rest = []
|
| 89 |
+
for n, w, h in flexible:
|
| 90 |
+
if need > 0 and bucket in eligible(w, h) and n not in assign:
|
| 91 |
+
assign[n] = bucket
|
| 92 |
+
counts[bucket] += 1
|
| 93 |
+
need -= 1
|
| 94 |
+
else:
|
| 95 |
+
rest.append((n, w, h))
|
| 96 |
+
flexible = rest
|
| 97 |
+
# leftovers -> largest eligible bucket below its quota, else v384
|
| 98 |
+
for n, w, h in flexible:
|
| 99 |
+
if n in assign:
|
| 100 |
+
continue
|
| 101 |
+
el = eligible(w, h)
|
| 102 |
+
assign[n] = el[-1] if el else "v288"
|
| 103 |
+
counts[assign[n]] += 1
|
| 104 |
+
|
| 105 |
+
print("assignment:", counts)
|
| 106 |
+
|
| 107 |
+
for b in BUCKETS:
|
| 108 |
+
d = DST / b
|
| 109 |
+
d.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
made = 0
|
| 111 |
+
for name, bucket in assign.items():
|
| 112 |
+
src_mp4 = SRC / name
|
| 113 |
+
src_txt = src_mp4.with_suffix(".txt")
|
| 114 |
+
for s in (src_mp4, src_txt):
|
| 115 |
+
link = DST / bucket / s.name
|
| 116 |
+
if not link.exists() and s.exists():
|
| 117 |
+
link.symlink_to(s)
|
| 118 |
+
made += 1
|
| 119 |
+
print(f"{made} symlinks created")
|
| 120 |
+
|
| 121 |
+
nbmap = {name: (w, h, nb) for name, w, h, nb in info}
|
| 122 |
+
with open(MANIFEST, "w", newline="") as f:
|
| 123 |
+
wr = csv.writer(f)
|
| 124 |
+
wr.writerow(["file", "bucket", "src_width", "src_height", "src_frames_24fps"])
|
| 125 |
+
for name in sorted(assign):
|
| 126 |
+
w, h, nb = nbmap[name]
|
| 127 |
+
wr.writerow([name, assign[name], w, h, nb])
|
| 128 |
+
print(f"manifest: {MANIFEST}")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
main()
|