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,268 @@
1
+ """Provider for Chunkr PARSE."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from parse_bench.inference.providers.base import (
9
+ Provider,
10
+ ProviderConfigError,
11
+ ProviderPermanentError,
12
+ ProviderTransientError,
13
+ )
14
+ from parse_bench.inference.providers.registry import register_provider
15
+ from parse_bench.schemas.parse_output import ParseOutput
16
+ from parse_bench.schemas.pipeline import PipelineSpec
17
+ from parse_bench.schemas.pipeline_io import (
18
+ InferenceRequest,
19
+ InferenceResult,
20
+ RawInferenceResult,
21
+ )
22
+ from parse_bench.schemas.product import ProductType
23
+
24
+
25
+ @register_provider("chunkr")
26
+ class ChunkrProvider(Provider):
27
+ """
28
+ Provider for Chunkr PARSE.
29
+
30
+ Uses the Chunkr API for parsing documents with HTML table output.
31
+ """
32
+
33
+ def __init__(self, provider_name: str, base_config: dict[str, Any] | None = None):
34
+ """
35
+ Initialize the provider.
36
+
37
+ :param provider_name: Name of the provider
38
+ :param base_config: Optional configuration with:
39
+ - `api_key`: Chunkr API key (defaults to CHUNKR_API_KEY env var)
40
+ - `segmentation_strategy`: "LayoutAnalysis" or "Page" (default: "LayoutAnalysis")
41
+ - `ocr_strategy`: "Auto" or "All" (default: "Auto")
42
+ - `high_resolution`: Whether to use high resolution processing (default: False)
43
+ """
44
+ super().__init__(provider_name, base_config)
45
+
46
+ # Get API key
47
+ self._api_key = self.base_config.get("api_key") or os.getenv("CHUNKR_API_KEY")
48
+ if not self._api_key:
49
+ raise ProviderConfigError(
50
+ "Chunkr API key is required. Set CHUNKR_API_KEY environment variable or pass api_key in base_config."
51
+ )
52
+
53
+ # Configuration options
54
+ self._segmentation_strategy = self.base_config.get("segmentation_strategy", "LayoutAnalysis")
55
+ self._ocr_strategy = self.base_config.get("ocr_strategy", "Auto")
56
+ self._high_resolution = self.base_config.get("high_resolution", False)
57
+
58
+ async def _parse_document_async(self, file_path: str) -> dict[str, Any]:
59
+ """
60
+ Parse a document using Chunkr API (async).
61
+
62
+ :param file_path: Path to the document file
63
+ :return: Raw API response as dictionary
64
+ :raises ProviderError: For any API errors
65
+ """
66
+ try:
67
+ from chunkr_ai import Chunkr # type: ignore[import-untyped]
68
+ from chunkr_ai.models import ( # type: ignore[import-untyped]
69
+ Configuration,
70
+ OcrStrategy,
71
+ SegmentationStrategy,
72
+ )
73
+
74
+ # Map string config values to enum values
75
+ segmentation_map = {
76
+ "layoutanalysis": SegmentationStrategy.LAYOUT_ANALYSIS,
77
+ "layout_analysis": SegmentationStrategy.LAYOUT_ANALYSIS,
78
+ "page": SegmentationStrategy.PAGE,
79
+ }
80
+ ocr_map = {
81
+ "auto": OcrStrategy.AUTO,
82
+ "all": OcrStrategy.ALL,
83
+ }
84
+
85
+ seg_strategy = segmentation_map.get(
86
+ self._segmentation_strategy.lower(),
87
+ SegmentationStrategy.LAYOUT_ANALYSIS,
88
+ )
89
+ ocr_strategy = ocr_map.get(
90
+ self._ocr_strategy.lower(),
91
+ OcrStrategy.AUTO,
92
+ )
93
+
94
+ # Initialize client
95
+ client = Chunkr(api_key=self._api_key)
96
+
97
+ try:
98
+ # Configure for HTML output (tables are HTML by default in Chunkr)
99
+ config = Configuration(
100
+ segmentation_strategy=seg_strategy,
101
+ ocr_strategy=ocr_strategy,
102
+ high_resolution=self._high_resolution,
103
+ )
104
+
105
+ # Upload and process
106
+ # Note: The Chunkr SDK is async-native with an @anywhere() decorator.
107
+ # We must call it directly as async (not via asyncio.to_thread) to avoid
108
+ # race conditions with the SDK's global _sync_loop singleton.
109
+ task = await client.upload(file_path, config)
110
+
111
+ # poll() ensures task is complete (no-op if already complete)
112
+ if hasattr(task, "poll") and callable(task.poll):
113
+ task = await task.poll()
114
+
115
+ # Extract raw response
116
+ if hasattr(task, "model_dump"):
117
+ raw_response = task.model_dump()
118
+ elif hasattr(task, "dict"):
119
+ raw_response = task.dict()
120
+ else:
121
+ # Manual extraction as fallback
122
+ raw_response = {
123
+ "task_id": getattr(task, "task_id", None),
124
+ "status": getattr(task, "status", None),
125
+ "output": getattr(task, "output", None),
126
+ }
127
+
128
+ # Get HTML content (includes tables as HTML)
129
+ try:
130
+ html_content = await task.html() if hasattr(task, "html") else ""
131
+ except Exception:
132
+ html_content = ""
133
+ raw_response["_html_content"] = html_content
134
+
135
+ # Get markdown content as alternative
136
+ try:
137
+ markdown_content = await task.markdown() if hasattr(task, "markdown") else ""
138
+ except Exception:
139
+ markdown_content = ""
140
+ raw_response["_markdown_content"] = markdown_content
141
+
142
+ # Store configuration for reference
143
+ raw_response["_config"] = {
144
+ "segmentation_strategy": self._segmentation_strategy,
145
+ "ocr_strategy": self._ocr_strategy,
146
+ "high_resolution": self._high_resolution,
147
+ }
148
+
149
+ result: dict[str, Any] = raw_response
150
+ return result
151
+
152
+ finally:
153
+ # Close the client (async method with @anywhere decorator)
154
+ await client.close()
155
+
156
+ except ImportError as e:
157
+ raise ProviderConfigError("chunkr-ai package not installed. Run: pip install chunkr-ai") from e
158
+ except Exception as e:
159
+ error_str = str(e).lower()
160
+ transient_keywords = ["timeout", "network", "connection", "503", "502", "504"]
161
+ if any(keyword in error_str for keyword in transient_keywords):
162
+ raise ProviderTransientError(f"Transient error during parsing: {e}") from e
163
+ raise ProviderPermanentError(f"Error during parsing: {e}") from e
164
+
165
+ def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
166
+ """
167
+ Run inference and return raw results.
168
+
169
+ :param pipeline: Pipeline specification
170
+ :param request: Inference request
171
+ :return: Raw inference result
172
+ :raises ProviderError: For any provider-related failures
173
+ """
174
+ if request.product_type != ProductType.PARSE:
175
+ raise ProviderPermanentError(f"ChunkrProvider only supports PARSE product type, got {request.product_type}")
176
+
177
+ started_at = datetime.now()
178
+
179
+ file_path = Path(request.source_file_path)
180
+ if not file_path.exists():
181
+ raise ProviderPermanentError(f"File not found: {file_path}")
182
+
183
+ try:
184
+ raw_output = self.run_async_from_sync(self._parse_document_async(str(file_path)))
185
+
186
+ completed_at = datetime.now()
187
+ latency_ms = int((completed_at - started_at).total_seconds() * 1000)
188
+
189
+ return RawInferenceResult(
190
+ request=request,
191
+ pipeline=pipeline,
192
+ pipeline_name=pipeline.pipeline_name,
193
+ product_type=request.product_type,
194
+ raw_output=raw_output,
195
+ started_at=started_at,
196
+ completed_at=completed_at,
197
+ latency_in_ms=latency_ms,
198
+ )
199
+
200
+ except (ProviderPermanentError, ProviderTransientError):
201
+ raise
202
+ except Exception as e:
203
+ raise ProviderPermanentError(f"Unexpected error during inference: {e}") from e
204
+
205
+ def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
206
+ """
207
+ Normalize raw inference result to produce ParseOutput.
208
+
209
+ :param raw_result: Raw inference result from run_inference()
210
+ :return: Inference result with both raw and normalized outputs
211
+ :raises ProviderError: For any normalization failures
212
+ """
213
+ if raw_result.product_type != ProductType.PARSE:
214
+ raise ProviderPermanentError(
215
+ f"ChunkrProvider only supports PARSE product type, got {raw_result.product_type}"
216
+ )
217
+
218
+ # Extract HTML content (preferred for table tests)
219
+ html_content = raw_result.raw_output.get("_html_content", "")
220
+
221
+ # Fallback: extract from output.chunks if _html_content not available
222
+ if not html_content:
223
+ output = raw_result.raw_output.get("output", {})
224
+ chunks = output.get("chunks", [])
225
+ # Concatenate HTML from all chunks/segments
226
+ html_parts = []
227
+ for chunk in chunks:
228
+ segments = chunk.get("segments", [])
229
+ for segment in segments:
230
+ html = segment.get("html", "")
231
+ if html:
232
+ html_parts.append(html)
233
+ html_content = "\n".join(html_parts)
234
+
235
+ # If still no HTML content, try markdown
236
+ if not html_content:
237
+ html_content = raw_result.raw_output.get("_markdown_content", "")
238
+
239
+ # Final fallback: concatenate content from chunks
240
+ if not html_content:
241
+ output = raw_result.raw_output.get("output", {})
242
+ chunks = output.get("chunks", [])
243
+ content_parts = []
244
+ for chunk in chunks:
245
+ content = chunk.get("content", "")
246
+ if content:
247
+ content_parts.append(content)
248
+ html_content = "\n".join(content_parts)
249
+
250
+ output = ParseOutput(
251
+ task_type="parse",
252
+ example_id=raw_result.request.example_id,
253
+ pipeline_name=raw_result.pipeline_name,
254
+ pages=[], # Chunkr doesn't provide per-page split by default
255
+ markdown=html_content, # HTML content goes here for table tests
256
+ job_id=raw_result.raw_output.get("task_id"),
257
+ )
258
+
259
+ return InferenceResult(
260
+ request=raw_result.request,
261
+ pipeline_name=raw_result.pipeline_name,
262
+ product_type=raw_result.product_type,
263
+ raw_output=raw_result.raw_output,
264
+ output=output,
265
+ started_at=raw_result.started_at,
266
+ completed_at=raw_result.completed_at,
267
+ latency_in_ms=raw_result.latency_in_ms,
268
+ )