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
|
@@ -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
|
+
)
|