run_validation.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env python3
  2. """Proact-VL one-stop validation runner.
  3. Runs the official evaluation metrics on a prediction JSONL:
  4. 1. F1 (speaking-second overlap) -> f1_only.py (no API)
  5. 2. F1 + Time-Diff -> f1_timediff.py (no API)
  6. 3. LLM Score (LiveU / FinalQ) -> llm_score.py (API, optional)
  7. 4. CC win-rate vs a baseline -> llm_judge.py (API, optional)
  8. 5. PAUC (progressive area under curve) -> pauc.py (API, optional)
  9. Example:
  10. python -m proactvl_eval.run_validation \
  11. --pred results/pred.jsonl \
  12. --reference results/anns/all_in_one_val_proactive.jsonl \
  13. --asr_jsonl results/anns/all_in_one.jsonl \
  14. --baseline_id gemini2.5-pro --baseline_jsonl results/baseline/captions/gemini-2.5-pro.jsonl \
  15. --judge_model gpt-4o --judge_limit 50
  16. API metrics are skipped automatically when no OPENAI_API_KEY is set
  17. (override with --force_llm_metrics).
  18. """
  19. import argparse
  20. import json
  21. import os
  22. import subprocess
  23. import sys
  24. from pathlib import Path
  25. ROOT = Path(__file__).resolve().parent.parent
  26. def run_script(script: str, args: list, env_extra: dict = None) -> None:
  27. cmd = [sys.executable, "-m", f"proactvl_eval.{script}", *args]
  28. env = dict(os.environ)
  29. if env_extra:
  30. env.update(env_extra)
  31. print(f"\n{'=' * 70}\n$ {' '.join(cmd)}\n{'=' * 70}")
  32. subprocess.run(cmd, cwd=str(ROOT), env=env, check=True)
  33. def load_json(path: Path):
  34. with open(path, encoding="utf-8") as f:
  35. return json.load(f)
  36. def load_jsonl(path: Path):
  37. rows = []
  38. with open(path, encoding="utf-8") as f:
  39. for line in f:
  40. line = line.strip()
  41. if line:
  42. rows.append(json.loads(line))
  43. return rows
  44. def mean(vals):
  45. return sum(vals) / len(vals) if vals else float("nan")
  46. def summarize(out_dir: Path, model_id: str, do_cc: bool, do_pauc: bool):
  47. """Parse metric outputs and print one consolidated table."""
  48. rows = []
  49. f1 = out_dir / "f1.json"
  50. if f1.exists():
  51. d = load_json(f1)
  52. m = d["overall"]["overlap_micro"]
  53. rows.append(("F1 (micro)", f"{m['f1'] * 100:.2f} (P={m['precision'] * 100:.1f} R={m['recall'] * 100:.1f}, n={m['sample_count']})"))
  54. ft = out_dir / "f1_timediff.json"
  55. if ft.exists():
  56. d = load_json(ft)
  57. t = d["overall"]["timediff"]
  58. f1m = d["overall"]["overlap_micro"]["f1"]
  59. rows.append(("F1+TimeDiff (micro)", f"F1={f1m * 100:.2f} TimeDiff avg={t['avg']:.3f} (n={t['count']})"))
  60. liveu = out_dir / "llm_score" / f"llm_score_liveu_{model_id}.jsonl"
  61. if liveu.exists():
  62. rows_ = load_jsonl(liveu)
  63. for k in ("Time", "Rate", "TextU"):
  64. vals = [r["liveu_score"][k] for r in rows_ if k in r.get("liveu_score", {})]
  65. rows.append((f"LiveU {k}", f"{mean(vals):.2f} (n={len(vals)})"))
  66. finalq = out_dir / "llm_score" / f"llm_score_finalq_{model_id}.jsonl"
  67. if finalq.exists():
  68. rows_ = load_jsonl(finalq)
  69. for k in ("Fidelity", "Continuity", "Substance"):
  70. vals = [r["finalq_score"][k] for r in rows_ if k in r.get("finalq_score", {})]
  71. rows.append((f"FinalQ {k}", f"{mean(vals):.2f} (n={len(vals)})"))
  72. if do_cc:
  73. cc = out_dir / "cc" / f"llm_judge_{model_id}.jsonl"
  74. if cc.exists():
  75. rows_ = load_jsonl(cc)
  76. a = sum(1 for r in rows_ if r.get("ab_winner") == model_id)
  77. b = sum(1 for r in rows_ if r.get("ba_winner") == model_id)
  78. n = max(len(rows_), 1)
  79. rows.append(("CC win-rate", f"A={a / n * 100:.1f}% B={b / n * 100:.1f}% (n={n})"))
  80. if do_pauc:
  81. pauc = out_dir / "pauc" / f"pauc_{model_id}.json"
  82. if pauc.exists():
  83. d = load_json(pauc)
  84. for omega, stat in (("overall", d.get("overall") or {})):
  85. m_auc = stat.get("mean_auc", {}).get("0.5")
  86. if m_auc is not None:
  87. rows.append(("PAUC (omega=0.5)", f"{m_auc * 100:.2f}"))
  88. if not any(r[0].startswith("PAUC") for r in rows):
  89. rows.append(("PAUC", "see " + str(pauc)))
  90. print(f"\n{'#' * 70}\n# Validation summary ({model_id})\n{'#' * 70}")
  91. for name, val in rows:
  92. print(f" {name:<20} {val}")
  93. if not rows:
  94. print(" No metric outputs found.")
  95. def main():
  96. p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
  97. p.add_argument("--pred", required=True, help="Prediction JSONL ({video_id, dataset_name, begin, end, idx, tag, pred})")
  98. p.add_argument("--reference", required=True, help="Proactive reference JSONL with per-second labels (e.g. all_in_one_val_proactive.jsonl)")
  99. p.add_argument("--model_id", default="model", help="Model id used in outputs")
  100. p.add_argument("--out_dir", default="./eval_out", help="Output directory")
  101. # LLM-judged metrics (optional, need an OpenAI-compatible API key)
  102. p.add_argument("--asr_jsonl", default=None, help="Annotation/ASR JSONL for LLM Score & CC")
  103. p.add_argument("--baseline_id", default=None, help="Baseline id for CC win-rate")
  104. p.add_argument("--baseline_jsonl", default=None, help="Baseline prediction JSONL for CC win-rate")
  105. p.add_argument("--judge_model", default=os.getenv("OPENAI_JUDGE_MODEL", "gpt-5.1_2025-11-13"))
  106. p.add_argument("--judge_limit", type=int, default=100, help="Max new PAUC judge requests (-1 = all)")
  107. p.add_argument("--concurrency", type=int, default=8)
  108. p.add_argument("--skip_f1", action="store_true")
  109. p.add_argument("--skip_llm_metrics", action="store_true", help="Skip all API-based metrics")
  110. p.add_argument("--force_llm_metrics", action="store_true")
  111. args = p.parse_args()
  112. out_dir = Path(args.out_dir)
  113. out_dir.mkdir(parents=True, exist_ok=True)
  114. env = {"OPENAI_JUDGE_MODEL": args.judge_model}
  115. need_api = not args.skip_llm_metrics and (args.asr_jsonl or args.force_llm_metrics)
  116. if need_api and not os.getenv("OPENAI_API_KEY") and not args.force_llm_metrics:
  117. print("OPENAI_API_KEY not set -> running deterministic metrics only (F1, F1+TimeDiff).")
  118. need_api = False
  119. do_cc = need_api and bool(args.baseline_jsonl and args.baseline_id and args.asr_jsonl)
  120. if not args.skip_f1:
  121. run_script("f1_only", [args.pred, "--reference", args.reference,
  122. "--output", str(out_dir / "f1.json"), "--verbose"], env)
  123. run_script("f1_timediff", [args.pred, "--reference", args.reference,
  124. "--output", str(out_dir / "f1_timediff.json"),
  125. "--alpha", "0.2", "--verbose"], env)
  126. if need_api:
  127. if not args.asr_jsonl:
  128. print("--asr_jsonl is required for LLM metrics; skipping them.")
  129. else:
  130. if not args.skip_f1:
  131. pass
  132. run_script("llm_score", ["--model_id", args.model_id,
  133. "--prediction_jsonl", args.pred,
  134. "--asr_jsonl", args.asr_jsonl,
  135. "--output_dir", str(out_dir / "llm_score"),
  136. "--num_workers", str(args.concurrency)], env)
  137. if do_cc:
  138. run_script("llm_judge", ["--model_id", args.model_id,
  139. "--prediction_jsonl", args.pred,
  140. "--num_workers", str(args.concurrency),
  141. "--baseline_id", args.baseline_id,
  142. "--baseline_jsonl", args.baseline_jsonl,
  143. "--asr_jsonl", args.asr_jsonl,
  144. "--output_dir", str(out_dir / "cc")], env)
  145. pauc_out = out_dir / "pauc" / f"pauc_{args.model_id}.json"
  146. pauc_out.parent.mkdir(parents=True, exist_ok=True)
  147. run_script("pauc", ["--func", "one_step",
  148. "--pred_fname", args.pred,
  149. "--gold_fname", args.reference,
  150. "--output_fname", str(pauc_out),
  151. "--openai_model", args.judge_model,
  152. "--concurrency", str(args.concurrency),
  153. "--start_score", "0",
  154. "--judge_limit", str(args.judge_limit),
  155. "--resume"], env)
  156. summarize(out_dir, args.model_id, do_cc, need_api)
  157. print(f"\nAll outputs under: {out_dir.resolve()}")
  158. if __name__ == "__main__":
  159. main()