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
@@ -0,0 +1,350 @@
1
+ """Provider for LiteParse PARSE."""
2
+
3
+ import json as _json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from parse_bench.inference.providers.base import (
12
+ Provider,
13
+ ProviderConfigError,
14
+ ProviderPermanentError,
15
+ )
16
+ from parse_bench.inference.providers.registry import register_provider
17
+ from parse_bench.schemas.parse_output import LayoutItemIR, LayoutSegmentIR, PageIR, ParseLayoutPageIR, ParseOutput
18
+ from parse_bench.schemas.pipeline import PipelineSpec
19
+ from parse_bench.schemas.pipeline_io import (
20
+ InferenceRequest,
21
+ InferenceResult,
22
+ RawInferenceResult,
23
+ )
24
+ from parse_bench.schemas.product import ProductType
25
+
26
+ # Path to the `lit` CLI binary. Resolution order: ``LITEPARSE_BIN`` env var,
27
+ # then ``lit`` on ``PATH``. Falls back to a plain ``lit`` so the error message
28
+ # below still names the binary when neither is set.
29
+ _LIT_BIN = Path(os.environ.get("LITEPARSE_BIN") or shutil.which("lit") or "lit")
30
+
31
+ # Kwargs accepted in base_config that map directly to CLI flags.
32
+ _CLI_FLAG_MAP: dict[str, str] = {
33
+ "ocr_server_url": "--ocr-server-url",
34
+ "ocr_language": "--ocr-language",
35
+ "tessdata_path": "--tessdata-path",
36
+ "dpi": "--dpi",
37
+ "num_workers": "--num-workers",
38
+ "image_mode": "--image-mode",
39
+ }
40
+
41
+ # ``lit --extract-blocks`` block kinds -> LayoutItemIR.type. Kinds with no
42
+ # visual region of their own (``rule``) are skipped. Labels are the benchmark's
43
+ # canonical names so the layout label mapper is a straight lookup.
44
+ _BLOCK_KIND_TO_LABEL: dict[str, str] = {
45
+ "heading": "Section-header",
46
+ "paragraph": "Text",
47
+ "list_item": "List-item",
48
+ "table": "Table",
49
+ "grid_fallback": "Table",
50
+ "figure": "Picture",
51
+ "code": "Code",
52
+ }
53
+
54
+
55
+ def _block_text(block: dict[str, Any]) -> str:
56
+ """Best-effort text for a block: ``text`` for prose, cell text for tables."""
57
+ text = block.get("text")
58
+ if isinstance(text, str) and text:
59
+ return text
60
+ if block.get("kind") in {"table", "grid_fallback"}:
61
+ rows = []
62
+ header = block.get("header")
63
+ if isinstance(header, list):
64
+ rows.append(header)
65
+ rows.extend(r for r in (block.get("rows") or []) if isinstance(r, list))
66
+ lines = []
67
+ for row in rows:
68
+ cells = [str(c.get("text", "") if isinstance(c, dict) else c) for c in row]
69
+ lines.append(" | ".join(cells))
70
+ return "\n".join(lines)
71
+ if block.get("kind") == "code":
72
+ lines = block.get("lines")
73
+ if isinstance(lines, list):
74
+ return "\n".join(str(line) for line in lines)
75
+ return ""
76
+
77
+
78
+ def _build_layout_pages(pages_raw: list[dict[str, Any]]) -> list[ParseLayoutPageIR]:
79
+ """Convert ``lit --extract-blocks`` output into normalized layout pages.
80
+
81
+ Block bboxes are ``{x, y, width, height}`` in PDF points on the page's own
82
+ coordinate system; they are normalized by the page size to the [0, 1]
83
+ frame ``LayoutSegmentIR`` expects.
84
+ """
85
+ layout_pages: list[ParseLayoutPageIR] = []
86
+ for page_data in pages_raw:
87
+ blocks = page_data.get("blocks") or []
88
+ width = page_data.get("width")
89
+ height = page_data.get("height")
90
+ if not blocks or not isinstance(width, int | float) or not isinstance(height, int | float):
91
+ continue
92
+ if width <= 0 or height <= 0:
93
+ continue
94
+ items: list[LayoutItemIR] = []
95
+ for block in blocks:
96
+ if not isinstance(block, dict):
97
+ continue
98
+ label = _BLOCK_KIND_TO_LABEL.get(str(block.get("kind", "")))
99
+ bbox = block.get("bbox")
100
+ if label is None or not isinstance(bbox, dict):
101
+ continue
102
+ try:
103
+ x = float(bbox["x"]) / width
104
+ y = float(bbox["y"]) / height
105
+ w = float(bbox["width"]) / width
106
+ h = float(bbox["height"]) / height
107
+ except (KeyError, TypeError, ValueError):
108
+ continue
109
+ if w <= 0 or h <= 0:
110
+ continue
111
+ segment = LayoutSegmentIR(x=x, y=y, w=w, h=h, confidence=1.0, label=label)
112
+ text = _block_text(block)
113
+ items.append(
114
+ LayoutItemIR(
115
+ type=label,
116
+ value=text,
117
+ md=text,
118
+ bbox=segment,
119
+ layout_segments=[segment],
120
+ )
121
+ )
122
+ if not items:
123
+ continue
124
+ layout_pages.append(
125
+ ParseLayoutPageIR(
126
+ page_number=int(page_data.get("page_index", 0)) + 1,
127
+ width=float(width),
128
+ height=float(height),
129
+ md=page_data.get("text", "") or "",
130
+ items=items,
131
+ )
132
+ )
133
+ return layout_pages
134
+
135
+
136
+ @register_provider("liteparse")
137
+ class LiteParseProvider(Provider):
138
+ """
139
+ Provider for LiteParse PARSE.
140
+
141
+ Uses the local in-process LiteParse Python bindings (no API key required).
142
+ """
143
+
144
+ def __init__(self, provider_name: str, base_config: dict[str, Any] | None = None):
145
+ """
146
+ Initialize the provider.
147
+
148
+ :param provider_name: Name of the provider
149
+ :param base_config: Optional configuration. Recognized keys:
150
+ - `output_format`: "markdown" | "text" | "json" (default: "markdown")
151
+ - `ocr_enabled`: bool
152
+ - `ocr_server_url`: str
153
+ - `ocr_language`: str
154
+ - `dpi`: float
155
+ """
156
+ super().__init__(provider_name, base_config)
157
+ self._output_format = self.base_config.get("output_format", "markdown")
158
+ self._ocr_enabled = self.base_config.get("ocr_enabled", True)
159
+ self._preserve_small_text = self.base_config.get("preserve_very_small_text", False)
160
+ self._extract_blocks = bool(self.base_config.get("extract_blocks", True))
161
+ self._flag_kwargs = {k: v for k, v in self.base_config.items() if k in _CLI_FLAG_MAP and v is not None}
162
+
163
+ def _build_cli_args(self, pdf_path: str, fmt: str) -> list[str]:
164
+ args: list[str] = [str(_LIT_BIN), "parse", pdf_path, "--format", fmt, "--quiet"]
165
+ if fmt == "json" and self._extract_blocks:
166
+ # Layout blocks (kind + bbox in points) feed ParseOutput.layout_pages
167
+ # so the Visual Grounding metrics can score LiteParse.
168
+ args.append("--extract-blocks")
169
+ # Ground truth uses plain text (no markdown link syntax), so disable
170
+ # hyperlink extraction for benchmark parity.
171
+ args.append("--no-links")
172
+ if not self._ocr_enabled:
173
+ args.append("--no-ocr")
174
+ if self._preserve_small_text:
175
+ args.append("--preserve-small-text")
176
+ for key, flag in _CLI_FLAG_MAP.items():
177
+ if key in self._flag_kwargs:
178
+ args.extend([flag, str(self._flag_kwargs[key])])
179
+ return args
180
+
181
+ def _invoke_cli(self, pdf_path: str, fmt: str) -> str:
182
+ try:
183
+ proc = subprocess.run(
184
+ self._build_cli_args(pdf_path, fmt),
185
+ capture_output=True,
186
+ text=True,
187
+ check=False,
188
+ )
189
+ except FileNotFoundError as e:
190
+ raise ProviderConfigError(
191
+ f"liteparse CLI binary not found at {_LIT_BIN}. "
192
+ f"Install it with `pip install liteparse` (or `parse-bench[liteparse]`), "
193
+ f"or point LITEPARSE_BIN at a `lit` binary."
194
+ ) from e
195
+ if proc.returncode != 0:
196
+ raise ProviderPermanentError(
197
+ f"lit parse --format {fmt} failed (exit {proc.returncode}): "
198
+ f"{proc.stderr.strip() or proc.stdout.strip()}"
199
+ )
200
+ return proc.stdout
201
+
202
+ def _parse(self, pdf_path: str) -> dict[str, Any]:
203
+ json_stdout = self._invoke_cli(pdf_path, "json")
204
+ try:
205
+ parsed = _json.loads(json_stdout)
206
+ except _json.JSONDecodeError as e:
207
+ raise ProviderPermanentError(f"Failed to parse lit JSON output: {e}") from e
208
+
209
+ pages_raw = parsed.get("pages", []) or []
210
+ pages = [
211
+ {
212
+ "page_index": (p.get("page", 1) - 1) if isinstance(p.get("page"), int) and p["page"] > 0 else 0,
213
+ "text": p.get("text", "") or "",
214
+ "width": p.get("width"),
215
+ "height": p.get("height"),
216
+ "blocks": p.get("blocks") or [],
217
+ }
218
+ for p in pages_raw
219
+ ]
220
+
221
+ if self._output_format == "json":
222
+ full_text = ""
223
+ elif self._output_format == "markdown":
224
+ full_text = self._invoke_cli(pdf_path, "markdown")
225
+ else:
226
+ full_text = "\n".join(p["text"] for p in pages)
227
+
228
+ return {
229
+ "pages": pages,
230
+ "num_pages": len(pages),
231
+ "text": full_text,
232
+ "output_format": self._output_format,
233
+ }
234
+
235
+ def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
236
+ if request.product_type != ProductType.PARSE:
237
+ raise ProviderPermanentError(
238
+ f"LiteParseProvider only supports PARSE product type, got {request.product_type}"
239
+ )
240
+
241
+ pdf_path = Path(request.source_file_path)
242
+ if not pdf_path.exists():
243
+ raise ProviderPermanentError(f"File not found: {pdf_path}")
244
+
245
+ started_at = datetime.now()
246
+ try:
247
+ raw_output = self._parse(str(pdf_path))
248
+ completed_at = datetime.now()
249
+ latency_ms = int((completed_at - started_at).total_seconds() * 1000)
250
+
251
+ return RawInferenceResult(
252
+ request=request,
253
+ pipeline=pipeline,
254
+ pipeline_name=pipeline.pipeline_name,
255
+ product_type=request.product_type,
256
+ raw_output=raw_output,
257
+ started_at=started_at,
258
+ completed_at=completed_at,
259
+ latency_in_ms=latency_ms,
260
+ )
261
+ except (ProviderPermanentError, ProviderConfigError):
262
+ raise
263
+ except Exception as e:
264
+ raise ProviderPermanentError(f"Unexpected error during inference: {e}") from e
265
+
266
+ @staticmethod
267
+ def _convert_md_tables_to_html(content: str) -> str:
268
+ """Convert markdown pipe tables to HTML so GriTS / TRM can score them.
269
+
270
+ ParseBench's table metrics only see ``<table>`` blocks; pipe tables in
271
+ markdown are invisible to them. We convert here at the boundary rather
272
+ than changing liteparse's emitter.
273
+ """
274
+ import markdown2
275
+
276
+ lines = content.split("\n")
277
+ result_parts: list[str] = []
278
+ table_lines: list[str] = []
279
+ in_table = False
280
+
281
+ def _flush() -> None:
282
+ nonlocal table_lines
283
+ if len(table_lines) >= 2:
284
+ table_md = "\n".join(table_lines)
285
+ html = markdown2.markdown(table_md, extras=["tables"]).strip()
286
+ if "<table>" in html.lower():
287
+ result_parts.append(html)
288
+ else:
289
+ result_parts.extend(table_lines)
290
+ else:
291
+ result_parts.extend(table_lines)
292
+ table_lines = []
293
+
294
+ for line in lines:
295
+ is_table_line = "|" in line and line.strip().startswith("|")
296
+ if is_table_line:
297
+ in_table = True
298
+ table_lines.append(line)
299
+ else:
300
+ if in_table:
301
+ _flush()
302
+ in_table = False
303
+ result_parts.append(line)
304
+
305
+ if in_table:
306
+ _flush()
307
+
308
+ return "\n".join(result_parts)
309
+
310
+ def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
311
+ if raw_result.product_type != ProductType.PARSE:
312
+ raise ProviderPermanentError(
313
+ f"LiteParseProvider only supports PARSE product type, got {raw_result.product_type}"
314
+ )
315
+
316
+ convert_tables = self._output_format == "markdown"
317
+
318
+ pages: list[PageIR] = []
319
+ page_texts: list[str] = []
320
+ for page_data in raw_result.raw_output.get("pages", []):
321
+ page_index = page_data.get("page_index", 0)
322
+ text = page_data.get("text", "") or ""
323
+ if convert_tables:
324
+ text = self._convert_md_tables_to_html(text)
325
+ pages.append(PageIR(page_index=page_index, markdown=text))
326
+ page_texts.append(text)
327
+
328
+ full_text = raw_result.raw_output.get("text") or "\n\n".join(page_texts)
329
+ if convert_tables and full_text:
330
+ full_text = self._convert_md_tables_to_html(full_text)
331
+
332
+ output = ParseOutput(
333
+ task_type="parse",
334
+ example_id=raw_result.request.example_id,
335
+ pipeline_name=raw_result.pipeline_name,
336
+ pages=pages,
337
+ layout_pages=_build_layout_pages(raw_result.raw_output.get("pages", [])),
338
+ markdown=full_text,
339
+ )
340
+
341
+ return InferenceResult(
342
+ request=raw_result.request,
343
+ pipeline_name=raw_result.pipeline_name,
344
+ product_type=raw_result.product_type,
345
+ raw_output=raw_result.raw_output,
346
+ output=output,
347
+ started_at=raw_result.started_at,
348
+ completed_at=raw_result.completed_at,
349
+ latency_in_ms=raw_result.latency_in_ms,
350
+ )