Spaces:
Paused
Paused
File size: 5,801 Bytes
7a55d0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | """Run the grader over the labelled dataset LOCALLY and write benchmark_results.json.
The dataset is confidential and never leaves this machine except as pseudonymised
transcripts sent to the LLM. The public results file holds only batch IDs, labels,
predictions and scores - no quotes, names or evidence text.
usage: python scripts/benchmark.py <dataset_root> [--only B482] [--video] [--rejudge]
"""
from __future__ import annotations
import argparse
import json
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import anthropic # noqa: E402
from dotenv import find_dotenv, load_dotenv # noqa: E402
from grader import judge as J # noqa: E402
from grader import score as S # noqa: E402
from grader import transcript as T # noqa: E402
CACHE = ROOT / ".cache" / "benchmark"
def pair_key(name: str) -> str:
"""Zoom names the .vtt differently from the .mp4 (no date, different spacing),
so pair on the batch ID, or 'demo <language>' for the demo folder."""
toks = Path(name).stem.lower().replace("-", " ").split()
return " ".join(toks[:2]) if toks[0] == "demo" else toks[0]
def find_pairs(root: Path) -> list[dict]:
pairs = []
for folder in sorted(p for p in root.iterdir() if p.is_dir()):
vtts = {pair_key(f.name): f for f in folder.glob("*.vtt")}
for mp4 in sorted(folder.glob("*.mp4")):
key = pair_key(mp4.name)
pairs.append({
"id": f"{folder.name}/{key.upper() if key[0] == 'b' else key}",
"batch": key.upper() if key[0] == "b" else key,
"label": S.LABELS.get(folder.name), # None for demo recordings
"mp4": mp4, "vtt": vtts.get(key),
})
return pairs
def run_one(p: dict, client, rejudge: bool, video: bool) -> dict:
slug = p["id"].replace("/", "__").replace(" ", "_")
cache_file = CACHE / J.PROMPT_VERSION / f"{slug}.json"
tr = T.load(p["vtt"].read_text(encoding="utf-8"))
m = T.metrics(tr)
media = None
if video:
from grader import media as M
media = M.analyse(str(p["mp4"]), tr)
fresh = not (cache_file.exists() and not rejudge)
if not fresh:
cached = json.loads(cache_file.read_text(encoding="utf-8"))
jd, usage = J.Judgement.model_validate(cached["judgement"]), cached["usage"]
else:
jd, usage = J.judge(T.render(tr), m, client=client) # audio is informational, not judged
cache_file.write_text(json.dumps({"judgement": jd.model_dump(), "usage": usage}, indent=1),
encoding="utf-8")
pillars = {k: getattr(jd, k).score for k in J.PILLARS}
visual = (media or {}).get("visual_score")
result = S.grade(pillars, m.get("english_ratio"), jd.confidence, visual)
pillars["visual_compliance"] = visual
return {"id": p["id"], "batch": p["batch"], "label": p["label"], "predicted": result["grade"],
"llm_holistic": jd.holistic_grade, "weighted_score": result["weighted_score"],
"confidence": result["confidence"], "pillars": pillars,
"duration_min": m.get("duration_min"), "english_ratio": m.get("english_ratio"),
"visual": {k: v for k, v in (media or {}).items() if k not in ("audio", "timeline")} or None,
"tokens": usage if fresh else {"input_tokens": 0, "output_tokens": 0}}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("root", type=Path)
ap.add_argument("--only", help="substring of the id to run")
ap.add_argument("--video", action="store_true", help="also run video/audio analysis")
ap.add_argument("--rejudge", action="store_true", help="ignore cached LLM judgements")
ap.add_argument("--workers", type=int, default=2)
args = ap.parse_args()
load_dotenv(find_dotenv(usecwd=True))
(CACHE / J.PROMPT_VERSION).mkdir(parents=True, exist_ok=True)
client = anthropic.Anthropic(max_retries=6)
pairs = [p for p in find_pairs(args.root) if p["vtt"] and (not args.only or args.only in p["id"])]
with ThreadPoolExecutor(args.workers) as ex:
rows = list(ex.map(lambda p: run_one(p, client, args.rejudge, args.video), pairs))
order = {g: i for i, g in enumerate(S.GRADES)}
labelled = [r for r in rows if r["label"]]
exact = sum(r["predicted"] == r["label"] for r in labelled)
within1 = sum(abs(order[r["predicted"]] - order[r["label"]]) <= 1 for r in labelled)
holistic = sum(r["llm_holistic"] == r["label"] for r in labelled)
print(f"{'id':28} {'actual':10} {'pred':10} {'llm':10} {'score':>5} pillars")
for r in rows:
ps = " ".join(f"{v if v is not None else '-'}" for v in r["pillars"].values())
print(f"{r['id']:28} {str(r['label']):10} {r['predicted']:10} {r['llm_holistic']:10} "
f"{r['weighted_score']:>5} {ps}")
if labelled:
print(f"\nexact {exact}/{len(labelled)} | within one grade {within1}/{len(labelled)} | "
f"LLM holistic exact {holistic}/{len(labelled)}")
tok = sum(r["tokens"]["input_tokens"] for r in rows), sum(r["tokens"]["output_tokens"] for r in rows)
print(f"tokens in/out: {tok[0]}/{tok[1]} (~${tok[0] * 2e-6 + tok[1] * 10e-6:.2f} on Sonnet 5.5)")
if not args.only:
public = [{k: v for k, v in r.items() if k != "tokens"} for r in rows]
summary = {"prompt_version": J.PROMPT_VERSION, "model": J.MODEL,
"n_labelled": len(labelled), "exact": exact, "within_one": within1,
"llm_holistic_exact": holistic}
(ROOT / "benchmark_results.json").write_text(
json.dumps({"summary": summary, "rows": public}, indent=1), encoding="utf-8")
if __name__ == "__main__":
main()
|