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,203 @@
|
|
|
1
|
+
"""Base class for HuggingFace layout detection providers."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from PIL import Image
|
|
11
|
+
|
|
12
|
+
from parse_bench.inference.providers.base import (
|
|
13
|
+
Provider,
|
|
14
|
+
ProviderConfigError,
|
|
15
|
+
ProviderPermanentError,
|
|
16
|
+
ProviderRateLimitError,
|
|
17
|
+
ProviderTransientError,
|
|
18
|
+
)
|
|
19
|
+
from parse_bench.schemas.layout_detection_output import (
|
|
20
|
+
LayoutDetectionModel,
|
|
21
|
+
LayoutPrediction,
|
|
22
|
+
)
|
|
23
|
+
from parse_bench.schemas.pipeline import PipelineSpec
|
|
24
|
+
from parse_bench.schemas.pipeline_io import (
|
|
25
|
+
InferenceRequest,
|
|
26
|
+
InferenceResult,
|
|
27
|
+
RawInferenceResult,
|
|
28
|
+
)
|
|
29
|
+
from parse_bench.schemas.product import ProductType
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def image_to_bytes(image: Image.Image, format: str = "PNG") -> bytes:
|
|
33
|
+
"""Convert PIL Image to bytes."""
|
|
34
|
+
buffer = io.BytesIO()
|
|
35
|
+
image.save(buffer, format=format)
|
|
36
|
+
buffer.seek(0)
|
|
37
|
+
return buffer.read()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HFLayoutDetProvider(Provider):
|
|
41
|
+
"""
|
|
42
|
+
Base class for HuggingFace layout detection inference providers.
|
|
43
|
+
|
|
44
|
+
Subclasses must set:
|
|
45
|
+
- endpoint_url: The HF inference endpoint URL (class attribute)
|
|
46
|
+
- model_type: The LayoutDetectionModel enum value
|
|
47
|
+
|
|
48
|
+
Subclasses must implement:
|
|
49
|
+
- _parse_response(): Convert raw API response to list of predictions
|
|
50
|
+
- normalize(): Convert raw result to LayoutOutput
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Subclasses must override these
|
|
54
|
+
endpoint_url: str = ""
|
|
55
|
+
model_type: LayoutDetectionModel
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
provider_name: str,
|
|
60
|
+
base_config: dict[str, Any] | None = None,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Initialize the HF layout detection provider.
|
|
64
|
+
|
|
65
|
+
:param provider_name: Name of the provider
|
|
66
|
+
:param base_config: Optional configuration with:
|
|
67
|
+
- `hf_token`: HuggingFace token (defaults to HF_TOKEN env var)
|
|
68
|
+
- `timeout`: Request timeout in seconds (default: 60)
|
|
69
|
+
"""
|
|
70
|
+
super().__init__(provider_name, base_config)
|
|
71
|
+
|
|
72
|
+
# Get HF token
|
|
73
|
+
self._hf_token = self.base_config.get("hf_token") or os.getenv("HF_TOKEN")
|
|
74
|
+
if not self._hf_token:
|
|
75
|
+
raise ProviderConfigError(
|
|
76
|
+
"HuggingFace token is required. Set HF_TOKEN environment variable or pass hf_token in base_config."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Get timeout (default 60 seconds)
|
|
80
|
+
self._timeout = self.base_config.get("timeout", 60)
|
|
81
|
+
|
|
82
|
+
def _call_endpoint(self, image: Image.Image) -> dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
Call the HuggingFace inference endpoint with an image.
|
|
85
|
+
|
|
86
|
+
:param image: PIL Image to send
|
|
87
|
+
:return: Raw JSON response from the endpoint
|
|
88
|
+
:raises ProviderError: For any API errors
|
|
89
|
+
"""
|
|
90
|
+
if not self.endpoint_url:
|
|
91
|
+
raise ProviderConfigError("Endpoint URL not configured")
|
|
92
|
+
|
|
93
|
+
headers = {
|
|
94
|
+
"Authorization": f"Bearer {self._hf_token}",
|
|
95
|
+
"Content-Type": "image/png",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
image_bytes = image_to_bytes(image, format="PNG")
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
response = requests.post(
|
|
102
|
+
self.endpoint_url,
|
|
103
|
+
headers=headers,
|
|
104
|
+
data=image_bytes,
|
|
105
|
+
timeout=self._timeout,
|
|
106
|
+
)
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
return response.json() # type: ignore[no-any-return]
|
|
109
|
+
|
|
110
|
+
except requests.exceptions.Timeout as e:
|
|
111
|
+
raise ProviderTransientError(f"Request timed out: {e}") from e
|
|
112
|
+
except requests.exceptions.ConnectionError as e:
|
|
113
|
+
raise ProviderTransientError(f"Connection error: {e}") from e
|
|
114
|
+
except requests.exceptions.HTTPError as e:
|
|
115
|
+
status_code = e.response.status_code if e.response else None
|
|
116
|
+
if status_code == 429:
|
|
117
|
+
raise ProviderRateLimitError(f"Rate limit exceeded: {e}") from e
|
|
118
|
+
elif status_code and 500 <= status_code < 600:
|
|
119
|
+
raise ProviderTransientError(f"Server error ({status_code}): {e}") from e
|
|
120
|
+
elif status_code and 400 <= status_code < 500:
|
|
121
|
+
raise ProviderPermanentError(f"Client error ({status_code}): {e}") from e
|
|
122
|
+
else:
|
|
123
|
+
raise ProviderPermanentError(f"HTTP error: {e}") from e
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise ProviderPermanentError(f"Unexpected error calling endpoint: {e}") from e
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def _parse_response(self, response: dict[str, Any]) -> list[LayoutPrediction]:
|
|
129
|
+
"""
|
|
130
|
+
Parse the raw API response into a list of layout predictions.
|
|
131
|
+
|
|
132
|
+
Subclasses must implement this to handle model-specific response formats.
|
|
133
|
+
|
|
134
|
+
:param response: Raw JSON response from the endpoint
|
|
135
|
+
:return: List of layout predictions
|
|
136
|
+
"""
|
|
137
|
+
raise NotImplementedError("Subclasses must implement _parse_response")
|
|
138
|
+
|
|
139
|
+
def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
|
|
140
|
+
"""
|
|
141
|
+
Run layout detection inference on an image.
|
|
142
|
+
|
|
143
|
+
:param pipeline: Pipeline specification
|
|
144
|
+
:param request: Inference request (source_file_path should be an image)
|
|
145
|
+
:return: Raw inference result
|
|
146
|
+
:raises ProviderError: For any provider-related failures
|
|
147
|
+
"""
|
|
148
|
+
if request.product_type != ProductType.LAYOUT_DETECTION:
|
|
149
|
+
raise ProviderPermanentError(
|
|
150
|
+
f"{self.__class__.__name__} only supports LAYOUT_DETECTION product type, got {request.product_type}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
started_at = datetime.now()
|
|
154
|
+
|
|
155
|
+
# Load the image
|
|
156
|
+
try:
|
|
157
|
+
image = Image.open(request.source_file_path)
|
|
158
|
+
# Ensure image is in RGB mode for PNG conversion
|
|
159
|
+
if image.mode not in ("RGB", "RGBA"):
|
|
160
|
+
image = image.convert("RGB") # type: ignore[assignment]
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise ProviderPermanentError(f"Failed to load image: {e}") from e
|
|
163
|
+
|
|
164
|
+
# Get image dimensions
|
|
165
|
+
image_width, image_height = image.size
|
|
166
|
+
|
|
167
|
+
# Call the endpoint
|
|
168
|
+
raw_response = self._call_endpoint(image)
|
|
169
|
+
|
|
170
|
+
completed_at = datetime.now()
|
|
171
|
+
latency_ms = int((completed_at - started_at).total_seconds() * 1000)
|
|
172
|
+
|
|
173
|
+
# Store image dimensions in raw output for normalization
|
|
174
|
+
raw_output = {
|
|
175
|
+
"response": raw_response,
|
|
176
|
+
"image_width": image_width,
|
|
177
|
+
"image_height": image_height,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return RawInferenceResult(
|
|
181
|
+
request=request,
|
|
182
|
+
pipeline=pipeline,
|
|
183
|
+
pipeline_name=pipeline.pipeline_name,
|
|
184
|
+
product_type=request.product_type,
|
|
185
|
+
raw_output=raw_output,
|
|
186
|
+
started_at=started_at,
|
|
187
|
+
completed_at=completed_at,
|
|
188
|
+
latency_in_ms=latency_ms,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@abstractmethod
|
|
192
|
+
def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
|
|
193
|
+
"""
|
|
194
|
+
Normalize raw inference result to produce LayoutOutput.
|
|
195
|
+
|
|
196
|
+
Each subclass MUST implement this method to return normalized
|
|
197
|
+
provider predictions in the common layout interface.
|
|
198
|
+
|
|
199
|
+
:param raw_result: Raw inference result from run_inference()
|
|
200
|
+
:return: Inference result with both raw and normalized outputs
|
|
201
|
+
:raises ProviderError: For any normalization failures
|
|
202
|
+
"""
|
|
203
|
+
raise NotImplementedError("Subclasses must implement normalize")
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""Provider for Chandra OCR layout detection via Modal OpenAI-compatible API."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from openai import OpenAI
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
from parse_bench.inference.providers.base import (
|
|
15
|
+
Provider,
|
|
16
|
+
ProviderPermanentError,
|
|
17
|
+
ProviderTransientError,
|
|
18
|
+
)
|
|
19
|
+
from parse_bench.inference.providers.registry import register_provider
|
|
20
|
+
from parse_bench.schemas.layout_detection_output import (
|
|
21
|
+
CHANDRA_STR_TO_LABEL,
|
|
22
|
+
LayoutDetectionModel,
|
|
23
|
+
LayoutOutput,
|
|
24
|
+
LayoutPrediction,
|
|
25
|
+
)
|
|
26
|
+
from parse_bench.schemas.pipeline import PipelineSpec
|
|
27
|
+
from parse_bench.schemas.pipeline_io import (
|
|
28
|
+
InferenceRequest,
|
|
29
|
+
InferenceResult,
|
|
30
|
+
RawInferenceResult,
|
|
31
|
+
)
|
|
32
|
+
from parse_bench.schemas.product import ProductType
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
# Allowed HTML tags for Chandra output
|
|
37
|
+
ALLOWED_TAGS = [
|
|
38
|
+
"math",
|
|
39
|
+
"br",
|
|
40
|
+
"i",
|
|
41
|
+
"b",
|
|
42
|
+
"u",
|
|
43
|
+
"del",
|
|
44
|
+
"sup",
|
|
45
|
+
"sub",
|
|
46
|
+
"table",
|
|
47
|
+
"tr",
|
|
48
|
+
"td",
|
|
49
|
+
"p",
|
|
50
|
+
"th",
|
|
51
|
+
"div",
|
|
52
|
+
"pre",
|
|
53
|
+
"h1",
|
|
54
|
+
"h2",
|
|
55
|
+
"h3",
|
|
56
|
+
"h4",
|
|
57
|
+
"h5",
|
|
58
|
+
"ul",
|
|
59
|
+
"ol",
|
|
60
|
+
"li",
|
|
61
|
+
"input",
|
|
62
|
+
"a",
|
|
63
|
+
"span",
|
|
64
|
+
"img",
|
|
65
|
+
"hr",
|
|
66
|
+
"tbody",
|
|
67
|
+
"small",
|
|
68
|
+
"caption",
|
|
69
|
+
"strong",
|
|
70
|
+
"thead",
|
|
71
|
+
"big",
|
|
72
|
+
"code",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
ALLOWED_ATTRIBUTES = [
|
|
76
|
+
"class",
|
|
77
|
+
"colspan",
|
|
78
|
+
"rowspan",
|
|
79
|
+
"display",
|
|
80
|
+
"checked",
|
|
81
|
+
"type",
|
|
82
|
+
"border",
|
|
83
|
+
"value",
|
|
84
|
+
"style",
|
|
85
|
+
"href",
|
|
86
|
+
"alt",
|
|
87
|
+
"align",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
PROMPT_ENDING = (
|
|
91
|
+
f"Only use these tags {ALLOWED_TAGS}, "
|
|
92
|
+
f"and these attributes {ALLOWED_ATTRIBUTES}."
|
|
93
|
+
"\n\nGuidelines:\n"
|
|
94
|
+
"* Inline math: Surround math with <math>...</math> "
|
|
95
|
+
"tags. Math expressions should be rendered in "
|
|
96
|
+
"KaTeX-compatible LaTeX. Use display for block "
|
|
97
|
+
"math.\n"
|
|
98
|
+
"* Tables: Use colspan and rowspan attributes to "
|
|
99
|
+
"match table structure.\n"
|
|
100
|
+
"* Formatting: Maintain consistent formatting with "
|
|
101
|
+
"the image, including spacing, indentation, "
|
|
102
|
+
"subscripts/superscripts, and special characters.\n"
|
|
103
|
+
"* Images: Include a description of any images in "
|
|
104
|
+
"the alt attribute of an <img> tag. Do not fill out "
|
|
105
|
+
"the src property.\n"
|
|
106
|
+
"* Forms: Mark checkboxes and radio buttons "
|
|
107
|
+
"properly.\n"
|
|
108
|
+
"* Text: join lines together properly into paragraphs "
|
|
109
|
+
"using <p>...</p> tags. Use <br> tags for line "
|
|
110
|
+
"breaks within paragraphs, but only when absolutely "
|
|
111
|
+
"necessary to maintain meaning.\n"
|
|
112
|
+
"* Use the simplest possible HTML structure that "
|
|
113
|
+
"accurately represents the content of the block.\n"
|
|
114
|
+
"* Make sure the text is accurate and easy for a "
|
|
115
|
+
"human to read and interpret. Reading order should "
|
|
116
|
+
"be correct and natural."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@register_provider("chandra_layout")
|
|
121
|
+
class ChandraLayoutProvider(Provider):
|
|
122
|
+
"""
|
|
123
|
+
Layout detection using Chandra OCR via Modal OpenAI-compatible API.
|
|
124
|
+
|
|
125
|
+
This provider sends images to the Chandra model using the ocr_layout prompt
|
|
126
|
+
and parses the HTML response containing layout blocks with bounding boxes.
|
|
127
|
+
|
|
128
|
+
Response format (HTML):
|
|
129
|
+
<div data-bbox="[x0, y0, x1, y1]" data-label="Text">content...</div>
|
|
130
|
+
<div data-bbox="[x0, y0, x1, y1]" data-label="Table">...</div>
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
Coordinates are normalized to [0-1024] and converted to pixel coords.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
DEFAULT_ENDPOINT_URL = "https://llamaindex--chandra-vllm-inference-serve.modal.run/v1"
|
|
137
|
+
model_type = LayoutDetectionModel.CHANDRA
|
|
138
|
+
|
|
139
|
+
# Chandra uses 0-1024 normalized bbox coordinates
|
|
140
|
+
BBOX_SCALE = 1024
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
provider_name: str,
|
|
145
|
+
base_config: dict[str, Any] | None = None,
|
|
146
|
+
):
|
|
147
|
+
"""Initialize the Chandra layout detection provider."""
|
|
148
|
+
super().__init__(provider_name, base_config)
|
|
149
|
+
|
|
150
|
+
# Get endpoint URL from config or use default
|
|
151
|
+
endpoint_url = self.base_config.get("endpoint_url", self.DEFAULT_ENDPOINT_URL)
|
|
152
|
+
|
|
153
|
+
# Initialize OpenAI client for Modal endpoint
|
|
154
|
+
self._client = OpenAI(
|
|
155
|
+
base_url=endpoint_url,
|
|
156
|
+
api_key="not-needed",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Get timeout (default 180 seconds for VLM - Chandra can be slow)
|
|
160
|
+
self._timeout = self.base_config.get("timeout", 180)
|
|
161
|
+
|
|
162
|
+
# Build the prompt with bbox_scale
|
|
163
|
+
self._prompt = self._build_ocr_layout_prompt()
|
|
164
|
+
|
|
165
|
+
def _build_ocr_layout_prompt(self) -> str:
|
|
166
|
+
"""Build the OCR layout prompt with the correct bbox_scale."""
|
|
167
|
+
return (
|
|
168
|
+
"OCR this image to HTML, arranged as layout "
|
|
169
|
+
"blocks. Each layout block should be a div "
|
|
170
|
+
"with the data-bbox attribute representing the "
|
|
171
|
+
"bounding box of the block in "
|
|
172
|
+
"[x0, y0, x1, y1] format. "
|
|
173
|
+
f"Bboxes are normalized 0-{self.BBOX_SCALE}. "
|
|
174
|
+
"The data-label attribute is the label for "
|
|
175
|
+
"the block."
|
|
176
|
+
"\n\nUse the following labels:\n"
|
|
177
|
+
"- Caption\n"
|
|
178
|
+
"- Footnote\n"
|
|
179
|
+
"- Equation-Block\n"
|
|
180
|
+
"- List-Group\n"
|
|
181
|
+
"- Page-Header\n"
|
|
182
|
+
"- Page-Footer\n"
|
|
183
|
+
"- Image\n"
|
|
184
|
+
"- Section-Header\n"
|
|
185
|
+
"- Table\n"
|
|
186
|
+
"- Text\n"
|
|
187
|
+
"- Complex-Block\n"
|
|
188
|
+
"- Code-Block\n"
|
|
189
|
+
"- Form\n"
|
|
190
|
+
"- Table-Of-Contents\n"
|
|
191
|
+
"- Figure"
|
|
192
|
+
f"\n\n{PROMPT_ENDING}"
|
|
193
|
+
).strip()
|
|
194
|
+
|
|
195
|
+
def _image_to_base64(self, image: Image.Image) -> str:
|
|
196
|
+
"""Convert PIL Image to base64 string."""
|
|
197
|
+
buffer = io.BytesIO()
|
|
198
|
+
image.save(buffer, format="PNG")
|
|
199
|
+
buffer.seek(0)
|
|
200
|
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
201
|
+
|
|
202
|
+
def _parse_html_layout_blocks(self, html_content: str) -> list[dict[str, Any]]:
|
|
203
|
+
"""
|
|
204
|
+
Parse HTML content to extract layout blocks with data-bbox and data-label.
|
|
205
|
+
|
|
206
|
+
:param html_content: Raw HTML response from Chandra
|
|
207
|
+
:return: List of dicts with 'bbox', 'label' keys
|
|
208
|
+
"""
|
|
209
|
+
predictions: list[dict[str, Any]] = []
|
|
210
|
+
|
|
211
|
+
# Pattern to match divs with data-bbox and data-label attributes
|
|
212
|
+
# Handles both attribute orders: data-bbox first or data-label first
|
|
213
|
+
pattern = r'<div[^>]*data-bbox=["\'](\[[^\]]+\])["\'][^>]*data-label=["\']([^"\']+)["\']'
|
|
214
|
+
pattern_alt = r'<div[^>]*data-label=["\']([^"\']+)["\'][^>]*data-bbox=["\'](\[[^\]]+\])["\']'
|
|
215
|
+
|
|
216
|
+
# Find all matches with data-bbox first
|
|
217
|
+
for match in re.finditer(pattern, html_content):
|
|
218
|
+
bbox_str = match.group(1)
|
|
219
|
+
label = match.group(2)
|
|
220
|
+
try:
|
|
221
|
+
bbox = json.loads(bbox_str)
|
|
222
|
+
if isinstance(bbox, list) and len(bbox) == 4:
|
|
223
|
+
predictions.append(
|
|
224
|
+
{
|
|
225
|
+
"bbox": bbox,
|
|
226
|
+
"label": label,
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
except json.JSONDecodeError:
|
|
230
|
+
logger.warning(f"Failed to parse bbox: {bbox_str}")
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Find all matches with data-label first
|
|
234
|
+
for match in re.finditer(pattern_alt, html_content):
|
|
235
|
+
label = match.group(1)
|
|
236
|
+
bbox_str = match.group(2)
|
|
237
|
+
try:
|
|
238
|
+
bbox = json.loads(bbox_str)
|
|
239
|
+
if isinstance(bbox, list) and len(bbox) == 4:
|
|
240
|
+
predictions.append(
|
|
241
|
+
{
|
|
242
|
+
"bbox": bbox,
|
|
243
|
+
"label": label,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
except json.JSONDecodeError:
|
|
247
|
+
logger.warning(f"Failed to parse bbox: {bbox_str}")
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
return predictions
|
|
251
|
+
|
|
252
|
+
def _normalize_bbox(
|
|
253
|
+
self,
|
|
254
|
+
bbox_normalized: list[float],
|
|
255
|
+
image_width: int,
|
|
256
|
+
image_height: int,
|
|
257
|
+
) -> list[float]:
|
|
258
|
+
"""
|
|
259
|
+
Convert [0-1024] normalized coords to pixel coords.
|
|
260
|
+
|
|
261
|
+
:param bbox_normalized: Bounding box in [0-1024] normalized coords
|
|
262
|
+
:param image_width: Actual image width in pixels
|
|
263
|
+
:param image_height: Actual image height in pixels
|
|
264
|
+
:return: Bounding box in pixel coordinates [x1, y1, x2, y2]
|
|
265
|
+
"""
|
|
266
|
+
x1, y1, x2, y2 = bbox_normalized
|
|
267
|
+
|
|
268
|
+
# Clamp to valid range
|
|
269
|
+
x1 = max(0, min(self.BBOX_SCALE, x1))
|
|
270
|
+
y1 = max(0, min(self.BBOX_SCALE, y1))
|
|
271
|
+
x2 = max(0, min(self.BBOX_SCALE, x2))
|
|
272
|
+
y2 = max(0, min(self.BBOX_SCALE, y2))
|
|
273
|
+
|
|
274
|
+
return [
|
|
275
|
+
x1 * image_width / self.BBOX_SCALE,
|
|
276
|
+
y1 * image_height / self.BBOX_SCALE,
|
|
277
|
+
x2 * image_width / self.BBOX_SCALE,
|
|
278
|
+
y2 * image_height / self.BBOX_SCALE,
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
def _call_endpoint(self, image: Image.Image) -> tuple[list[dict], str]:
|
|
282
|
+
"""
|
|
283
|
+
Call Chandra via OpenAI API and return parsed predictions.
|
|
284
|
+
|
|
285
|
+
:param image: PIL Image to analyze
|
|
286
|
+
:return: Tuple of (parsed predictions list, raw response content)
|
|
287
|
+
:raises ProviderError: For API errors
|
|
288
|
+
"""
|
|
289
|
+
img_base64 = self._image_to_base64(image)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
response = self._client.chat.completions.create(
|
|
293
|
+
model="chandra", # Model name configured in vLLM
|
|
294
|
+
messages=[
|
|
295
|
+
{
|
|
296
|
+
"role": "user",
|
|
297
|
+
"content": [
|
|
298
|
+
{"type": "text", "text": self._prompt},
|
|
299
|
+
{
|
|
300
|
+
"type": "image_url",
|
|
301
|
+
"image_url": {"url": f"data:image/png;base64,{img_base64}"},
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
max_tokens=8192,
|
|
307
|
+
temperature=0.1,
|
|
308
|
+
top_p=0.1,
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
error_msg = str(e).lower()
|
|
312
|
+
if "timeout" in error_msg or "connection" in error_msg:
|
|
313
|
+
raise ProviderTransientError(f"API call failed: {e}") from e
|
|
314
|
+
raise ProviderPermanentError(f"API call failed: {e}") from e
|
|
315
|
+
|
|
316
|
+
content = response.choices[0].message.content
|
|
317
|
+
if not content:
|
|
318
|
+
raise ProviderPermanentError("Empty response from model")
|
|
319
|
+
|
|
320
|
+
# Parse HTML to extract layout blocks
|
|
321
|
+
predictions = self._parse_html_layout_blocks(content)
|
|
322
|
+
|
|
323
|
+
return predictions, content
|
|
324
|
+
|
|
325
|
+
def run_inference(self, pipeline: PipelineSpec, request: InferenceRequest) -> RawInferenceResult:
|
|
326
|
+
"""
|
|
327
|
+
Run layout detection inference on an image.
|
|
328
|
+
|
|
329
|
+
:param pipeline: Pipeline specification
|
|
330
|
+
:param request: Inference request (source_file_path should be an image)
|
|
331
|
+
:return: Raw inference result
|
|
332
|
+
:raises ProviderError: For any provider-related failures
|
|
333
|
+
"""
|
|
334
|
+
if request.product_type != ProductType.LAYOUT_DETECTION:
|
|
335
|
+
raise ProviderPermanentError(
|
|
336
|
+
f"{self.__class__.__name__} only supports LAYOUT_DETECTION product type, got {request.product_type}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
started_at = datetime.now()
|
|
340
|
+
|
|
341
|
+
# Load the image
|
|
342
|
+
try:
|
|
343
|
+
image: Image.Image = Image.open(request.source_file_path)
|
|
344
|
+
# Ensure image is in RGB mode
|
|
345
|
+
if image.mode not in ("RGB", "RGBA"):
|
|
346
|
+
image = image.convert("RGB")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
raise ProviderPermanentError(f"Failed to load image: {e}") from e
|
|
349
|
+
|
|
350
|
+
# Get image dimensions
|
|
351
|
+
image_width, image_height = image.size
|
|
352
|
+
|
|
353
|
+
# Call the endpoint
|
|
354
|
+
predictions, raw_content = self._call_endpoint(image)
|
|
355
|
+
|
|
356
|
+
completed_at = datetime.now()
|
|
357
|
+
latency_ms = int((completed_at - started_at).total_seconds() * 1000)
|
|
358
|
+
|
|
359
|
+
# Store in raw output for normalization
|
|
360
|
+
raw_output = {
|
|
361
|
+
"response": predictions,
|
|
362
|
+
"raw_content": raw_content,
|
|
363
|
+
"image_width": image_width,
|
|
364
|
+
"image_height": image_height,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return RawInferenceResult(
|
|
368
|
+
request=request,
|
|
369
|
+
pipeline=pipeline,
|
|
370
|
+
pipeline_name=pipeline.pipeline_name,
|
|
371
|
+
product_type=request.product_type,
|
|
372
|
+
raw_output=raw_output,
|
|
373
|
+
started_at=started_at,
|
|
374
|
+
completed_at=completed_at,
|
|
375
|
+
latency_in_ms=latency_ms,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def normalize(self, raw_result: RawInferenceResult) -> InferenceResult:
|
|
379
|
+
"""
|
|
380
|
+
Normalize raw inference result to produce LayoutOutput.
|
|
381
|
+
|
|
382
|
+
Converts normalized [0-1024] coordinates to pixel coordinates and
|
|
383
|
+
maps string labels to canonical labels.
|
|
384
|
+
|
|
385
|
+
:param raw_result: Raw inference result from run_inference()
|
|
386
|
+
:return: Inference result with both raw and normalized outputs
|
|
387
|
+
:raises ProviderError: For any normalization failures
|
|
388
|
+
"""
|
|
389
|
+
if raw_result.product_type != ProductType.LAYOUT_DETECTION:
|
|
390
|
+
raise ProviderPermanentError(
|
|
391
|
+
f"{self.__class__.__name__} only supports LAYOUT_DETECTION product type, got {raw_result.product_type}"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Get image dimensions
|
|
395
|
+
image_width = raw_result.raw_output.get("image_width", 0)
|
|
396
|
+
image_height = raw_result.raw_output.get("image_height", 0)
|
|
397
|
+
|
|
398
|
+
# Parse the response into predictions
|
|
399
|
+
response = raw_result.raw_output.get("response", [])
|
|
400
|
+
|
|
401
|
+
raw_predictions: list[LayoutPrediction] = []
|
|
402
|
+
|
|
403
|
+
for item in response:
|
|
404
|
+
label_str = item.get("label", "")
|
|
405
|
+
bbox_normalized = item.get("bbox", [0, 0, 0, 0])
|
|
406
|
+
|
|
407
|
+
# Convert string label to enum
|
|
408
|
+
label_enum = CHANDRA_STR_TO_LABEL.get(label_str)
|
|
409
|
+
if label_enum is None:
|
|
410
|
+
# Unknown label, skip
|
|
411
|
+
logger.warning(f"Unknown Chandra label: {label_str}")
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# Chandra doesn't output confidence scores, use 1.0
|
|
415
|
+
score = 1.0
|
|
416
|
+
|
|
417
|
+
# Convert normalized coords to pixel coords
|
|
418
|
+
bbox_pixels = self._normalize_bbox(bbox_normalized, image_width, image_height)
|
|
419
|
+
|
|
420
|
+
# Create raw prediction
|
|
421
|
+
raw_predictions.append(
|
|
422
|
+
LayoutPrediction(
|
|
423
|
+
bbox=bbox_pixels,
|
|
424
|
+
score=score,
|
|
425
|
+
label=str(int(label_enum)),
|
|
426
|
+
provider_metadata={"label_name": label_enum.name},
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
output = LayoutOutput(
|
|
431
|
+
task_type="layout_detection",
|
|
432
|
+
example_id=raw_result.request.example_id,
|
|
433
|
+
pipeline_name=raw_result.pipeline_name,
|
|
434
|
+
model=self.model_type,
|
|
435
|
+
image_width=max(int(image_width), 1),
|
|
436
|
+
image_height=max(int(image_height), 1),
|
|
437
|
+
predictions=raw_predictions,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return InferenceResult(
|
|
441
|
+
request=raw_result.request,
|
|
442
|
+
pipeline_name=raw_result.pipeline_name,
|
|
443
|
+
product_type=raw_result.product_type,
|
|
444
|
+
raw_output=raw_result.raw_output,
|
|
445
|
+
output=output,
|
|
446
|
+
started_at=raw_result.started_at,
|
|
447
|
+
completed_at=raw_result.completed_at,
|
|
448
|
+
latency_in_ms=raw_result.latency_in_ms,
|
|
449
|
+
)
|