parse-bench 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parse_bench/__init__.py +3 -0
- parse_bench/analysis/__init__.py +6 -0
- parse_bench/analysis/aggregation_report.py +582 -0
- parse_bench/analysis/cli.py +472 -0
- parse_bench/analysis/comparison.py +382 -0
- parse_bench/analysis/comparison_core.py +357 -0
- parse_bench/analysis/comparison_report.py +2066 -0
- parse_bench/analysis/detailed_report.py +2254 -0
- parse_bench/analysis/leaderboard_report.py +852 -0
- parse_bench/analysis/metric_definitions.py +771 -0
- parse_bench/cli.py +267 -0
- parse_bench/data/__init__.py +1 -0
- parse_bench/data/cli.py +118 -0
- parse_bench/data/download.py +127 -0
- parse_bench/evaluation/__init__.py +11 -0
- parse_bench/evaluation/cli.py +435 -0
- parse_bench/evaluation/evaluators/__init__.py +17 -0
- parse_bench/evaluation/evaluators/base.py +34 -0
- parse_bench/evaluation/evaluators/extract.py +429 -0
- parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
- parse_bench/evaluation/evaluators/parse.py +1353 -0
- parse_bench/evaluation/evaluators/qa.py +199 -0
- parse_bench/evaluation/layout_adapters/__init__.py +21 -0
- parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
- parse_bench/evaluation/layout_adapters/base.py +105 -0
- parse_bench/evaluation/layout_adapters/registry.py +109 -0
- parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
- parse_bench/evaluation/layout_label_mappers/base.py +66 -0
- parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
- parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
- parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
- parse_bench/evaluation/metric_aggregation.py +56 -0
- parse_bench/evaluation/metrics/__init__.py +5 -0
- parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
- parse_bench/evaluation/metrics/attribution/constants.py +12 -0
- parse_bench/evaluation/metrics/attribution/core.py +1108 -0
- parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
- parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
- parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
- parse_bench/evaluation/metrics/base.py +33 -0
- parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
- parse_bench/evaluation/metrics/extract/__init__.py +29 -0
- parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
- parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
- parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
- parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
- parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
- parse_bench/evaluation/metrics/extract/test_types.py +11 -0
- parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
- parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
- parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
- parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
- parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
- parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
- parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
- parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
- parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
- parse_bench/evaluation/metrics/parse/__init__.py +5 -0
- parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
- parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
- parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
- parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
- parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
- parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
- parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
- parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
- parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
- parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
- parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
- parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
- parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
- parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
- parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
- parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
- parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
- parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
- parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
- parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
- parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
- parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
- parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
- parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
- parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
- parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
- parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
- parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
- parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
- parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
- parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
- parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
- parse_bench/evaluation/metrics/parse/test_types.py +103 -0
- parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
- parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
- parse_bench/evaluation/metrics/parse/utils.py +885 -0
- parse_bench/evaluation/metrics/qa/__init__.py +5 -0
- parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
- parse_bench/evaluation/qa/__init__.py +5 -0
- parse_bench/evaluation/qa/llm_service.py +335 -0
- parse_bench/evaluation/reports/__init__.py +8 -0
- parse_bench/evaluation/reports/csv.py +64 -0
- parse_bench/evaluation/reports/html.py +338 -0
- parse_bench/evaluation/reports/markdown.py +98 -0
- parse_bench/evaluation/reports/rule_csv.py +22 -0
- parse_bench/evaluation/runner.py +1864 -0
- parse_bench/evaluation/stats.py +104 -0
- parse_bench/extensions.py +72 -0
- parse_bench/inference/__init__.py +33 -0
- parse_bench/inference/chunkr_layout_extraction.py +160 -0
- parse_bench/inference/cli.py +484 -0
- parse_bench/inference/layout_extraction.py +422 -0
- parse_bench/inference/pipelines/__init__.py +59 -0
- parse_bench/inference/pipelines/extract.py +39 -0
- parse_bench/inference/pipelines/layout.py +142 -0
- parse_bench/inference/pipelines/parse.py +2603 -0
- parse_bench/inference/pipelines.py +0 -0
- parse_bench/inference/providers/__init__.py +28 -0
- parse_bench/inference/providers/base.py +196 -0
- parse_bench/inference/providers/cancellation.py +137 -0
- parse_bench/inference/providers/extract/__init__.py +22 -0
- parse_bench/inference/providers/extract/citations.py +549 -0
- parse_bench/inference/providers/extract/extend.py +851 -0
- parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
- parse_bench/inference/providers/layoutdet/__init__.py +25 -0
- parse_bench/inference/providers/layoutdet/adapters.py +946 -0
- parse_bench/inference/providers/layoutdet/base.py +203 -0
- parse_bench/inference/providers/layoutdet/chandra.py +449 -0
- parse_bench/inference/providers/layoutdet/docling.py +125 -0
- parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
- parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
- parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
- parse_bench/inference/providers/layoutdet/paddle.py +117 -0
- parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
- parse_bench/inference/providers/layoutdet/surya.py +250 -0
- parse_bench/inference/providers/layoutdet/yolo.py +109 -0
- parse_bench/inference/providers/parse/__init__.py +64 -0
- parse_bench/inference/providers/parse/_docling_common.py +233 -0
- parse_bench/inference/providers/parse/_layout_utils.py +611 -0
- parse_bench/inference/providers/parse/amazon_nova.py +515 -0
- parse_bench/inference/providers/parse/anthropic.py +882 -0
- parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
- parse_bench/inference/providers/parse/chandra2.py +633 -0
- parse_bench/inference/providers/parse/chunkr.py +268 -0
- parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
- parse_bench/inference/providers/parse/datalab.py +370 -0
- parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
- parse_bench/inference/providers/parse/docling.py +281 -0
- parse_bench/inference/providers/parse/docling_serve.py +289 -0
- parse_bench/inference/providers/parse/dots_ocr.py +574 -0
- parse_bench/inference/providers/parse/extend_parse.py +710 -0
- parse_bench/inference/providers/parse/falconocr.py +436 -0
- parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
- parse_bench/inference/providers/parse/gemma4.py +472 -0
- parse_bench/inference/providers/parse/glm_zai.py +229 -0
- parse_bench/inference/providers/parse/google.py +1125 -0
- parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
- parse_bench/inference/providers/parse/google_docai.py +776 -0
- parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
- parse_bench/inference/providers/parse/granite_vision.py +515 -0
- parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
- parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
- parse_bench/inference/providers/parse/landingai.py +452 -0
- parse_bench/inference/providers/parse/liteparse.py +350 -0
- parse_bench/inference/providers/parse/llamaparse.py +677 -0
- parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
- parse_bench/inference/providers/parse/markitdown.py +138 -0
- parse_bench/inference/providers/parse/mineru25.py +405 -0
- parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
- parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
- parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
- parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
- parse_bench/inference/providers/parse/oi_parser.py +222 -0
- parse_bench/inference/providers/parse/openai.py +740 -0
- parse_bench/inference/providers/parse/opendataloader.py +152 -0
- parse_bench/inference/providers/parse/paddleocr.py +624 -0
- parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
- parse_bench/inference/providers/parse/pulse.py +785 -0
- parse_bench/inference/providers/parse/pymupdf.py +207 -0
- parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
- parse_bench/inference/providers/parse/pypdf.py +179 -0
- parse_bench/inference/providers/parse/qwen.py +678 -0
- parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
- parse_bench/inference/providers/parse/reducto.py +546 -0
- parse_bench/inference/providers/parse/surya2.py +372 -0
- parse_bench/inference/providers/parse/tesseract.py +301 -0
- parse_bench/inference/providers/parse/textract.py +694 -0
- parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
- parse_bench/inference/providers/parse/unstructured.py +485 -0
- parse_bench/inference/providers/parse/warp_ingest.py +199 -0
- parse_bench/inference/providers/registry.py +49 -0
- parse_bench/inference/renormalize.py +170 -0
- parse_bench/inference/runner.py +2023 -0
- parse_bench/layout_label_mapping.py +424 -0
- parse_bench/layout_projection.py +179 -0
- parse_bench/pipeline/__init__.py +1 -0
- parse_bench/pipeline/cli.py +549 -0
- parse_bench/schemas/__init__.py +33 -0
- parse_bench/schemas/evaluation.py +93 -0
- parse_bench/schemas/extract_output.py +36 -0
- parse_bench/schemas/layout_detection_output.py +545 -0
- parse_bench/schemas/layout_ontology.py +315 -0
- parse_bench/schemas/metrics.py +69 -0
- parse_bench/schemas/parse_output.py +152 -0
- parse_bench/schemas/pipeline.py +22 -0
- parse_bench/schemas/pipeline_io.py +106 -0
- parse_bench/schemas/product.py +97 -0
- parse_bench/test_cases/__init__.py +25 -0
- parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
- parse_bench/test_cases/extract_field_paths.py +164 -0
- parse_bench/test_cases/layout_attribution_generation.py +287 -0
- parse_bench/test_cases/loader.py +652 -0
- parse_bench/test_cases/parse_rule_schemas.py +1071 -0
- parse_bench/test_cases/rule_filters.py +32 -0
- parse_bench/test_cases/rule_ids.py +107 -0
- parse_bench/test_cases/schema.py +427 -0
- parse_bench/utils/__init__.py +15 -0
- parse_bench/utils/gemini_layout_utils.py +670 -0
- parse_bench/utils/text_aggregation.py +100 -0
- parse_bench-1.0.0.dist-info/METADATA +476 -0
- parse_bench-1.0.0.dist-info/RECORD +227 -0
- parse_bench-1.0.0.dist-info/WHEEL +4 -0
- parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
- parse_bench-1.0.0.dist-info/licenses/LICENSE +201 -0
parse_bench/cli.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Unified command-line interface for parse-bench."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import fire
|
|
7
|
+
from dotenv import find_dotenv, load_dotenv
|
|
8
|
+
|
|
9
|
+
from parse_bench import __version__
|
|
10
|
+
from parse_bench.analysis.cli import AnalysisCLI
|
|
11
|
+
from parse_bench.data.cli import DataCLI
|
|
12
|
+
from parse_bench.evaluation.cli import EvaluationCLI
|
|
13
|
+
from parse_bench.inference.cli import InferenceCLI
|
|
14
|
+
from parse_bench.pipeline.cli import PipelineCLI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Load .env file if it exists. Search the current directory and its parents so
|
|
18
|
+
# the CLI works both from a repo checkout and as an installed package.
|
|
19
|
+
def _load_env() -> None:
|
|
20
|
+
"""Load environment variables from the nearest .env file."""
|
|
21
|
+
env_path = find_dotenv(usecwd=True)
|
|
22
|
+
if env_path:
|
|
23
|
+
load_dotenv(env_path, override=False) # Don't override existing env vars
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_pipeline_dir(name_or_path: str | Path) -> Path:
|
|
27
|
+
"""Resolve a pipeline name or path to a directory.
|
|
28
|
+
|
|
29
|
+
If the input is an existing directory, use it as-is.
|
|
30
|
+
Otherwise, try ./output/<name>.
|
|
31
|
+
"""
|
|
32
|
+
p = Path(name_or_path)
|
|
33
|
+
if p.exists():
|
|
34
|
+
return p
|
|
35
|
+
candidate = Path("./output") / p
|
|
36
|
+
if candidate.exists():
|
|
37
|
+
return candidate
|
|
38
|
+
return p # Return original; caller will handle the error
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BenchCLI:
|
|
42
|
+
"""Unified CLI for parse-bench.
|
|
43
|
+
|
|
44
|
+
Top-level commands (recommended):
|
|
45
|
+
run Run end-to-end benchmark pipeline
|
|
46
|
+
download Download dataset from HuggingFace
|
|
47
|
+
status Check if dataset is ready
|
|
48
|
+
pipelines List available pipeline configurations
|
|
49
|
+
compare Compare two pipeline results
|
|
50
|
+
serve View reports in browser with PDF support
|
|
51
|
+
version Print the installed parse-bench version
|
|
52
|
+
|
|
53
|
+
Advanced subcommands:
|
|
54
|
+
inference Run inference only
|
|
55
|
+
evaluation Run evaluation only
|
|
56
|
+
analysis Generate reports, dashboards, comparisons
|
|
57
|
+
pipeline End-to-end pipeline (same as 'run')
|
|
58
|
+
data Dataset management (same as 'download'/'status')
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self.inference = InferenceCLI()
|
|
63
|
+
self.evaluation = EvaluationCLI()
|
|
64
|
+
self.analysis = AnalysisCLI()
|
|
65
|
+
self.pipeline = PipelineCLI()
|
|
66
|
+
self.data = DataCLI()
|
|
67
|
+
|
|
68
|
+
# ── Top-level convenience commands ──────────────────────────────
|
|
69
|
+
|
|
70
|
+
def version(self) -> str:
|
|
71
|
+
"""Print the installed parse-bench version."""
|
|
72
|
+
return __version__
|
|
73
|
+
|
|
74
|
+
def run(
|
|
75
|
+
self,
|
|
76
|
+
pipeline: str,
|
|
77
|
+
input_dir: str | Path | None = None,
|
|
78
|
+
file: str | Path | None = None,
|
|
79
|
+
output_dir: str | Path | None = None,
|
|
80
|
+
max_concurrent: int = 20,
|
|
81
|
+
force: bool = False,
|
|
82
|
+
verbose: bool = False,
|
|
83
|
+
group: str | None = None,
|
|
84
|
+
tags: str | tuple[str, ...] | list[str] | None = None,
|
|
85
|
+
open_report: bool = True,
|
|
86
|
+
skip_inference: bool = False,
|
|
87
|
+
test: bool = False,
|
|
88
|
+
) -> int:
|
|
89
|
+
"""Run end-to-end benchmark: inference -> evaluation -> report.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
pipeline: Pipeline name (e.g., 'llamaparse_agentic', 'llamaparse_cost_effective')
|
|
93
|
+
input_dir: Directory containing test cases/PDFs (default: ./data)
|
|
94
|
+
file: Single file to run (PDF/image)
|
|
95
|
+
output_dir: Directory to save results (default: ./output)
|
|
96
|
+
max_concurrent: Maximum concurrent inference requests (default: 20)
|
|
97
|
+
force: Force regeneration even if results exist (default: False)
|
|
98
|
+
verbose: Enable verbose output (default: False)
|
|
99
|
+
group: Filter by category (e.g., 'chart', 'table')
|
|
100
|
+
tags: Tags for this run
|
|
101
|
+
open_report: Open HTML report in browser (default: True)
|
|
102
|
+
skip_inference: Skip inference, only re-evaluate (default: False)
|
|
103
|
+
test: Download and run on the small test dataset (3 files per category)
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
parse-bench run llamaparse_agentic
|
|
107
|
+
parse-bench run llamaparse_agentic --group chart
|
|
108
|
+
parse-bench run llamaparse_agentic --skip_inference
|
|
109
|
+
parse-bench run llamaparse_agentic --test
|
|
110
|
+
"""
|
|
111
|
+
return self.pipeline.run(
|
|
112
|
+
pipeline=pipeline,
|
|
113
|
+
input_dir=input_dir,
|
|
114
|
+
file=file,
|
|
115
|
+
output_dir=output_dir,
|
|
116
|
+
max_concurrent=max_concurrent,
|
|
117
|
+
force=force,
|
|
118
|
+
verbose=verbose,
|
|
119
|
+
group=group,
|
|
120
|
+
tags=tags,
|
|
121
|
+
open_report=open_report,
|
|
122
|
+
skip_inference=skip_inference,
|
|
123
|
+
test=test,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def download(
|
|
127
|
+
self,
|
|
128
|
+
data_dir: str | Path | None = None,
|
|
129
|
+
force: bool = False,
|
|
130
|
+
test: bool = False,
|
|
131
|
+
) -> int:
|
|
132
|
+
"""Download the benchmark dataset from HuggingFace.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
data_dir: Directory to store dataset (default: ./data)
|
|
136
|
+
force: Force re-download even if data exists
|
|
137
|
+
test: Download the small test dataset (3 files per category)
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
parse-bench download
|
|
141
|
+
parse-bench download --test
|
|
142
|
+
"""
|
|
143
|
+
return self.data.download(data_dir=data_dir, force=force, test=test)
|
|
144
|
+
|
|
145
|
+
def status(
|
|
146
|
+
self,
|
|
147
|
+
data_dir: str | Path | None = None,
|
|
148
|
+
test: bool = False,
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Check if the benchmark dataset is downloaded and ready.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
data_dir: Data directory to check
|
|
154
|
+
(default: ./data, or ./data/test when --test is set)
|
|
155
|
+
test: Check the small test dataset instead of the full dataset
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
parse-bench status
|
|
159
|
+
parse-bench status --test
|
|
160
|
+
parse-bench status data/
|
|
161
|
+
"""
|
|
162
|
+
return self.data.status(data_dir=data_dir, test=test)
|
|
163
|
+
|
|
164
|
+
def pipelines(self) -> None:
|
|
165
|
+
"""List all available pipeline configurations."""
|
|
166
|
+
return self.inference.list_pipelines()
|
|
167
|
+
|
|
168
|
+
def compare(
|
|
169
|
+
self,
|
|
170
|
+
pipeline_a: str | Path,
|
|
171
|
+
pipeline_b: str | Path,
|
|
172
|
+
test_cases_dir: str | Path | None = None,
|
|
173
|
+
output_file: str | Path | None = None,
|
|
174
|
+
) -> int:
|
|
175
|
+
"""Compare results from two pipelines.
|
|
176
|
+
|
|
177
|
+
Pipeline names are auto-resolved to ./output/<name> if the path
|
|
178
|
+
doesn't exist as-is.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
pipeline_a: Pipeline A name or directory (e.g., 'llamaparse_agentic')
|
|
182
|
+
pipeline_b: Pipeline B name or directory (e.g., 'llamaparse_cost_effective')
|
|
183
|
+
test_cases_dir: Directory containing test cases (default: auto-detect)
|
|
184
|
+
output_file: Path to save comparison report (default: auto)
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
parse-bench compare llamaparse_agentic llamaparse_cost_effective
|
|
188
|
+
parse-bench compare ./output/llamaparse_agentic ./output/llamaparse_cost_effective
|
|
189
|
+
"""
|
|
190
|
+
return self.analysis.compare_pipelines(
|
|
191
|
+
pipeline_a_dir=_resolve_pipeline_dir(pipeline_a),
|
|
192
|
+
pipeline_b_dir=_resolve_pipeline_dir(pipeline_b),
|
|
193
|
+
test_cases_dir=test_cases_dir,
|
|
194
|
+
output_file=output_file,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def leaderboard(
|
|
198
|
+
self,
|
|
199
|
+
*pipelines: str,
|
|
200
|
+
output_dir: str | Path = "./output",
|
|
201
|
+
output_file: str | Path | None = None,
|
|
202
|
+
) -> int:
|
|
203
|
+
"""Generate a leaderboard comparing all pipelines side-by-side.
|
|
204
|
+
|
|
205
|
+
If no pipeline names are given, auto-discovers all pipelines in the
|
|
206
|
+
output directory.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
*pipelines: Optional pipeline names to include (e.g., 'llamaparse_agentic')
|
|
210
|
+
output_dir: Parent directory containing pipeline subdirectories
|
|
211
|
+
output_file: Path to save the leaderboard HTML
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
parse-bench leaderboard
|
|
215
|
+
parse-bench leaderboard llamaparse_agentic llamaparse_cost_effective
|
|
216
|
+
"""
|
|
217
|
+
pipeline_list = list(pipelines) if pipelines else None
|
|
218
|
+
return self.analysis.generate_leaderboard(
|
|
219
|
+
output_dir=output_dir,
|
|
220
|
+
pipelines=pipeline_list,
|
|
221
|
+
output_file=output_file,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def serve(
|
|
225
|
+
self,
|
|
226
|
+
pipeline: str | Path | None = None,
|
|
227
|
+
port: int = 8080,
|
|
228
|
+
root: str | Path = ".",
|
|
229
|
+
) -> int:
|
|
230
|
+
"""Start a local server to view reports with PDF rendering support.
|
|
231
|
+
|
|
232
|
+
Pipeline names are auto-resolved to ./output/<name> if the path
|
|
233
|
+
doesn't exist as-is.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
pipeline: Pipeline name or directory (e.g., 'llamaparse_agentic')
|
|
237
|
+
port: Port number (default: 8080)
|
|
238
|
+
root: Root directory to serve (default: current directory)
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
parse-bench serve llamaparse_agentic
|
|
242
|
+
parse-bench serve ./output/llamaparse_agentic
|
|
243
|
+
parse-bench serve
|
|
244
|
+
"""
|
|
245
|
+
pipeline_dir = _resolve_pipeline_dir(pipeline) if pipeline else None
|
|
246
|
+
return self.analysis.serve(
|
|
247
|
+
pipeline_dir=pipeline_dir,
|
|
248
|
+
port=port,
|
|
249
|
+
root=root,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main() -> int:
|
|
254
|
+
"""Main entry point for the unified CLI."""
|
|
255
|
+
# Load .env file before any commands run
|
|
256
|
+
_load_env()
|
|
257
|
+
cli = BenchCLI()
|
|
258
|
+
result = fire.Fire(cli)
|
|
259
|
+
# Fire returns the result of the called method
|
|
260
|
+
# If it's an integer (exit code), use it; otherwise default to 0
|
|
261
|
+
if isinstance(result, int):
|
|
262
|
+
return result
|
|
263
|
+
return 0
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
if __name__ == "__main__":
|
|
267
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data management for parse-bench datasets."""
|
parse_bench/data/cli.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Command-line interface for data management."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from parse_bench.data.download import default_data_dir, download_dataset, is_dataset_ready
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataCLI:
|
|
10
|
+
"""Command-line interface for managing benchmark datasets."""
|
|
11
|
+
|
|
12
|
+
def download(
|
|
13
|
+
self,
|
|
14
|
+
data_dir: str | Path | None = None,
|
|
15
|
+
force: bool = False,
|
|
16
|
+
test: bool = False,
|
|
17
|
+
) -> int:
|
|
18
|
+
"""Download the parse-bench dataset from HuggingFace.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data_dir: Local directory to store the dataset
|
|
22
|
+
(default: ./data, or ./data/test when --test is set)
|
|
23
|
+
force: Force re-download even if data already exists
|
|
24
|
+
test: Download the small test dataset (3 files per category)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Exit code (0 for success, non-zero for failure)
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
data_path = Path(data_dir) if data_dir else default_data_dir(test=test)
|
|
31
|
+
download_dataset(data_dir=data_path, force=force, test=test)
|
|
32
|
+
return 0
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Error downloading dataset: {e}", file=sys.stderr)
|
|
35
|
+
import traceback
|
|
36
|
+
|
|
37
|
+
traceback.print_exc()
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
def status(
|
|
41
|
+
self,
|
|
42
|
+
data_dir: str | Path | None = None,
|
|
43
|
+
test: bool = False,
|
|
44
|
+
) -> int:
|
|
45
|
+
"""Check if the dataset is downloaded and show summary statistics.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data_dir: Data directory to check
|
|
49
|
+
(default: ./data, or ./data/test when --test is set)
|
|
50
|
+
test: Check the small test dataset instead of the full dataset
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Exit code (0 if ready, 1 if not)
|
|
54
|
+
"""
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
data_path = Path(data_dir) if data_dir else Path.cwd() / default_data_dir(test=test)
|
|
58
|
+
ready = is_dataset_ready(data_path)
|
|
59
|
+
|
|
60
|
+
if not ready:
|
|
61
|
+
print(f"Dataset is NOT ready at: {data_path}")
|
|
62
|
+
print("Run 'parse-bench download' to download it.")
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
print(f"Dataset: {data_path}")
|
|
66
|
+
print()
|
|
67
|
+
|
|
68
|
+
# Gather per-category stats from JSONL files
|
|
69
|
+
jsonl_files = sorted(data_path.glob("*.jsonl"))
|
|
70
|
+
total_cases = 0
|
|
71
|
+
total_pdfs = 0
|
|
72
|
+
all_pdfs: set[str] = set() # track unique PDFs across all categories
|
|
73
|
+
rows: list[tuple[str, int, int]] = []
|
|
74
|
+
|
|
75
|
+
for jf in jsonl_files:
|
|
76
|
+
category = jf.stem
|
|
77
|
+
lines = jf.read_text().strip().splitlines()
|
|
78
|
+
n_cases = len(lines)
|
|
79
|
+
pdfs: set[str] = set()
|
|
80
|
+
for line in lines:
|
|
81
|
+
rec = json.loads(line)
|
|
82
|
+
pdfs.add(rec.get("pdf", ""))
|
|
83
|
+
n_pdfs = len(pdfs)
|
|
84
|
+
rows.append((category, n_cases, n_pdfs))
|
|
85
|
+
total_cases += n_cases
|
|
86
|
+
total_pdfs += n_pdfs
|
|
87
|
+
all_pdfs.update(pdfs)
|
|
88
|
+
|
|
89
|
+
# Count docs on disk per category
|
|
90
|
+
doc_counts: dict[str, int] = {}
|
|
91
|
+
docs_dir = data_path / "docs"
|
|
92
|
+
if docs_dir.exists():
|
|
93
|
+
for cat_dir in sorted(docs_dir.iterdir()):
|
|
94
|
+
if cat_dir.is_dir():
|
|
95
|
+
doc_counts[cat_dir.name] = sum(1 for _ in cat_dir.rglob("*") if _.is_file())
|
|
96
|
+
|
|
97
|
+
# Print table
|
|
98
|
+
hdr = f"{'Category':<20} {'Test Cases':>12} {'PDFs':>8}"
|
|
99
|
+
print(hdr)
|
|
100
|
+
print("-" * len(hdr))
|
|
101
|
+
for category, n_cases, n_pdfs in rows:
|
|
102
|
+
print(f"{category:<20} {n_cases:>12,} {n_pdfs:>8,}")
|
|
103
|
+
print("-" * len(hdr))
|
|
104
|
+
print(f"{'Total':<20} {total_cases:>12,} {total_pdfs:>8,}")
|
|
105
|
+
n_unique = len(all_pdfs)
|
|
106
|
+
if n_unique < total_pdfs:
|
|
107
|
+
print(f"{'Unique documents':<20} {'':>12} {n_unique:>8,}")
|
|
108
|
+
print(" (text_content and text_formatting share the same PDF files)")
|
|
109
|
+
print()
|
|
110
|
+
|
|
111
|
+
# Docs on disk
|
|
112
|
+
if doc_counts:
|
|
113
|
+
print("Documents on disk:")
|
|
114
|
+
for cat, count in doc_counts.items():
|
|
115
|
+
print(f" {cat:<18} {count:>6,} files")
|
|
116
|
+
print(f" {'total':<18} {sum(doc_counts.values()):>6,} files")
|
|
117
|
+
|
|
118
|
+
return 0
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Download parse-bench dataset from HuggingFace.
|
|
2
|
+
|
|
3
|
+
Dataset: https://huggingface.co/datasets/llamaindex/ParseBench
|
|
4
|
+
|
|
5
|
+
Structure after download:
|
|
6
|
+
<local_dir>/
|
|
7
|
+
├── chart.jsonl
|
|
8
|
+
├── layout.jsonl
|
|
9
|
+
├── table.jsonl
|
|
10
|
+
├── text.jsonl
|
|
11
|
+
├── expected_markdown.json
|
|
12
|
+
└── pdfs/{chart,layout,table,text}/*.pdf
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
DATASET_REPO = "llamaindex/ParseBench"
|
|
20
|
+
DATASET_REPO_TYPE = "dataset"
|
|
21
|
+
TEST_DATA_REVISION = "test-data"
|
|
22
|
+
|
|
23
|
+
# Default on-disk locations. Test data lives in a sibling subdirectory so the
|
|
24
|
+
# two datasets coexist and `--test` does not silently overlay or get masked
|
|
25
|
+
# by an existing full download.
|
|
26
|
+
DEFAULT_DATA_DIR = Path("./data")
|
|
27
|
+
DEFAULT_TEST_DATA_DIR = Path("./data/test")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_data_dir(test: bool = False) -> Path:
|
|
31
|
+
"""Return the default on-disk dataset path for the given mode.
|
|
32
|
+
|
|
33
|
+
Used by every CLI surface that takes ``--test`` so the routing is
|
|
34
|
+
consistent between ``download``, ``run`` and ``status``.
|
|
35
|
+
"""
|
|
36
|
+
return DEFAULT_TEST_DATA_DIR if test else DEFAULT_DATA_DIR
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Files that must exist for the dataset to be considered complete
|
|
40
|
+
_REQUIRED_FILES = [
|
|
41
|
+
"chart.jsonl",
|
|
42
|
+
"layout.jsonl",
|
|
43
|
+
"table.jsonl",
|
|
44
|
+
"text_content.jsonl",
|
|
45
|
+
"text_formatting.jsonl",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# At least one document must exist per category
|
|
49
|
+
_REQUIRED_DOC_DIRS = ["docs/chart", "docs/layout", "docs/table", "docs/text"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_dataset_ready(data_dir: Path) -> bool:
|
|
53
|
+
"""Check if the dataset is already downloaded and complete.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
data_dir: Path to the data directory.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
True if all required files and at least one PDF per category exist.
|
|
60
|
+
"""
|
|
61
|
+
if not data_dir.exists():
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
for f in _REQUIRED_FILES:
|
|
65
|
+
if not (data_dir / f).exists():
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
for d in _REQUIRED_DOC_DIRS:
|
|
69
|
+
doc_dir = data_dir / d
|
|
70
|
+
if not doc_dir.exists():
|
|
71
|
+
return False
|
|
72
|
+
# Check for any supported document file (PDF, image, etc.)
|
|
73
|
+
if not any(doc_dir.rglob("*.*")):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def download_dataset(
|
|
80
|
+
data_dir: Path | None = None,
|
|
81
|
+
force: bool = False,
|
|
82
|
+
test: bool = False,
|
|
83
|
+
) -> Path:
|
|
84
|
+
"""Download the parse-bench dataset from HuggingFace.
|
|
85
|
+
|
|
86
|
+
Uses huggingface_hub's snapshot_download to fetch the full dataset,
|
|
87
|
+
including JSONL files and PDFs.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
data_dir: Local directory to store the dataset.
|
|
91
|
+
Defaults to ./data in the current working directory.
|
|
92
|
+
force: Force re-download even if data already exists.
|
|
93
|
+
test: Download the small test dataset (3 files per category)
|
|
94
|
+
instead of the full dataset.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Path to the downloaded dataset directory.
|
|
98
|
+
"""
|
|
99
|
+
from huggingface_hub import snapshot_download
|
|
100
|
+
|
|
101
|
+
if data_dir is None:
|
|
102
|
+
data_dir = Path.cwd() / "data"
|
|
103
|
+
|
|
104
|
+
revision = TEST_DATA_REVISION if test else None
|
|
105
|
+
|
|
106
|
+
if not force and is_dataset_ready(data_dir):
|
|
107
|
+
print(f"Dataset already downloaded at: {data_dir}")
|
|
108
|
+
return data_dir
|
|
109
|
+
|
|
110
|
+
label = "test dataset" if test else "dataset"
|
|
111
|
+
print(f"Downloading {label} from HuggingFace: {DATASET_REPO}")
|
|
112
|
+
if test:
|
|
113
|
+
print(f"Branch: {TEST_DATA_REVISION}")
|
|
114
|
+
print(f"Destination: {data_dir}")
|
|
115
|
+
|
|
116
|
+
snapshot_download(
|
|
117
|
+
repo_id=DATASET_REPO,
|
|
118
|
+
repo_type=DATASET_REPO_TYPE,
|
|
119
|
+
local_dir=str(data_dir),
|
|
120
|
+
revision=revision,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if not is_dataset_ready(data_dir):
|
|
124
|
+
raise RuntimeError(f"Dataset download completed but validation failed. Check {data_dir} for missing files.")
|
|
125
|
+
|
|
126
|
+
print(f"Dataset ready at: {data_dir}")
|
|
127
|
+
return data_dir
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Evaluation package exports."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["EvaluationRunner"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name: str) -> object: # pragma: no cover - simple lazy import shim.
|
|
7
|
+
if name == "EvaluationRunner":
|
|
8
|
+
from .runner import EvaluationRunner
|
|
9
|
+
|
|
10
|
+
return EvaluationRunner
|
|
11
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|