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,436 @@
|
|
|
1
|
+
"""Provider for Falcon-OCR server.
|
|
2
|
+
|
|
3
|
+
Falcon-OCR (tiiuae/Falcon-OCR) is a 300M early-fusion document OCR VLM
|
|
4
|
+
with built-in layout-aware OCR via `generate_with_layout`. The server
|
|
5
|
+
exposes a simple JSON endpoint at /predict that accepts a base64 image
|
|
6
|
+
and returns assembled markdown plus per-region layout metadata.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import io
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import aiohttp
|
|
19
|
+
|
|
20
|
+
from parse_bench.inference.providers.base import (
|
|
21
|
+
Provider,
|
|
22
|
+
ProviderConfigError,
|
|
23
|
+
ProviderPermanentError,
|
|
24
|
+
ProviderTransientError,
|
|
25
|
+
)
|
|
26
|
+
from parse_bench.inference.providers.registry import register_provider
|
|
27
|
+
from parse_bench.schemas.layout_ontology import CanonicalLabel
|
|
28
|
+
from parse_bench.schemas.parse_output import (
|
|
29
|
+
LayoutItemIR,
|
|
30
|
+
LayoutSegmentIR,
|
|
31
|
+
ParseLayoutPageIR,
|
|
32
|
+
ParseOutput,
|
|
33
|
+
)
|
|
34
|
+
from parse_bench.schemas.pipeline import PipelineSpec
|
|
35
|
+
from parse_bench.schemas.pipeline_io import (
|
|
36
|
+
InferenceRequest,
|
|
37
|
+
InferenceResult,
|
|
38
|
+
RawInferenceResult,
|
|
39
|
+
)
|
|
40
|
+
from parse_bench.schemas.product import ProductType
|
|
41
|
+
|
|
42
|
+
# Falcon-OCR uses PP-DocLayoutV3 internally, so the raw region labels match
|
|
43
|
+
# the PP-DocLayoutV3 label set.
|
|
44
|
+
_FALCONOCR_LABEL_TO_CANONICAL: dict[str, tuple[str, dict[str, str]]] = {
|
|
45
|
+
"doc_title": (CanonicalLabel.TITLE.value, {"title_level": "document"}),
|
|
46
|
+
"paragraph_title": (CanonicalLabel.SECTION_HEADER.value, {"title_level": "paragraph"}),
|
|
47
|
+
"text": (CanonicalLabel.TEXT.value, {}),
|
|
48
|
+
"vertical_text": (CanonicalLabel.TEXT.value, {"text_role": "vertical"}),
|
|
49
|
+
"number": (CanonicalLabel.TEXT.value, {"text_role": "page_number"}),
|
|
50
|
+
"abstract": (CanonicalLabel.TEXT.value, {"text_role": "abstract"}),
|
|
51
|
+
"content": (CanonicalLabel.TEXT.value, {"text_role": "body"}),
|
|
52
|
+
"reference": (CanonicalLabel.TEXT.value, {"text_role": "references"}),
|
|
53
|
+
"aside_text": (CanonicalLabel.TEXT.value, {"text_role": "sidebar"}),
|
|
54
|
+
"reference_content": (CanonicalLabel.TEXT.value, {"text_role": "references"}),
|
|
55
|
+
"formula_number": (CanonicalLabel.TEXT.value, {"text_role": "formula_number"}),
|
|
56
|
+
"header": (CanonicalLabel.PAGE_HEADER.value, {"furniture": "page-header"}),
|
|
57
|
+
"header_image": (CanonicalLabel.PAGE_HEADER.value, {"furniture": "page-header"}),
|
|
58
|
+
"footer": (CanonicalLabel.PAGE_FOOTER.value, {"furniture": "page-footer"}),
|
|
59
|
+
"footer_image": (CanonicalLabel.PAGE_FOOTER.value, {"furniture": "page-footer"}),
|
|
60
|
+
"footnote": (CanonicalLabel.FOOTNOTE.value, {}),
|
|
61
|
+
"vision_footnote": (CanonicalLabel.FOOTNOTE.value, {"footnote_of": "picture"}),
|
|
62
|
+
"image": (CanonicalLabel.PICTURE.value, {"picture_type": "image"}),
|
|
63
|
+
"chart": (CanonicalLabel.PICTURE.value, {"picture_type": "chart"}),
|
|
64
|
+
"seal": (CanonicalLabel.PICTURE.value, {"picture_type": "seal"}),
|
|
65
|
+
"figure_title": (CanonicalLabel.CAPTION.value, {"caption_of": "picture"}),
|
|
66
|
+
"table": (CanonicalLabel.TABLE.value, {}),
|
|
67
|
+
"formula": (CanonicalLabel.FORMULA.value, {}),
|
|
68
|
+
"display_formula": (CanonicalLabel.FORMULA.value, {"formula_style": "display"}),
|
|
69
|
+
"inline_formula": (CanonicalLabel.FORMULA.value, {"formula_style": "inline"}),
|
|
70
|
+
"algorithm": (CanonicalLabel.CODE.value, {}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _regions_to_layout_items(regions: list[dict[str, Any]]) -> list[LayoutItemIR]:
|
|
75
|
+
"""Map Falcon-OCR `generate_with_layout` regions to LayoutItemIR.
|
|
76
|
+
|
|
77
|
+
Each region is `{category, bbox: [x0,y0,x1,y1], score, text}` where text
|
|
78
|
+
already has markdown formatting baked in by the model.
|
|
79
|
+
"""
|
|
80
|
+
items: list[LayoutItemIR] = []
|
|
81
|
+
for region in regions:
|
|
82
|
+
label_raw = str(region.get("category", "")).strip().lower()
|
|
83
|
+
mapping = _FALCONOCR_LABEL_TO_CANONICAL.get(label_raw)
|
|
84
|
+
if mapping is None:
|
|
85
|
+
continue
|
|
86
|
+
canonical, _attrs = mapping
|
|
87
|
+
|
|
88
|
+
bbox = region.get("bbox")
|
|
89
|
+
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
|
|
90
|
+
continue
|
|
91
|
+
try:
|
|
92
|
+
x1, y1, x2, y2 = (float(v) for v in bbox)
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
score = float(region.get("score", 1.0))
|
|
98
|
+
except (TypeError, ValueError):
|
|
99
|
+
score = 1.0
|
|
100
|
+
score = max(0.0, min(1.0, score))
|
|
101
|
+
|
|
102
|
+
seg = LayoutSegmentIR(
|
|
103
|
+
x=x1,
|
|
104
|
+
y=y1,
|
|
105
|
+
w=max(0.0, x2 - x1),
|
|
106
|
+
h=max(0.0, y2 - y1),
|
|
107
|
+
confidence=score,
|
|
108
|
+
label=canonical,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
text = region.get("text") or ""
|
|
112
|
+
item_md = ""
|
|
113
|
+
item_html = ""
|
|
114
|
+
item_value = ""
|
|
115
|
+
norm = canonical.strip().lower()
|
|
116
|
+
if text and norm != "picture":
|
|
117
|
+
if norm == "table":
|
|
118
|
+
item_html = str(text)
|
|
119
|
+
item_type = "table"
|
|
120
|
+
else:
|
|
121
|
+
item_md = str(text)
|
|
122
|
+
item_value = str(text)
|
|
123
|
+
item_type = "text"
|
|
124
|
+
elif norm == "picture":
|
|
125
|
+
item_type = "image"
|
|
126
|
+
else:
|
|
127
|
+
item_type = "text"
|
|
128
|
+
|
|
129
|
+
items.append(
|
|
130
|
+
LayoutItemIR(
|
|
131
|
+
type=item_type,
|
|
132
|
+
md=item_md,
|
|
133
|
+
html=item_html,
|
|
134
|
+
value=item_value,
|
|
135
|
+
bbox=seg,
|
|
136
|
+
layout_segments=[seg],
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return items
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@register_provider("falconocr")
|
|
143
|
+
class FalconOcrProvider(Provider):
|
|
144
|
+
"""Provider for Falcon-OCR server.
|
|
145
|
+
|
|
146
|
+
Configuration options:
|
|
147
|
+
- server_url (str): server URL root (no /predict). Falls back to
|
|
148
|
+
the ``FALCONOCR_SERVER_URL`` environment variable.
|
|
149
|
+
- task (str, default="ocr"): "ocr" (layout-aware) or a generate()
|
|
150
|
+
category like "plain", "text", "table", "formula".
|
|
151
|
+
- timeout (int, default=600): Request timeout in seconds.
|
|
152
|
+
- dpi (int, default=200): DPI for PDF-to-image conversion.
|
|
153
|
+
- max_new_tokens (int, default=4096): Generation budget.
|
|
154
|
+
- temperature (float, default=0.0): Sampling temperature.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, provider_name: str, base_config: dict[str, Any] | None = None):
|
|
158
|
+
super().__init__(provider_name, base_config)
|
|
159
|
+
|
|
160
|
+
server_url = self.base_config.get("server_url") or os.getenv("FALCONOCR_SERVER_URL")
|
|
161
|
+
if not server_url:
|
|
162
|
+
raise ProviderConfigError(
|
|
163
|
+
"FalconOCR provider requires 'server_url' in config or FALCONOCR_SERVER_URL in the environment."
|
|
164
|
+
)
|
|
165
|
+
self._server_url: str = str(server_url).rstrip("/")
|
|
166
|
+
self._task: str = str(self.base_config.get("task", "ocr"))
|
|
167
|
+
self._timeout = int(self.base_config.get("timeout", 600))
|
|
168
|
+
self._dpi = int(self.base_config.get("dpi", 200))
|
|
169
|
+
self._max_new_tokens = int(self.base_config.get("max_new_tokens", 4096))
|
|
170
|
+
self._temperature = float(self.base_config.get("temperature", 0.0))
|
|
171
|
+
|
|
172
|
+
def _pdf_to_images(self, pdf_path: Path) -> list[bytes]:
|
|
173
|
+
"""Render every PDF page to PNG bytes in source order."""
|
|
174
|
+
try:
|
|
175
|
+
from pdf2image import convert_from_path
|
|
176
|
+
|
|
177
|
+
images = convert_from_path(pdf_path, dpi=self._dpi)
|
|
178
|
+
if not images:
|
|
179
|
+
raise ProviderPermanentError(f"No pages found in PDF: {pdf_path}")
|
|
180
|
+
encoded: list[bytes] = []
|
|
181
|
+
for image in images:
|
|
182
|
+
buf = io.BytesIO()
|
|
183
|
+
image.save(buf, format="PNG")
|
|
184
|
+
encoded.append(buf.getvalue())
|
|
185
|
+
return encoded
|
|
186
|
+
except ImportError as e:
|
|
187
|
+
raise ProviderPermanentError("pdf2image is required. Install with: pip install pdf2image") from e
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if "pdf2image" in str(e).lower():
|
|
190
|
+
raise
|
|
191
|
+
raise ProviderPermanentError(f"Error converting PDF to image: {e}") from e
|
|
192
|
+
|
|
193
|
+
def _read_image(self, file_path: Path) -> bytes:
|
|
194
|
+
try:
|
|
195
|
+
return file_path.read_bytes()
|
|
196
|
+
except Exception as e:
|
|
197
|
+
raise ProviderPermanentError(f"Error reading image file: {e}") from e
|
|
198
|
+
|
|
199
|
+
async def _call_api(self, session: aiohttp.ClientSession, image_b64: str) -> dict[str, Any]:
|
|
200
|
+
api_url = f"{self._server_url}/predict"
|
|
201
|
+
payload = {
|
|
202
|
+
"image_base64": image_b64,
|
|
203
|
+
"task": self._task,
|
|
204
|
+
"max_new_tokens": self._max_new_tokens,
|
|
205
|
+
"temperature": self._temperature,
|
|
206
|
+
}
|
|
207
|
+
async with session.post(
|
|
208
|
+
api_url,
|
|
209
|
+
json=payload,
|
|
210
|
+
headers={"Content-Type": "application/json"},
|
|
211
|
+
timeout=aiohttp.ClientTimeout(total=self._timeout),
|
|
212
|
+
) as resp:
|
|
213
|
+
if resp.status != 200:
|
|
214
|
+
error_text = await resp.text()
|
|
215
|
+
if resp.status in (408, 502, 503, 504):
|
|
216
|
+
raise ProviderTransientError(f"HTTP {resp.status}: {error_text[:200]}")
|
|
217
|
+
raise ProviderPermanentError(f"HTTP {resp.status}: {error_text[:200]}")
|
|
218
|
+
result: dict[str, Any] = await resp.json()
|
|
219
|
+
|
|
220
|
+
if result.get("status") != "success":
|
|
221
|
+
raise ProviderPermanentError(
|
|
222
|
+
f"Server returned status={result.get('status')}: {str(result.get('error'))[:200]}"
|
|
223
|
+
)
|
|
224
|
+
return result
|
|
225
|
+
|
|
226
|
+
async def _run_inference_async(self, image_bytes: bytes) -> dict[str, Any]:
|
|
227
|
+
image_b64 = base64.b64encode(image_bytes).decode()
|
|
228
|
+
|
|
229
|
+
async with aiohttp.ClientSession() as session:
|
|
230
|
+
response = await self._call_api(session, image_b64)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"markdown": response.get("markdown", ""),
|
|
234
|
+
"regions": response.get("regions", []),
|
|
235
|
+
"image_width": response.get("image_width"),
|
|
236
|
+
"image_height": response.get("image_height"),
|
|
237
|
+
"_task_used": response.get("task"),
|
|
238
|
+
"_config": {
|
|
239
|
+
"server_url": self._server_url,
|
|
240
|
+
"task": self._task,
|
|
241
|
+
"dpi": self._dpi,
|
|
242
|
+
"max_new_tokens": self._max_new_tokens,
|
|
243
|
+
"temperature": self._temperature,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async def _run_inference_pages_async(self, pages: list[bytes]) -> dict[str, Any]:
|
|
248
|
+
"""Run each input page in order, retaining the legacy one-page shape."""
|
|
249
|
+
results = [await self._run_inference_async(page) for page in pages]
|
|
250
|
+
first = results[0]
|
|
251
|
+
if len(results) == 1:
|
|
252
|
+
return first
|
|
253
|
+
merged = dict(first)
|
|
254
|
+
merged["page_results"] = results
|
|
255
|
+
return merged
|
|
256
|
+
|
|
257
|
+
def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
|
|
258
|
+
if request.product_type != ProductType.PARSE:
|
|
259
|
+
raise ProviderPermanentError(
|
|
260
|
+
f"FalconOcrProvider only supports PARSE product type, got {request.product_type}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
started_at = datetime.now()
|
|
264
|
+
|
|
265
|
+
file_path = Path(request.source_file_path)
|
|
266
|
+
if not file_path.exists():
|
|
267
|
+
raise ProviderPermanentError(f"Source file not found: {file_path}")
|
|
268
|
+
|
|
269
|
+
suffix = file_path.suffix.lower()
|
|
270
|
+
if suffix == ".pdf":
|
|
271
|
+
page_images = self._pdf_to_images(file_path)
|
|
272
|
+
elif suffix in (".png", ".jpg", ".jpeg", ".webp", ".tiff", ".bmp"):
|
|
273
|
+
page_images = [self._read_image(file_path)]
|
|
274
|
+
else:
|
|
275
|
+
raise ProviderPermanentError(
|
|
276
|
+
f"Unsupported file type: {suffix}. Supported: .pdf, .png, .jpg, .jpeg, .webp, .tiff, .bmp"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
raw_output = asyncio.run(self._run_inference_pages_async(page_images))
|
|
281
|
+
completed_at = datetime.now()
|
|
282
|
+
latency_ms = int((completed_at - started_at).total_seconds() * 1000)
|
|
283
|
+
|
|
284
|
+
return RawInferenceResult(
|
|
285
|
+
request=request,
|
|
286
|
+
pipeline=pipeline,
|
|
287
|
+
pipeline_name=pipeline.pipeline_name,
|
|
288
|
+
product_type=request.product_type,
|
|
289
|
+
raw_output=raw_output,
|
|
290
|
+
started_at=started_at,
|
|
291
|
+
completed_at=completed_at,
|
|
292
|
+
latency_in_ms=latency_ms,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
except (ProviderPermanentError, ProviderTransientError):
|
|
296
|
+
raise
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
completed_at = datetime.now()
|
|
300
|
+
latency_ms = int((completed_at - started_at).total_seconds() * 1000)
|
|
301
|
+
error_msg = str(e)
|
|
302
|
+
if isinstance(e, asyncio.TimeoutError):
|
|
303
|
+
error_msg = f"Request timed out after {self._timeout} seconds"
|
|
304
|
+
return RawInferenceResult(
|
|
305
|
+
request=request,
|
|
306
|
+
pipeline=pipeline,
|
|
307
|
+
pipeline_name=pipeline.pipeline_name,
|
|
308
|
+
product_type=request.product_type,
|
|
309
|
+
raw_output={
|
|
310
|
+
"markdown": "",
|
|
311
|
+
"_error": error_msg,
|
|
312
|
+
"_error_type": type(e).__name__,
|
|
313
|
+
"_config": {"server_url": self._server_url, "dpi": self._dpi},
|
|
314
|
+
},
|
|
315
|
+
started_at=started_at,
|
|
316
|
+
completed_at=completed_at,
|
|
317
|
+
latency_in_ms=latency_ms,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _sanitize_html_attributes(markdown: str) -> str:
|
|
322
|
+
"""Quote unquoted HTML attributes for XML-based metric parsers."""
|
|
323
|
+
|
|
324
|
+
def _quote_attrs(match: re.Match) -> str:
|
|
325
|
+
tag_text = match.group(0)
|
|
326
|
+
tag_text = re.sub(
|
|
327
|
+
r'(\w+)=([^\s"\'<>=]+)',
|
|
328
|
+
r'\1="\2"',
|
|
329
|
+
tag_text,
|
|
330
|
+
)
|
|
331
|
+
return tag_text
|
|
332
|
+
|
|
333
|
+
return re.sub(r"<[^>]+>", _quote_attrs, markdown)
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def _convert_md_tables_to_html(content: str) -> str:
|
|
337
|
+
"""Convert markdown pipe tables to HTML <table> elements.
|
|
338
|
+
|
|
339
|
+
Falcon-OCR's table category emits HTML <table> directly, but mixed
|
|
340
|
+
outputs (e.g. plain task on a doc with tables) may include pipe
|
|
341
|
+
tables. GriTS/TEDS metrics only parse HTML, so we convert.
|
|
342
|
+
"""
|
|
343
|
+
import markdown2
|
|
344
|
+
|
|
345
|
+
lines = content.split("\n")
|
|
346
|
+
result_parts: list[str] = []
|
|
347
|
+
table_lines: list[str] = []
|
|
348
|
+
in_table = False
|
|
349
|
+
|
|
350
|
+
for line in lines:
|
|
351
|
+
is_table_line = "|" in line and line.strip().startswith("|")
|
|
352
|
+
if is_table_line:
|
|
353
|
+
if not in_table:
|
|
354
|
+
in_table = True
|
|
355
|
+
table_lines = [line]
|
|
356
|
+
else:
|
|
357
|
+
table_lines.append(line)
|
|
358
|
+
else:
|
|
359
|
+
if in_table:
|
|
360
|
+
if len(table_lines) >= 2:
|
|
361
|
+
table_md = "\n".join(table_lines)
|
|
362
|
+
html = markdown2.markdown(table_md, extras=["tables"]).strip()
|
|
363
|
+
if "<table>" in html.lower():
|
|
364
|
+
result_parts.append(html)
|
|
365
|
+
else:
|
|
366
|
+
result_parts.extend(table_lines)
|
|
367
|
+
else:
|
|
368
|
+
result_parts.extend(table_lines)
|
|
369
|
+
table_lines = []
|
|
370
|
+
in_table = False
|
|
371
|
+
result_parts.append(line)
|
|
372
|
+
|
|
373
|
+
if in_table and len(table_lines) >= 2:
|
|
374
|
+
table_md = "\n".join(table_lines)
|
|
375
|
+
html = markdown2.markdown(table_md, extras=["tables"]).strip()
|
|
376
|
+
if "<table>" in html.lower():
|
|
377
|
+
result_parts.append(html)
|
|
378
|
+
else:
|
|
379
|
+
result_parts.extend(table_lines)
|
|
380
|
+
elif in_table:
|
|
381
|
+
result_parts.extend(table_lines)
|
|
382
|
+
|
|
383
|
+
return "\n".join(result_parts)
|
|
384
|
+
|
|
385
|
+
def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
|
|
386
|
+
if raw_result.product_type != ProductType.PARSE:
|
|
387
|
+
raise ProviderPermanentError(
|
|
388
|
+
f"FalconOcrProvider only supports PARSE product type, got {raw_result.product_type}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
page_results = raw_result.raw_output.get("page_results")
|
|
392
|
+
if not isinstance(page_results, list) or not page_results:
|
|
393
|
+
page_results = [raw_result.raw_output]
|
|
394
|
+
|
|
395
|
+
page_markdowns: list[str] = []
|
|
396
|
+
layout_pages: list[ParseLayoutPageIR] = []
|
|
397
|
+
for page_number, page_raw in enumerate(page_results, start=1):
|
|
398
|
+
markdown = page_raw.get("markdown", "")
|
|
399
|
+
if markdown:
|
|
400
|
+
markdown = self._convert_md_tables_to_html(markdown)
|
|
401
|
+
markdown = self._sanitize_html_attributes(markdown)
|
|
402
|
+
page_markdowns.append(markdown)
|
|
403
|
+
|
|
404
|
+
image_width = max(int(page_raw.get("image_width") or 1), 1)
|
|
405
|
+
image_height = max(int(page_raw.get("image_height") or 1), 1)
|
|
406
|
+
items = _regions_to_layout_items(page_raw.get("regions") or [])
|
|
407
|
+
if items:
|
|
408
|
+
layout_pages.append(
|
|
409
|
+
ParseLayoutPageIR(
|
|
410
|
+
page_number=page_number,
|
|
411
|
+
width=float(image_width),
|
|
412
|
+
height=float(image_height),
|
|
413
|
+
items=items,
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
markdown = "\n\n".join(page_markdowns)
|
|
417
|
+
|
|
418
|
+
output = ParseOutput(
|
|
419
|
+
task_type="parse",
|
|
420
|
+
example_id=raw_result.request.example_id,
|
|
421
|
+
pipeline_name=raw_result.pipeline_name,
|
|
422
|
+
pages=[],
|
|
423
|
+
markdown=markdown,
|
|
424
|
+
layout_pages=layout_pages,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return InferenceResult(
|
|
428
|
+
request=raw_result.request,
|
|
429
|
+
pipeline_name=raw_result.pipeline_name,
|
|
430
|
+
product_type=raw_result.product_type,
|
|
431
|
+
raw_output=raw_result.raw_output,
|
|
432
|
+
output=output,
|
|
433
|
+
started_at=raw_result.started_at,
|
|
434
|
+
completed_at=raw_result.completed_at,
|
|
435
|
+
latency_in_ms=raw_result.latency_in_ms,
|
|
436
|
+
)
|