| """ |
| Walk extracted videos, verify each is decodable with decord and has ≥ FRAMES_MIN |
| frames. Outputs a per-source manifest jsonl with one valid entry per video: |
| |
| {"src": "tb", "rel_path": "long_video/ActivityNet/abc.mp4", |
| "frames": 240, "fps": 30.0, "duration_s": 8.0} |
| |
| Multiprocessing; safe to interrupt and re-run (writes incrementally). |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import sys |
| import time |
| from concurrent.futures import ProcessPoolExecutor, as_completed |
| from pathlib import Path |
|
|
| |
| os.environ.setdefault("DECORD_EOF_RETRY_MAX", "1024") |
|
|
| ROOT = Path("/mnt/local-fast/opd_zt") |
| VIDEOS = ROOT / "data" / "videos" |
| FILTERED = ROOT / "data" / "filtered" |
| FRAMES_MIN = 32 |
|
|
| VIDEO_EXTS = {".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"} |
|
|
|
|
| def probe(path: Path) -> dict | None: |
| """Return manifest entry or None if invalid / too short / unreadable.""" |
| try: |
| |
| from decord import VideoReader, cpu |
| vr = VideoReader(str(path), ctx=cpu(0), num_threads=1) |
| n = len(vr) |
| if n < FRAMES_MIN: |
| return None |
| fps = float(vr.get_avg_fps()) |
| |
| _ = vr[n - 1].asnumpy() |
| duration = n / fps if fps > 0 else 0.0 |
| return { |
| "frames": int(n), |
| "fps": fps, |
| "duration_s": duration, |
| } |
| except Exception: |
| return None |
|
|
|
|
| def worker(src_root: str, rel_path: str) -> tuple[str, dict | None]: |
| full = Path(src_root) / rel_path |
| info = probe(full) |
| if info is None: |
| return rel_path, None |
| info["rel_path"] = rel_path |
| return rel_path, info |
|
|
|
|
| def gather_videos(src_root: Path) -> list[str]: |
| files: list[str] = [] |
| for p in src_root.rglob("*"): |
| if p.is_file() and p.suffix.lower() in VIDEO_EXTS: |
| files.append(str(p.relative_to(src_root))) |
| return files |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--src", choices=["tb", "lv178k"], required=True) |
| parser.add_argument("--workers", type=int, default=os.cpu_count() or 8) |
| parser.add_argument("--limit", type=int, default=0, help="0 = no limit") |
| args = parser.parse_args() |
|
|
| src_root = VIDEOS / args.src |
| FILTERED.mkdir(parents=True, exist_ok=True) |
| out_path = FILTERED / f"{args.src}_manifest.jsonl" |
| bad_path = FILTERED / f"{args.src}_invalid.txt" |
|
|
| if not src_root.exists(): |
| print(f"FATAL: source dir does not exist: {src_root}", file=sys.stderr) |
| sys.exit(2) |
|
|
| |
| seen: set[str] = set() |
| if out_path.exists(): |
| with out_path.open() as f: |
| for line in f: |
| try: |
| seen.add(json.loads(line)["rel_path"]) |
| except Exception: |
| pass |
| if bad_path.exists(): |
| with bad_path.open() as f: |
| for line in f: |
| seen.add(line.strip()) |
| print(f"[scan] gathering videos under {src_root}", flush=True) |
| all_files = gather_videos(src_root) |
| print(f"[scan] found {len(all_files)} video files; {len(seen)} already processed", flush=True) |
| todo = [p for p in all_files if p not in seen] |
| if args.limit: |
| todo = todo[: args.limit] |
| print(f"[scan] {len(todo)} to probe", flush=True) |
|
|
| |
| n_ok = 0 |
| n_bad = 0 |
| t0 = time.time() |
| with out_path.open("a") as out_f, bad_path.open("a") as bad_f, \ |
| ProcessPoolExecutor(max_workers=args.workers) as ex: |
| futs = {ex.submit(worker, str(src_root), p): p for p in todo} |
| for i, fut in enumerate(as_completed(futs), 1): |
| rel, info = fut.result() |
| if info is None: |
| bad_f.write(rel + "\n") |
| bad_f.flush() |
| n_bad += 1 |
| else: |
| info["src"] = args.src |
| out_f.write(json.dumps(info) + "\n") |
| out_f.flush() |
| n_ok += 1 |
| if i % 200 == 0: |
| rate = i / max(1.0, time.time() - t0) |
| print(f"[probe] {i}/{len(todo)} ok={n_ok} bad={n_bad} " |
| f"({rate:.1f}/s)", flush=True) |
| print(f"DONE ok={n_ok} bad={n_bad} out={out_path}", flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|