rpx_benchmark.vqa.evaluate

Run any callable VQA model with the published RPX prompts and scoring.

 1"""Run any callable VQA model with the published RPX prompts and scoring."""
 2from __future__ import annotations
 3
 4import hashlib
 5import json
 6import time
 7from dataclasses import asdict
 8from pathlib import Path
 9from typing import Callable
10
11from ..exceptions import AdapterError, ConfigError, ManifestError
12from .contract import load_manifest
13from .hub_rgb import fetch_images
14from .metrics import score_predictions
15from .outputs import ParsedOutput, parse_output
16from .prompts import build_prompt
17
18
19def evaluate_vqa(
20    manifest_path: str | Path,
21    predict: Callable[[tuple[Path, ...], str, int, str], str],
22    *,
23    model_name: str,
24    output_dir: str | Path,
25    image_cache: str | Path,
26    model_revision: str,
27) -> dict:
28    """Evaluate a local model or API callable, without changing the model roster.
29
30    ``predict(image_paths, prompt, max_new_tokens, output_kind)`` returns raw
31    text. Only input images and the public prompt reach the callable; answers,
32    evidence and target boxes remain inside the evaluator. Two-image requests
33    preserve reference-then-target order and use the verified reference crop.
34
35    The generic bbox contract is one JSON object with ``label`` and ``bbox``
36    (XYXY, normalized 0–1000). Binary questions require ``yes`` or ``no``.
37    Malformed outputs and inference exceptions score as failures and remain
38    in the denominator. Dataset download errors abort rather than dropping rows.
39    Use a fresh output directory for each run; no silent overwrites or resume.
40    """
41    if not model_name.strip() or not model_revision.strip():
42        raise ConfigError("model_name and model_revision must identify the evaluated model")
43    manifest_path = Path(manifest_path)
44    samples = load_manifest(manifest_path)
45    if not samples:
46        raise ManifestError("VQA manifest contains no samples")
47    output = Path(output_dir)
48    if output.exists() and any(output.iterdir()):
49        raise ConfigError(f"Use an empty output directory: {output}")
50    output.mkdir(parents=True, exist_ok=True)
51    config = {
52        "model": model_name,
53        "model_revision": model_revision,
54        "manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
55        "num_samples": len(samples),
56        "protocol": "generic_single_call",
57        "timing": "end_to_end_callable_wall_ms_including_API_transport",
58    }
59    (output / "run_config.json").write_text(json.dumps(config, indent=2) + "\n")
60    scored = []
61    with (output / "predictions.jsonl").open("w", encoding="utf-8") as handle:
62        for sample in samples:
63            images = fetch_images(sample, image_cache)
64            prompt = build_prompt(sample, "custom")
65            started = time.perf_counter()
66            raw = None
67            error = None
68            try:
69                raw = predict(images, prompt.text, prompt.max_new_tokens, prompt.output_kind)
70                if not isinstance(raw, str):
71                    raise AdapterError("The VQA callable must return raw text")
72                parsed = parse_output(sample, raw, "custom")
73            except Exception as exc:
74                error = f"{type(exc).__name__}: {exc}"
75                parsed = ParsedOutput(False, error=error)
76            elapsed_ms = (time.perf_counter() - started) * 1000
77            scored.append((sample, parsed))
78            row = {
79                "sample_id": sample.sample_id,
80                "raw_output": raw if isinstance(raw, str) else None,
81                "parsed": asdict(parsed),
82                "inference_error": error,
83                "callable_wall_ms": elapsed_ms,
84            }
85            handle.write(json.dumps(row, allow_nan=False) + "\n")
86            handle.flush()
87    report = {**config, "metrics": score_predictions(scored)}
88    (output / "result.json").write_text(json.dumps(report, indent=2, allow_nan=False) + "\n")
89    return report
def evaluate_vqa( manifest_path: str | pathlib.Path, predict: Callable[[tuple[pathlib.Path, ...], str, int, str], str], *, model_name: str, output_dir: str | pathlib.Path, image_cache: str | pathlib.Path, model_revision: str) -> dict:
20def evaluate_vqa(
21    manifest_path: str | Path,
22    predict: Callable[[tuple[Path, ...], str, int, str], str],
23    *,
24    model_name: str,
25    output_dir: str | Path,
26    image_cache: str | Path,
27    model_revision: str,
28) -> dict:
29    """Evaluate a local model or API callable, without changing the model roster.
30
31    ``predict(image_paths, prompt, max_new_tokens, output_kind)`` returns raw
32    text. Only input images and the public prompt reach the callable; answers,
33    evidence and target boxes remain inside the evaluator. Two-image requests
34    preserve reference-then-target order and use the verified reference crop.
35
36    The generic bbox contract is one JSON object with ``label`` and ``bbox``
37    (XYXY, normalized 0–1000). Binary questions require ``yes`` or ``no``.
38    Malformed outputs and inference exceptions score as failures and remain
39    in the denominator. Dataset download errors abort rather than dropping rows.
40    Use a fresh output directory for each run; no silent overwrites or resume.
41    """
42    if not model_name.strip() or not model_revision.strip():
43        raise ConfigError("model_name and model_revision must identify the evaluated model")
44    manifest_path = Path(manifest_path)
45    samples = load_manifest(manifest_path)
46    if not samples:
47        raise ManifestError("VQA manifest contains no samples")
48    output = Path(output_dir)
49    if output.exists() and any(output.iterdir()):
50        raise ConfigError(f"Use an empty output directory: {output}")
51    output.mkdir(parents=True, exist_ok=True)
52    config = {
53        "model": model_name,
54        "model_revision": model_revision,
55        "manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
56        "num_samples": len(samples),
57        "protocol": "generic_single_call",
58        "timing": "end_to_end_callable_wall_ms_including_API_transport",
59    }
60    (output / "run_config.json").write_text(json.dumps(config, indent=2) + "\n")
61    scored = []
62    with (output / "predictions.jsonl").open("w", encoding="utf-8") as handle:
63        for sample in samples:
64            images = fetch_images(sample, image_cache)
65            prompt = build_prompt(sample, "custom")
66            started = time.perf_counter()
67            raw = None
68            error = None
69            try:
70                raw = predict(images, prompt.text, prompt.max_new_tokens, prompt.output_kind)
71                if not isinstance(raw, str):
72                    raise AdapterError("The VQA callable must return raw text")
73                parsed = parse_output(sample, raw, "custom")
74            except Exception as exc:
75                error = f"{type(exc).__name__}: {exc}"
76                parsed = ParsedOutput(False, error=error)
77            elapsed_ms = (time.perf_counter() - started) * 1000
78            scored.append((sample, parsed))
79            row = {
80                "sample_id": sample.sample_id,
81                "raw_output": raw if isinstance(raw, str) else None,
82                "parsed": asdict(parsed),
83                "inference_error": error,
84                "callable_wall_ms": elapsed_ms,
85            }
86            handle.write(json.dumps(row, allow_nan=False) + "\n")
87            handle.flush()
88    report = {**config, "metrics": score_predictions(scored)}
89    (output / "result.json").write_text(json.dumps(report, indent=2, allow_nan=False) + "\n")
90    return report

Evaluate a local model or API callable, without changing the model roster.

predict(image_paths, prompt, max_new_tokens, output_kind) returns raw text. Only input images and the public prompt reach the callable; answers, evidence and target boxes remain inside the evaluator. Two-image requests preserve reference-then-target order and use the verified reference crop.

The generic bbox contract is one JSON object with label and bbox (XYXY, normalized 0–1000). Binary questions require yes or no. Malformed outputs and inference exceptions score as failures and remain in the denominator. Dataset download errors abort rather than dropping rows. Use a fresh output directory for each run; no silent overwrites or resume.