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.
Files changed (227) hide show
  1. parse_bench/__init__.py +3 -0
  2. parse_bench/analysis/__init__.py +6 -0
  3. parse_bench/analysis/aggregation_report.py +582 -0
  4. parse_bench/analysis/cli.py +472 -0
  5. parse_bench/analysis/comparison.py +382 -0
  6. parse_bench/analysis/comparison_core.py +357 -0
  7. parse_bench/analysis/comparison_report.py +2066 -0
  8. parse_bench/analysis/detailed_report.py +2254 -0
  9. parse_bench/analysis/leaderboard_report.py +852 -0
  10. parse_bench/analysis/metric_definitions.py +771 -0
  11. parse_bench/cli.py +267 -0
  12. parse_bench/data/__init__.py +1 -0
  13. parse_bench/data/cli.py +118 -0
  14. parse_bench/data/download.py +127 -0
  15. parse_bench/evaluation/__init__.py +11 -0
  16. parse_bench/evaluation/cli.py +435 -0
  17. parse_bench/evaluation/evaluators/__init__.py +17 -0
  18. parse_bench/evaluation/evaluators/base.py +34 -0
  19. parse_bench/evaluation/evaluators/extract.py +429 -0
  20. parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
  21. parse_bench/evaluation/evaluators/parse.py +1353 -0
  22. parse_bench/evaluation/evaluators/qa.py +199 -0
  23. parse_bench/evaluation/layout_adapters/__init__.py +21 -0
  24. parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
  25. parse_bench/evaluation/layout_adapters/base.py +105 -0
  26. parse_bench/evaluation/layout_adapters/registry.py +109 -0
  27. parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
  28. parse_bench/evaluation/layout_label_mappers/base.py +66 -0
  29. parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
  30. parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
  31. parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
  32. parse_bench/evaluation/metric_aggregation.py +56 -0
  33. parse_bench/evaluation/metrics/__init__.py +5 -0
  34. parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
  35. parse_bench/evaluation/metrics/attribution/constants.py +12 -0
  36. parse_bench/evaluation/metrics/attribution/core.py +1108 -0
  37. parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
  38. parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
  39. parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
  40. parse_bench/evaluation/metrics/base.py +33 -0
  41. parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
  42. parse_bench/evaluation/metrics/extract/__init__.py +29 -0
  43. parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
  44. parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
  45. parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
  46. parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
  47. parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
  48. parse_bench/evaluation/metrics/extract/test_types.py +11 -0
  49. parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
  50. parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
  51. parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
  52. parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
  53. parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
  54. parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
  55. parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
  56. parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
  57. parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
  58. parse_bench/evaluation/metrics/parse/__init__.py +5 -0
  59. parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
  60. parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
  61. parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
  62. parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
  63. parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
  64. parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
  65. parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
  66. parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
  67. parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
  68. parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
  69. parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
  70. parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
  71. parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
  72. parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
  73. parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
  74. parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
  75. parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
  76. parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
  77. parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
  78. parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
  79. parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
  80. parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
  81. parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
  82. parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
  83. parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
  84. parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
  85. parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
  86. parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
  87. parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
  88. parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
  89. parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
  90. parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
  91. parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
  92. parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
  93. parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
  94. parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
  95. parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
  96. parse_bench/evaluation/metrics/parse/test_types.py +103 -0
  97. parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
  98. parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
  99. parse_bench/evaluation/metrics/parse/utils.py +885 -0
  100. parse_bench/evaluation/metrics/qa/__init__.py +5 -0
  101. parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
  102. parse_bench/evaluation/qa/__init__.py +5 -0
  103. parse_bench/evaluation/qa/llm_service.py +335 -0
  104. parse_bench/evaluation/reports/__init__.py +8 -0
  105. parse_bench/evaluation/reports/csv.py +64 -0
  106. parse_bench/evaluation/reports/html.py +338 -0
  107. parse_bench/evaluation/reports/markdown.py +98 -0
  108. parse_bench/evaluation/reports/rule_csv.py +22 -0
  109. parse_bench/evaluation/runner.py +1864 -0
  110. parse_bench/evaluation/stats.py +104 -0
  111. parse_bench/extensions.py +72 -0
  112. parse_bench/inference/__init__.py +33 -0
  113. parse_bench/inference/chunkr_layout_extraction.py +160 -0
  114. parse_bench/inference/cli.py +484 -0
  115. parse_bench/inference/layout_extraction.py +422 -0
  116. parse_bench/inference/pipelines/__init__.py +59 -0
  117. parse_bench/inference/pipelines/extract.py +39 -0
  118. parse_bench/inference/pipelines/layout.py +142 -0
  119. parse_bench/inference/pipelines/parse.py +2603 -0
  120. parse_bench/inference/pipelines.py +0 -0
  121. parse_bench/inference/providers/__init__.py +28 -0
  122. parse_bench/inference/providers/base.py +196 -0
  123. parse_bench/inference/providers/cancellation.py +137 -0
  124. parse_bench/inference/providers/extract/__init__.py +22 -0
  125. parse_bench/inference/providers/extract/citations.py +549 -0
  126. parse_bench/inference/providers/extract/extend.py +851 -0
  127. parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
  128. parse_bench/inference/providers/layoutdet/__init__.py +25 -0
  129. parse_bench/inference/providers/layoutdet/adapters.py +946 -0
  130. parse_bench/inference/providers/layoutdet/base.py +203 -0
  131. parse_bench/inference/providers/layoutdet/chandra.py +449 -0
  132. parse_bench/inference/providers/layoutdet/docling.py +125 -0
  133. parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
  134. parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
  135. parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
  136. parse_bench/inference/providers/layoutdet/paddle.py +117 -0
  137. parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
  138. parse_bench/inference/providers/layoutdet/surya.py +250 -0
  139. parse_bench/inference/providers/layoutdet/yolo.py +109 -0
  140. parse_bench/inference/providers/parse/__init__.py +64 -0
  141. parse_bench/inference/providers/parse/_docling_common.py +233 -0
  142. parse_bench/inference/providers/parse/_layout_utils.py +611 -0
  143. parse_bench/inference/providers/parse/amazon_nova.py +515 -0
  144. parse_bench/inference/providers/parse/anthropic.py +882 -0
  145. parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
  146. parse_bench/inference/providers/parse/chandra2.py +633 -0
  147. parse_bench/inference/providers/parse/chunkr.py +268 -0
  148. parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
  149. parse_bench/inference/providers/parse/datalab.py +370 -0
  150. parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
  151. parse_bench/inference/providers/parse/docling.py +281 -0
  152. parse_bench/inference/providers/parse/docling_serve.py +289 -0
  153. parse_bench/inference/providers/parse/dots_ocr.py +574 -0
  154. parse_bench/inference/providers/parse/extend_parse.py +710 -0
  155. parse_bench/inference/providers/parse/falconocr.py +436 -0
  156. parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
  157. parse_bench/inference/providers/parse/gemma4.py +472 -0
  158. parse_bench/inference/providers/parse/glm_zai.py +229 -0
  159. parse_bench/inference/providers/parse/google.py +1125 -0
  160. parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
  161. parse_bench/inference/providers/parse/google_docai.py +776 -0
  162. parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
  163. parse_bench/inference/providers/parse/granite_vision.py +515 -0
  164. parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
  165. parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
  166. parse_bench/inference/providers/parse/landingai.py +452 -0
  167. parse_bench/inference/providers/parse/liteparse.py +350 -0
  168. parse_bench/inference/providers/parse/llamaparse.py +677 -0
  169. parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
  170. parse_bench/inference/providers/parse/markitdown.py +138 -0
  171. parse_bench/inference/providers/parse/mineru25.py +405 -0
  172. parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
  173. parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
  174. parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
  175. parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
  176. parse_bench/inference/providers/parse/oi_parser.py +222 -0
  177. parse_bench/inference/providers/parse/openai.py +740 -0
  178. parse_bench/inference/providers/parse/opendataloader.py +152 -0
  179. parse_bench/inference/providers/parse/paddleocr.py +624 -0
  180. parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
  181. parse_bench/inference/providers/parse/pulse.py +785 -0
  182. parse_bench/inference/providers/parse/pymupdf.py +207 -0
  183. parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
  184. parse_bench/inference/providers/parse/pypdf.py +179 -0
  185. parse_bench/inference/providers/parse/qwen.py +678 -0
  186. parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
  187. parse_bench/inference/providers/parse/reducto.py +546 -0
  188. parse_bench/inference/providers/parse/surya2.py +372 -0
  189. parse_bench/inference/providers/parse/tesseract.py +301 -0
  190. parse_bench/inference/providers/parse/textract.py +694 -0
  191. parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
  192. parse_bench/inference/providers/parse/unstructured.py +485 -0
  193. parse_bench/inference/providers/parse/warp_ingest.py +199 -0
  194. parse_bench/inference/providers/registry.py +49 -0
  195. parse_bench/inference/renormalize.py +170 -0
  196. parse_bench/inference/runner.py +2023 -0
  197. parse_bench/layout_label_mapping.py +424 -0
  198. parse_bench/layout_projection.py +179 -0
  199. parse_bench/pipeline/__init__.py +1 -0
  200. parse_bench/pipeline/cli.py +549 -0
  201. parse_bench/schemas/__init__.py +33 -0
  202. parse_bench/schemas/evaluation.py +93 -0
  203. parse_bench/schemas/extract_output.py +36 -0
  204. parse_bench/schemas/layout_detection_output.py +545 -0
  205. parse_bench/schemas/layout_ontology.py +315 -0
  206. parse_bench/schemas/metrics.py +69 -0
  207. parse_bench/schemas/parse_output.py +152 -0
  208. parse_bench/schemas/pipeline.py +22 -0
  209. parse_bench/schemas/pipeline_io.py +106 -0
  210. parse_bench/schemas/product.py +97 -0
  211. parse_bench/test_cases/__init__.py +25 -0
  212. parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
  213. parse_bench/test_cases/extract_field_paths.py +164 -0
  214. parse_bench/test_cases/layout_attribution_generation.py +287 -0
  215. parse_bench/test_cases/loader.py +652 -0
  216. parse_bench/test_cases/parse_rule_schemas.py +1071 -0
  217. parse_bench/test_cases/rule_filters.py +32 -0
  218. parse_bench/test_cases/rule_ids.py +107 -0
  219. parse_bench/test_cases/schema.py +427 -0
  220. parse_bench/utils/__init__.py +15 -0
  221. parse_bench/utils/gemini_layout_utils.py +670 -0
  222. parse_bench/utils/text_aggregation.py +100 -0
  223. parse_bench-1.0.0.dist-info/METADATA +476 -0
  224. parse_bench-1.0.0.dist-info/RECORD +227 -0
  225. parse_bench-1.0.0.dist-info/WHEEL +4 -0
  226. parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
  227. 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."""
@@ -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}")