#!/usr/bin/env python3 """Build a binary-label manifest for DAD clips → drop-in input for make_belief_cache_v2.py in binary pretraining mode (Stage K0). Output (matches make_policy_labels.py schema): { "samples": [ { "video_id": "dad_positive_training_000001", "source": "dad", "category": "ego_positive" | "safe_neg", "source_dir": "data/pretrain_v2/dad_frames/positive/000001", "frame_indices": [last N frame ids, tail-biased], "tta_raw": -1.0, "action_label": 2 (positive) | 0 (negative), "ce_weight": 1.0, "metadata": {"fps": 20.0, "n_frames": 100, "split": "training"|"testing"|"single"} }, ... ], "label_counts": {"ALERT": n_pos, "SILENT": n_neg}, "excluded": {"missing_frames": k} } DAD has no TTA; tta_raw=-1.0 and action_label is a 0/2 binary mapping that downstream binary heads reduce to {0, 1}. 3-class PolicyHead should NOT be trained on this manifest. """ from __future__ import annotations import argparse import json from collections import Counter from pathlib import Path from typing import List, Tuple def _build_frame_indices(n_frames: int, window: int) -> List[int]: last = n_frames - 1 first = max(0, last - window + 1) return list(range(first, last + 1)) def _discover_splits(root: Path) -> List[Tuple[str, Path]]: """Return [(tag, dir)] for each split dir. Falls back to a no-split layout.""" split_names = ["training", "testing"] found = [(s, root / s) for s in split_names if (root / s).exists()] if found: return found if (root / "positive").exists() or (root / "negative").exists(): return [("single", root)] return [] def _scan(split_tag: str, split_dir: Path, frame_window: int, excluded: Counter) -> List[dict]: out: List[dict] = [] for cls_name, label in [("positive", 2), ("negative", 0)]: cls_dir = split_dir / cls_name if not cls_dir.exists(): continue for clip_dir in sorted(cls_dir.iterdir()): if not clip_dir.is_dir(): continue ann_path = clip_dir / "annotation.json" if ann_path.exists(): ann = json.load(open(ann_path)) n_frames = int(ann.get("n_frames", 0)) fps = float(ann.get("fps", 20.0)) else: n_frames = len(list(clip_dir.glob("*.jpg"))) fps = 20.0 if n_frames <= 0: excluded["missing_frames"] += 1 continue frame_idx = _build_frame_indices(n_frames, frame_window) tail = frame_idx[-1] if not (clip_dir / f"{tail:03d}.jpg").exists(): excluded["missing_frames"] += 1 continue out.append({ "video_id": f"dad_{cls_name}_{split_tag}_{clip_dir.name}", "source": "dad", "category": "ego_positive" if label == 2 else "safe_neg", "source_dir": str(clip_dir), "frame_indices": frame_idx, "tta_raw": -1.0, "action_label": label, "ce_weight": 1.0, "metadata": { "fps": fps, "n_frames": n_frames, "split": split_tag, }, }) return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--frames_root", default="data/pretrain_v2/dad_frames") ap.add_argument("--out", default="data/policy_labels/dad_binary.json") ap.add_argument("--frame_window", type=int, default=60, help="tail-biased window of frame ids baked into manifest " "(must be ≥ n_frames used in cache build)") ap.add_argument("--exclude_testing", action="store_true", help="keep only DAD training split (default: include both)") args = ap.parse_args() root = Path(args.frames_root) if not root.exists(): raise SystemExit(f"frames_root {root} does not exist") splits = _discover_splits(root) if not splits: raise SystemExit(f"no split/class directories under {root}") if args.exclude_testing: splits = [s for s in splits if s[0] != "testing"] samples: List[dict] = [] excluded: Counter = Counter() for tag, d in splits: samples.extend(_scan(tag, d, args.frame_window, excluded)) label_counts = Counter("ALERT" if s["action_label"] == 2 else "SILENT" for s in samples) out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) with open(out_path, "w") as f: json.dump({ "samples": samples, "label_counts": dict(label_counts), "excluded": dict(excluded), }, f) by_split = Counter(s["metadata"]["split"] for s in samples) print(f"[dad_manifest] {len(samples)} clips " f"labels={dict(label_counts)} splits={dict(by_split)} " f"excluded={dict(excluded)} → {out_path}") if __name__ == "__main__": main()