| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import argparse |
| import glob |
| import json |
| import os |
| import sys |
| import threading |
| from concurrent.futures import ThreadPoolExecutor |
|
|
| import humomni.core.emb_cache as emb_cache |
| import humomni.core.judges as judges |
| from humomni.tuning.cache import VLMCache, frames_of |
| from humomni.phase1.dedup import EntailmentGuard |
| from humomni.core.validate_submission import validate, gold_ids_from_dir |
|
|
| _save_lock = threading.Lock() |
| _progress = [0] |
|
|
|
|
| def _checkpoint(every=20): |
| """Persist the (LLM-dedup) judge cache every `every` samples so a Vertex 429 mid-build never |
| re-pays for finished work β a resubmit resumes from the disk cache. The original one-off never |
| saved these, which is why a fresh build floods Vertex with uncached entailment calls.""" |
| with _save_lock: |
| _progress[0] += 1 |
| if _progress[0] % every == 0: |
| judges.jc_save() |
|
|
|
|
| def build(data_dir, out, cfg, cache_tpl, workers, limit=0, build_workers=8): |
| |
| |
| if not any(os.path.isdir(x) for x in glob.glob(os.path.join(data_dir, "*"))): |
| sys.exit(f"no sample dirs found under {data_dir!r} β expected the test set at " |
| f"data/phase1/data/<id>/ (frames 0.5.jpg, 1.0.jpg, ... + question.json), " |
| f"or pass --data_dir") |
| ens = cfg.get("ensemble") |
| if not ens: |
| sys.exit("config.json has no 'ensemble' block β the best method is the v5+v3 ensemble; add it.") |
| prompts = ens["prompts"] |
| recent_k = ens.get("recent_k", cfg.get("dedup", {}).get("recent_k", 4)) |
| theta = cfg.get("theta", 0.6) |
| d = cfg.get("dedup", {}) |
|
|
| |
| |
| |
| g = cfg.get("gate", {}) |
| novelty = None |
| if g.get("use_gate"): |
| from humomni.tuning.cache import build_novelty |
| nov_path = "cache/phase1_novelty.json" |
| if not os.path.exists(nov_path): |
| print(f"[gate] computing per-frame novelty for {data_dir} -> {nov_path} (CPU, one-off) β¦", flush=True) |
| build_novelty(data_dir=data_dir, out=nov_path) |
| novelty = json.load(open(nov_path, encoding="utf-8")) |
| print(f"[gate] ON β novelty>={g['novelty']}, max_silence={g['max_silence_s']}s, warmup={g['warmup_s']}s", flush=True) |
|
|
| |
| |
| |
| |
| from humomni.tuning.cache import build_web_cache |
| from humomni.phase1.vlm_client import prompt_for |
| caches = {} |
| for p in prompts: |
| path = cache_tpl.format(p) |
| print(f"[cache] ensuring {path} (prompt {p}) from {data_dir} β¦", flush=True) |
| build_web_cache(data_dir=data_dir, cache_path=path, limit=limit, workers=build_workers, prompt=prompt_for(p)) |
| caches[p] = VLMCache(path) |
|
|
| emb_cache.load_disk() |
| judges.jc_load() |
| warm = [v["draft"].strip() for c in caches.values() for v in c.mem.values() if v.get("draft", "").strip()] |
| if warm: |
| emb_cache.embed_many(warm) |
| dirs = sorted(x for x in glob.glob(os.path.join(data_dir, "*")) if os.path.isdir(x)) |
| if limit: |
| dirs = dirs[:limit] |
|
|
| |
| |
| |
| partial = out + ".partial" |
| done = {} |
| if os.path.exists(partial): |
| for line in open(partial, encoding="utf-8"): |
| try: |
| done[json.loads(line)["question_id"]] = line.rstrip("\n") |
| except Exception: |
| pass |
| if done: |
| print(f"[resume] {len(done)} samples already done in {partial}", flush=True) |
| pw = open(partial, "a", encoding="utf-8") |
| pwlock = threading.Lock() |
|
|
| def one(sd): |
| qid = json.load(open(os.path.join(sd, "question.json"), encoding="utf-8"))["question_id"] |
| if qid in done: |
| return |
| |
| guard = EntailmentGuard(sim_high=d.get("sim_high", 0.97), sim_low=d.get("sim_low", 0.55), |
| recent_k=recent_k, use_llm=d.get("use_llm", True)) |
| said, res, last_fire = [], [], -1e9 |
| for t, _ in frames_of(qid, data_dir): |
| if novelty is not None: |
| nov = novelty[qid][f"{t:.1f}"] |
| fire = (t <= g["warmup_s"]) or (nov >= g["novelty"]) or (t - last_fire >= g["max_silence_s"]) |
| if not fire: |
| continue |
| last_fire = t |
| for p in prompts: |
| dec = caches[p].get(qid, t) |
| if dec and dec["decision"] == "NEW_ANSWER" and dec["confidence"] >= theta: |
| draft = dec["draft"].strip() |
| if draft and not guard.is_duplicate(draft, said): |
| said.append(draft) |
| res.append({"time": round(t, 3), "content": draft}) |
| line = json.dumps({"question_id": qid, "model_response_list": res}, ensure_ascii=False) |
| with pwlock: |
| pw.write(line + "\n"); pw.flush(); os.fsync(pw.fileno()) |
| done[qid] = line |
| _checkpoint() |
|
|
| try: |
| with ThreadPoolExecutor(max_workers=workers) as ex: |
| list(ex.map(one, dirs)) |
| finally: |
| pw.close() |
| judges.jc_save() |
| emb_cache.save_disk() |
| order = [json.load(open(os.path.join(sd, "question.json"), encoding="utf-8"))["question_id"] for sd in dirs] |
| records = [json.loads(done[q]) for q in order if q in done] |
| with open(out, "w", encoding="utf-8") as w: |
| for r in records: |
| w.write(json.dumps(r, ensure_ascii=False) + "\n") |
| if len(records) == len(dirs) and os.path.exists(partial): |
| os.remove(partial) |
| return records |
|
|
|
|
| if __name__ == "__main__": |
| ap = argparse.ArgumentParser(description="Build + validate the best Phase-1 submission from per-prompt caches.") |
| ap.add_argument("--data_dir", default="data/phase1/data") |
| ap.add_argument("--out", default="submissions/submission.jsonl") |
| ap.add_argument("--config", default="config.json") |
| ap.add_argument("--cache_tpl", default="cache/phase1_{}.jsonl", help="per-prompt cache path ({} = prompt name)") |
| ap.add_argument("--workers", type=int, default=1, |
| help="parallel samples. Keep at 1 on a COLD judge cache (the LLM dedup floods Vertex β 429); " |
| "raise to 3-4 once cached (re-run is then free).") |
| ap.add_argument("--limit", type=int, default=0, help="only the first N samples (quick correctness check)") |
| ap.add_argument("--build_workers", type=int, default=8, help="parallelism for BUILDING missing per-prompt VLM caches") |
| a = ap.parse_args() |
| cfg = json.load(open(a.config, encoding="utf-8")) |
| os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True) |
|
|
| records = build(a.data_dir, a.out, cfg, a.cache_tpl, a.workers, a.limit, a.build_workers) |
| avg = sum(len(r["model_response_list"]) for r in records) / max(1, len(records)) |
| ens = cfg["ensemble"] |
| print(f"wrote {a.out}: {len(records)} samples, avg {avg:.1f} resp/sample " |
| f"(ensemble {ens['prompts']} @ recent_k={ens.get('recent_k')})") |
|
|
| errs = validate(a.out, gold_ids_from_dir(a.data_dir)) |
| if errs: |
| print(f"INVALID β {len(errs)} problem(s):") |
| for e in errs[:20]: |
| print(" -", e) |
| sys.exit(1) |
| print(f"VALID β all ids present, times are multiples of 0.5. Rename {a.out} to your Team ID to upload.") |
|
|