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,315 @@
|
|
|
1
|
+
"""Layout detection ontology definitions.
|
|
2
|
+
|
|
3
|
+
This module defines:
|
|
4
|
+
- CanonicalLabel: Canonical17 labels (the standardized dataset label set)
|
|
5
|
+
- CORE_LABELS: Core11 subset (DocLayNet-compatible labels)
|
|
6
|
+
- CANONICAL_TO_CORE: Mapping from Canonical17 to Core11
|
|
7
|
+
- DOCLAYNET_ID_TO_LABEL: DocLayNet ground truth category ID mapping
|
|
8
|
+
- Ontology classes for dataset-side label management
|
|
9
|
+
|
|
10
|
+
Reference: layout_detection_class_label_canonicalization_proposal.md
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Canonical17 Label Enum (Standardized Dataset Labels)
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CanonicalLabel(StrEnum):
|
|
22
|
+
"""Canonical17 layout detection labels.
|
|
23
|
+
|
|
24
|
+
This is the standardized label set based on Docling Heron's 17-class schema.
|
|
25
|
+
It serves as the canonical representation for all dataset ground truth.
|
|
26
|
+
|
|
27
|
+
Core11 (DocLayNet-compatible) is a subset: use CORE_LABELS to check membership.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Core11 labels (DocLayNet-compatible) - these 11 are the minimum required
|
|
31
|
+
CAPTION = "Caption"
|
|
32
|
+
FOOTNOTE = "Footnote"
|
|
33
|
+
FORMULA = "Formula"
|
|
34
|
+
LIST_ITEM = "List-item"
|
|
35
|
+
PAGE_FOOTER = "Page-footer"
|
|
36
|
+
PAGE_HEADER = "Page-header"
|
|
37
|
+
PICTURE = "Picture"
|
|
38
|
+
SECTION_HEADER = "Section-header"
|
|
39
|
+
TABLE = "Table"
|
|
40
|
+
TEXT = "Text"
|
|
41
|
+
TITLE = "Title"
|
|
42
|
+
|
|
43
|
+
# Extended Canonical17 labels (not in Core11)
|
|
44
|
+
CHECKBOX_SELECTED = "Checkbox-Selected"
|
|
45
|
+
CHECKBOX_UNSELECTED = "Checkbox-Unselected"
|
|
46
|
+
CODE = "Code"
|
|
47
|
+
DOCUMENT_INDEX = "Document Index"
|
|
48
|
+
FORM = "Form"
|
|
49
|
+
KEY_VALUE_REGION = "Key-Value Region"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Core11 Subset (DocLayNet-compatible labels)
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
CORE_LABELS: frozenset[CanonicalLabel] = frozenset(
|
|
57
|
+
{
|
|
58
|
+
CanonicalLabel.CAPTION,
|
|
59
|
+
CanonicalLabel.FOOTNOTE,
|
|
60
|
+
CanonicalLabel.FORMULA,
|
|
61
|
+
CanonicalLabel.LIST_ITEM,
|
|
62
|
+
CanonicalLabel.PAGE_FOOTER,
|
|
63
|
+
CanonicalLabel.PAGE_HEADER,
|
|
64
|
+
CanonicalLabel.PICTURE,
|
|
65
|
+
CanonicalLabel.SECTION_HEADER,
|
|
66
|
+
CanonicalLabel.TABLE,
|
|
67
|
+
CanonicalLabel.TEXT,
|
|
68
|
+
CanonicalLabel.TITLE,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Canonical17 to Core11 Mapping
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
# Labels in Core11 map to themselves, others map to closest Core11 equivalent or None
|
|
78
|
+
CANONICAL_TO_CORE: dict[CanonicalLabel, CanonicalLabel | None] = {
|
|
79
|
+
# Core11 labels map to themselves
|
|
80
|
+
CanonicalLabel.CAPTION: CanonicalLabel.CAPTION,
|
|
81
|
+
CanonicalLabel.FOOTNOTE: CanonicalLabel.FOOTNOTE,
|
|
82
|
+
CanonicalLabel.FORMULA: CanonicalLabel.FORMULA,
|
|
83
|
+
CanonicalLabel.LIST_ITEM: CanonicalLabel.LIST_ITEM,
|
|
84
|
+
CanonicalLabel.PAGE_FOOTER: CanonicalLabel.PAGE_FOOTER,
|
|
85
|
+
CanonicalLabel.PAGE_HEADER: CanonicalLabel.PAGE_HEADER,
|
|
86
|
+
CanonicalLabel.PICTURE: CanonicalLabel.PICTURE,
|
|
87
|
+
CanonicalLabel.SECTION_HEADER: CanonicalLabel.SECTION_HEADER,
|
|
88
|
+
CanonicalLabel.TABLE: CanonicalLabel.TABLE,
|
|
89
|
+
CanonicalLabel.TEXT: CanonicalLabel.TEXT,
|
|
90
|
+
CanonicalLabel.TITLE: CanonicalLabel.TITLE,
|
|
91
|
+
# Extended labels map to closest Core11 equivalent or None
|
|
92
|
+
CanonicalLabel.CHECKBOX_SELECTED: None, # No Core11 equivalent
|
|
93
|
+
CanonicalLabel.CHECKBOX_UNSELECTED: None, # No Core11 equivalent
|
|
94
|
+
CanonicalLabel.CODE: CanonicalLabel.TEXT, # Code is text content
|
|
95
|
+
CanonicalLabel.DOCUMENT_INDEX: CanonicalLabel.TEXT, # Index is text content
|
|
96
|
+
CanonicalLabel.FORM: None, # No Core11 equivalent
|
|
97
|
+
CanonicalLabel.KEY_VALUE_REGION: CanonicalLabel.TEXT, # KV regions contain text
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# Basic7 Label Enum (Simplified Ontology)
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BasicLabel(StrEnum):
|
|
107
|
+
"""Basic7 layout detection labels (simplified ontology).
|
|
108
|
+
|
|
109
|
+
Merges Title + Section-header → Section
|
|
110
|
+
Merges List-item → Text (with text_role attribute)
|
|
111
|
+
Merges Caption + Footnote + Formula → Text (with text_role attribute)
|
|
112
|
+
|
|
113
|
+
This ontology is useful for evaluation when finer distinctions
|
|
114
|
+
between Title/Section-header or Text/List-item are not needed.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Kept as a legacy accepted label so older on-disk "basic" datasets remain loadable.
|
|
118
|
+
FORMULA = "Formula"
|
|
119
|
+
PAGE_FOOTER = "Page-footer"
|
|
120
|
+
PAGE_HEADER = "Page-header"
|
|
121
|
+
PICTURE = "Picture"
|
|
122
|
+
SECTION = "Section" # Title + Section-header
|
|
123
|
+
TABLE = "Table"
|
|
124
|
+
TEXT = "Text" # Includes List-item, Caption, Footnote
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
BASIC_LABELS: frozenset[BasicLabel] = frozenset(BasicLabel)
|
|
128
|
+
|
|
129
|
+
# Evaluation ontology defaults/choices (used by evaluation-time adaptation).
|
|
130
|
+
DEFAULT_LAYOUT_EVALUATION_ONTOLOGY = "basic"
|
|
131
|
+
SUPPORTED_LAYOUT_EVALUATION_ONTOLOGIES = frozenset({"basic", "canonical", "core"})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# Canonical17 to Basic7 Mapping
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Canonical17 → Basic7 mapping with attributes for reversibility
|
|
140
|
+
CANONICAL_TO_BASIC: dict[CanonicalLabel, tuple[BasicLabel, dict[str, str]]] = {
|
|
141
|
+
# Merged into Section
|
|
142
|
+
CanonicalLabel.TITLE: (BasicLabel.SECTION, {"title_level": "title"}),
|
|
143
|
+
CanonicalLabel.SECTION_HEADER: (BasicLabel.SECTION, {"title_level": "section-header"}),
|
|
144
|
+
# Merged into Text
|
|
145
|
+
CanonicalLabel.TEXT: (BasicLabel.TEXT, {}),
|
|
146
|
+
CanonicalLabel.LIST_ITEM: (BasicLabel.TEXT, {"text_role": "list-item"}),
|
|
147
|
+
CanonicalLabel.CAPTION: (BasicLabel.TEXT, {"text_role": "caption"}),
|
|
148
|
+
CanonicalLabel.FOOTNOTE: (BasicLabel.TEXT, {"text_role": "footnote"}),
|
|
149
|
+
CanonicalLabel.FORMULA: (BasicLabel.TEXT, {"text_role": "formula"}),
|
|
150
|
+
# Identity mappings (unchanged)
|
|
151
|
+
CanonicalLabel.PAGE_FOOTER: (BasicLabel.PAGE_FOOTER, {}),
|
|
152
|
+
CanonicalLabel.PAGE_HEADER: (BasicLabel.PAGE_HEADER, {}),
|
|
153
|
+
CanonicalLabel.PICTURE: (BasicLabel.PICTURE, {}),
|
|
154
|
+
CanonicalLabel.TABLE: (BasicLabel.TABLE, {}),
|
|
155
|
+
# Extended Canonical17 labels → Text (not present in current datasets)
|
|
156
|
+
CanonicalLabel.CODE: (BasicLabel.TEXT, {"text_role": "code"}),
|
|
157
|
+
CanonicalLabel.DOCUMENT_INDEX: (BasicLabel.TEXT, {"text_role": "document-index"}),
|
|
158
|
+
CanonicalLabel.KEY_VALUE_REGION: (BasicLabel.TEXT, {"text_role": "key-value"}),
|
|
159
|
+
CanonicalLabel.FORM: (BasicLabel.TEXT, {"text_role": "form"}),
|
|
160
|
+
CanonicalLabel.CHECKBOX_SELECTED: (BasicLabel.TEXT, {"text_role": "checkbox-selected"}),
|
|
161
|
+
CanonicalLabel.CHECKBOX_UNSELECTED: (BasicLabel.TEXT, {"text_role": "checkbox-unselected"}),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# =============================================================================
|
|
166
|
+
# DocLayNet Ground Truth Mapping (1-indexed category IDs)
|
|
167
|
+
# =============================================================================
|
|
168
|
+
|
|
169
|
+
DOCLAYNET_ID_TO_LABEL: dict[int, CanonicalLabel] = {
|
|
170
|
+
1: CanonicalLabel.CAPTION,
|
|
171
|
+
2: CanonicalLabel.FOOTNOTE,
|
|
172
|
+
3: CanonicalLabel.FORMULA,
|
|
173
|
+
4: CanonicalLabel.LIST_ITEM,
|
|
174
|
+
5: CanonicalLabel.PAGE_FOOTER,
|
|
175
|
+
6: CanonicalLabel.PAGE_HEADER,
|
|
176
|
+
7: CanonicalLabel.PICTURE,
|
|
177
|
+
8: CanonicalLabel.SECTION_HEADER,
|
|
178
|
+
9: CanonicalLabel.TABLE,
|
|
179
|
+
10: CanonicalLabel.TEXT,
|
|
180
|
+
11: CanonicalLabel.TITLE,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# Ontology Base Class
|
|
186
|
+
# =============================================================================
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class LayoutDetectionOntology(ABC):
|
|
190
|
+
"""Abstract base class for layout detection ontologies.
|
|
191
|
+
|
|
192
|
+
Ontologies define which labels are valid for a given dataset evaluation view.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(self) -> None:
|
|
196
|
+
self._labels: frozenset[CanonicalLabel] = frozenset()
|
|
197
|
+
self._setup()
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def _setup(self) -> None:
|
|
201
|
+
"""Initialize the label set."""
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def labels(self) -> frozenset[CanonicalLabel]:
|
|
206
|
+
"""Return all labels in this ontology."""
|
|
207
|
+
return self._labels
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def label_names(self) -> list[str]:
|
|
211
|
+
"""Return sorted list of label names (string values)."""
|
|
212
|
+
return sorted(label.value for label in self._labels)
|
|
213
|
+
|
|
214
|
+
def has_label(self, label: CanonicalLabel | str) -> bool:
|
|
215
|
+
"""Check if a label exists in this ontology.
|
|
216
|
+
|
|
217
|
+
:param label: CanonicalLabel enum or string value
|
|
218
|
+
:return: True if label exists
|
|
219
|
+
"""
|
|
220
|
+
if isinstance(label, str):
|
|
221
|
+
return label in {lbl.value for lbl in self._labels}
|
|
222
|
+
return label in self._labels
|
|
223
|
+
|
|
224
|
+
def map_label(
|
|
225
|
+
self,
|
|
226
|
+
source_label: str,
|
|
227
|
+
source_schema: str,
|
|
228
|
+
) -> tuple[str, dict[str, str]]:
|
|
229
|
+
"""Map a label from a source schema to canonical class and attributes.
|
|
230
|
+
|
|
231
|
+
:param source_label: The label in the source schema
|
|
232
|
+
:param source_schema: Name of the source schema (e.g., "doclaynet")
|
|
233
|
+
:return: Tuple of (canonical_class_name, attributes_dict)
|
|
234
|
+
"""
|
|
235
|
+
# For DocLayNet, it's an identity mapping (Core11 labels)
|
|
236
|
+
if source_schema == "doclaynet":
|
|
237
|
+
if self.has_label(source_label):
|
|
238
|
+
return (source_label, {})
|
|
239
|
+
# Fallback to Text if label not found
|
|
240
|
+
return ("Text", {})
|
|
241
|
+
|
|
242
|
+
# Unknown schema, fallback to Text
|
|
243
|
+
return ("Text", {})
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# =============================================================================
|
|
247
|
+
# Core11 Ontology (DocLayNet compatible)
|
|
248
|
+
# =============================================================================
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class CoreLayoutDetectionOntology(LayoutDetectionOntology):
|
|
252
|
+
"""Core11 layout detection ontology.
|
|
253
|
+
|
|
254
|
+
This ontology contains the 11 classes from DocLayNet, which is the
|
|
255
|
+
intersection view for compatibility with DocLayNet and many other benchmarks.
|
|
256
|
+
|
|
257
|
+
Labels: Caption, Footnote, Formula, List-item, Page-footer, Page-header,
|
|
258
|
+
Picture, Section-header, Table, Text, Title
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def _setup(self) -> None:
|
|
262
|
+
"""Initialize Core11 labels."""
|
|
263
|
+
self._labels = CORE_LABELS
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =============================================================================
|
|
267
|
+
# Canonical17 Ontology (Full canonical schema)
|
|
268
|
+
# =============================================================================
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class CanonicalLayoutDetectionOntology(LayoutDetectionOntology):
|
|
272
|
+
"""Canonical17 layout detection ontology.
|
|
273
|
+
|
|
274
|
+
This ontology adopts Docling Heron's 17-class schema as the primary
|
|
275
|
+
canonical class set, because it is a superset of DocLayNet's 11 labels
|
|
276
|
+
and covers several additional document elements commonly needed in
|
|
277
|
+
real pipelines.
|
|
278
|
+
|
|
279
|
+
Additional labels beyond Core11: Checkbox-Selected, Checkbox-Unselected,
|
|
280
|
+
Code, Document Index, Form, Key-Value Region
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def _setup(self) -> None:
|
|
284
|
+
"""Initialize Canonical17 labels."""
|
|
285
|
+
self._labels = frozenset(CanonicalLabel)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# =============================================================================
|
|
289
|
+
# Ontology Type Enum and Factory
|
|
290
|
+
# =============================================================================
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class OntologyType(StrEnum):
|
|
294
|
+
"""Supported ontology types."""
|
|
295
|
+
|
|
296
|
+
CORE = "core"
|
|
297
|
+
CANONICAL = "canonical"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_ontology(ontology_type: OntologyType | str) -> LayoutDetectionOntology:
|
|
301
|
+
"""Factory function to get an ontology instance.
|
|
302
|
+
|
|
303
|
+
:param ontology_type: Either OntologyType enum or string ("core" or "canonical")
|
|
304
|
+
:return: LayoutDetectionOntology instance
|
|
305
|
+
:raises ValueError: If unknown ontology type
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(ontology_type, str):
|
|
308
|
+
ontology_type = OntologyType(ontology_type.lower())
|
|
309
|
+
|
|
310
|
+
if ontology_type == OntologyType.CORE:
|
|
311
|
+
return CoreLayoutDetectionOntology()
|
|
312
|
+
elif ontology_type == OntologyType.CANONICAL:
|
|
313
|
+
return CanonicalLayoutDetectionOntology()
|
|
314
|
+
else:
|
|
315
|
+
raise ValueError(f"Unknown ontology type: {ontology_type}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Schemas for evaluation metrics and confusion matrix data."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfusionMatrixCell(BaseModel):
|
|
9
|
+
"""Single cell in confusion matrix.
|
|
10
|
+
|
|
11
|
+
Represents a specific (GT class, Pred class) pair with count,
|
|
12
|
+
percentage, and the test case IDs that contributed to this cell.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
gt_class: str = Field(..., description="Ground truth class name")
|
|
16
|
+
pred_class: str = Field(..., description="Predicted class name")
|
|
17
|
+
count: int = Field(..., description="Number of instances in this cell")
|
|
18
|
+
percentage: float = Field(
|
|
19
|
+
...,
|
|
20
|
+
description="Percentage of total GT instances for this gt_class (count / gt_total * 100)",
|
|
21
|
+
)
|
|
22
|
+
example_ids: list[str] = Field(
|
|
23
|
+
default_factory=list,
|
|
24
|
+
description="Test case IDs (test_id) that contributed to this cell",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfusionMatrixMetrics(BaseModel):
|
|
29
|
+
"""Confusion matrix computed during evaluation.
|
|
30
|
+
|
|
31
|
+
Contains full confusion matrix data with metadata to enable
|
|
32
|
+
interactive filtering in the HTML report.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
iou_threshold: float = Field(default=0.5, description="IoU threshold used for matching predictions to GT")
|
|
36
|
+
evaluation_view: Literal["core", "canonical"] = Field(
|
|
37
|
+
default="core",
|
|
38
|
+
description="Evaluation view: 'core' for Core11 labels, 'canonical' for Canonical17",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
cells: list[ConfusionMatrixCell] = Field(
|
|
42
|
+
default_factory=list,
|
|
43
|
+
description="All confusion matrix cells (including diagonal for correct predictions)",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
false_negatives: dict[str, list[str]] = Field(
|
|
47
|
+
default_factory=dict,
|
|
48
|
+
description="Unmatched GT boxes by class. Maps class name → list of test_ids",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
false_positives: dict[str, list[str]] = Field(
|
|
52
|
+
default_factory=dict,
|
|
53
|
+
description="Unmatched predictions by class. Maps class name → list of test_ids",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
gt_totals: dict[str, int] = Field(
|
|
57
|
+
default_factory=dict,
|
|
58
|
+
description="Total GT instances per class. Maps class name → count",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
pred_totals: dict[str, int] = Field(
|
|
62
|
+
default_factory=dict,
|
|
63
|
+
description="Total predictions per class. Maps class name → count",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
all_classes: list[str] = Field(
|
|
67
|
+
default_factory=list,
|
|
68
|
+
description="Sorted list of all unique class names in GT and predictions",
|
|
69
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Normalized output schema for parse tasks.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PageIR(BaseModel):
|
|
11
|
+
"""Intermediate representation of a single page."""
|
|
12
|
+
|
|
13
|
+
page_index: int = Field(ge=0, description="0-indexed page number")
|
|
14
|
+
markdown: str = Field(description="Markdown content of the page")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LayoutSegmentIR(BaseModel):
|
|
18
|
+
"""Normalized layout segment coordinates and attribution span metadata."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
21
|
+
|
|
22
|
+
x: float
|
|
23
|
+
y: float
|
|
24
|
+
w: float
|
|
25
|
+
h: float
|
|
26
|
+
confidence: float | None = None
|
|
27
|
+
label: str | None = None
|
|
28
|
+
start_index: int | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
validation_alias=AliasChoices("start_index", "startIndex"),
|
|
31
|
+
)
|
|
32
|
+
end_index: int | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
validation_alias=AliasChoices("end_index", "endIndex"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LayoutItemIR(BaseModel):
|
|
39
|
+
"""Normalized layout item used for attribution/layout reconstruction."""
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
42
|
+
|
|
43
|
+
type: str = "text"
|
|
44
|
+
md: str = Field(
|
|
45
|
+
default="",
|
|
46
|
+
validation_alias=AliasChoices("md", "markdown"),
|
|
47
|
+
)
|
|
48
|
+
html: str = ""
|
|
49
|
+
value: str = ""
|
|
50
|
+
bbox: LayoutSegmentIR | None = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
validation_alias=AliasChoices("bbox", "bBox"),
|
|
53
|
+
)
|
|
54
|
+
layout_segments: list[LayoutSegmentIR] = Field(
|
|
55
|
+
default_factory=list,
|
|
56
|
+
validation_alias=AliasChoices("layout_segments", "layoutAwareBbox"),
|
|
57
|
+
)
|
|
58
|
+
# Populated by layout-detection providers; left default for parse providers.
|
|
59
|
+
score: float | None = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
ge=0.0,
|
|
62
|
+
le=1.0,
|
|
63
|
+
description="Detector confidence in [0,1]. None for parse-pipeline items.",
|
|
64
|
+
)
|
|
65
|
+
attributes: dict[str, str] = Field(
|
|
66
|
+
default_factory=dict,
|
|
67
|
+
description="Semantic attributes (e.g. scope=mark, picture_type=chart).",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@field_validator("type", mode="before")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _normalize_type(cls, value: object) -> str:
|
|
73
|
+
if value is None:
|
|
74
|
+
return "text"
|
|
75
|
+
return str(value)
|
|
76
|
+
|
|
77
|
+
@field_validator("md", "html", "value", mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _normalize_value(cls, value: object) -> str:
|
|
80
|
+
if value is None:
|
|
81
|
+
return ""
|
|
82
|
+
return str(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ParseLayoutPageIR(BaseModel):
|
|
86
|
+
"""Normalized per-page layout payload embedded in ParseOutput."""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
89
|
+
|
|
90
|
+
page_number: int = Field(
|
|
91
|
+
ge=1,
|
|
92
|
+
validation_alias=AliasChoices("page_number", "page"),
|
|
93
|
+
)
|
|
94
|
+
width: float | None = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
validation_alias=AliasChoices("width", "page_width"),
|
|
97
|
+
)
|
|
98
|
+
height: float | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
validation_alias=AliasChoices("height", "page_height"),
|
|
101
|
+
)
|
|
102
|
+
md: str = ""
|
|
103
|
+
text: str = ""
|
|
104
|
+
page_header_markdown: str = Field(
|
|
105
|
+
default="",
|
|
106
|
+
validation_alias=AliasChoices("page_header_markdown", "pageHeaderMarkdown"),
|
|
107
|
+
)
|
|
108
|
+
page_footer_markdown: str = Field(
|
|
109
|
+
default="",
|
|
110
|
+
validation_alias=AliasChoices("page_footer_markdown", "pageFooterMarkdown"),
|
|
111
|
+
)
|
|
112
|
+
printed_page_number: str = Field(
|
|
113
|
+
default="",
|
|
114
|
+
validation_alias=AliasChoices("printed_page_number", "printedPageNumber"),
|
|
115
|
+
)
|
|
116
|
+
original_orientation_angle: int | None = Field(
|
|
117
|
+
default=None,
|
|
118
|
+
validation_alias=AliasChoices(
|
|
119
|
+
"original_orientation_angle",
|
|
120
|
+
"originalOrientationAngle",
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
items: list[LayoutItemIR] = Field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
@field_validator(
|
|
126
|
+
"md",
|
|
127
|
+
"text",
|
|
128
|
+
"page_header_markdown",
|
|
129
|
+
"page_footer_markdown",
|
|
130
|
+
"printed_page_number",
|
|
131
|
+
mode="before",
|
|
132
|
+
)
|
|
133
|
+
@classmethod
|
|
134
|
+
def _normalize_optional_text(cls, value: object) -> str:
|
|
135
|
+
if value is None:
|
|
136
|
+
return ""
|
|
137
|
+
return str(value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ParseOutput(BaseModel):
|
|
141
|
+
"""Normalized output for parse tasks."""
|
|
142
|
+
|
|
143
|
+
task_type: Literal["parse"] = Field(default="parse", frozen=True, description="Task type discriminator")
|
|
144
|
+
example_id: str = Field(description="Unique identifier for the example")
|
|
145
|
+
pipeline_name: str = Field(description="Name of the pipeline that produced this output")
|
|
146
|
+
pages: list[PageIR] = Field(default_factory=list, description="List of parsed pages")
|
|
147
|
+
layout_pages: list[ParseLayoutPageIR] = Field(
|
|
148
|
+
default_factory=list,
|
|
149
|
+
description=("Normalized page/item/segment layout payload used by layout attribution and overlays"),
|
|
150
|
+
)
|
|
151
|
+
markdown: str = Field(description="Markdown content of the entire document")
|
|
152
|
+
job_id: str | None = Field(default=None, description="Optional job ID from the provider (e.g., LlamaParse job ID)")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from parse_bench.schemas.product import ProductTypeName
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PipelineSpec(BaseModel):
|
|
9
|
+
"""Specification for a pipeline configuration."""
|
|
10
|
+
|
|
11
|
+
pipeline_name: str = Field(description="Name of this pipeline")
|
|
12
|
+
provider_name: str = Field(description="Name of the provider (e.g., 'llama', 'openai')")
|
|
13
|
+
product_type: ProductTypeName = Field(description="Type of product task (parse or extract)")
|
|
14
|
+
config: dict[str, Any] = Field(default_factory=dict, description="Configuration dictionary to pass to the provider")
|
|
15
|
+
per_file_timeout: float | None = Field(
|
|
16
|
+
default=None,
|
|
17
|
+
description=(
|
|
18
|
+
"Optional per-pipeline override (seconds) for the runner's per-file inference timeout. "
|
|
19
|
+
"The run-level --per_file_timeout takes precedence when set. None → use the run-level "
|
|
20
|
+
"value, then the global default. Kept off `config` so providers never receive it."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated, Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, BeforeValidator, Discriminator, Field, SerializeAsAny
|
|
5
|
+
|
|
6
|
+
from parse_bench.schemas.extract_output import ExtractOutput
|
|
7
|
+
from parse_bench.schemas.layout_detection_output import LayoutOutput
|
|
8
|
+
from parse_bench.schemas.parse_output import ParseOutput
|
|
9
|
+
from parse_bench.schemas.pipeline import PipelineSpec
|
|
10
|
+
from parse_bench.schemas.product import ProductTypeName
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InferenceRequest(BaseModel):
|
|
14
|
+
"""Request for running inference on a document."""
|
|
15
|
+
|
|
16
|
+
example_id: str = Field(description="Unique identifier for the example")
|
|
17
|
+
source_file_path: str = Field(description="Path to the source file (PDF, etc.)")
|
|
18
|
+
product_type: ProductTypeName = Field(description="Type of product task to run")
|
|
19
|
+
schema_override: dict[str, Any] | None = Field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Optional schema override",
|
|
22
|
+
)
|
|
23
|
+
config_override: dict[str, Any] | None = Field(
|
|
24
|
+
default=None,
|
|
25
|
+
description=("Optional configuration override to merge with pipeline config"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PipelineOutputType = Annotated[
|
|
30
|
+
ParseOutput | LayoutOutput | ExtractOutput,
|
|
31
|
+
Discriminator("task_type"),
|
|
32
|
+
]
|
|
33
|
+
"""Union of the built-in normalized output models (for type annotations)."""
|
|
34
|
+
|
|
35
|
+
_OUTPUT_MODEL_REGISTRY: dict[str, type[BaseModel]] = {
|
|
36
|
+
"parse": ParseOutput,
|
|
37
|
+
"layout_detection": LayoutOutput,
|
|
38
|
+
"extract": ExtractOutput,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_output_model(task_type: str, model: type[BaseModel]) -> None:
|
|
43
|
+
"""Register a normalized output model for ``task_type``.
|
|
44
|
+
|
|
45
|
+
``InferenceResult.output`` dispatches on the ``task_type`` discriminator, so
|
|
46
|
+
extensions that add a product (e.g. a QA task) register the model their
|
|
47
|
+
provider returns here and saved results round-trip through JSON unchanged.
|
|
48
|
+
Re-registering a built-in task type raises ``ValueError``.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(task_type, str) or not task_type:
|
|
51
|
+
raise ValueError("task_type must be a non-empty string")
|
|
52
|
+
if task_type in {"parse", "layout_detection", "extract"} and _OUTPUT_MODEL_REGISTRY[task_type] is not model:
|
|
53
|
+
raise ValueError(f"{task_type!r} is a built-in output model and cannot be replaced")
|
|
54
|
+
_OUTPUT_MODEL_REGISTRY[task_type] = model
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def registered_output_models() -> dict[str, type[BaseModel]]:
|
|
58
|
+
"""Snapshot of ``task_type`` -> model for every registered output."""
|
|
59
|
+
return dict(_OUTPUT_MODEL_REGISTRY)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def coerce_pipeline_output(value: Any) -> BaseModel:
|
|
63
|
+
"""Validate a normalized output payload against the registered model for its ``task_type``."""
|
|
64
|
+
if isinstance(value, BaseModel):
|
|
65
|
+
return value
|
|
66
|
+
if not isinstance(value, dict):
|
|
67
|
+
raise TypeError(f"Pipeline output must be a dict or model, got {type(value).__name__}")
|
|
68
|
+
task_type = value.get("task_type")
|
|
69
|
+
model = _OUTPUT_MODEL_REGISTRY.get(task_type) if isinstance(task_type, str) else None
|
|
70
|
+
if model is None:
|
|
71
|
+
raise ValueError(f"Unknown output task_type {task_type!r}; registered: {sorted(_OUTPUT_MODEL_REGISTRY)}")
|
|
72
|
+
return model.model_validate(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
PipelineOutput = Annotated[SerializeAsAny[BaseModel], BeforeValidator(coerce_pipeline_output)]
|
|
76
|
+
"""Field annotation for a normalized output: dispatches on ``task_type`` via the registry."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RawInferenceResult(BaseModel):
|
|
80
|
+
"""Raw result from provider before normalization."""
|
|
81
|
+
|
|
82
|
+
request: InferenceRequest = Field(description="Original inference request")
|
|
83
|
+
pipeline: PipelineSpec = Field(description="Pipeline used")
|
|
84
|
+
pipeline_name: str = Field(description="Name of the pipeline used")
|
|
85
|
+
product_type: ProductTypeName = Field(description="Type of product task that was run")
|
|
86
|
+
raw_output: dict = Field(description="Raw output from the provider API")
|
|
87
|
+
started_at: datetime = Field(description="Timestamp when inference started")
|
|
88
|
+
completed_at: datetime = Field(description="Timestamp when inference completed")
|
|
89
|
+
latency_in_ms: int = Field(ge=0, description="Latency in milliseconds")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class InferenceResult(BaseModel):
|
|
93
|
+
"""Result of running inference on a document with both raw and normalized outputs."""
|
|
94
|
+
|
|
95
|
+
request: InferenceRequest = Field(description="Original inference request")
|
|
96
|
+
pipeline_name: str = Field(description="Name of the pipeline used")
|
|
97
|
+
product_type: ProductTypeName = Field(description="Type of product task that was run")
|
|
98
|
+
|
|
99
|
+
# Both outputs stored here
|
|
100
|
+
raw_output: dict = Field(description="Raw output from the provider (for debugging/re-normalization)")
|
|
101
|
+
output: PipelineOutput = Field(description="Normalized output from the pipeline")
|
|
102
|
+
|
|
103
|
+
# metadata
|
|
104
|
+
started_at: datetime = Field(description="Timestamp when inference started")
|
|
105
|
+
completed_at: datetime = Field(description="Timestamp when inference completed")
|
|
106
|
+
latency_in_ms: int = Field(ge=0, description="Latency in milliseconds")
|