Upload folder using huggingface_hub
Browse files- README.md +28 -10
- config_phase2.json +22 -0
- paper_experiments/README.md +1 -2
- paper_experiments/run.py +1 -5
- pyproject.toml +1 -3
- scripts/build_submission.py +38 -11
- scripts/infer_simple.py +9 -6
- src/humomni/core/streaming_driver.py +7 -6
- src/humomni/core/validate_submission.py +0 -4
- src/humomni/tuning/cache.py +13 -7
README.md
CHANGED
|
@@ -47,25 +47,33 @@ Run every command **from the repo root** — modules use repo-root-relative `dat
|
|
| 47 |
```bash
|
| 48 |
python -m venv .venv && .venv\Scripts\activate # Windows (use: source .venv/bin/activate on *nix)
|
| 49 |
pip install -e . # installs humomni.* + deps (pyproject.toml)
|
| 50 |
-
cp .env.example .env #
|
| 51 |
```
|
| 52 |
`.env` needs `OPENROUTER_API_KEY` (the VLM) and `VERTEX_KEY` (the Gemini judge used for the LLM-entailment
|
| 53 |
-
dedup) — only for regenerating decisions from frames; the offline replay needs no keys. Put the test set
|
| 54 |
-
|
| 55 |
-
`question.json` per sample
|
| 56 |
-
|
| 57 |
|
| 58 |
## Build the submission — one command
|
| 59 |
`config.json` holds the entire method (`"ensemble": ["v5","v3","v8"] @ 40`, `dedup`/`theta`, and the
|
| 60 |
perception `"gate"` disabled by default — `use_gate:false` gives the shipped max-score point; enabling it
|
| 61 |
at novelty ≥ 16 / silence 3 s selects a lower-cost operating point):
|
| 62 |
```bash
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
```
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
The live equivalent produces byte-identical output (paid on every run — use when caches are absent):
|
| 71 |
```bash
|
|
@@ -80,3 +88,13 @@ Both drive the same `APIPolicy` from `config.json`, so all three commands point
|
|
| 80 |
python tests/test_causality.py # causality assert fires on shuffled / out-of-order frames
|
| 81 |
python tests/test_inference_parity.py # run_inference == infer_simple; ensemble multi-emit per frame
|
| 82 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
```bash
|
| 48 |
python -m venv .venv && .venv\Scripts\activate # Windows (use: source .venv/bin/activate on *nix)
|
| 49 |
pip install -e . # installs humomni.* + deps (pyproject.toml)
|
| 50 |
+
cp .env.example .env # then fill in the keys (below)
|
| 51 |
```
|
| 52 |
`.env` needs `OPENROUTER_API_KEY` (the VLM) and `VERTEX_KEY` (the Gemini judge used for the LLM-entailment
|
| 53 |
+
dedup) — only for regenerating decisions from frames; the offline replay needs no keys. Put the test set at
|
| 54 |
+
`data/phase1/data/<id>/` — frames `0.5.jpg, 1.0.jpg, …` (`.png` also supported, as shipped with the
|
| 55 |
+
phase-2 test set) plus `question.json` per sample; `--data_dir` selects the test set (per-prompt caches
|
| 56 |
+
are kept separate per phase automatically).
|
| 57 |
|
| 58 |
## Build the submission — one command
|
| 59 |
`config.json` holds the entire method (`"ensemble": ["v5","v3","v8"] @ 40`, `dedup`/`theta`, and the
|
| 60 |
perception `"gate"` disabled by default — `use_gate:false` gives the shipped max-score point; enabling it
|
| 61 |
at novelty ≥ 16 / silence 3 s selects a lower-cost operating point):
|
| 62 |
```bash
|
| 63 |
+
# phase-1 test set
|
| 64 |
+
python scripts/build_submission.py --data_dir data/phase1/data --out submissions/tryanderror2.jsonl
|
| 65 |
+
# phase-2 test set — same pipeline; its predictions use the EXH+COMP @ k32 operating point
|
| 66 |
+
# (config_phase2.json), selected for cost; see Table I of the report
|
| 67 |
+
python scripts/build_submission.py --data_dir data/phase2/data --config config_phase2.json --out submissions/tryanderror2_phase2.jsonl
|
| 68 |
```
|
| 69 |
+
`--data_dir` points at any folder of `<id>/` sample dirs (frames `0.5.jpg`/`0.5.png`, `1.0.…`, plus
|
| 70 |
+
`question.json`; both extensions are auto-detected). For each ensemble prompt it ensures a per-prompt VLM
|
| 71 |
+
decision cache — **building it from the test frames if missing** (paid: ~1 VLM call per prompt per frame;
|
| 72 |
+
resumable, skips cached frames) — then replays the merge + LLM-entailment dedup and **validates** the
|
| 73 |
+
output (all ids present, times multiples of 0.5). Re-runs are free once the caches exist. Runs on
|
| 74 |
+
different test sets never mix: decision/novelty caches are namespaced per data folder
|
| 75 |
+
(`cache/phase1_*.jsonl` vs `cache/phase2_*.jsonl`), and the resume file is fingerprinted by
|
| 76 |
+
(data path + config) so a leftover from a different run is set aside instead of resumed.
|
| 77 |
|
| 78 |
The live equivalent produces byte-identical output (paid on every run — use when caches are absent):
|
| 79 |
```bash
|
|
|
|
| 88 |
python tests/test_causality.py # causality assert fires on shuffled / out-of-order frames
|
| 89 |
python tests/test_inference_parity.py # run_inference == infer_simple; ensemble multi-emit per frame
|
| 90 |
```
|
| 91 |
+
|
| 92 |
+
## Compliance
|
| 93 |
+
Track 2 permits any open-source or commercial LLM API (organizer clarification, 2026-07-07), with one
|
| 94 |
+
condition: **when generating a response at time t, only frames at time ≤ t may be used as visual input.**
|
| 95 |
+
This system enforces that structurally (below) and tests it.
|
| 96 |
+
|
| 97 |
+
## The one hard rule — causality
|
| 98 |
+
Acting at time *t* uses **only** frames with timestamp ≤ *t*. Enforced structurally in
|
| 99 |
+
`humomni.core.streaming_driver.drive` (`assert t > last_t`) and guarded by `tests/test_causality.py`
|
| 100 |
+
(the shuffled-frame test). Keep it green always.
|
config_phase2.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"theta": 0.6,
|
| 3 |
+
"gate": {
|
| 4 |
+
"use_gate": false,
|
| 5 |
+
"novelty": 16,
|
| 6 |
+
"max_silence_s": 3.0,
|
| 7 |
+
"warmup_s": 1.0
|
| 8 |
+
},
|
| 9 |
+
"dedup": {
|
| 10 |
+
"sim_high": 0.97,
|
| 11 |
+
"sim_low": 0.55,
|
| 12 |
+
"recent_k": 16,
|
| 13 |
+
"use_llm": true
|
| 14 |
+
},
|
| 15 |
+
"ensemble": {
|
| 16 |
+
"prompts": [
|
| 17 |
+
"v5",
|
| 18 |
+
"v3"
|
| 19 |
+
],
|
| 20 |
+
"recent_k": 32
|
| 21 |
+
}
|
| 22 |
+
}
|
paper_experiments/README.md
CHANGED
|
@@ -7,7 +7,7 @@ calls** and needs **no keys**.
|
|
| 7 |
## Setup (once)
|
| 8 |
|
| 9 |
```bash
|
| 10 |
-
pip install -e . # from the repo root;
|
| 11 |
```
|
| 12 |
|
| 13 |
## Run
|
|
@@ -32,7 +32,6 @@ Runs in a few minutes on CPU.
|
|
| 32 |
| `data/gold_holdout.json` | the 100 untouched holdout items (public WEB split, items 400–499) |
|
| 33 |
| `caches/web_{base,comp,exh,fs,fs_alt}.jsonl` | per-frame VLM decisions for each prompt (temperature 0), holdout videos only |
|
| 34 |
| `caches/judge_cache.json` | every LLM-judge / entailment result, keyed by input hash |
|
| 35 |
-
| `caches/emb_cache.{npy,keys.json}` | MiniLM embeddings of every cached draft (the cosine-dedup prefilter), so the replay never loads — or downloads — the embedder |
|
| 36 |
|
| 37 |
`run.py` replays the cached decisions through the *same* merge + entailment-dedup code the live system
|
| 38 |
runs (`humomni.phase1.dedup.EntailmentGuard`), then scores with the official-protocol harness
|
|
|
|
| 7 |
## Setup (once)
|
| 8 |
|
| 9 |
```bash
|
| 10 |
+
pip install -e . # from the repo root; first run downloads the MiniLM embedder (~90 MB)
|
| 11 |
```
|
| 12 |
|
| 13 |
## Run
|
|
|
|
| 32 |
| `data/gold_holdout.json` | the 100 untouched holdout items (public WEB split, items 400–499) |
|
| 33 |
| `caches/web_{base,comp,exh,fs,fs_alt}.jsonl` | per-frame VLM decisions for each prompt (temperature 0), holdout videos only |
|
| 34 |
| `caches/judge_cache.json` | every LLM-judge / entailment result, keyed by input hash |
|
|
|
|
| 35 |
|
| 36 |
`run.py` replays the cached decisions through the *same* merge + entailment-dedup code the live system
|
| 37 |
runs (`humomni.phase1.dedup.EntailmentGuard`), then scores with the official-protocol harness
|
paper_experiments/run.py
CHANGED
|
@@ -13,8 +13,7 @@
|
|
| 13 |
# with the official-protocol harness using the shipped LLM-judge cache (caches/judge_cache.json) —
|
| 14 |
# so a rerun makes ZERO API calls and reproduces the reported numbers exactly. The perception gate is
|
| 15 |
# replayed from the shipped per-frame novelty cache (caches/novelty_holdout.json). Requires only
|
| 16 |
-
# `pip install -e .`
|
| 17 |
-
# the run is fully offline.
|
| 18 |
import argparse
|
| 19 |
import json
|
| 20 |
import os
|
|
@@ -50,9 +49,6 @@ for c in CACHES.values():
|
|
| 50 |
_TIMES = {q: sorted(ts) for q, ts in _TIMES.items()}
|
| 51 |
|
| 52 |
emb_cache.load_disk()
|
| 53 |
-
emb_cache.load_disk(os.path.join(HERE, "caches", "emb_cache")) # shipped draft embeddings —
|
| 54 |
-
# covers every draft below, so the MiniLM model (a ~90 MB first-run download) is never needed
|
| 55 |
-
# for the replay; embed_many only encodes cache MISSES.
|
| 56 |
emb_cache.embed_many([d["draft"].strip() for c in CACHES.values()
|
| 57 |
for d in c.mem.values() if d.get("draft", "").strip()])
|
| 58 |
|
|
|
|
| 13 |
# with the official-protocol harness using the shipped LLM-judge cache (caches/judge_cache.json) —
|
| 14 |
# so a rerun makes ZERO API calls and reproduces the reported numbers exactly. The perception gate is
|
| 15 |
# replayed from the shipped per-frame novelty cache (caches/novelty_holdout.json). Requires only
|
| 16 |
+
# `pip install -e .` (first run downloads the MiniLM sentence embedder, ~90 MB).
|
|
|
|
| 17 |
import argparse
|
| 18 |
import json
|
| 19 |
import os
|
|
|
|
| 49 |
_TIMES = {q: sorted(ts) for q, ts in _TIMES.items()}
|
| 50 |
|
| 51 |
emb_cache.load_disk()
|
|
|
|
|
|
|
|
|
|
| 52 |
emb_cache.embed_many([d["draft"].strip() for c in CACHES.values()
|
| 53 |
for d in c.mem.values() if d.get("draft", "").strip()])
|
| 54 |
|
pyproject.toml
CHANGED
|
@@ -7,10 +7,8 @@ name = "humomni-track2"
|
|
| 7 |
version = "0.1.0"
|
| 8 |
description = "HumOmni 2026 Track 2 (ProactivEval) — causal proactive streaming video QA (Phase-1 API orchestrator)"
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
-
# Pinned to the versions the reported results were produced with (report Appendix, "Libraries").
|
| 11 |
dependencies = [
|
| 12 |
-
"openai
|
| 13 |
-
"scikit-learn==1.7.2", "sentence-transformers==5.6.0", "tqdm==4.68.3",
|
| 14 |
]
|
| 15 |
# Note: the Gemini judge talks to Vertex AI express mode over plain REST (stdlib urllib) — no SDK dep.
|
| 16 |
|
|
|
|
| 7 |
version = "0.1.0"
|
| 8 |
description = "HumOmni 2026 Track 2 (ProactivEval) — causal proactive streaming video QA (Phase-1 API orchestrator)"
|
| 9 |
requires-python = ">=3.10"
|
|
|
|
| 10 |
dependencies = [
|
| 11 |
+
"openai", "pillow", "numpy", "scikit-learn", "sentence-transformers", "tqdm",
|
|
|
|
| 12 |
]
|
| 13 |
# Note: the Gemini judge talks to Vertex AI express mode over plain REST (stdlib urllib) — no SDK dep.
|
| 14 |
|
scripts/build_submission.py
CHANGED
|
@@ -42,13 +42,15 @@ def _checkpoint(every=20):
|
|
| 42 |
judges.jc_save()
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def build(data_dir, out, cfg, cache_tpl, workers, limit=0, build_workers=8):
|
| 46 |
-
# Fail LOUDLY on a wrong/empty data_dir: an empty gold set would otherwise sail through
|
| 47 |
-
# validation ("wrote 0 samples ... VALID") and produce an uploadable-looking empty file.
|
| 48 |
-
if not any(os.path.isdir(x) for x in glob.glob(os.path.join(data_dir, "*"))):
|
| 49 |
-
sys.exit(f"no sample dirs found under {data_dir!r} — expected the test set at "
|
| 50 |
-
f"data/phase1/data/<id>/ (frames 0.5.jpg, 1.0.jpg, ... + question.json), "
|
| 51 |
-
f"or pass --data_dir")
|
| 52 |
ens = cfg.get("ensemble")
|
| 53 |
if not ens:
|
| 54 |
sys.exit("config.json has no 'ensemble' block — the best method is the v5+v3 ensemble; add it.")
|
|
@@ -64,7 +66,7 @@ def build(data_dir, out, cfg, cache_tpl, workers, limit=0, build_workers=8):
|
|
| 64 |
novelty = None
|
| 65 |
if g.get("use_gate"):
|
| 66 |
from humomni.tuning.cache import build_novelty
|
| 67 |
-
nov_path = "cache/
|
| 68 |
if not os.path.exists(nov_path):
|
| 69 |
print(f"[gate] computing per-frame novelty for {data_dir} -> {nov_path} (CPU, one-off) …", flush=True)
|
| 70 |
build_novelty(data_dir=data_dir, out=nov_path) # ALWAYS full — a --limit-partial cache would poison later runs
|
|
@@ -96,17 +98,38 @@ def build(data_dir, out, cfg, cache_tpl, workers, limit=0, build_workers=8):
|
|
| 96 |
# Sample-level RESUME: completed records are appended to a `<out>.partial` sidecar as they finish,
|
| 97 |
# so a killed build resumes instead of restarting (the dedup judge cache is already persisted
|
| 98 |
# incrementally). The final `out` is written — and the sidecar dropped — only once all ids are done.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
partial = out + ".partial"
|
| 100 |
done = {}
|
| 101 |
if os.path.exists(partial):
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
try:
|
| 104 |
-
|
| 105 |
except Exception:
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
if done:
|
| 108 |
print(f"[resume] {len(done)} samples already done in {partial}", flush=True)
|
| 109 |
pw = open(partial, "a", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
| 110 |
pwlock = threading.Lock()
|
| 111 |
|
| 112 |
def one(sd):
|
|
@@ -159,13 +182,17 @@ if __name__ == "__main__":
|
|
| 159 |
ap.add_argument("--data_dir", default="data/phase1/data")
|
| 160 |
ap.add_argument("--out", default="submissions/submission.jsonl")
|
| 161 |
ap.add_argument("--config", default="config.json")
|
| 162 |
-
ap.add_argument("--cache_tpl", default=
|
|
|
|
|
|
|
| 163 |
ap.add_argument("--workers", type=int, default=1,
|
| 164 |
help="parallel samples. Keep at 1 on a COLD judge cache (the LLM dedup floods Vertex → 429); "
|
| 165 |
"raise to 3-4 once cached (re-run is then free).")
|
| 166 |
ap.add_argument("--limit", type=int, default=0, help="only the first N samples (quick correctness check)")
|
| 167 |
ap.add_argument("--build_workers", type=int, default=8, help="parallelism for BUILDING missing per-prompt VLM caches")
|
| 168 |
a = ap.parse_args()
|
|
|
|
|
|
|
| 169 |
cfg = json.load(open(a.config, encoding="utf-8"))
|
| 170 |
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
|
| 171 |
|
|
|
|
| 42 |
judges.jc_save()
|
| 43 |
|
| 44 |
|
| 45 |
+
def _phase_of(data_dir):
|
| 46 |
+
"""Phase tag derived from the data path: data/phase1/data -> 'phase1', data/phase2/data -> 'phase2'.
|
| 47 |
+
Keeps the per-prompt decision caches and the gate-novelty cache of different test sets separate,
|
| 48 |
+
so the SAME command works on any phase by changing only --data_dir."""
|
| 49 |
+
parent = os.path.basename(os.path.dirname(os.path.normpath(data_dir)))
|
| 50 |
+
return parent or os.path.basename(os.path.normpath(data_dir)) or "phase1"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
def build(data_dir, out, cfg, cache_tpl, workers, limit=0, build_workers=8):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
ens = cfg.get("ensemble")
|
| 55 |
if not ens:
|
| 56 |
sys.exit("config.json has no 'ensemble' block — the best method is the v5+v3 ensemble; add it.")
|
|
|
|
| 66 |
novelty = None
|
| 67 |
if g.get("use_gate"):
|
| 68 |
from humomni.tuning.cache import build_novelty
|
| 69 |
+
nov_path = f"cache/{_phase_of(data_dir)}_novelty.json"
|
| 70 |
if not os.path.exists(nov_path):
|
| 71 |
print(f"[gate] computing per-frame novelty for {data_dir} -> {nov_path} (CPU, one-off) …", flush=True)
|
| 72 |
build_novelty(data_dir=data_dir, out=nov_path) # ALWAYS full — a --limit-partial cache would poison later runs
|
|
|
|
| 98 |
# Sample-level RESUME: completed records are appended to a `<out>.partial` sidecar as they finish,
|
| 99 |
# so a killed build resumes instead of restarting (the dedup judge cache is already persisted
|
| 100 |
# incrementally). The final `out` is written — and the sidecar dropped — only once all ids are done.
|
| 101 |
+
# The sidecar's first line is a FINGERPRINT of (data_dir + method config): a leftover partial from a
|
| 102 |
+
# different test set or a different config is set aside instead of resumed, so two runs can never mix
|
| 103 |
+
# samples computed under different settings into one output.
|
| 104 |
+
import hashlib
|
| 105 |
+
fp = hashlib.md5((os.path.normpath(data_dir) + "\x00"
|
| 106 |
+
+ json.dumps(cfg, sort_keys=True, ensure_ascii=False)).encode("utf-8")).hexdigest()
|
| 107 |
partial = out + ".partial"
|
| 108 |
done = {}
|
| 109 |
if os.path.exists(partial):
|
| 110 |
+
lines = open(partial, encoding="utf-8").read().splitlines()
|
| 111 |
+
meta_ok = False
|
| 112 |
+
if lines:
|
| 113 |
try:
|
| 114 |
+
meta_ok = json.loads(lines[0]).get("__partial_config__") == fp
|
| 115 |
except Exception:
|
| 116 |
+
meta_ok = False
|
| 117 |
+
if meta_ok:
|
| 118 |
+
for line in lines[1:]:
|
| 119 |
+
try:
|
| 120 |
+
done[json.loads(line)["question_id"]] = line
|
| 121 |
+
except Exception:
|
| 122 |
+
pass
|
| 123 |
+
else:
|
| 124 |
+
os.replace(partial, partial + ".stale")
|
| 125 |
+
print(f"[resume] {partial} was built under a DIFFERENT data set or config — "
|
| 126 |
+
f"set aside as .stale, starting fresh", flush=True)
|
| 127 |
if done:
|
| 128 |
print(f"[resume] {len(done)} samples already done in {partial}", flush=True)
|
| 129 |
pw = open(partial, "a", encoding="utf-8")
|
| 130 |
+
if not done and os.path.getsize(partial) == 0: # fresh sidecar -> stamp the fingerprint header
|
| 131 |
+
pw.write(json.dumps({"__partial_config__": fp}) + "\n")
|
| 132 |
+
pw.flush()
|
| 133 |
pwlock = threading.Lock()
|
| 134 |
|
| 135 |
def one(sd):
|
|
|
|
| 182 |
ap.add_argument("--data_dir", default="data/phase1/data")
|
| 183 |
ap.add_argument("--out", default="submissions/submission.jsonl")
|
| 184 |
ap.add_argument("--config", default="config.json")
|
| 185 |
+
ap.add_argument("--cache_tpl", default=None,
|
| 186 |
+
help="per-prompt cache path ({} = prompt name); default derives from --data_dir, "
|
| 187 |
+
"e.g. data/phase2/data -> cache/phase2_{}.jsonl")
|
| 188 |
ap.add_argument("--workers", type=int, default=1,
|
| 189 |
help="parallel samples. Keep at 1 on a COLD judge cache (the LLM dedup floods Vertex → 429); "
|
| 190 |
"raise to 3-4 once cached (re-run is then free).")
|
| 191 |
ap.add_argument("--limit", type=int, default=0, help="only the first N samples (quick correctness check)")
|
| 192 |
ap.add_argument("--build_workers", type=int, default=8, help="parallelism for BUILDING missing per-prompt VLM caches")
|
| 193 |
a = ap.parse_args()
|
| 194 |
+
if not a.cache_tpl:
|
| 195 |
+
a.cache_tpl = f"cache/{_phase_of(a.data_dir)}_{{}}.jsonl"
|
| 196 |
cfg = json.load(open(a.config, encoding="utf-8"))
|
| 197 |
os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
|
| 198 |
|
scripts/infer_simple.py
CHANGED
|
@@ -25,13 +25,14 @@ def make_policy(name):
|
|
| 25 |
|
| 26 |
def _video_length(sample_dir):
|
| 27 |
"""The latest frame timestamp present — the 'video_length' the loop steps up to (frames sit on a
|
| 28 |
-
0.5 s grid: 0.5.jpg, 1.0.jpg, …)."""
|
| 29 |
ts = []
|
| 30 |
-
for
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
| 35 |
return max(ts) if ts else 0.0
|
| 36 |
|
| 37 |
|
|
@@ -60,6 +61,8 @@ def infer_one(sample_dir, policy):
|
|
| 60 |
for step in range(1, int(round(video_length / 0.5)) + 1): # current_time = 0.5 .. video_length
|
| 61 |
current_time = step * 0.5
|
| 62 |
frame_path = os.path.join(sample_dir, f"{current_time:.1f}.jpg")
|
|
|
|
|
|
|
| 63 |
if not os.path.exists(frame_path): # missing tick on the grid — skip
|
| 64 |
continue
|
| 65 |
assert current_time > last_t, "frames must arrive in order (no future frames)"
|
|
|
|
| 25 |
|
| 26 |
def _video_length(sample_dir):
|
| 27 |
"""The latest frame timestamp present — the 'video_length' the loop steps up to (frames sit on a
|
| 28 |
+
0.5 s grid: 0.5.jpg / 0.5.png, 1.0.jpg / 1.0.png, …)."""
|
| 29 |
ts = []
|
| 30 |
+
for pat in ("*.jpg", "*.png"): # phase-1 ships .jpg, phase-2 .png
|
| 31 |
+
for p in glob.glob(os.path.join(sample_dir, pat)):
|
| 32 |
+
try:
|
| 33 |
+
ts.append(float(os.path.basename(p)[:-4]))
|
| 34 |
+
except ValueError:
|
| 35 |
+
pass # skip non-frame files
|
| 36 |
return max(ts) if ts else 0.0
|
| 37 |
|
| 38 |
|
|
|
|
| 61 |
for step in range(1, int(round(video_length / 0.5)) + 1): # current_time = 0.5 .. video_length
|
| 62 |
current_time = step * 0.5
|
| 63 |
frame_path = os.path.join(sample_dir, f"{current_time:.1f}.jpg")
|
| 64 |
+
if not os.path.exists(frame_path): # phase-2 frames are .png
|
| 65 |
+
frame_path = os.path.join(sample_dir, f"{current_time:.1f}.png")
|
| 66 |
if not os.path.exists(frame_path): # missing tick on the grid — skip
|
| 67 |
continue
|
| 68 |
assert current_time > last_t, "frames must arrive in order (no future frames)"
|
src/humomni/core/streaming_driver.py
CHANGED
|
@@ -16,12 +16,13 @@ def iter_frames(sample_dir):
|
|
| 16 |
Non-frame files (e.g. question.json) and any unparsable stems are skipped.
|
| 17 |
"""
|
| 18 |
items = []
|
| 19 |
-
for
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
items.sort(key=lambda x: x[0])
|
| 26 |
out, last = [], None
|
| 27 |
for t, f in items: # drop a malformed duplicate-timestamp frame (would trip drive()'s assert)
|
|
|
|
| 16 |
Non-frame files (e.g. question.json) and any unparsable stems are skipped.
|
| 17 |
"""
|
| 18 |
items = []
|
| 19 |
+
for pat in ("*.jpg", "*.png"): # phase-1 test data ships .jpg frames, phase-2 ships .png
|
| 20 |
+
for f in glob.glob(os.path.join(sample_dir, pat)):
|
| 21 |
+
stem = os.path.basename(f)[:-4]
|
| 22 |
+
try:
|
| 23 |
+
items.append((float(stem), f))
|
| 24 |
+
except ValueError:
|
| 25 |
+
pass # skip non-frame files
|
| 26 |
items.sort(key=lambda x: x[0])
|
| 27 |
out, last = [], None
|
| 28 |
for t, f in items: # drop a malformed duplicate-timestamp frame (would trip drive()'s assert)
|
src/humomni/core/validate_submission.py
CHANGED
|
@@ -10,10 +10,6 @@ import sys
|
|
| 10 |
|
| 11 |
def validate(path, gold_ids):
|
| 12 |
seen, errs = set(), []
|
| 13 |
-
if not gold_ids:
|
| 14 |
-
# An empty gold set would validate ANY file (even an empty one) vacuously — that is
|
| 15 |
-
# always a mispathed --data_dir, never a real check.
|
| 16 |
-
return ["gold id list is empty — check --data_dir (expected data/phase1/data/<id>/)"]
|
| 17 |
with open(path, encoding="utf-8") as f:
|
| 18 |
for i, ln in enumerate(f, 1):
|
| 19 |
ln = ln.strip()
|
|
|
|
| 10 |
|
| 11 |
def validate(path, gold_ids):
|
| 12 |
seen, errs = set(), []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
with open(path, encoding="utf-8") as f:
|
| 14 |
for i, ln in enumerate(f, 1):
|
| 15 |
ln = ln.strip()
|
src/humomni/tuning/cache.py
CHANGED
|
@@ -53,12 +53,20 @@ class VLMCache:
|
|
| 53 |
return len(self.mem)
|
| 54 |
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def frames_of(qid, data_dir="data/web_frames"):
|
| 57 |
"""Ascending (t, path) for a question_id (dir name drops the .mp4 suffix)."""
|
| 58 |
name = qid[:-4] if qid.endswith(".mp4") else qid
|
| 59 |
-
|
| 60 |
-
return sorted([(float(os.path.basename(p)[:-4]), p) for p in glob.glob(d + "/*.jpg")],
|
| 61 |
-
key=lambda x: x[0])
|
| 62 |
|
| 63 |
|
| 64 |
def _sample_dirs(data_dir, limit):
|
|
@@ -77,8 +85,7 @@ def build_web_cache(data_dir="data/web_frames", cache_path=CACHE_PATH, limit=0,
|
|
| 77 |
for d in _sample_dirs(data_dir, limit):
|
| 78 |
q = json.load(open(os.path.join(d, "question.json"), encoding="utf-8"))
|
| 79 |
qid, question = q["question_id"], q["question"]
|
| 80 |
-
fr =
|
| 81 |
-
key=lambda x: x[0])
|
| 82 |
for i, (t, _) in enumerate(fr):
|
| 83 |
if cache.get(qid, t) is None:
|
| 84 |
tasks.append((qid, question, fr[:i + 1], t))
|
|
@@ -109,8 +116,7 @@ def build_novelty(data_dir="data/web_frames", out=NOVELTY_PATH, limit=0):
|
|
| 109 |
res = {}
|
| 110 |
for d in _sample_dirs(data_dir, limit):
|
| 111 |
qid = json.load(open(os.path.join(d, "question.json"), encoding="utf-8"))["question_id"]
|
| 112 |
-
fr =
|
| 113 |
-
key=lambda x: x[0])
|
| 114 |
prev, nov = None, {}
|
| 115 |
for t, p in fr:
|
| 116 |
f = np.asarray(Image.open(p).convert("L").resize((64, 36)), dtype=np.float32)
|
|
|
|
| 53 |
return len(self.mem)
|
| 54 |
|
| 55 |
|
| 56 |
+
def _frames_in(d):
|
| 57 |
+
"""Ascending (t, path) for every frame image in dir d — stems on the 0.5-s grid, e.g. 12.5.jpg.
|
| 58 |
+
Both extensions are supported: phase-1 test data ships .jpg frames, phase-2 ships .png."""
|
| 59 |
+
out = []
|
| 60 |
+
for pat in ("*.jpg", "*.png"):
|
| 61 |
+
for p in glob.glob(os.path.join(d, pat)):
|
| 62 |
+
out.append((float(os.path.basename(p)[:-4]), p))
|
| 63 |
+
return sorted(out, key=lambda x: x[0])
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def frames_of(qid, data_dir="data/web_frames"):
|
| 67 |
"""Ascending (t, path) for a question_id (dir name drops the .mp4 suffix)."""
|
| 68 |
name = qid[:-4] if qid.endswith(".mp4") else qid
|
| 69 |
+
return _frames_in(os.path.join(data_dir, name))
|
|
|
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
def _sample_dirs(data_dir, limit):
|
|
|
|
| 85 |
for d in _sample_dirs(data_dir, limit):
|
| 86 |
q = json.load(open(os.path.join(d, "question.json"), encoding="utf-8"))
|
| 87 |
qid, question = q["question_id"], q["question"]
|
| 88 |
+
fr = _frames_in(d)
|
|
|
|
| 89 |
for i, (t, _) in enumerate(fr):
|
| 90 |
if cache.get(qid, t) is None:
|
| 91 |
tasks.append((qid, question, fr[:i + 1], t))
|
|
|
|
| 116 |
res = {}
|
| 117 |
for d in _sample_dirs(data_dir, limit):
|
| 118 |
qid = json.load(open(os.path.join(d, "question.json"), encoding="utf-8"))["question_id"]
|
| 119 |
+
fr = _frames_in(d)
|
|
|
|
| 120 |
prev, nov = None, {}
|
| 121 |
for t, p in fr:
|
| 122 |
f = np.asarray(Image.open(p).convert("L").resize((64, 36)), dtype=np.float32)
|