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,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")