Round four of the independent review found seven record and coverage
defects. The functional side it verified by running; these are the fixes.
- `_tracked` asked the current checkout's index, so a reviewer in a worktree
pointing at the main checkout's committed report was told it was untracked.
It now asks the repository that owns the path, and the guard no longer
compares directories: any tracked file is committed evidence.
- `retrieval.py` claimed the dense index "catches up inside search". It does
not: `_stale` is only used by `sync()` and `stats()`, so a store upgraded
from v11 ranks an empty dense leg until `ely-eye build-dense-index` runs.
The docstring now states that, and `doctor` already names it.
- Four wirings had no discriminating test, each proven by mutation:
a stubbed `_tracked` (now a test uses the real git index), `copy_store`
claiming the scratch (now a live holder blocks the copy before a byte is
copied), `_stale` reading the atoms table (now a closed memory that loses
its vector is owed one), and the v10/v11 to v12 `DROP VIEW` gate (now a
hand-made v11 store is upgraded and its view body re-checked).
- The record corrected against the reviewer's measurements: the five
mutations did not each turn one test red (M1 turned two, M3 turned two,
`_stale` alone turned none), the pyright/mypy file set was wrong, and the
round added seven tests rather than three.
Gates: 422 passed; ruff clean; filesize clean; audit clean; mypy 24 errors in
9 files repo-wide, equal to the 9cb46c1 baseline; pyright 0 on the nine
touched files; zero whole-line comment blocks over 157 tracked files.
325 lines
14 KiB
Python
325 lines
14 KiB
Python
"""What speech is worth on the video questions: the same store and probes, with audio added.
|
|
|
|
The frozen stores were ingested before ASR landed, so every video answers from frames and OCR
|
|
alone and video recall@10 sits at 0.455 while code sits at 0.945. This harness copies the dense
|
|
store, re-ingests the same Video-MME files with a speech snapshot wired in, and ranks the same
|
|
probes before and after. The only thing that moved between the two rows is the speech.
|
|
|
|
Run: python -m bench.video_audio_check
|
|
Writes: bench/reports/VIDEO_AUDIO.md, bench/reports/video_audio.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from bench.leg_check import SHIPPED_TOP_K, _groups, _source_ids, rank_all
|
|
from bench.metrics import aggregate, score_query
|
|
from bench.query_set import load_queries, scale_probes, source_times
|
|
from bench.report import git_commit
|
|
from bench.run import latency
|
|
from bench.scratch import copy_store
|
|
from bench.sets_common import DATA_ROOT
|
|
from bench.systems import machine_facts
|
|
|
|
DEFAULT_HOME = Path("bench/runs/dense_scale")
|
|
DEFAULT_SCRATCH = Path("bench/runs/video_audio")
|
|
DEFAULT_ASR = Path("bench/models/faster-whisper-tiny")
|
|
DEFAULT_PROBES = 300
|
|
VIDEO_MODALITY = "video"
|
|
REPORT = Path("bench/reports/VIDEO_AUDIO.md")
|
|
PAYLOAD = Path("bench/reports/video_audio.json")
|
|
|
|
|
|
def video_queries() -> list[Any]:
|
|
"""Every answerable video question in the frozen suite, measured whole rather than sampled."""
|
|
queries = [query for query in load_queries() if query.modality == VIDEO_MODALITY and query.gold]
|
|
if not queries:
|
|
raise SystemExit("the frozen suite carries no answerable video query; there would be nothing to score")
|
|
return queries
|
|
|
|
|
|
def guard_videos(videos: Path, store: Path) -> list[Path]:
|
|
"""The files the store already holds, so re-ingesting adds speech instead of a second copy."""
|
|
import sqlite3
|
|
|
|
if not videos.is_dir():
|
|
raise SystemExit(
|
|
f"{videos}: the Video-MME files are not here; they are staged from the corpus zips at "
|
|
"the data root (set ELY_EYE_BENCH_DATA to wherever they live)"
|
|
)
|
|
files = sorted(videos.glob("*.mp4"))
|
|
if not files:
|
|
raise SystemExit(f"{videos}: no .mp4 files to re-ingest")
|
|
with sqlite3.connect(f"file:{store / 'data' / 'ely_eye.sqlite'}?mode=ro", uri=True) as connection:
|
|
stored = {row[0] for row in connection.execute("SELECT source_id FROM sources WHERE kind = 'video'")}
|
|
if not stored:
|
|
raise SystemExit(f"{store}: the store holds no video sources, so speech would have nothing to join to")
|
|
return files
|
|
|
|
|
|
def speech_facts(store: Path) -> dict[str, Any]:
|
|
"""How much speech the store carries, and how many words of it."""
|
|
import sqlite3
|
|
|
|
with sqlite3.connect(f"file:{store / 'data' / 'ely_eye.sqlite'}?mode=ro", uri=True) as connection:
|
|
atoms, characters = connection.execute(
|
|
"SELECT count(*), coalesce(sum(length(text)), 0) FROM atoms WHERE modality = 'audio'"
|
|
).fetchone()
|
|
return {"atoms": int(atoms), "characters": int(characters)}
|
|
|
|
|
|
def rank_arm(home: Path, probes: list[Any], top_k: int) -> dict[str, Any] | None:
|
|
"""Rank the probes through the shipped words-only path and score them; None skips the arm."""
|
|
from ely_eye.config import Settings
|
|
from ely_eye.db import Database
|
|
from ely_eye.retrieval import RetrievalService
|
|
|
|
if not probes:
|
|
return None
|
|
settings = Settings(
|
|
home=home,
|
|
dense_embeddings_enabled=False,
|
|
embedding_model_dir=None,
|
|
clip_model_dir=None,
|
|
)
|
|
if settings.retrieval_top_k != SHIPPED_TOP_K:
|
|
raise SystemExit(
|
|
f"the product asks for {settings.retrieval_top_k} atoms per search but this harness "
|
|
f"labels its row 'as shipped' at {SHIPPED_TOP_K}; update the label before trusting it"
|
|
)
|
|
database = Database(settings)
|
|
service = RetrievalService(settings=settings, database=database)
|
|
ids = _source_ids(database)
|
|
rankings, distinct, timings = rank_all(service, ids, probes, top_k)
|
|
scores = [
|
|
score_query(query.query_id, ranking, set(query.gold), query.holder)
|
|
for query, ranking in zip(probes, rankings, strict=True)
|
|
]
|
|
summary = aggregate(scores)
|
|
timing = latency(timings)
|
|
return {
|
|
"queries": len(probes),
|
|
"top_k": top_k,
|
|
"recall_at_10": summary["recall_at_10"],
|
|
"mrr": summary["mrr"],
|
|
"hit_at_10": summary["hit_at_10"],
|
|
"wilson_lower": summary["hit_at_10_wilson_lower"],
|
|
"p50_seconds": timing["p50"],
|
|
"p95_seconds": timing["p95"],
|
|
"median_distinct_sources": statistics.median(distinct),
|
|
**_groups(probes, scores, rankings, source_times()),
|
|
}
|
|
|
|
|
|
def add_speech(home: Path, files: list[Path], asr: Path, limit: int) -> dict[str, Any]:
|
|
"""Re-ingest the same files with the speech snapshot, and time the whole pass."""
|
|
from ely_eye.config import Settings
|
|
from ely_eye.ingestion import IngestionService
|
|
|
|
if not (asr / "model.bin").exists():
|
|
raise SystemExit(
|
|
f"{asr}: no speech snapshot here; the ingest would defer every video and the after row "
|
|
"would silently equal the before row"
|
|
)
|
|
if limit:
|
|
files = files[:limit]
|
|
settings = Settings(home=home, asr_model_dir=asr)
|
|
service = IngestionService(settings)
|
|
began = time.perf_counter()
|
|
batches = 0
|
|
for index, path in enumerate(files, start=1):
|
|
service.ingest_path(path)
|
|
batches += 1
|
|
if index % 10 == 0:
|
|
print(f" ingested {index}/{len(files)}", flush=True)
|
|
return {"files": batches, "seconds": time.perf_counter() - began}
|
|
|
|
|
|
def probe_sample(probes: int, limit: int) -> list[Any]:
|
|
"""The standard stratified sample, asked about video only when the smoke knob is on.
|
|
|
|
A small `--probes` is not "measure a little": the sample strides the whole suite, so a
|
|
count under a few dozen can miss this modality entirely and the arm would rank nothing.
|
|
"""
|
|
if probes <= 0:
|
|
return []
|
|
picked = [query for query in scale_probes(load_queries(), probes) if query.gold]
|
|
if limit:
|
|
picked = [query for query in picked if query.modality == VIDEO_MODALITY][:limit]
|
|
if not picked:
|
|
raise SystemExit(
|
|
f"--probes {probes} picked no answerable video query: the sample strides the whole "
|
|
"suite, so a small count can skip this modality and the arm would rank nothing. "
|
|
"Pass --probes 0 to skip the probe arm, or the shipped 300 to keep every modality."
|
|
)
|
|
return picked
|
|
|
|
|
|
def measure(home: Path, scratch: Path, videos: Path, asr: Path, probes: int, limit: int) -> dict[str, Any]:
|
|
"""Score the video questions and the standard probe sample before and after the speech pass."""
|
|
if not (home / "data" / "ely_eye.sqlite").exists():
|
|
raise SystemExit(f"{home}: no store to copy; run `python -m bench.dense_scale` first")
|
|
files = guard_videos(videos, home)
|
|
whole = video_queries() if not limit else video_queries()[:limit]
|
|
sample = probe_sample(probes, limit)
|
|
print(f"{len(whole)} answerable video queries, {len(sample)} stratified probes, {len(files)} files", flush=True)
|
|
|
|
copy_home, copied = copy_store(home, scratch)
|
|
print(f"copied the store in {copied:.1f}s to {copy_home}", flush=True)
|
|
before_speech = speech_facts(copy_home)
|
|
if before_speech["atoms"]:
|
|
print(f"note: the copy already carries {before_speech['atoms']} speech atoms", flush=True)
|
|
before_video = _required(rank_arm(copy_home, whole, SHIPPED_TOP_K), "every video question")
|
|
before_sample = rank_arm(copy_home, sample, SHIPPED_TOP_K)
|
|
print(
|
|
f" before: video recall@10 {before_video['recall_at_10']:.3f}, "
|
|
f"probes {_score_of(before_sample)}",
|
|
flush=True,
|
|
)
|
|
|
|
ingest = add_speech(copy_home, files, asr, limit)
|
|
after_speech = speech_facts(copy_home)
|
|
if after_speech["atoms"] == before_speech["atoms"]:
|
|
raise SystemExit(
|
|
f"re-ingesting {len(files)} files added no speech atoms; the snapshot at {asr} is "
|
|
"available but produced nothing, so the after row would be a measurement of nothing"
|
|
)
|
|
after_video = _required(rank_arm(copy_home, whole, SHIPPED_TOP_K), "every video question")
|
|
after_sample = rank_arm(copy_home, sample, SHIPPED_TOP_K)
|
|
print(
|
|
f" after: video recall@10 {after_video['recall_at_10']:.3f}, "
|
|
f"probes {_score_of(after_sample)}, speech atoms {after_speech['atoms']}",
|
|
flush=True,
|
|
)
|
|
return {
|
|
"commit": git_commit(),
|
|
"machine": machine_facts(),
|
|
"source_store": str(home),
|
|
"videos": str(videos),
|
|
"files": len(files),
|
|
"asr_model": str(asr),
|
|
"ingest": ingest,
|
|
"speech_before": before_speech,
|
|
"speech_after": after_speech,
|
|
"before": {"video": before_video, "probes": before_sample},
|
|
"after": {"video": after_video, "probes": after_sample},
|
|
}
|
|
|
|
|
|
def _required(row: dict[str, Any] | None, label: str) -> dict[str, Any]:
|
|
"""The video arm is never empty; a row of zeros there would be a silent lie."""
|
|
if row is None:
|
|
raise SystemExit(f"{label}: nothing to rank, so this run would print a row of zeros")
|
|
return row
|
|
|
|
|
|
def _score_of(row: dict[str, Any] | None) -> str:
|
|
"""One number for the progress line, or the reason there is none."""
|
|
return "skipped" if row is None else f"{row['recall_at_10']:.3f}"
|
|
|
|
|
|
def write_markdown(report: dict[str, Any], destination: Path) -> None:
|
|
"""Print the two rows beside each other, with the load they were measured under."""
|
|
load = report["machine"].get("load_average_1_5_15")
|
|
lines = [
|
|
"# What speech is worth on the video questions",
|
|
"",
|
|
f"- commit: `{report['commit']}`",
|
|
f"- store measured: a copy of `{report['source_store']}`; the source is never opened for write",
|
|
f"- machine: {report['machine'].get('cpus')} cpus, load {load}",
|
|
f"- speech snapshot: `{report['asr_model']}` (the shipped default; its word error rate is in ASR.md)",
|
|
f"- re-ingest: {report['ingest']['files']} files in {report['ingest']['seconds']:.0f}s",
|
|
(
|
|
f"- speech atoms: {report['speech_before']['atoms']} before → {report['speech_after']['atoms']} after"
|
|
f" ({report['speech_after']['characters']} characters)"
|
|
),
|
|
"",
|
|
"The only difference between the rows is the speech: same store, same files, same probes, same words-only path.",
|
|
"",
|
|
"| asked | n | recall@10 | MRR | hit@10 | 95% lower | p50 s |",
|
|
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
]
|
|
for stage in ("before", "after"):
|
|
for arm, label in (("video", "every video question"), ("probes", "the stratified probe sample")):
|
|
row = report[stage][arm]
|
|
if row is None:
|
|
lines.append(f"| {label} ({stage}) | 0 | skipped (--probes 0) | — | — | — | — |")
|
|
continue
|
|
lines.append(
|
|
f"| {label} ({stage}) | {row['queries']} | {row['recall_at_10']:.3f} | {row['mrr']:.3f} | "
|
|
f"{row['hit_at_10']:.3f} | {row['wilson_lower']:.3f} | {row['p50_seconds']:.3f} |"
|
|
)
|
|
gained = report["after"]["video"]["recall_at_10"] - report["before"]["video"]["recall_at_10"]
|
|
probe_row = report["after"]["probes"]
|
|
overall = "not measured (--probes 0)" if probe_row is None else f"{probe_row['recall_at_10'] - report['before']['probes']['recall_at_10']:+.3f}"
|
|
lines += [
|
|
"",
|
|
f"Speech moved video recall@10 by {gained:+.3f} and the whole probe sample by {overall}.",
|
|
"",
|
|
"Latency is read from the same pass and moves with machine load; the recall columns do not.",
|
|
"",
|
|
]
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
|
def _tracked(path: Path) -> bool:
|
|
"""Only committed evidence is frozen; a harness's own untracked output is fair game.
|
|
|
|
The question goes to the repository that owns the path, so a reviewer working in a worktree
|
|
cannot point at another checkout's committed report and be told it is untracked.
|
|
"""
|
|
owner = path.resolve().parent
|
|
return (
|
|
subprocess.run(
|
|
["git", "-C", str(owner), "ls-files", "--error-unmatch", path.resolve().name],
|
|
capture_output=True,
|
|
check=False,
|
|
).returncode
|
|
== 0
|
|
)
|
|
|
|
|
|
def refuse_frozen_overwrite(destination: Path) -> None:
|
|
"""A reviewer running this in a worktree of its own must not overwrite the committed evidence."""
|
|
if destination.exists() and _tracked(destination):
|
|
raise SystemExit(
|
|
f"{destination} is a frozen report; point --report and --payload somewhere else, or "
|
|
"delete the frozen copy on purpose rather than under a harness"
|
|
)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--home", type=Path, default=DEFAULT_HOME, help="store to copy")
|
|
parser.add_argument("--scratch", type=Path, default=DEFAULT_SCRATCH, help="where the copy lives")
|
|
parser.add_argument("--videos", type=Path, default=DATA_ROOT / "videomme", help="where the mp4s are staged")
|
|
parser.add_argument("--asr", type=Path, default=DEFAULT_ASR, help="speech snapshot")
|
|
parser.add_argument("--probes", type=int, default=DEFAULT_PROBES, help="probe sample size")
|
|
parser.add_argument("--limit", type=int, default=0, help="smoke knob: only this many files and queries")
|
|
parser.add_argument("--report", type=Path, default=REPORT, help="markdown destination")
|
|
parser.add_argument("--payload", type=Path, default=PAYLOAD, help="json destination")
|
|
arguments = parser.parse_args(argv)
|
|
for destination in (arguments.report, arguments.payload):
|
|
refuse_frozen_overwrite(destination)
|
|
report = measure(
|
|
arguments.home, arguments.scratch, arguments.videos, arguments.asr, arguments.probes, arguments.limit
|
|
)
|
|
arguments.payload.parent.mkdir(parents=True, exist_ok=True)
|
|
arguments.payload.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
write_markdown(report, arguments.report)
|
|
print(f"wrote {arguments.report}", flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|