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,946 @@
1
+ """Label adapters for canonicalizing layout detection predictions.
2
+
3
+ This module implements the canonicalization logic defined in
4
+ layout_detection_class_label_canonicalization_proposal.md.
5
+
6
+ Each model has its own adapter that converts model-specific labels
7
+ to the Canonical17 schema with optional attributes, and then to
8
+ the Core11 (DocLayNet-compatible) schema for evaluation.
9
+ """
10
+
11
+ from abc import ABC, abstractmethod
12
+
13
+ from parse_bench.layout_label_mapping import (
14
+ LLAMAPARSE_V2_RAW_TO_CANONICAL,
15
+ LLAMAPARSE_V3_RAW_TO_CANONICAL,
16
+ )
17
+ from parse_bench.schemas.layout_detection_output import (
18
+ CanonicalLayoutPrediction,
19
+ ChandraLabel,
20
+ CoreLayoutPrediction,
21
+ DoclingLabel,
22
+ LayoutV3Label,
23
+ PPDocLayoutLabel,
24
+ Qwen3VLLabel,
25
+ SuryaLabel,
26
+ )
27
+ from parse_bench.schemas.layout_ontology import (
28
+ CANONICAL_TO_BASIC,
29
+ CANONICAL_TO_CORE,
30
+ CanonicalLabel,
31
+ )
32
+
33
+
34
+ def canonical_to_core(
35
+ canonical_pred: CanonicalLayoutPrediction,
36
+ ) -> CoreLayoutPrediction | None:
37
+ """
38
+ Convert a Canonical17 prediction to Core11 prediction.
39
+
40
+ :param canonical_pred: Canonical17 prediction
41
+ :return: CoreLayoutPrediction or None if no Core11 equivalent
42
+ """
43
+ core_class = CANONICAL_TO_CORE.get(canonical_pred.canonical_class)
44
+ if core_class is None:
45
+ return None
46
+
47
+ return CoreLayoutPrediction(
48
+ bbox=canonical_pred.bbox,
49
+ score=canonical_pred.score,
50
+ core_class=core_class,
51
+ attributes=canonical_pred.attributes,
52
+ original_label=canonical_pred.original_label,
53
+ )
54
+
55
+
56
+ def canonical_to_basic(
57
+ canonical_pred: CanonicalLayoutPrediction,
58
+ ) -> tuple[str, dict[str, str]] | None:
59
+ """
60
+ Convert a Canonical17 prediction to a Basic label and merged attributes.
61
+
62
+ Existing attributes take precedence over conversion attributes.
63
+ """
64
+ mapping = CANONICAL_TO_BASIC.get(canonical_pred.canonical_class)
65
+ if mapping is None:
66
+ return None
67
+
68
+ basic_label, conversion_attrs = mapping
69
+ merged_attrs = dict(conversion_attrs)
70
+ merged_attrs.update(canonical_pred.attributes)
71
+ return basic_label.value, merged_attrs
72
+
73
+
74
+ class BaseLabelAdapter(ABC):
75
+ """Base class for label canonicalization adapters."""
76
+
77
+ @abstractmethod
78
+ def to_canonical(
79
+ self,
80
+ label: int,
81
+ score: float,
82
+ bbox: list[float],
83
+ ) -> CanonicalLayoutPrediction | None:
84
+ """
85
+ Convert model-specific label to canonical prediction.
86
+
87
+ :param label: Model-specific label as int
88
+ :param score: Confidence score (0-1)
89
+ :param bbox: Bounding box [x1, y1, x2, y2]
90
+ :return: CanonicalLayoutPrediction or None if label should be skipped
91
+ """
92
+ raise NotImplementedError
93
+
94
+ def to_core(
95
+ self,
96
+ label: int,
97
+ score: float,
98
+ bbox: list[float],
99
+ ) -> CoreLayoutPrediction | None:
100
+ """
101
+ Convert model-specific label to Core11 prediction.
102
+
103
+ First converts to Canonical17, then maps to Core11.
104
+
105
+ :param label: Model-specific label as int
106
+ :param score: Confidence score (0-1)
107
+ :param bbox: Bounding box [x1, y1, x2, y2]
108
+ :return: CoreLayoutPrediction or None if no Core11 equivalent
109
+ """
110
+ canonical = self.to_canonical(label, score, bbox)
111
+ if canonical is None:
112
+ return None
113
+ return canonical_to_core(canonical)
114
+
115
+
116
+ class YoloLayoutDetLabelAdapter(BaseLabelAdapter):
117
+ """Adapter for YOLO-DocLayNet labels -> Canonical17.
118
+
119
+ YOLO outputs Core11 labels (DocLayNet) that map 1:1 to Canonical17 (identity mapping).
120
+ """
121
+
122
+ # YoloLabel -> (CanonicalLabel, attributes)
123
+ # Identity mapping - YOLO labels are Core11 which map directly to Canonical17
124
+ MAPPING: dict[int, tuple[CanonicalLabel, dict[str, str]]] = {
125
+ 0: (CanonicalLabel.CAPTION, {}),
126
+ 1: (CanonicalLabel.FOOTNOTE, {}),
127
+ 2: (CanonicalLabel.FORMULA, {}),
128
+ 3: (CanonicalLabel.LIST_ITEM, {}),
129
+ 4: (CanonicalLabel.PAGE_FOOTER, {}),
130
+ 5: (CanonicalLabel.PAGE_HEADER, {}),
131
+ 6: (CanonicalLabel.PICTURE, {}),
132
+ 7: (CanonicalLabel.SECTION_HEADER, {}),
133
+ 8: (CanonicalLabel.TABLE, {}),
134
+ 9: (CanonicalLabel.TEXT, {}),
135
+ 10: (CanonicalLabel.TITLE, {}),
136
+ }
137
+
138
+ def to_canonical(
139
+ self,
140
+ label: int,
141
+ score: float,
142
+ bbox: list[float],
143
+ ) -> CanonicalLayoutPrediction | None:
144
+ """
145
+ Convert YOLO label to canonical prediction.
146
+
147
+ :param label: YOLO label as int (0-10)
148
+ :param score: Confidence score (0-1)
149
+ :param bbox: Bounding box [x1, y1, x2, y2]
150
+ :return: CanonicalLayoutPrediction or None if unknown label
151
+ """
152
+ mapping = self.MAPPING.get(label)
153
+ if mapping is None:
154
+ # Unknown label, skip
155
+ return None
156
+
157
+ canonical_class, attributes = mapping
158
+
159
+ return CanonicalLayoutPrediction(
160
+ bbox=bbox,
161
+ score=score,
162
+ canonical_class=canonical_class,
163
+ attributes=attributes,
164
+ original_label=label,
165
+ )
166
+
167
+
168
+ class DoclingLayoutDetLabelAdapter(BaseLabelAdapter):
169
+ """Adapter for Docling RT-DETR labels -> Canonical17.
170
+
171
+ Docling Heron outputs labels that map 1:1 to Canonical17 (identity mapping).
172
+ See proposal: "Docling Heron → Canonical17: No mapping required."
173
+ """
174
+
175
+ # DoclingLabel -> (CanonicalLabel, attributes)
176
+ # Identity mapping - Docling labels are already Canonical17
177
+ MAPPING: dict[DoclingLabel, tuple[CanonicalLabel, dict[str, str]]] = {
178
+ DoclingLabel.CAPTION: (CanonicalLabel.CAPTION, {}),
179
+ DoclingLabel.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
180
+ DoclingLabel.FORMULA: (CanonicalLabel.FORMULA, {}),
181
+ DoclingLabel.LIST_ITEM: (CanonicalLabel.LIST_ITEM, {}),
182
+ DoclingLabel.PAGE_FOOTER: (CanonicalLabel.PAGE_FOOTER, {}),
183
+ DoclingLabel.PAGE_HEADER: (CanonicalLabel.PAGE_HEADER, {}),
184
+ DoclingLabel.PICTURE: (CanonicalLabel.PICTURE, {}),
185
+ DoclingLabel.SECTION_HEADER: (CanonicalLabel.SECTION_HEADER, {}),
186
+ DoclingLabel.TABLE: (CanonicalLabel.TABLE, {}),
187
+ DoclingLabel.TEXT: (CanonicalLabel.TEXT, {}),
188
+ DoclingLabel.TITLE: (CanonicalLabel.TITLE, {}),
189
+ DoclingLabel.DOCUMENT_INDEX: (CanonicalLabel.DOCUMENT_INDEX, {}),
190
+ DoclingLabel.CODE: (CanonicalLabel.CODE, {}),
191
+ DoclingLabel.CHECKBOX_SELECTED: (CanonicalLabel.CHECKBOX_SELECTED, {}),
192
+ DoclingLabel.CHECKBOX_UNSELECTED: (CanonicalLabel.CHECKBOX_UNSELECTED, {}),
193
+ DoclingLabel.FORM: (CanonicalLabel.FORM, {}),
194
+ DoclingLabel.KEY_VALUE_REGION: (CanonicalLabel.KEY_VALUE_REGION, {}),
195
+ }
196
+
197
+ def to_canonical(
198
+ self,
199
+ label: int,
200
+ score: float,
201
+ bbox: list[float],
202
+ ) -> CanonicalLayoutPrediction | None:
203
+ """
204
+ Convert Docling label to canonical prediction.
205
+
206
+ :param label: Docling label as int (0-16)
207
+ :param score: Confidence score (0-1)
208
+ :param bbox: Bounding box [x1, y1, x2, y2]
209
+ :return: CanonicalLayoutPrediction or None if unknown label
210
+ """
211
+ try:
212
+ docling_label = DoclingLabel(label)
213
+ except ValueError:
214
+ # Unknown label, skip
215
+ return None
216
+
217
+ mapping = self.MAPPING.get(docling_label)
218
+ if mapping is None:
219
+ return None
220
+
221
+ canonical_class, attributes = mapping
222
+
223
+ return CanonicalLayoutPrediction(
224
+ bbox=bbox,
225
+ score=score,
226
+ canonical_class=canonical_class,
227
+ attributes=attributes,
228
+ original_label=label,
229
+ )
230
+
231
+
232
+ class LayoutV3LabelAdapter(BaseLabelAdapter):
233
+ """Adapter for Layout-V3 labels -> Canonical17 with figure classification.
234
+
235
+ Layout-V3 outputs the same 17-class schema as Docling Heron.
236
+ This adapter extends the mapping to support figure classification
237
+ attributes for Picture detections.
238
+ """
239
+
240
+ # LayoutV3Label -> (CanonicalLabel, attributes)
241
+ # Identity mapping - Layout-V3 labels are already Canonical17
242
+ MAPPING: dict[LayoutV3Label, tuple[CanonicalLabel, dict[str, str]]] = {
243
+ LayoutV3Label.CAPTION: (CanonicalLabel.CAPTION, {}),
244
+ LayoutV3Label.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
245
+ LayoutV3Label.FORMULA: (CanonicalLabel.FORMULA, {}),
246
+ LayoutV3Label.LIST_ITEM: (CanonicalLabel.LIST_ITEM, {}),
247
+ LayoutV3Label.PAGE_FOOTER: (CanonicalLabel.PAGE_FOOTER, {}),
248
+ LayoutV3Label.PAGE_HEADER: (CanonicalLabel.PAGE_HEADER, {}),
249
+ LayoutV3Label.PICTURE: (CanonicalLabel.PICTURE, {}),
250
+ LayoutV3Label.SECTION_HEADER: (CanonicalLabel.SECTION_HEADER, {}),
251
+ LayoutV3Label.TABLE: (CanonicalLabel.TABLE, {}),
252
+ LayoutV3Label.TEXT: (CanonicalLabel.TEXT, {}),
253
+ LayoutV3Label.TITLE: (CanonicalLabel.TITLE, {}),
254
+ LayoutV3Label.DOCUMENT_INDEX: (CanonicalLabel.DOCUMENT_INDEX, {}),
255
+ LayoutV3Label.CODE: (CanonicalLabel.CODE, {}),
256
+ LayoutV3Label.CHECKBOX_SELECTED: (CanonicalLabel.CHECKBOX_SELECTED, {}),
257
+ LayoutV3Label.CHECKBOX_UNSELECTED: (CanonicalLabel.CHECKBOX_UNSELECTED, {}),
258
+ LayoutV3Label.FORM: (CanonicalLabel.FORM, {}),
259
+ LayoutV3Label.KEY_VALUE_REGION: (CanonicalLabel.KEY_VALUE_REGION, {}),
260
+ }
261
+
262
+ def to_canonical_with_figure_class(
263
+ self,
264
+ label: int,
265
+ score: float,
266
+ bbox: list[float],
267
+ figure_class: str | None = None,
268
+ figure_score: float | None = None,
269
+ ) -> CanonicalLayoutPrediction | None:
270
+ """
271
+ Convert Layout-V3 label to canonical prediction with figure classification.
272
+
273
+ For Picture detections, figure classification is stored as attributes:
274
+ - picture_type: The classified figure type (e.g., "bar_chart", "logo")
275
+ - figure_score: The figure classification confidence score
276
+
277
+ :param label: Layout-V3 label as int (0-16)
278
+ :param score: Detection confidence score (0-1)
279
+ :param bbox: Bounding box [x1, y1, x2, y2]
280
+ :param figure_class: Figure classification type (for Picture labels only)
281
+ :param figure_score: Figure classification confidence (for Picture labels only)
282
+ :return: CanonicalLayoutPrediction or None if unknown label
283
+ """
284
+ try:
285
+ v3_label = LayoutV3Label(label)
286
+ except ValueError:
287
+ # Unknown label, skip
288
+ return None
289
+
290
+ mapping = self.MAPPING.get(v3_label)
291
+ if mapping is None:
292
+ return None
293
+
294
+ canonical_class, base_attributes = mapping
295
+
296
+ # Build attributes, adding figure classification if present
297
+ attributes = dict(base_attributes)
298
+ if figure_class is not None and canonical_class == CanonicalLabel.PICTURE:
299
+ attributes["picture_type"] = figure_class
300
+ if figure_score is not None:
301
+ attributes["figure_score"] = str(round(figure_score, 4))
302
+
303
+ return CanonicalLayoutPrediction(
304
+ bbox=bbox,
305
+ score=score,
306
+ canonical_class=canonical_class,
307
+ attributes=attributes,
308
+ original_label=label,
309
+ )
310
+
311
+ def to_canonical(
312
+ self,
313
+ label: int,
314
+ score: float,
315
+ bbox: list[float],
316
+ ) -> CanonicalLayoutPrediction | None:
317
+ """
318
+ Convert Layout-V3 label to canonical prediction (without figure class).
319
+
320
+ :param label: Layout-V3 label as int (0-16)
321
+ :param score: Confidence score (0-1)
322
+ :param bbox: Bounding box [x1, y1, x2, y2]
323
+ :return: CanonicalLayoutPrediction or None if unknown label
324
+ """
325
+ return self.to_canonical_with_figure_class(label, score, bbox)
326
+
327
+
328
+ class PPLayoutDetLabelAdapter(BaseLabelAdapter):
329
+ """Adapter for Paddle PP-DocLayout labels -> Canonical17 + attributes.
330
+
331
+ See proposal section: "DocLayout / PP-DocLayout-style → Canonical17 + attributes"
332
+
333
+ This adapter generates attributes for finer semantic information
334
+ (e.g., text_role, picture_type, title_level).
335
+ """
336
+
337
+ # PPDocLayoutLabel -> (CanonicalLabel, attributes)
338
+ MAPPING: dict[PPDocLayoutLabel, tuple[CanonicalLabel, dict[str, str]]] = {
339
+ # Title variants
340
+ PPDocLayoutLabel.DOC_TITLE: (CanonicalLabel.TITLE, {"title_level": "document"}),
341
+ PPDocLayoutLabel.PARAGRAPH_TITLE: (
342
+ CanonicalLabel.SECTION_HEADER,
343
+ {"title_level": "paragraph"},
344
+ ),
345
+ # Text variants
346
+ PPDocLayoutLabel.TEXT: (CanonicalLabel.TEXT, {}),
347
+ PPDocLayoutLabel.NUMBER: (CanonicalLabel.TEXT, {"text_role": "page_number"}),
348
+ PPDocLayoutLabel.ABSTRACT: (CanonicalLabel.TEXT, {"text_role": "abstract"}),
349
+ PPDocLayoutLabel.CONTENT: (CanonicalLabel.TEXT, {"text_role": "body"}),
350
+ PPDocLayoutLabel.REFERENCE: (CanonicalLabel.TEXT, {"text_role": "references"}),
351
+ PPDocLayoutLabel.ASIDE_TEXT: (CanonicalLabel.TEXT, {"text_role": "sidebar"}),
352
+ PPDocLayoutLabel.REFERENCE_CONTENT: (CanonicalLabel.TEXT, {"text_role": "references"}),
353
+ PPDocLayoutLabel.FORMULA_NUMBER: (CanonicalLabel.TEXT, {"text_role": "formula_number"}),
354
+ # Page furniture
355
+ PPDocLayoutLabel.HEADER: (CanonicalLabel.PAGE_HEADER, {"furniture": "page-header"}),
356
+ PPDocLayoutLabel.FOOTER: (CanonicalLabel.PAGE_FOOTER, {"furniture": "page-footer"}),
357
+ PPDocLayoutLabel.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
358
+ # Pictures
359
+ PPDocLayoutLabel.IMAGE: (CanonicalLabel.PICTURE, {"picture_type": "image"}),
360
+ PPDocLayoutLabel.CHART: (CanonicalLabel.PICTURE, {"picture_type": "chart"}),
361
+ PPDocLayoutLabel.SEAL: (CanonicalLabel.PICTURE, {"picture_type": "seal"}),
362
+ # Captions
363
+ PPDocLayoutLabel.FIGURE_TITLE: (CanonicalLabel.CAPTION, {"caption_of": "picture"}),
364
+ # Other
365
+ PPDocLayoutLabel.TABLE: (CanonicalLabel.TABLE, {}),
366
+ PPDocLayoutLabel.FORMULA: (CanonicalLabel.FORMULA, {}),
367
+ PPDocLayoutLabel.ALGORITHM: (CanonicalLabel.CODE, {}),
368
+ }
369
+
370
+ def to_canonical(
371
+ self,
372
+ label: int,
373
+ score: float,
374
+ bbox: list[float],
375
+ ) -> CanonicalLayoutPrediction | None:
376
+ """
377
+ Convert PP-DocLayout label to canonical prediction with attributes.
378
+
379
+ :param label: PP-DocLayout label as int (0-19)
380
+ :param score: Confidence score (0-1)
381
+ :param bbox: Bounding box [x1, y1, x2, y2]
382
+ :return: CanonicalLayoutPrediction or None if unknown label
383
+ """
384
+ try:
385
+ pp_label = PPDocLayoutLabel(label)
386
+ except ValueError:
387
+ # Unknown label, skip
388
+ return None
389
+
390
+ mapping = self.MAPPING.get(pp_label)
391
+ if mapping is None:
392
+ return None
393
+
394
+ canonical_class, attributes = mapping
395
+
396
+ return CanonicalLayoutPrediction(
397
+ bbox=bbox,
398
+ score=score,
399
+ canonical_class=canonical_class,
400
+ attributes=attributes,
401
+ original_label=label,
402
+ )
403
+
404
+
405
+ # =============================================================================
406
+ # Qwen3VL Adapter (Qwen3VLLabel -> Canonical17)
407
+ # =============================================================================
408
+
409
+
410
+ class Qwen3VLLayoutDetLabelAdapter(BaseLabelAdapter):
411
+ """Adapter for Qwen3VL labels -> Canonical17.
412
+
413
+ Qwen3VL outputs Core11 labels. Identity mapping to Canonical17.
414
+ """
415
+
416
+ # Qwen3VLLabel -> (CanonicalLabel, attributes)
417
+ # Identity mapping for Core11 labels
418
+ MAPPING: dict[Qwen3VLLabel, tuple[CanonicalLabel, dict[str, str]]] = {
419
+ Qwen3VLLabel.CAPTION: (CanonicalLabel.CAPTION, {}),
420
+ Qwen3VLLabel.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
421
+ Qwen3VLLabel.FORMULA: (CanonicalLabel.FORMULA, {}),
422
+ Qwen3VLLabel.LIST_ITEM: (CanonicalLabel.LIST_ITEM, {}),
423
+ Qwen3VLLabel.PAGE_FOOTER: (CanonicalLabel.PAGE_FOOTER, {}),
424
+ Qwen3VLLabel.PAGE_HEADER: (CanonicalLabel.PAGE_HEADER, {}),
425
+ Qwen3VLLabel.PICTURE: (CanonicalLabel.PICTURE, {}),
426
+ Qwen3VLLabel.SECTION_HEADER: (CanonicalLabel.SECTION_HEADER, {}),
427
+ Qwen3VLLabel.TABLE: (CanonicalLabel.TABLE, {}),
428
+ Qwen3VLLabel.TEXT: (CanonicalLabel.TEXT, {}),
429
+ Qwen3VLLabel.TITLE: (CanonicalLabel.TITLE, {}),
430
+ }
431
+
432
+ def to_canonical(
433
+ self,
434
+ label: int,
435
+ score: float,
436
+ bbox: list[float],
437
+ ) -> CanonicalLayoutPrediction | None:
438
+ """
439
+ Convert Qwen3VL label to canonical prediction.
440
+
441
+ :param label: Qwen3VL label as int (0-10)
442
+ :param score: Confidence score (0-1)
443
+ :param bbox: Bounding box [x1, y1, x2, y2]
444
+ :return: CanonicalLayoutPrediction or None if unknown label
445
+ """
446
+ qwen_label = Qwen3VLLabel(label)
447
+ mapping = self.MAPPING.get(qwen_label)
448
+ if mapping is None:
449
+ return None
450
+
451
+ canonical_class, attributes = mapping
452
+
453
+ return CanonicalLayoutPrediction(
454
+ bbox=bbox,
455
+ score=score,
456
+ canonical_class=canonical_class,
457
+ attributes=attributes,
458
+ original_label=label,
459
+ )
460
+
461
+
462
+ # =============================================================================
463
+ # dots.ocr Adapter (string labels -> Canonical17)
464
+ # =============================================================================
465
+
466
+
467
+ class DotsOcrLayoutDetLabelAdapter:
468
+ """Adapter for dots.ocr layout labels -> Canonical17 + attributes.
469
+
470
+ dots.ocr outputs Core11-style labels as strings (e.g., Caption, Text).
471
+ This adapter normalizes labels and maps to Canonical17 with optional
472
+ attributes for picture variants.
473
+ """
474
+
475
+ # Normalized string label -> (CanonicalLabel, attributes)
476
+ MAPPING: dict[str, tuple[CanonicalLabel, dict[str, str]]] = {
477
+ # Core11 direct mappings
478
+ "caption": (CanonicalLabel.CAPTION, {}),
479
+ "footnote": (CanonicalLabel.FOOTNOTE, {}),
480
+ "formula": (CanonicalLabel.FORMULA, {}),
481
+ "list-item": (CanonicalLabel.LIST_ITEM, {}),
482
+ "listitem": (CanonicalLabel.LIST_ITEM, {}),
483
+ "page-footer": (CanonicalLabel.PAGE_FOOTER, {}),
484
+ "pagefooter": (CanonicalLabel.PAGE_FOOTER, {}),
485
+ "page-header": (CanonicalLabel.PAGE_HEADER, {}),
486
+ "pageheader": (CanonicalLabel.PAGE_HEADER, {}),
487
+ "picture": (CanonicalLabel.PICTURE, {}),
488
+ "section-header": (CanonicalLabel.SECTION_HEADER, {}),
489
+ "sectionheader": (CanonicalLabel.SECTION_HEADER, {}),
490
+ "table": (CanonicalLabel.TABLE, {}),
491
+ "text": (CanonicalLabel.TEXT, {}),
492
+ "title": (CanonicalLabel.TITLE, {}),
493
+ # Picture variants
494
+ "image": (CanonicalLabel.PICTURE, {"picture_type": "image"}),
495
+ "figure": (CanonicalLabel.PICTURE, {"picture_type": "figure"}),
496
+ }
497
+
498
+ def to_canonical(
499
+ self,
500
+ label: str,
501
+ score: float,
502
+ bbox: list[float],
503
+ ) -> CanonicalLayoutPrediction | None:
504
+ """Convert dots.ocr string label to canonical prediction."""
505
+ normalized = _normalize_dots_label(label)
506
+ mapping = self.MAPPING.get(normalized)
507
+ if mapping is None:
508
+ return None
509
+
510
+ canonical_class, attributes = mapping
511
+
512
+ return CanonicalLayoutPrediction(
513
+ bbox=bbox,
514
+ score=score,
515
+ canonical_class=canonical_class,
516
+ attributes=attributes,
517
+ original_label=label,
518
+ )
519
+
520
+ def to_core(
521
+ self,
522
+ label: str,
523
+ score: float,
524
+ bbox: list[float],
525
+ ) -> CoreLayoutPrediction | None:
526
+ """Convert dots.ocr label to Core11 prediction."""
527
+ canonical = self.to_canonical(label, score, bbox)
528
+ if canonical is None:
529
+ return None
530
+ return canonical_to_core(canonical)
531
+
532
+ def to_basic(
533
+ self,
534
+ label: str,
535
+ score: float,
536
+ bbox: list[float],
537
+ ) -> tuple[str, dict[str, str]] | None:
538
+ """Convert dots.ocr label to Basic label and merged attributes."""
539
+ canonical = self.to_canonical(label, score, bbox)
540
+ if canonical is None:
541
+ return None
542
+ return canonical_to_basic(canonical)
543
+
544
+
545
+ def _normalize_dots_label(label: str) -> str:
546
+ normalized = label.strip().lower()
547
+ normalized = normalized.replace("_", "-").replace(" ", "-")
548
+ return normalized
549
+
550
+
551
+ # =============================================================================
552
+ # Surya Adapter (SuryaLabel -> Canonical17)
553
+ # =============================================================================
554
+
555
+
556
+ class SuryaLayoutDetLabelAdapter(BaseLabelAdapter):
557
+ """Adapter for Surya OCR layout labels -> Canonical17 + attributes.
558
+
559
+ Surya outputs 16 layout classes. Mapping based on semantic equivalence
560
+ to DocLayNet/Canonical17 labels.
561
+
562
+ Surya labels: Caption, Footnote, Formula/Equation, List-item, Page-footer,
563
+ Page-header, Picture, Figure, Section-header, Table, Form,
564
+ Table-of-contents, Handwriting, Text, Text-inline-math, Code
565
+ """
566
+
567
+ # SuryaLabel -> (CanonicalLabel, attributes)
568
+ MAPPING: dict[SuryaLabel, tuple[CanonicalLabel, dict[str, str]]] = {
569
+ # Core11 identity mappings
570
+ SuryaLabel.CAPTION: (CanonicalLabel.CAPTION, {}),
571
+ SuryaLabel.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
572
+ SuryaLabel.FORMULA: (CanonicalLabel.FORMULA, {}), # Also "Equation" in v0.17+
573
+ SuryaLabel.LIST_ITEM: (CanonicalLabel.LIST_ITEM, {}),
574
+ SuryaLabel.PAGE_FOOTER: (CanonicalLabel.PAGE_FOOTER, {}),
575
+ SuryaLabel.PAGE_HEADER: (CanonicalLabel.PAGE_HEADER, {}),
576
+ SuryaLabel.PICTURE: (CanonicalLabel.PICTURE, {}),
577
+ SuryaLabel.SECTION_HEADER: (CanonicalLabel.SECTION_HEADER, {}),
578
+ SuryaLabel.TABLE: (CanonicalLabel.TABLE, {}),
579
+ SuryaLabel.TEXT: (CanonicalLabel.TEXT, {}),
580
+ # Figure -> Picture with attribute
581
+ SuryaLabel.FIGURE: (CanonicalLabel.PICTURE, {"picture_type": "figure"}),
582
+ # Form -> Form (Canonical17 extended, no Core11 equivalent)
583
+ SuryaLabel.FORM: (CanonicalLabel.FORM, {}),
584
+ # Table-of-contents -> Document Index
585
+ SuryaLabel.TABLE_OF_CONTENTS: (CanonicalLabel.DOCUMENT_INDEX, {}),
586
+ # Handwriting -> Text with attribute
587
+ SuryaLabel.HANDWRITING: (CanonicalLabel.TEXT, {"text_role": "handwriting"}),
588
+ # Text-inline-math -> Formula with attribute
589
+ SuryaLabel.TEXT_INLINE_MATH: (CanonicalLabel.FORMULA, {"formula_type": "inline"}),
590
+ # Code -> Code (new in Surya v0.17+)
591
+ SuryaLabel.CODE: (CanonicalLabel.CODE, {}),
592
+ }
593
+
594
+ def to_canonical(
595
+ self,
596
+ label: int,
597
+ score: float,
598
+ bbox: list[float],
599
+ ) -> CanonicalLayoutPrediction | None:
600
+ """
601
+ Convert Surya label to canonical prediction with attributes.
602
+
603
+ :param label: Surya label as int (0-14)
604
+ :param score: Confidence score (0-1)
605
+ :param bbox: Bounding box [x1, y1, x2, y2]
606
+ :return: CanonicalLayoutPrediction or None if unknown label
607
+ """
608
+ try:
609
+ surya_label = SuryaLabel(label)
610
+ except ValueError:
611
+ # Unknown label, skip
612
+ return None
613
+
614
+ mapping = self.MAPPING.get(surya_label)
615
+ if mapping is None:
616
+ return None
617
+
618
+ canonical_class, attributes = mapping
619
+
620
+ return CanonicalLayoutPrediction(
621
+ bbox=bbox,
622
+ score=score,
623
+ canonical_class=canonical_class,
624
+ attributes=attributes,
625
+ original_label=label,
626
+ )
627
+
628
+
629
+ # =============================================================================
630
+ # Chandra Adapter (ChandraLabel -> Canonical17)
631
+ # =============================================================================
632
+
633
+
634
+ class ChandraLayoutDetLabelAdapter(BaseLabelAdapter):
635
+ """Adapter for Chandra OCR layout labels -> Canonical17 + attributes.
636
+
637
+ Chandra outputs 15 layout classes via its ocr_layout prompt mode.
638
+ Mapping based on semantic equivalence to DocLayNet/Canonical17 labels.
639
+
640
+ Chandra labels: Caption, Footnote, Equation-Block, List-Group, Page-Header,
641
+ Page-Footer, Image, Section-Header, Table, Text, Complex-Block, Code-Block,
642
+ Form, Table-Of-Contents, Figure
643
+ """
644
+
645
+ # ChandraLabel -> (CanonicalLabel, attributes)
646
+ MAPPING: dict[ChandraLabel, tuple[CanonicalLabel, dict[str, str]]] = {
647
+ # Core11 identity mappings
648
+ ChandraLabel.CAPTION: (CanonicalLabel.CAPTION, {}),
649
+ ChandraLabel.FOOTNOTE: (CanonicalLabel.FOOTNOTE, {}),
650
+ ChandraLabel.EQUATION_BLOCK: (CanonicalLabel.FORMULA, {}),
651
+ ChandraLabel.LIST_GROUP: (CanonicalLabel.LIST_ITEM, {}),
652
+ ChandraLabel.PAGE_HEADER: (CanonicalLabel.PAGE_HEADER, {}),
653
+ ChandraLabel.PAGE_FOOTER: (CanonicalLabel.PAGE_FOOTER, {}),
654
+ ChandraLabel.SECTION_HEADER: (CanonicalLabel.SECTION_HEADER, {}),
655
+ ChandraLabel.TABLE: (CanonicalLabel.TABLE, {}),
656
+ ChandraLabel.TEXT: (CanonicalLabel.TEXT, {}),
657
+ # Image -> Picture with attribute
658
+ ChandraLabel.IMAGE: (CanonicalLabel.PICTURE, {"picture_type": "image"}),
659
+ # Figure -> Picture with attribute
660
+ ChandraLabel.FIGURE: (CanonicalLabel.PICTURE, {"picture_type": "figure"}),
661
+ # Complex-Block -> Text with attribute (generic complex content)
662
+ ChandraLabel.COMPLEX_BLOCK: (CanonicalLabel.TEXT, {"text_role": "complex"}),
663
+ # Code-Block -> Code
664
+ ChandraLabel.CODE_BLOCK: (CanonicalLabel.CODE, {}),
665
+ # Form -> Form (Canonical17 extended, no Core11 equivalent)
666
+ ChandraLabel.FORM: (CanonicalLabel.FORM, {}),
667
+ # Table-Of-Contents -> Document Index
668
+ ChandraLabel.TABLE_OF_CONTENTS: (CanonicalLabel.DOCUMENT_INDEX, {}),
669
+ }
670
+
671
+ def to_canonical(
672
+ self,
673
+ label: int,
674
+ score: float,
675
+ bbox: list[float],
676
+ ) -> CanonicalLayoutPrediction | None:
677
+ """
678
+ Convert Chandra label to canonical prediction with attributes.
679
+
680
+ :param label: Chandra label as int (0-14)
681
+ :param score: Confidence score (0-1)
682
+ :param bbox: Bounding box [x1, y1, x2, y2]
683
+ :return: CanonicalLayoutPrediction or None if unknown label
684
+ """
685
+ try:
686
+ chandra_label = ChandraLabel(label)
687
+ except ValueError:
688
+ # Unknown label, skip
689
+ return None
690
+
691
+ mapping = self.MAPPING.get(chandra_label)
692
+ if mapping is None:
693
+ return None
694
+
695
+ canonical_class, attributes = mapping
696
+
697
+ return CanonicalLayoutPrediction(
698
+ bbox=bbox,
699
+ score=score,
700
+ canonical_class=canonical_class,
701
+ attributes=attributes,
702
+ original_label=label,
703
+ )
704
+
705
+
706
+ # =============================================================================
707
+ # LlamaParse Adapter (V2 string labels -> Canonical17)
708
+ # =============================================================================
709
+
710
+
711
+ class LlamaParseLayoutDetLabelAdapter:
712
+ """Adapter for LlamaParse V2 layout labels -> Canonical17 + attributes.
713
+
714
+ LlamaParse uses the same V2 label schema as Paddle PP-DocLayout (20 classes),
715
+ but returns string labels instead of integer indices.
716
+
717
+ V2 labels:
718
+ - 0: paragraph_title, 1: image, 2: text, 3: number, 4: abstract, 5: content,
719
+ - 6: figure_title, 7: formula, 8: table, 9: reference, 10: doc_title,
720
+ - 11: footnote, 12: header, 13: algorithm, 14: footer, 15: seal,
721
+ - 16: chart, 17: formula_number, 18: aside_text, 19: reference_content
722
+ """
723
+
724
+ # Shared central mapping.
725
+ MAPPING: dict[str, tuple[CanonicalLabel, dict[str, str]]] = LLAMAPARSE_V2_RAW_TO_CANONICAL
726
+
727
+ def to_canonical(
728
+ self,
729
+ label: str,
730
+ score: float,
731
+ bbox: list[float],
732
+ ) -> CanonicalLayoutPrediction | None:
733
+ """
734
+ Convert LlamaParse string label to canonical prediction with attributes.
735
+
736
+ :param label: LlamaParse V2 label as string (e.g., "text", "table")
737
+ :param score: Confidence score (0-1)
738
+ :param bbox: Bounding box [x1, y1, x2, y2]
739
+ :return: CanonicalLayoutPrediction or None if unknown label
740
+ """
741
+ mapping = self.MAPPING.get(label)
742
+ if mapping is None:
743
+ # Unknown label, skip
744
+ return None
745
+
746
+ canonical_class, attributes = mapping
747
+
748
+ return CanonicalLayoutPrediction(
749
+ bbox=bbox,
750
+ score=score,
751
+ canonical_class=canonical_class,
752
+ attributes=attributes,
753
+ original_label=label,
754
+ )
755
+
756
+ def to_core(
757
+ self,
758
+ label: str,
759
+ score: float,
760
+ bbox: list[float],
761
+ ) -> CoreLayoutPrediction | None:
762
+ """
763
+ Convert LlamaParse string label to Core11 prediction.
764
+
765
+ First converts to Canonical17, then maps to Core11.
766
+
767
+ :param label: LlamaParse V2 label as string
768
+ :param score: Confidence score (0-1)
769
+ :param bbox: Bounding box [x1, y1, x2, y2]
770
+ :return: CoreLayoutPrediction or None if no Core11 equivalent
771
+ """
772
+ canonical = self.to_canonical(label, score, bbox)
773
+ if canonical is None:
774
+ return None
775
+ return canonical_to_core(canonical)
776
+
777
+ def to_basic(
778
+ self,
779
+ label: str,
780
+ score: float,
781
+ bbox: list[float],
782
+ ) -> tuple[str, dict[str, str]] | None:
783
+ """
784
+ Convert LlamaParse string label to Basic label and merged attributes.
785
+ """
786
+ canonical = self.to_canonical(label, score, bbox)
787
+ if canonical is None:
788
+ return None
789
+ return canonical_to_basic(canonical)
790
+
791
+
792
+ class LlamaParseV3LayoutDetLabelAdapter:
793
+ """Adapter for LlamaParse V3 layout labels -> Canonical17.
794
+
795
+ V3 labels align closely with the Canonical17 schema, requiring
796
+ simpler 1:1 identity mapping for most labels.
797
+
798
+ V3 labels (18 classes):
799
+ - caption, footnote, formula, list-item, page-footer, page-header,
800
+ - picture, section-header, table, text, title, document-index,
801
+ - code, checkbox-selected, checkbox-unselected, form, key-value-region, chart
802
+
803
+ Also includes V2 label fallbacks for mixed V2/V3 responses from staging API.
804
+ """
805
+
806
+ # Shared central mapping.
807
+ MAPPING: dict[str, tuple[CanonicalLabel, dict[str, str]]] = LLAMAPARSE_V3_RAW_TO_CANONICAL
808
+
809
+ def to_canonical(
810
+ self,
811
+ label: str,
812
+ score: float,
813
+ bbox: list[float],
814
+ ) -> CanonicalLayoutPrediction | None:
815
+ """Convert LlamaParse V3 string label to canonical prediction."""
816
+ mapping = self.MAPPING.get(label)
817
+ if mapping is None:
818
+ return None
819
+
820
+ canonical_class, attributes = mapping
821
+
822
+ return CanonicalLayoutPrediction(
823
+ bbox=bbox,
824
+ score=score,
825
+ canonical_class=canonical_class,
826
+ attributes=attributes,
827
+ original_label=label,
828
+ )
829
+
830
+ def to_core(
831
+ self,
832
+ label: str,
833
+ score: float,
834
+ bbox: list[float],
835
+ ) -> CoreLayoutPrediction | None:
836
+ """Convert V3 label to Core11 prediction."""
837
+ canonical = self.to_canonical(label, score, bbox)
838
+ if canonical is None:
839
+ return None
840
+ return canonical_to_core(canonical)
841
+
842
+ def to_basic(
843
+ self,
844
+ label: str,
845
+ score: float,
846
+ bbox: list[float],
847
+ ) -> tuple[str, dict[str, str]] | None:
848
+ """Convert V3 label to Basic label and merged attributes."""
849
+ canonical = self.to_canonical(label, score, bbox)
850
+ if canonical is None:
851
+ return None
852
+ return canonical_to_basic(canonical)
853
+
854
+
855
+ # =============================================================================
856
+ # Chunkr Adapter (ChunkrLabel string -> Canonical17)
857
+ # =============================================================================
858
+
859
+
860
+ class ChunkrLayoutDetLabelAdapter:
861
+ """Adapter for Chunkr layout labels -> Canonical17 + attributes.
862
+
863
+ Chunkr outputs 17 segment types as string labels. Mapping based on
864
+ semantic equivalence to DocLayNet/Canonical17 labels.
865
+
866
+ Chunkr segment types (from docs):
867
+ Caption, Footnote, Formula, FormRegion, GraphicalItem, Legend,
868
+ LineNumber, ListItem, PageFooter, PageHeader, PageNumber, Picture,
869
+ Table, Text, Title, Unknown, Page
870
+ """
871
+
872
+ # String label -> (CanonicalLabel, attributes)
873
+ # Using exact Chunkr segment_type values from docs
874
+ MAPPING: dict[str, tuple[CanonicalLabel, dict[str, str]]] = {
875
+ # Core11 direct mappings (exact Chunkr names)
876
+ "Caption": (CanonicalLabel.CAPTION, {}),
877
+ "Footnote": (CanonicalLabel.FOOTNOTE, {}),
878
+ "Formula": (CanonicalLabel.FORMULA, {}),
879
+ "ListItem": (CanonicalLabel.LIST_ITEM, {}),
880
+ "PageFooter": (CanonicalLabel.PAGE_FOOTER, {}),
881
+ "PageHeader": (CanonicalLabel.PAGE_HEADER, {}),
882
+ "Picture": (CanonicalLabel.PICTURE, {}),
883
+ "Table": (CanonicalLabel.TABLE, {}),
884
+ "Text": (CanonicalLabel.TEXT, {}),
885
+ "Title": (CanonicalLabel.TITLE, {}),
886
+ # Text variants with attributes
887
+ "LineNumber": (CanonicalLabel.TEXT, {"text_role": "line_number"}),
888
+ "PageNumber": (CanonicalLabel.TEXT, {"text_role": "page_number"}),
889
+ # Caption variant
890
+ "Legend": (CanonicalLabel.CAPTION, {"caption_of": "chart"}),
891
+ # Picture variant
892
+ "GraphicalItem": (CanonicalLabel.PICTURE, {"picture_type": "chart"}),
893
+ # Extended mappings (no Core11 equivalent)
894
+ "FormRegion": (CanonicalLabel.FORM, {}),
895
+ # Unknown and Page are skipped (no mapping)
896
+ }
897
+
898
+ def to_canonical(
899
+ self,
900
+ label: str,
901
+ score: float,
902
+ bbox: list[float],
903
+ ) -> CanonicalLayoutPrediction | None:
904
+ """
905
+ Convert Chunkr string label to canonical prediction.
906
+
907
+ :param label: Chunkr segment_type string (e.g., "Text Block", "Table")
908
+ :param score: Confidence score (0-1)
909
+ :param bbox: Bounding box [x1, y1, x2, y2]
910
+ :return: CanonicalLayoutPrediction or None if unknown/unmapped label
911
+ """
912
+ mapping = self.MAPPING.get(label)
913
+ if mapping is None:
914
+ # Unknown or Page label, skip
915
+ return None
916
+
917
+ canonical_class, attributes = mapping
918
+
919
+ return CanonicalLayoutPrediction(
920
+ bbox=bbox,
921
+ score=score,
922
+ canonical_class=canonical_class,
923
+ attributes=attributes,
924
+ original_label=label,
925
+ )
926
+
927
+ def to_core(
928
+ self,
929
+ label: str,
930
+ score: float,
931
+ bbox: list[float],
932
+ ) -> CoreLayoutPrediction | None:
933
+ """
934
+ Convert Chunkr string label to Core11 prediction.
935
+
936
+ First converts to Canonical17, then maps to Core11.
937
+
938
+ :param label: Chunkr segment_type string
939
+ :param score: Confidence score (0-1)
940
+ :param bbox: Bounding box [x1, y1, x2, y2]
941
+ :return: CoreLayoutPrediction or None if no Core11 equivalent
942
+ """
943
+ canonical = self.to_canonical(label, score, bbox)
944
+ if canonical is None:
945
+ return None
946
+ return canonical_to_core(canonical)