opd_zt / scripts /filter_videos.py
sdzt's picture
Add files using upload-large-folder tool
bf46e5d verified
Raw
History Blame Contribute Delete
4.49 kB
"""
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
# Decord on CPU only — we don't need GPU for frame-count check.
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:
# Lazy import inside worker to avoid heavy load on parent.
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())
# Sanity: try decoding the last frame to catch corrupt tails.
_ = 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)
# Resume support: if manifest exists, skip files already there.
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)
# Append-mode writers.
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()