-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.py
More file actions
362 lines (319 loc) · 13.4 KB
/
Copy pathrun.py
File metadata and controls
362 lines (319 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
"""Run the official ParseBench benchmark against Warp-Ingest + local baselines.
This drives the *real* LlamaIndex ParseBench framework
(https://github.com/run-llama/ParseBench) in-process: it registers the
``warp_ingest`` provider/pipeline (defined in this repo), points the built-in
``liteparse`` provider at the PATH ``lit`` binary, then runs ParseBench's own
inference → deterministic rule-based evaluation → report pipeline for each
parser. Nothing about ParseBench's scoring is reimplemented here, so the
numbers are directly comparable to the published leaderboard.
Prereqforms (one-time): ``benchmarks/parsebench/setup_parsebench.sh`` installs
the framework + the local parsers it compares against.
Usage:
python -m benchmarks.parsebench.run --test
python -m benchmarks.parsebench.run --test --pipelines warp_ingest,liteparse_markdown
python -m benchmarks.parsebench.run --full --group table
"""
from __future__ import annotations
import argparse
import json
import multiprocessing
import os
import shutil
import sys
from pathlib import Path
# The warp provider/pipeline/layout-adapter registrations below live in *this*
# process, and ParseBench's evaluation ProcessPoolExecutor workers must inherit
# them or every layout example fails with "no provider adapter matched"
# (Visual Grounding silently collapses to ~10 while the run still exits 0).
# Python 3.14 changed the Linux default start method from fork to forkserver,
# whose fresh worker processes lose the registrations — pin fork explicitly.
if sys.platform.startswith("linux"):
multiprocessing.set_start_method("fork", force=True)
# Default comparison set: faithful Warp-Ingest + the four local-library
# baselines that also appear on the official leaderboard.
DEFAULT_PIPELINES = [
"warp_ingest_faithful",
"liteparse_markdown",
"markitdown",
"pymupdf_text",
"pypdf_baseline",
]
# Per-category headline metric (matches ParseBench's leaderboard defaults), as
# stored in each category's _evaluation_report.json aggregate_metrics, prefixed
# with "avg_". Values are 0..1; the leaderboard reports them ×100.
HEADLINE_METRIC = {
"table": "avg_grits_trm_composite",
"chart": "avg_rule_pass_rate",
"text_content": "avg_content_faithfulness",
"text_formatting": "avg_semantic_formatting",
"layout": "avg_layout_element_rule_pass_rate",
}
# Display order / names mirroring the official leaderboard columns.
DIMENSIONS = [
("table", "Tables"),
("chart", "Charts"),
("text_content", "Content Faith."),
("text_formatting", "Sem. Format."),
("layout", "Visual Ground."),
]
# Published leaderboard baselines (full dataset) for context — source:
# run-llama/ParseBench/leaderboard.csv. Overall, Tables, Charts, ContentFaith,
# SemFormat, VisualGround.
LEADERBOARD_BASELINES = {
"LiteParse (no OCR)": [32.8, 40.3, 3.4, 68.6, 44.6, 10.7],
"PyMuPDF4LLM": [30.88, 36.68, 1.58, 60.85, 44.63, 10.68],
"MarkItDown": [18.63, 15.77, 2.02, 64.54, 0.91, 9.90],
"PyMuPDF (Text)": [16.02, 0.00, 0.00, 68.28, 0.95, 10.86],
"pypdf": [14.87, 0.00, 0.00, 62.50, 0.91, 10.92],
"Docling-models (VLM)": [50.65, 66.41, 52.76, 66.93, 1.03, 66.11],
}
def _repo_root() -> Path:
# benchmarks/parsebench/run.py -> repo root is two parents up.
return Path(__file__).resolve().parents[2]
def _ensure_importable() -> None:
root = str(_repo_root())
if root not in sys.path:
sys.path.insert(0, root)
try:
import parse_bench # noqa: F401
except ImportError:
sys.exit(
"ERROR: parse_bench is not installed.\n"
"Run benchmarks/parsebench/setup_parsebench.sh first "
"(installs the official ParseBench framework + local parsers)."
)
def _register_warp() -> None:
"""Import the warp provider (self-registers) and register local pipelines."""
from parse_bench.inference.pipelines import get_pipeline, register_pipeline
from parse_bench.schemas.pipeline import PipelineSpec
from parse_bench.schemas.product import ProductType
import benchmarks.parsebench.warp_ingest_provider # noqa: F401 (registers provider)
import benchmarks.parsebench.warp_layout_adapter # noqa: F401 (registers layout adapter)
for pipeline_name in ("warp_ingest", "warp_ingest_faithful", "warp_ingest_quality"):
try:
get_pipeline(pipeline_name)
except ValueError:
register_pipeline(
PipelineSpec(
pipeline_name=pipeline_name,
provider_name="warp_ingest",
product_type=ProductType.PARSE,
config={},
)
)
def _configure_renderer(mode: str) -> dict[str, str]:
"""Set auditable renderer env vars before importing the Warp provider."""
keys = [
"WARP_TABLE_PROVIDER",
"WARP_HF_STRIP",
"WARP_CJK_STRIP",
"WARP_COL_REORDER",
]
if mode == "env":
return {k: os.environ.get(k, "<unset>") for k in keys}
if mode == "faithful":
# "native" is warp's OWN license-clean table engine
# (warp_ingest.ingestor.table_engine, pure MIT stack) — all output is
# still produced by this repo's code, no external parser, so the run
# stays a faithful Warp-only score.
desired = {
"WARP_TABLE_PROVIDER": "native",
"WARP_HF_STRIP": "0",
"WARP_CJK_STRIP": "0",
"WARP_COL_REORDER": "0",
}
elif mode == "quality":
desired = {
"WARP_TABLE_PROVIDER": "native",
"WARP_HF_STRIP": "1",
"WARP_CJK_STRIP": "1",
"WARP_COL_REORDER": "0",
}
else:
raise ValueError(f"unknown renderer mode: {mode}")
os.environ.update(desired)
return {k: os.environ[k] for k in keys}
def _patch_liteparse_binary() -> str | None:
"""Point ParseBench's liteparse provider at the PATH ``lit`` binary.
The upstream provider hardcodes a Rust workspace build path; the pip
``liteparse`` wheel ships the same engine as the ``lit`` console script, so
we redirect to it (same engine, same flags) when available.
"""
lit = shutil.which("lit")
if not lit:
return None
import parse_bench.inference.providers.parse.liteparse as lp_mod
lp_mod._LIT_BIN = Path(lit)
return lit
def _read_headline(output_dir: Path, pipeline: str) -> dict[str, float | None]:
"""Read each dimension's headline score (0..100) for one pipeline."""
scores: dict[str, float | None] = {}
pdir = output_dir / pipeline
for cat, _label in DIMENSIONS:
report = pdir / cat / "_evaluation_report.json"
val: float | None = None
if report.exists():
try:
data = json.loads(report.read_text())
agg = data.get("aggregate_metrics", {})
raw = agg.get(HEADLINE_METRIC[cat])
if raw is not None:
val = round(float(raw) * 100, 2)
except Exception:
val = None
scores[cat] = val
return scores
def _print_comparison(
output_dir: Path, pipelines: list[str], test: bool = True
) -> None:
rows: dict[str, dict[str, float | None]] = {
p: _read_headline(output_dir, p) for p in pipelines
}
name_w = max([len("Pipeline")] + [len(p) for p in pipelines]) + 2
header = f"{'Pipeline':<{name_w}}" + "".join(f"{lbl:>16}" for _c, lbl in DIMENSIONS)
sep = "-" * len(header)
print("\n" + "=" * len(header))
print(" ParseBench results (this run) — deterministic rule-based scores, 0–100")
print("=" * len(header))
print(header)
print(sep)
for p in pipelines:
sc = rows[p]
cells = "".join(
f"{(f'{sc[c]:.1f}' if sc[c] is not None else '—'):>16}"
for c, _l in DIMENSIONS
)
print(f"{p:<{name_w}}{cells}")
print(sep)
print(
"\n Published full-dataset leaderboard baselines (context only — not this run):"
)
print(f" {'Provider':<24}" + "".join(f"{lbl:>16}" for _c, lbl in DIMENSIONS))
for name, vals in LEADERBOARD_BASELINES.items():
cells = "".join(f"{v:>16.1f}" for v in vals[1:])
print(f" {name:<24}{cells}")
if test:
scope = (
" NB: '--test' uses 3 files/category (~15 pages) — directional, same-harness\n"
" comparison only; run '--full' for leaderboard-comparable numbers."
)
else:
scope = " NB: full-dataset run (~2,000 pages) — comparable to the published leaderboard."
print(
"\n" + scope + "\n"
" Metrics: Tables=GTRM, Charts=chart-rule pass-rate, Content=faithfulness,\n"
" Sem.Format=semantic-formatting, Visual Ground.=layout element pass-rate.\n"
" Visual Ground. needs a per-provider layout adapter; only warp_ingest has one\n"
" here (text-only baselines emit no geometry — see published baselines above)."
)
def main() -> int:
ap = argparse.ArgumentParser(
description="Run official ParseBench vs Warp-Ingest + baselines."
)
ap.add_argument(
"--pipelines",
default=",".join(DEFAULT_PIPELINES),
help=f"Comma-separated pipeline names (default: {','.join(DEFAULT_PIPELINES)}).",
)
mode = ap.add_mutually_exclusive_group()
mode.add_argument(
"--test", action="store_true", help="Small test split (3 files/category)."
)
mode.add_argument(
"--full", action="store_true", help="Full dataset (leaderboard-comparable)."
)
ap.add_argument(
"--group",
default=None,
help="Single dimension only (table/chart/text_content/text_formatting/layout).",
)
ap.add_argument(
"--work-dir",
default="parsebench_work",
help="Where ./data and ./output live (default: ./parsebench_work).",
)
ap.add_argument(
"--renderer-mode",
choices=["faithful", "quality", "env"],
default="faithful",
help=(
"Warp renderer controls. faithful = Warp output only, no external "
"table provider or content stripping (default); quality = opt into "
"local table-provider + strip transforms; env = respect current env."
),
)
ap.add_argument("--max-concurrent", type=int, default=8)
ap.add_argument(
"--skip-inference",
action="store_true",
help="Re-evaluate existing inference results.",
)
ap.add_argument(
"--force",
action="store_true",
help="Force fresh inference even if cached parse results exist "
"(use after changing the parser/engine).",
)
ap.add_argument(
"--summary-only",
action="store_true",
help="Only print the comparison from existing results.",
)
args = ap.parse_args()
test = not args.full # default to the cheap test split unless --full given
pipelines = [p.strip() for p in args.pipelines.split(",") if p.strip()]
# Force ParseBench's optional LLM chart-normalization OFF so the run is
# fully deterministic and offline (no Anthropic API calls). The framework
# otherwise defaults this env var to "judge" (on) and would post-process
# *failed chart rules* with claude-haiku IF an API key were present. That
# path only ever adds separate ``*_judge`` columns — the headline chart
# metric we read (``avg_rule_pass_rate``) is always the deterministic
# parent value — so disabling it changes nothing we report while making
# the "no LLM-as-a-judge, reproducible" guarantee literal. ``setdefault``
# respects an explicit override if the operator really wants the judge.
os.environ.setdefault("LLAMACLOUD_BENCH_LLM_NORMALIZATION", "off")
renderer_env = _configure_renderer(args.renderer_mode)
print(f"ParseBench renderer mode: {args.renderer_mode}")
for key, value in renderer_env.items():
print(f" {key}={value}")
_ensure_importable()
_register_warp()
lit = _patch_liteparse_binary()
if "liteparse_markdown" in pipelines and not lit:
print(
"WARNING: 'lit' binary not on PATH; liteparse pipeline will be skipped/fail.",
file=sys.stderr,
)
work = Path(args.work_dir).resolve()
work.mkdir(parents=True, exist_ok=True)
os.chdir(work)
output_dir = work / "output"
if not args.summary_only:
from parse_bench.pipeline.cli import PipelineCLI
for p in pipelines:
print(
f"\n{'#' * 70}\n# ParseBench pipeline: {p} ({'test' if test else 'full'})\n{'#' * 70}"
)
try:
PipelineCLI().run(
pipeline=p,
test=test,
group=args.group,
max_concurrent=args.max_concurrent,
open_report=False,
skip_inference=args.skip_inference,
force=args.force,
)
except Exception as e: # keep going so one bad parser doesn't abort the run
print(f"[ERROR] pipeline {p} failed: {e}", file=sys.stderr)
_print_comparison(output_dir, pipelines, test=test)
# Canonical cross-pipeline leaderboard via ParseBench's own generator.
try:
from parse_bench.analysis.leaderboard_report import generate_leaderboard_report
lb = generate_leaderboard_report(output_dir=output_dir)
print(f"\nParseBench leaderboard HTML: {lb}")
except Exception as e:
print(f"(leaderboard html skipped: {e})")
return 0
if __name__ == "__main__":
raise SystemExit(main())