Skip to content

Commit a1ce427

Browse files
AIwork4meclaude
andcommitted
feat(cli): hunyuan-ocr benchmark + report subcommands (P1)
benchmark: print the verified results from the lock (read-only), sharing the renderer with scripts/render_benchmark_tables.py (single source). report: assemble a benchmark release-artifact bundle (run_manifest + environment + commands + lock + checksums) per docs/release-artifact.md. Both are pure package code, CPU-tested. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f2b2e1a commit a1ce427

5 files changed

Lines changed: 255 additions & 37 deletions

File tree

scripts/render_benchmark_tables.py

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,16 @@
1919

2020
import yaml
2121

22+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
23+
from hunyuan_ocr.results import render_results_block # noqa: E402
24+
2225
REPO = Path(__file__).resolve().parents[1]
2326
LOCK_PATH = REPO / "reproducibility.lock.yaml"
2427
README_PATH = REPO / "README.md"
2528
REGION_RE = re.compile(r"<!-- BEGIN GENERATED RESULTS -->.*?<!-- END GENERATED RESULTS -->", re.DOTALL)
2629

27-
BACKEND_DISPLAY = {"llamacpp": "llama.cpp", "transformers": "transformers", "vllm": "vLLM"}
28-
29-
30-
def _fmt_value(v) -> str:
31-
if isinstance(v, str) and v.strip().lower() == "invalid":
32-
return "invalid (excluded; see reproducibility.lock.yaml)"
33-
return str(v)
34-
35-
36-
def render_block(lock: dict) -> str:
37-
"""Render the BEGIN..END block (without surrounding README text) from the lock."""
38-
bench = (lock or {}).get("benchmark", {}) or {}
39-
lines = [
40-
"<!-- BEGIN GENERATED RESULTS -->",
41-
"<!-- Auto-generated from reproducibility.lock.yaml by scripts/render_benchmark_tables.py (do not edit by hand). -->",
42-
"",
43-
"| Page set | Backend | Overall | Source |",
44-
"|---|---|---|---|",
45-
]
46-
for page_key, label in (("canary_148", "canary 148"), ("full_1651", "full 1651")):
47-
section = bench.get(page_key, {}) or {}
48-
rows = []
49-
for k, v in section.items():
50-
if not k.endswith("_overall"):
51-
continue
52-
backend = k[: -len("_overall")]
53-
rows.append((BACKEND_DISPLAY.get(backend, backend), _fmt_value(v)))
54-
for display, value in sorted(rows):
55-
lines.append(f"| {label} | {display} | {value} | reproducibility.lock.yaml |")
56-
official = bench.get("official_reference", {}) or {}
57-
if official:
58-
engine = official.get("inference_engine", "official")
59-
overall = _fmt_value(official.get("omnidocbench_overall"))
60-
lines.append(f"| official | {engine} | {overall} | official HunyuanOCR table |")
61-
lines.append("")
62-
lines.append("<!-- END GENERATED RESULTS -->")
63-
return "\n".join(lines)
30+
# Shared with the `hunyuan-ocr benchmark` CLI via hunyuan_ocr.results.
31+
render_block = render_results_block
6432

6533

6634
def current_region(readme: str) -> str | None:

src/hunyuan_ocr/cli.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
canary materialize— rebuild the 148-page canary from the full GT
1010
predict — multi-server predict via hunyuan_ocr.driver (llamacpp/vllm/openai)
1111
score — OmniDocBench scoring via hunyuan_ocr.scoring (needs the scorer venv)
12+
benchmark — print the verified results from reproducibility.lock.yaml (read-only)
13+
report — assemble a benchmark release-artifact bundle from a run_manifest.json
1214
1315
``predict --backend transformers`` is the one exception: it still delegates to the
1416
repo-only ``scripts/run_phase1_transformers.py`` driver and needs a ROCm torch.
@@ -387,6 +389,35 @@ def _clean_extra(extra):
387389
return extra
388390

389391

392+
def _benchmark(args) -> int:
393+
"""Print the verified benchmark results from reproducibility.lock.yaml (read-only)."""
394+
import yaml
395+
396+
from hunyuan_ocr.results import render_results_block
397+
398+
lock_path = Path(args.lock) if getattr(args, "lock", None) else Path.cwd() / "reproducibility.lock.yaml"
399+
if not lock_path.is_file():
400+
print(f"[error] lock not found: {lock_path}", file=sys.stderr)
401+
return 2
402+
lock = yaml.safe_load(lock_path.read_text(encoding="utf-8"))
403+
print(render_results_block(lock))
404+
return 0
405+
406+
407+
def _report(args) -> int:
408+
"""Assemble a benchmark release-artifact bundle (see docs/release-artifact.md)."""
409+
from hunyuan_ocr.report import assemble_release_artifact
410+
411+
repo_root = Path(args.repo_root) if getattr(args, "repo_root", None) else Path(__file__).resolve().parents[2]
412+
try:
413+
out = assemble_release_artifact(args.pred_dir, args.out, repo_root)
414+
except FileNotFoundError as exc:
415+
print(f"[error] {exc}", file=sys.stderr)
416+
return 2
417+
print(f"[OK] wrote release artifact -> {out} (run_manifest, environment, commands, lock, checksums)")
418+
return 0
419+
420+
390421
def build_parser() -> argparse.ArgumentParser:
391422
p = argparse.ArgumentParser(
392423
prog="hunyuan-ocr", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
@@ -437,6 +468,14 @@ def build_parser() -> argparse.ArgumentParser:
437468
sc.add_argument("--omnidocbench-repo", default=scoring.DEFAULT_OMNIDOCBENCH_REPO)
438469
sc.add_argument("--venv-python", default=scoring.DEFAULT_VENV_PYTHON)
439470
sc.add_argument("--skip-validation", action="store_true", help="DANGEROUS: bypass pre-score validation")
471+
472+
bm = sub.add_parser("benchmark", help="print the verified results from the lock (read-only)")
473+
bm.add_argument("--lock", help="path to reproducibility.lock.yaml (default: ./reproducibility.lock.yaml)")
474+
475+
rep = sub.add_parser("report", help="assemble a benchmark release-artifact bundle")
476+
rep.add_argument("--pred-dir", required=True, help="prediction dir containing run_manifest.json")
477+
rep.add_argument("--out", required=True, help="output artifact directory")
478+
rep.add_argument("--repo-root", help="repo root (to copy reproducibility.lock.yaml); default: this package's repo")
440479
return p
441480

442481

@@ -449,6 +488,8 @@ def main(argv=None) -> int:
449488
"canary": lambda a: _canary_materialize(a) if a.ccmd == "materialize" else 2,
450489
"predict": _predict,
451490
"score": _score,
491+
"benchmark": _benchmark,
492+
"report": _report,
452493
}
453494
handler = dispatch[args.cmd]
454495
rc = handler(args)

src/hunyuan_ocr/report.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 AIwork4me
3+
"""Assemble a benchmark release-artifact bundle from a run manifest + the lock.
4+
5+
See docs/release-artifact.md for the layout. This module is pure filesystem +
6+
stdlib (no GPU, no scorer); it packages the reproducibility evidence for a run:
7+
the manifest, its environment + command, the lock, and a tamper-evident checksum
8+
file. Metrics from the scorer are intentionally NOT produced here — run the
9+
scorer separately and drop its output into the bundle.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import hashlib
15+
import json
16+
from pathlib import Path
17+
18+
19+
def _sha256(path: Path) -> str:
20+
h = hashlib.sha256()
21+
h.update(path.read_bytes())
22+
return h.hexdigest()
23+
24+
25+
def assemble_release_artifact(pred_dir, out_dir, repo_root) -> Path:
26+
"""Build the bundle at ``out_dir`` from ``pred_dir/run_manifest.json`` + the
27+
repo's ``reproducibility.lock.yaml``. Returns the output directory."""
28+
pred_dir = Path(pred_dir)
29+
out_dir = Path(out_dir)
30+
out_dir.mkdir(parents=True, exist_ok=True)
31+
32+
manifest_path = pred_dir / "run_manifest.json"
33+
if not manifest_path.is_file():
34+
raise FileNotFoundError(f"no run_manifest.json in {pred_dir}")
35+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
36+
37+
# run_manifest.json (verbatim copy)
38+
(out_dir / "run_manifest.json").write_text(manifest_path.read_text(encoding="utf-8"), encoding="utf-8")
39+
40+
# environment.json (best-effort env + platform from the manifest)
41+
(out_dir / "environment.json").write_text(
42+
json.dumps({"env": manifest.get("env", {}), "platform": manifest.get("platform", {})}, indent=2),
43+
encoding="utf-8",
44+
)
45+
46+
# commands.txt (redacted argv already stored in the manifest)
47+
cmd = manifest.get("command")
48+
cmd_text = " ".join(cmd) + "\n" if isinstance(cmd, list) else str(cmd) + "\n"
49+
(out_dir / "commands.txt").write_text(cmd_text, encoding="utf-8")
50+
51+
# reproducibility.lock.yaml (copy from the repo)
52+
lock_src = Path(repo_root) / "reproducibility.lock.yaml"
53+
if lock_src.is_file():
54+
(out_dir / "reproducibility.lock.yaml").write_text(lock_src.read_text(encoding="utf-8"), encoding="utf-8")
55+
56+
# README.md describing the bundle
57+
backend = manifest.get("backend", "?")
58+
status = manifest.get("status", "?")
59+
sha = manifest.get("repo_commit") or "?"
60+
(out_dir / "README.md").write_text(
61+
"# Benchmark release artifact\n\n"
62+
f"- backend: `{backend}`\n- status: `{status}`\n- repo commit: `{sha}`\n\n"
63+
"Reproduce by checking out the pinned commits + weights in "
64+
"`reproducibility.lock.yaml`, then running the commands in `commands.txt`.\n"
65+
"Verify integrity with `sha256sum -c checksums.sha256`.\n",
66+
encoding="utf-8",
67+
)
68+
69+
# checksums.sha256 (over every other file in the bundle)
70+
lines = []
71+
for f in sorted(out_dir.iterdir()):
72+
if f.name == "checksums.sha256":
73+
continue
74+
lines.append(f"{_sha256(f)} {f.name}")
75+
(out_dir / "checksums.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
76+
return out_dir

src/hunyuan_ocr/results.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 AIwork4me
3+
"""Render the verified benchmark results from ``reproducibility.lock.yaml``.
4+
5+
Single source of truth for the headline numbers: the lock. Both the README
6+
generator (``scripts/render_benchmark_tables.py``) and the ``hunyuan-ocr
7+
benchmark`` CLI render through this module, so they can never disagree. No number
8+
is invented — only values present in the lock are emitted.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
BACKEND_DISPLAY = {"llamacpp": "llama.cpp", "transformers": "transformers", "vllm": "vLLM"}
14+
15+
16+
def _fmt_value(v) -> str:
17+
if isinstance(v, str) and v.strip().lower() == "invalid":
18+
return "invalid (excluded; see reproducibility.lock.yaml)"
19+
return str(v)
20+
21+
22+
def render_results_block(lock: dict) -> str:
23+
"""Render the BEGIN..END GENERATED RESULTS block (markdown) from the lock."""
24+
bench = (lock or {}).get("benchmark", {}) or {}
25+
lines = [
26+
"<!-- BEGIN GENERATED RESULTS -->",
27+
"<!-- Auto-generated from reproducibility.lock.yaml by scripts/render_benchmark_tables.py (do not edit by hand). -->",
28+
"",
29+
"| Page set | Backend | Overall | Source |",
30+
"|---|---|---|---|",
31+
]
32+
for page_key, label in (("canary_148", "canary 148"), ("full_1651", "full 1651")):
33+
section = bench.get(page_key, {}) or {}
34+
rows = []
35+
for k, v in section.items():
36+
if not k.endswith("_overall"):
37+
continue
38+
backend = k[: -len("_overall")]
39+
rows.append((BACKEND_DISPLAY.get(backend, backend), _fmt_value(v)))
40+
for display, value in sorted(rows):
41+
lines.append(f"| {label} | {display} | {value} | reproducibility.lock.yaml |")
42+
official = bench.get("official_reference", {}) or {}
43+
if official:
44+
engine = official.get("inference_engine", "official")
45+
overall = _fmt_value(official.get("omnidocbench_overall"))
46+
lines.append(f"| official | {engine} | {overall} | official HunyuanOCR table |")
47+
lines.append("")
48+
lines.append("<!-- END GENERATED RESULTS -->")
49+
return "\n".join(lines)

tests/test_cli_extras.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 AIwork4me
3+
"""CPU tests for the hunyuan-ocr benchmark + report subcommands (no network/GPU)."""
4+
5+
from __future__ import annotations
6+
7+
import hashlib
8+
import json
9+
from pathlib import Path
10+
11+
from hunyuan_ocr import cli
12+
13+
14+
# --- benchmark ---------------------------------------------------------------
15+
16+
17+
def test_benchmark_prints_lock_results(tmp_path, capsys):
18+
lock = tmp_path / "reproducibility.lock.yaml"
19+
lock.write_text(
20+
"benchmark:\n canary_148:\n vllm_overall: 94.81\n llamacpp_overall: 93.33\n",
21+
encoding="utf-8",
22+
)
23+
rc = cli.main(["benchmark", "--lock", str(lock)])
24+
assert rc == 0
25+
out = capsys.readouterr().out
26+
assert "94.81" in out and "93.33" in out and "BEGIN GENERATED RESULTS" in out
27+
28+
29+
def test_benchmark_missing_lock(tmp_path):
30+
rc = cli.main(["benchmark", "--lock", str(tmp_path / "nope.yaml")])
31+
assert rc == 2
32+
33+
34+
# --- report ------------------------------------------------------------------
35+
36+
37+
def _write_manifest(pred_dir: Path):
38+
manifest = {
39+
"schema_version": 2,
40+
"repo_commit": "abc123",
41+
"backend": "llamacpp",
42+
"model": "HYVL",
43+
"timestamp_iso": "2026-07-18T03:00:00Z",
44+
"status": "ok",
45+
"run_counts": {"attempted": 1, "succeeded": 1, "failed": 0, "skipped": 0, "interrupted": 0},
46+
"final_state": {"expected": 1, "complete": 1, "failed": 0, "pending": 0},
47+
"command": ["run_inference.py", "--backend-name", "llamacpp"],
48+
"env": {"torch": "2.9.1"},
49+
"platform": {"python": "3.12.3"},
50+
}
51+
pred_dir.mkdir(parents=True, exist_ok=True)
52+
(pred_dir / "run_manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
53+
return manifest
54+
55+
56+
def test_report_assembles_bundle_with_checksums(tmp_path):
57+
pred = tmp_path / "pred"
58+
_write_manifest(pred)
59+
repo_root = tmp_path / "repo"
60+
repo_root.mkdir()
61+
(repo_root / "reproducibility.lock.yaml").write_text("hunyuanocr_rocm:\n commit: x\n", encoding="utf-8")
62+
out = tmp_path / "artifact"
63+
64+
rc = cli.main(["report", "--pred-dir", str(pred), "--out", str(out), "--repo-root", str(repo_root)])
65+
assert rc == 0
66+
67+
assert (out / "run_manifest.json").is_file()
68+
assert (out / "environment.json").is_file()
69+
assert (out / "commands.txt").is_file()
70+
assert (out / "reproducibility.lock.yaml").is_file()
71+
assert (out / "README.md").is_file()
72+
# checksums cover every other file and verify
73+
sums = (out / "checksums.sha256").read_text(encoding="utf-8").strip().splitlines()
74+
assert len(sums) == 5
75+
for line in sums:
76+
digest, name = line.split(" ", 1)
77+
assert hashlib.sha256((out / name).read_bytes()).hexdigest() == digest
78+
79+
80+
def test_report_missing_manifest(tmp_path):
81+
pred = tmp_path / "empty"
82+
pred.mkdir()
83+
rc = cli.main(["report", "--pred-dir", str(pred), "--out", str(tmp_path / "o"), "--repo-root", str(tmp_path)])
84+
assert rc == 2

0 commit comments

Comments
 (0)