Files
ely-eye/bench/build_videomme.py
T
ZacharyZhang-NY fc0698c242 Add baseline harness: ingest, metrics, ablation arms, external counterpart, audit
- bench/ingest_baseline.py drives the unmodified product IngestionService over
  all 50,789 corpus files in sharded SQLite DBs, then merges them. Thread and
  memory ceilings are set explicitly: ONNX Runtime defaulted to every core
  (95 threads/worker, load 490) and its CPU arena kept freed pages mapped,
  which OOM-killed workers.
- bench/metrics.py: recall@k, MRR, nDCG@10, Wilson lower bound, chance
  baseline. No model, no product import, so the scorer cannot grade itself.
- bench/systems.py + bench/run.py: the five ablation arms over the merged DB,
  latency percentiles, scale curve, and a top-10 equivalence check against the
  live RetrievalService.
- bench/counterpart.py + bench/run_external.py: in-repo Mem0-style flat store
  and Zep-style temporal store as the external reference point.
- bench/audit.py: enforces single-attempt scoring, frozen n, a chance row
  beside every score, and producer != verifier. It already flags the product's
  retry-until-target loop at backend/src/ely_eye/evals.py:2351.
- backend/pyproject.toml: rapidocr-onnxruntime>=1.4.4 was unsatisfiable
  (PyPI maxes at 1.2.3); the successor is the renamed rapidocr package.
2026-10-06 14:36:02 -04:00

199 lines
7.0 KiB
Python

"""Build video-QA sets from Video-MME (lmms-eval/Video-MME).
Gold is the whole video: Video-MME annotators marked the correct option, not a
timestamp, so a timestamped gold region would be our own extraction rather than
a human judgement. Only videos whose file is actually on disk become queries,
and each domain is one memory holder so a question can also be asked of a
domain that never saw the clip.
"""
from __future__ import annotations
import json
import zipfile
from typing import Any
from pyarrow.parquet import ParquetFile
from bench.corpus_sources import source_for
from bench.sets_common import (
DATA_ROOT,
SETS,
corpus_path,
corpus_record,
parse_json_array,
query_record,
safe_id,
sample_by_holder,
write_jsonl,
)
DATASET = "lmms-eval/Video-MME"
SOURCE = source_for(DATASET)
REVISION = str(SOURCE["revision"])
SNAPSHOT_AT = str(SOURCE["snapshot_at"])
PARQUET = "videomme/test-00000-of-00001.parquet"
CHUNKS = ("videos_chunked_01.zip", "videos_chunked_02.zip", "videos_chunked_03.zip")
CLIPS = DATA_ROOT / "videomme"
SCENARIO = "video-qa"
def extract_clips() -> dict[str, str]:
"""Unpack the chunk archives; videoID -> path relative to DATA_ROOT."""
CLIPS.mkdir(parents=True, exist_ok=True)
mapping: dict[str, str] = {}
for chunk in CHUNKS:
archive = corpus_path(DATASET, chunk)
with zipfile.ZipFile(archive) as bundle:
for info in bundle.infolist():
if not info.filename.endswith(".mp4"):
continue
name = info.filename.rsplit("/", 1)[-1]
video_id = safe_id(name.removesuffix(".mp4"), f"{chunk} entry")
target = CLIPS / f"{video_id}.mp4"
if not target.is_file() or target.stat().st_size == 0:
with bundle.open(info) as source, target.open("wb") as sink:
while True:
block = source.read(1 << 22)
if not block:
break
sink.write(block)
mapping[video_id] = target.relative_to(DATA_ROOT).as_posix()
if not mapping:
raise SystemExit("no mp4 entries found in the Video-MME archives")
return mapping
def options_text(raw: Any, where: str) -> list[str]:
options = raw if isinstance(raw, list) else parse_json_array(str(raw), where)
return [str(option).strip() for option in options]
def domains_of(rows: list[dict[str, Any]]) -> dict[str, str]:
"""videoID -> domain, taken from the annotation table, never guessed."""
domains: dict[str, str] = {}
for row in rows:
video_id = str(row["videoID"])
domain = safe_id(
str(row["domain"]).replace(" & ", "-").replace(" ", "-"), "domain"
)
known = domains.get(video_id)
if known is not None and known != domain:
raise SystemExit(f"video {video_id} is annotated in two domains: {known}, {domain}")
domains[video_id] = domain
return domains
def build() -> tuple[list, list, dict]:
clips = extract_clips()
rows = ParquetFile(corpus_path(DATASET, PARQUET)).read().to_pylist()
domains = domains_of(rows)
held = sorted(set(domains) & set(clips))
if not held:
raise SystemExit("no Video-MME annotation row matches a clip on disk")
corpus = [
corpus_record(
source_id=f"videomme:{video_id}",
modality="video",
path=clips[video_id],
title=video_id,
observed_at=SNAPSHOT_AT,
holder=domains[video_id],
scenario=SCENARIO,
root="derived",
)
for video_id in held
]
queries: list[dict[str, Any]] = []
for row in rows:
video_id = str(row["videoID"])
if video_id not in clips:
continue
where = f"{row['question_id']}"
options = options_text(row["options"], where)
question = str(row["question"]).strip()
answer = str(row["answer"]).strip()
if not question or not answer or not options:
raise SystemExit(f"{where}: missing question, answer, or options")
queries.append(
query_record(
query_id=f"videomme:{row['question_id']}",
modality="video",
question=f"{question}\n" + "\n".join(options),
answer=answer,
answer_format="mcq",
gold=[{"source_id": f"videomme:{video_id}", "region": {}}],
holder=domains[video_id],
scenario=SCENARIO,
as_of=SNAPSHOT_AT,
origin={
"dataset": DATASET,
"revision": REVISION,
"item": f"question_id:{row['question_id']}",
"category": f"{row['domain']}/{row['task_type']}/{row['duration']}",
},
)
)
holders = sorted({record["holder"] for record in corpus})
picked = sample_by_holder(queries)
extras = []
for query in picked:
own = query["scope"]["holder"]
other = holders[(holders.index(own) + 1) % len(holders)]
extras.append(
query_record(
query_id=f"{query['query_id']}@{other}",
modality="video",
question=query["question"],
answer=None,
answer_format="mcq",
gold=[],
holder=other,
scenario=SCENARIO,
as_of=SNAPSHOT_AT,
origin=query["origin"] | {"construction": "cross-holder"},
)
)
report = {
"datasets": [DATASET],
"revision": REVISION,
"annotated_videos": len({str(row["videoID"]) for row in rows}),
"videos_on_disk": len(held),
"questions_for_held_videos": len(queries),
"holders": holders,
"queries": len(picked) + len(extras),
"queries_answerable": len(picked),
"queries_cross_holder_unanswerable": len(extras),
"by_duration": _counts(picked, "duration"),
}
return corpus, picked + extras, report
def _counts(queries: list[dict[str, Any]], field: str) -> dict[str, int]:
counts: dict[str, int] = {}
for query in queries:
parts = str(query["origin"]["category"]).split("/")
key = parts[-1] if field == "duration" else parts[0]
counts[key] = counts.get(key, 0) + 1
return counts
def main() -> None:
corpus, queries, report = build()
corpus_count, corpus_sha = write_jsonl(SETS / "corpus" / "video.jsonl", corpus)
_query_count, query_sha = write_jsonl(SETS / "video.jsonl", queries)
report |= {"corpus_records": corpus_count, "corpus_sha256": corpus_sha, "queries_sha256": query_sha}
(SETS / "reports").mkdir(parents=True, exist_ok=True)
(SETS / "reports" / "video.json").write_text(
json.dumps(report, indent=1, sort_keys=True), encoding="utf-8"
)
print(json.dumps(report, indent=1, sort_keys=True))
if __name__ == "__main__":
main()