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,120 @@
1
+ """Test rule implementations for parse evaluation.
2
+
3
+ This module re-exports all rule classes and helpers from the split submodules
4
+ for backward compatibility. New code should import directly from the
5
+ specific submodule (rules_base, rules_text, rules_bag, rules_formatting,
6
+ rules_table, rules_chart, rules_form).
7
+ """
8
+
9
+ # Base class, helpers, and factory
10
+ # Bag rules
11
+ from parse_bench.evaluation.metrics.parse.rules_bag import ( # noqa: F401
12
+ BagOfDigitPercentRule,
13
+ ExtraContentRule,
14
+ MissingSentencePercentRule,
15
+ MissingSentenceRule,
16
+ MissingSpecificSentenceRule,
17
+ MissingSpecificWordRule,
18
+ MissingWordPercentRule,
19
+ MissingWordRule,
20
+ SentenceBagRule,
21
+ TooManySentenceOccurencePercentRule,
22
+ TooManySentenceOccurenceRule,
23
+ TooManyWordOccurencePercentRule,
24
+ TooManyWordOccurenceRule,
25
+ UnexpectedSentencePercentRule,
26
+ UnexpectedSentenceRule,
27
+ UnexpectedWordPercentRule,
28
+ UnexpectedWordRule,
29
+ WordBagRule,
30
+ )
31
+ from parse_bench.evaluation.metrics.parse.rules_base import ( # noqa: F401
32
+ CELL_FUZZY_MATCH_THRESHOLD,
33
+ AdjacentTableRuleData,
34
+ NoBorderTableRuleData,
35
+ ParseTestRule,
36
+ SentenceBagRuleData,
37
+ WordBagRuleData,
38
+ _augment_with_table_cell_text,
39
+ _dates_match,
40
+ _detect_csv_skip_rows,
41
+ _detect_header_scale,
42
+ _extract_table_cell_texts,
43
+ _normalize_date_str,
44
+ _strip_and_replace_latex,
45
+ _strip_fenced_code_blocks,
46
+ _strip_html_tables_and_content,
47
+ _unescape_html_entities,
48
+ create_test_rule,
49
+ )
50
+
51
+ # Chart rules
52
+ from parse_bench.evaluation.metrics.parse.rules_chart import ( # noqa: F401
53
+ ChartDataArrayDataRule,
54
+ ChartDataArrayLabelsRule,
55
+ ChartDataPointRule,
56
+ RotateCheckRule,
57
+ extract_numeric_parts,
58
+ normalize_number_string,
59
+ numbers_match,
60
+ numeric_similarity,
61
+ )
62
+
63
+ # Form rules
64
+ from parse_bench.evaluation.metrics.parse.rules_form import ( # noqa: F401
65
+ FormFieldRule,
66
+ )
67
+
68
+ # Formatting rules
69
+ from parse_bench.evaluation.metrics.parse.rules_formatting import ( # noqa: F401
70
+ _FORMATTING_TEST_TYPES,
71
+ AbsentUnlessStrikeoutRule,
72
+ CodeBlockRule,
73
+ FormattingRule,
74
+ LatexRule,
75
+ MarkColorRule,
76
+ NotLatexRule,
77
+ PageSectionRule,
78
+ PresentAsStrikeoutRule,
79
+ TextColorRule,
80
+ TitleHierarchyPercentRule,
81
+ TitleLevelRule,
82
+ )
83
+
84
+ # List rules
85
+ from parse_bench.evaluation.metrics.parse.rules_list import ( # noqa: F401
86
+ ListLevelRule,
87
+ )
88
+
89
+ # Table rules
90
+ from parse_bench.evaluation.metrics.parse.rules_table import ( # noqa: F401
91
+ TableAdjacentDownRule,
92
+ TableAdjacentLeftRule,
93
+ TableAdjacentRightRule,
94
+ TableAdjacentRule,
95
+ TableAdjacentUpRule,
96
+ TableColspanRule,
97
+ TableHeaderChainRule,
98
+ TableLeftHeaderRule,
99
+ TableNoAboveRule,
100
+ TableNoBelowRule,
101
+ TableNoBorderRule,
102
+ TableNoLeftRule,
103
+ TableNoRightRule,
104
+ TableRowspanRule,
105
+ TableRule,
106
+ TableSameColumnRule,
107
+ TableSameRowRule,
108
+ TablesNumColsRule,
109
+ TablesNumRowsRule,
110
+ TablesValuesRule,
111
+ TableTopHeaderRule,
112
+ )
113
+
114
+ # Text rules
115
+ from parse_bench.evaluation.metrics.parse.rules_text import ( # noqa: F401
116
+ BaselineRule,
117
+ TextOrderRule,
118
+ TextPresenceRule,
119
+ )
120
+ from parse_bench.evaluation.metrics.parse.rules_watermark import WatermarkRemovalRule # noqa: F401
@@ -0,0 +1,103 @@
1
+ """Test type definitions for parse evaluation."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class TestType(StrEnum):
7
+ """Test types for parse evaluation."""
8
+
9
+ BASELINE = "baseline"
10
+ PRESENT = "present"
11
+ UNEXPECTED_SENTENCE = "unexpected_sentence"
12
+ UNEXPECTED_SENTENCE_PERCENT = "unexpected_sentence_percent"
13
+ TOO_MANY_SENTENCE_OCCURENCE = "too_many_sentence_occurence"
14
+ TOO_MANY_SENTENCE_OCCURENCE_PERCENT = "too_many_sentence_occurence_percent"
15
+ MISSING_SENTENCE = "missing_sentence"
16
+ MISSING_SENTENCE_PERCENT = "missing_sentence_percent"
17
+ MISSING_SPECIFIC_SENTENCE = "missing_specific_sentence"
18
+ UNEXPECTED_WORD = "unexpected_word"
19
+ UNEXPECTED_WORD_PERCENT = "unexpected_word_percent"
20
+ TOO_MANY_WORD_OCCURENCE = "too_many_word_occurence"
21
+ TOO_MANY_WORD_OCCURENCE_PERCENT = "too_many_word_occurence_percent"
22
+ MISSING_WORD = "missing_word"
23
+ MISSING_WORD_PERCENT = "missing_word_percent"
24
+ MISSING_SPECIFIC_WORD = "missing_specific_word"
25
+ # Keep for backward compatibility with earlier combined behavior
26
+ EXTRA_CONTENT = "extra_content"
27
+ ABSENT = "absent"
28
+ ORDER = "order"
29
+ TABLE = "table"
30
+ TABLES_VALUES = "tables_values"
31
+ TABLES_NUM_ROWS = "tables_num_rows"
32
+ TABLES_NUM_COLS = "tables_num_cols"
33
+ TABLE_MARKER_CELLS = "table_marker_cells"
34
+ MATH = "math"
35
+ # Table hierarchy test types
36
+ TABLE_COLSPAN = "table_colspan"
37
+ TABLE_ROWSPAN = "table_rowspan"
38
+ TABLE_SAME_ROW = "table_same_row"
39
+ TABLE_SAME_COLUMN = "table_same_column"
40
+ TABLE_HEADER_CHAIN = "table_header_chain" # Keep for backward compatibility
41
+ # New table adjacency test types
42
+ TABLE_ADJACENT_UP = "table_adjacent_up"
43
+ TABLE_ADJACENT_DOWN = "table_adjacent_down"
44
+ TABLE_ADJACENT_LEFT = "table_adjacent_left"
45
+ TABLE_ADJACENT_RIGHT = "table_adjacent_right"
46
+ TABLE_TOP_HEADER = "table_top_header"
47
+ TABLE_LEFT_HEADER = "table_left_header"
48
+ # Table border tests - verify absence of cells beyond boundaries
49
+ TABLE_NO_LEFT = "table_no_left"
50
+ TABLE_NO_RIGHT = "table_no_right"
51
+ TABLE_NO_ABOVE = "table_no_above"
52
+ TABLE_NO_BELOW = "table_no_below"
53
+ # Formatting test types - check if text has specific formatting
54
+ IS_UNDERLINE = "is_underline"
55
+ IS_NOT_UNDERLINE = "is_not_underline"
56
+ IS_BOLD = "is_bold"
57
+ IS_NOT_BOLD = "is_not_bold"
58
+ IS_STRIKEOUT = "is_strikeout"
59
+ IS_NOT_STRIKEOUT = "is_not_strikeout"
60
+ IS_ITALIC = "is_italic"
61
+ IS_NOT_ITALIC = "is_not_italic"
62
+ IS_MARK = "is_mark"
63
+ IS_NOT_MARK = "is_not_mark"
64
+ MARK_COLOR = "mark_color"
65
+ TEXT_COLOR = "text_color"
66
+ ABSENT_UNLESS_STRIKEOUT = "absent_unless_strikeout"
67
+ PRESENT_AS_STRIKEOUT = "present_as_strikeout"
68
+ IS_SUP = "is_sup"
69
+ IS_NOT_SUP = "is_not_sup"
70
+ IS_SUB = "is_sub"
71
+ IS_NOT_SUB = "is_not_sub"
72
+ IS_LATEX = "is_latex"
73
+ IS_NOT_LATEX = "is_not_latex"
74
+ IS_CODE_BLOCK = "is_code_block"
75
+ # Title / heading level test
76
+ IS_TITLE = "is_title"
77
+ TITLE_HIERARCHY_PERCENT = "title_hierarchy_percent"
78
+ HEADING_STRUCTURE = "heading_structure"
79
+ # List item nesting level test (CommonMark list semantics)
80
+ LIST_LEVEL = "list_level"
81
+ # Page header / footer tests
82
+ IS_HEADER = "is_header"
83
+ IS_FOOTER = "is_footer"
84
+ # Chart test types
85
+ CHART_DATA_POINT = "chart_data_point"
86
+ CHART_DATA_ARRAY_LABELS = "chart_data_array_labels"
87
+ CHART_DATA_ARRAY_DATA = "chart_data_array_data"
88
+ # Layout detection
89
+ LAYOUT = "layout"
90
+ # Digit bag rule
91
+ BAG_OF_DIGIT_PERCENT = "bag_of_digit_percent"
92
+ # Rotation check
93
+ ROTATE_CHECK = "rotate_check"
94
+ # Form field (key/value, checkbox, signature) extraction
95
+ FORM_FIELD = "form_field"
96
+ # Watermark suppression plus preservation of nearby document text.
97
+ WATERMARK_REMOVAL = "watermark_removal"
98
+ # Diagrams rendered as mermaid: graph semantics (nodes/edges), anchor relations, page-level count.
99
+ DIAGRAM_GRAPH = "diagram_graph"
100
+ DIAGRAM_EDGE = "diagram_edge"
101
+ DIAGRAM_COUNT = "diagram_count"
102
+ # Page furniture: running header, running footer, printed page number, and leakage into the body.
103
+ PAGE_DECORATION = "page_decoration"
@@ -0,0 +1,175 @@
1
+ """Canonical plain-text projection for ParseBench Text Content rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from bs4 import BeautifulSoup, Tag
8
+
9
+ _HTML_TABLE_TAG = re.compile(r"<table(?=[\s>])[^>]*>|</table\s*>", re.IGNORECASE)
10
+ _MARKDOWN_DELIMITER_CELL = re.compile(r"^:?-+:?$")
11
+
12
+
13
+ def _html_table_spans(content: str) -> list[tuple[int, int]]:
14
+ """Return matched top-level HTML table spans, recovering after malformed starts."""
15
+ stack: list[int] = []
16
+ matched: list[tuple[int, int]] = []
17
+
18
+ for tag in _HTML_TABLE_TAG.finditer(content):
19
+ if tag.group().lower().startswith("</"):
20
+ if stack:
21
+ matched.append((stack.pop(), tag.end()))
22
+ else:
23
+ stack.append(tag.start())
24
+
25
+ # A valid nested table is projected through its outer table. If an unmatched
26
+ # outer start precedes a later valid table, the later matched span survives.
27
+ return [
28
+ span
29
+ for span in matched
30
+ if not any(outer_start < span[0] and span[1] < outer_end for outer_start, outer_end in matched)
31
+ ]
32
+
33
+
34
+ def _normalize_cell_text(cell: Tag) -> str:
35
+ return " ".join(cell.get_text(" ", strip=True).split())
36
+
37
+
38
+ def _html_table_lines(table: Tag) -> list[str]:
39
+ """Emit captions, explicit rows, and orphan cells in document order."""
40
+ lines: list[str] = []
41
+ orphan_cells: list[str] = []
42
+
43
+ def flush_orphan_cells() -> None:
44
+ if orphan_cells:
45
+ lines.append("\t".join(orphan_cells))
46
+ orphan_cells.clear()
47
+
48
+ def visit(container: Tag) -> None:
49
+ for child in container.children:
50
+ if not isinstance(child, Tag):
51
+ continue
52
+ if child.name == "caption":
53
+ flush_orphan_cells()
54
+ text = _normalize_cell_text(child)
55
+ if text:
56
+ lines.append(text)
57
+ elif child.name == "tr":
58
+ flush_orphan_cells()
59
+ cells = [
60
+ _normalize_cell_text(cell)
61
+ for cell in child.find_all(["th", "td"])
62
+ if cell.find_parent("tr") is child
63
+ ]
64
+ if cells:
65
+ lines.append("\t".join(cells))
66
+ elif child.name in {"th", "td"}:
67
+ text = _normalize_cell_text(child)
68
+ if text:
69
+ orphan_cells.append(text)
70
+ elif child.name != "table":
71
+ visit(child)
72
+
73
+ visit(table)
74
+ flush_orphan_cells()
75
+ return lines
76
+
77
+
78
+ def _canonicalize_html_table(table_html: str) -> str:
79
+ soup = BeautifulSoup(table_html, "html.parser")
80
+ table = soup.find("table")
81
+ if table is None:
82
+ return table_html
83
+
84
+ return "\n".join(_html_table_lines(table))
85
+
86
+
87
+ def _split_markdown_row(line: str) -> list[str]:
88
+ """Split a Markdown table row without treating escaped/code pipes as separators."""
89
+ cells: list[str] = []
90
+ buffer: list[str] = []
91
+ code_ticks = 0
92
+ index = 0
93
+
94
+ while index < len(line):
95
+ char = line[index]
96
+ if char == "\\" and index + 1 < len(line):
97
+ buffer.extend((char, line[index + 1]))
98
+ index += 2
99
+ continue
100
+ if char == "`":
101
+ run_end = index + 1
102
+ while run_end < len(line) and line[run_end] == "`":
103
+ run_end += 1
104
+ run_length = run_end - index
105
+ if code_ticks == 0:
106
+ code_ticks = run_length
107
+ elif code_ticks == run_length:
108
+ code_ticks = 0
109
+ buffer.extend(line[index:run_end])
110
+ index = run_end
111
+ continue
112
+ if char == "|" and code_ticks == 0:
113
+ cells.append("".join(buffer).strip())
114
+ buffer = []
115
+ else:
116
+ buffer.append(char)
117
+ index += 1
118
+
119
+ cells.append("".join(buffer).strip())
120
+ if line.lstrip().startswith("|") and cells and not cells[0]:
121
+ cells = cells[1:]
122
+ if line.rstrip().endswith("|") and cells and not cells[-1]:
123
+ cells = cells[:-1]
124
+ return cells
125
+
126
+
127
+ def _is_markdown_delimiter(line: str) -> bool:
128
+ cells = _split_markdown_row(line)
129
+ return len(cells) > 0 and all(_MARKDOWN_DELIMITER_CELL.fullmatch(cell.replace(" ", "")) for cell in cells)
130
+
131
+
132
+ def _canonicalize_markdown_tables(content: str) -> str:
133
+ lines = content.splitlines()
134
+ output: list[str] = []
135
+ index = 0
136
+ changed = False
137
+
138
+ while index < len(lines):
139
+ if (
140
+ index + 1 < len(lines)
141
+ and "|" in lines[index]
142
+ and "|" in lines[index + 1]
143
+ and _is_markdown_delimiter(lines[index + 1])
144
+ ):
145
+ changed = True
146
+ table_lines = [lines[index]]
147
+ index += 2
148
+ while index < len(lines) and "|" in lines[index] and lines[index].strip():
149
+ table_lines.append(lines[index])
150
+ index += 1
151
+ output.extend("\t".join(_split_markdown_row(row)) for row in table_lines)
152
+ continue
153
+
154
+ output.append(lines[index])
155
+ index += 1
156
+
157
+ return "\n".join(output) if changed else content
158
+
159
+
160
+ def canonicalize_tables_for_text_content(markdown: str) -> str:
161
+ """Replace each table with one row-major text view for Text Content rules.
162
+
163
+ Every source cell is emitted once. Tabs separate cells and newlines separate
164
+ rows, preserving anchor continuity without adding alternate cell/row copies.
165
+ The caller retains the original Markdown for table and formatting metrics.
166
+ """
167
+ if not markdown:
168
+ return markdown
169
+
170
+ spans = _html_table_spans(markdown)
171
+ for start, end in reversed(spans):
172
+ replacement = _canonicalize_html_table(markdown[start:end])
173
+ markdown = f"{markdown[:start]}\n{replacement}\n{markdown[end:]}"
174
+
175
+ return _canonicalize_markdown_tables(markdown)
@@ -0,0 +1,61 @@
1
+ """Text similarity metric for comparing markdown against ground truth."""
2
+
3
+ from typing import Any
4
+
5
+ from parse_bench.evaluation.metrics.base import Metric
6
+ from parse_bench.schemas.evaluation import MetricValue
7
+
8
+
9
+ class TextSimilarityMetric(Metric):
10
+ """Metric for comparing markdown text against ground truth using Levenshtein distance."""
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ """Return the name of this metric."""
15
+ return "text_similarity"
16
+
17
+ def compute(self, expected: str, actual: str, **kwargs: Any) -> MetricValue:
18
+ """
19
+ Compute text similarity between expected and actual markdown.
20
+
21
+ :param expected: Expected markdown (ground truth)
22
+ :param actual: Actual markdown (from inference)
23
+ :param kwargs: Additional parameters (not used)
24
+ :return: MetricValue with similarity score (0.0 to 1.0)
25
+ """
26
+ from autoevals.string import Levenshtein
27
+
28
+ if not expected and not actual:
29
+ return MetricValue(
30
+ metric_name=self.name,
31
+ value=1.0,
32
+ metadata={"note": "Both empty"},
33
+ )
34
+
35
+ if not expected or not actual:
36
+ return MetricValue(
37
+ metric_name=self.name,
38
+ value=0.0,
39
+ metadata={"note": "One is empty"},
40
+ )
41
+
42
+ # Use Levenshtein distance from autoevals
43
+ levenshtein = Levenshtein()
44
+ result = levenshtein(expected, actual)
45
+
46
+ # autoevals ``Levenshtein`` already returns a normalized score in
47
+ # [0, 1] (1.0 identical, 0.0 fully disjoint), so use it directly.
48
+ # Clamp only as a defensive guard against float drift — do NOT divide
49
+ # by 100 (that collapsed every value by two orders of magnitude, e.g.
50
+ # 0.76 -> 0.0076).
51
+ similarity = max(0.0, min(1.0, result.score))
52
+
53
+ return MetricValue(
54
+ metric_name=self.name,
55
+ value=similarity,
56
+ metadata={
57
+ "levenshtein_score": result.score,
58
+ "expected_length": len(expected),
59
+ "actual_length": len(actual),
60
+ },
61
+ )