docvortex 0.2.1__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.
- docvortex/__init__.py +21 -0
- docvortex/analyzers/__init__.py +3 -0
- docvortex/analyzers/native/__init__.py +37 -0
- docvortex/analyzers/native/_shared/__init__.py +3 -0
- docvortex/analyzers/native/_shared/hyperlink.py +16 -0
- docvortex/analyzers/native/_shared/image.py +8 -0
- docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
- docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
- docvortex/analyzers/native/_shared/markup/formula.py +37 -0
- docvortex/analyzers/native/_shared/markup/projector.py +57 -0
- docvortex/analyzers/native/_shared/markup/styles.py +19 -0
- docvortex/analyzers/native/_shared/mathml.py +17 -0
- docvortex/analyzers/native/_shared/names.py +7 -0
- docvortex/analyzers/native/_shared/xycut.py +414 -0
- docvortex/analyzers/native/contracts.py +44 -0
- docvortex/analyzers/native/csv.py +351 -0
- docvortex/analyzers/native/epub/__init__.py +18 -0
- docvortex/analyzers/native/epub/constants.py +45 -0
- docvortex/analyzers/native/epub/converter.py +80 -0
- docvortex/analyzers/native/epub/errors.py +20 -0
- docvortex/analyzers/native/epub/metadata.py +31 -0
- docvortex/analyzers/native/epub/package.py +649 -0
- docvortex/analyzers/native/epub/xhtml.py +306 -0
- docvortex/analyzers/native/html/__init__.py +6 -0
- docvortex/analyzers/native/html/anchors.py +271 -0
- docvortex/analyzers/native/html/constants.py +25 -0
- docvortex/analyzers/native/html/contracts.py +7 -0
- docvortex/analyzers/native/html/converter.py +117 -0
- docvortex/analyzers/native/html/document.py +389 -0
- docvortex/analyzers/native/html/errors.py +12 -0
- docvortex/analyzers/native/html/resources.py +365 -0
- docvortex/analyzers/native/html/selector.py +421 -0
- docvortex/analyzers/native/models.py +209 -0
- docvortex/analyzers/native/ofd/__init__.py +14 -0
- docvortex/analyzers/native/ofd/constants.py +59 -0
- docvortex/analyzers/native/ofd/converter.py +31 -0
- docvortex/analyzers/native/ofd/errors.py +16 -0
- docvortex/analyzers/native/ofd/geometry.py +215 -0
- docvortex/analyzers/native/ofd/images.py +124 -0
- docvortex/analyzers/native/ofd/metadata.py +53 -0
- docvortex/analyzers/native/ofd/models.py +170 -0
- docvortex/analyzers/native/ofd/package.py +346 -0
- docvortex/analyzers/native/ofd/path.py +201 -0
- docvortex/analyzers/native/ofd/reading_order.py +293 -0
- docvortex/analyzers/native/ofd/resources.py +122 -0
- docvortex/analyzers/native/ofd/scene.py +347 -0
- docvortex/analyzers/native/ofd/table.py +240 -0
- docvortex/analyzers/native/ofd/text.py +521 -0
- docvortex/analyzers/native/office/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/__init__.py +3 -0
- docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
- docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
- docvortex/analyzers/native/office/doc/fib.py +261 -0
- docvortex/analyzers/native/office/doc/fields.py +101 -0
- docvortex/analyzers/native/office/doc/formatting.py +171 -0
- docvortex/analyzers/native/office/doc/images.py +162 -0
- docvortex/analyzers/native/office/doc/lists.py +356 -0
- docvortex/analyzers/native/office/doc/models.py +164 -0
- docvortex/analyzers/native/office/doc/parser.py +851 -0
- docvortex/analyzers/native/office/doc/pieces.py +256 -0
- docvortex/analyzers/native/office/doc/records.py +40 -0
- docvortex/analyzers/native/office/doc/sprm.py +269 -0
- docvortex/analyzers/native/office/doc/styles.py +215 -0
- docvortex/analyzers/native/office/docx/__init__.py +3 -0
- docvortex/analyzers/native/office/docx/context.py +71 -0
- docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
- docvortex/analyzers/native/office/docx/equationxml.py +119 -0
- docvortex/analyzers/native/office/docx/fields.py +816 -0
- docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
- docvortex/analyzers/native/office/docx/main.py +50 -0
- docvortex/analyzers/native/office/docx/numbering.py +491 -0
- docvortex/analyzers/native/office/docx/office_xml.py +57 -0
- docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
- docvortex/analyzers/native/office/docx/resources.py +526 -0
- docvortex/analyzers/native/office/docx/styles.py +693 -0
- docvortex/analyzers/native/office/docx/tables.py +515 -0
- docvortex/analyzers/native/office/equation/__init__.py +3 -0
- docvortex/analyzers/native/office/equation/image.py +470 -0
- docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
- docvortex/analyzers/native/office/equation/mtef.py +885 -0
- docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
- docvortex/analyzers/native/office/equation/omml.py +561 -0
- docvortex/analyzers/native/office/equation/ooxml.py +62 -0
- docvortex/analyzers/native/office/errors.py +33 -0
- docvortex/analyzers/native/office/image.py +307 -0
- docvortex/analyzers/native/office/legacy/__init__.py +3 -0
- docvortex/analyzers/native/office/legacy/binary.py +43 -0
- docvortex/analyzers/native/office/legacy/officeart.py +362 -0
- docvortex/analyzers/native/office/legacy/ole.py +110 -0
- docvortex/analyzers/native/office/limits.py +12 -0
- docvortex/analyzers/native/office/odf/__init__.py +3 -0
- docvortex/analyzers/native/office/odf/chart.py +76 -0
- docvortex/analyzers/native/office/odf/constants.py +70 -0
- docvortex/analyzers/native/office/odf/converters.py +403 -0
- docvortex/analyzers/native/office/odf/errors.py +18 -0
- docvortex/analyzers/native/office/odf/metadata.py +91 -0
- docvortex/analyzers/native/office/odf/models.py +176 -0
- docvortex/analyzers/native/office/odf/package.py +270 -0
- docvortex/analyzers/native/office/odf/styles.py +329 -0
- docvortex/analyzers/native/office/odf/table.py +469 -0
- docvortex/analyzers/native/office/odf/text.py +1002 -0
- docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
- docvortex/analyzers/native/office/opc.py +38 -0
- docvortex/analyzers/native/office/ppt/__init__.py +3 -0
- docvortex/analyzers/native/office/ppt/models.py +118 -0
- docvortex/analyzers/native/office/ppt/parser.py +1895 -0
- docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
- docvortex/analyzers/native/office/ppt/records.py +131 -0
- docvortex/analyzers/native/office/ppt/style_text.py +247 -0
- docvortex/analyzers/native/office/pptx/__init__.py +3 -0
- docvortex/analyzers/native/office/pptx/context.py +113 -0
- docvortex/analyzers/native/office/pptx/lists.py +558 -0
- docvortex/analyzers/native/office/pptx/main.py +20 -0
- docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
- docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
- docvortex/analyzers/native/office/pptx/resources.py +329 -0
- docvortex/analyzers/native/office/pptx/shapes.py +393 -0
- docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
- docvortex/analyzers/native/office/pptx/titles.py +178 -0
- docvortex/analyzers/native/office/rich_text.py +420 -0
- docvortex/analyzers/native/office/rtf/__init__.py +3 -0
- docvortex/analyzers/native/office/rtf/converter.py +708 -0
- docvortex/analyzers/native/office/rtf/lexer.py +213 -0
- docvortex/analyzers/native/office/rtf/math.py +339 -0
- docvortex/analyzers/native/office/rtf/models.py +185 -0
- docvortex/analyzers/native/office/rtf/parser.py +1553 -0
- docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
- docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
- docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
- docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
- docvortex/analyzers/native/office/streams.py +18 -0
- docvortex/analyzers/native/office/xls/__init__.py +3 -0
- docvortex/analyzers/native/office/xls/chart.py +132 -0
- docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
- docvortex/analyzers/native/office/xls/models.py +109 -0
- docvortex/analyzers/native/office/xls/number_format.py +521 -0
- docvortex/analyzers/native/office/xls/parser.py +1145 -0
- docvortex/analyzers/native/office/xls/records.py +201 -0
- docvortex/analyzers/native/office/xls/strings.py +205 -0
- docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
- docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
- docvortex/analyzers/native/office/xlsx/main.py +20 -0
- docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
- docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
- docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
- docvortex/analyzers/native/pdf/__init__.py +3 -0
- docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
- docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
- docvortex/analyzers/native/pdf/code_blocks.py +535 -0
- docvortex/analyzers/native/pdf/formulas.py +1985 -0
- docvortex/analyzers/native/pdf/geometry.py +281 -0
- docvortex/analyzers/native/pdf/graphics.py +1501 -0
- docvortex/analyzers/native/pdf/index_blocks.py +268 -0
- docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
- docvortex/analyzers/native/pdf/inline/common.py +112 -0
- docvortex/analyzers/native/pdf/inline/detection.py +590 -0
- docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
- docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
- docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
- docvortex/analyzers/native/pdf/inline/types.py +385 -0
- docvortex/analyzers/native/pdf/line_layout.py +1106 -0
- docvortex/analyzers/native/pdf/line_merging.py +1223 -0
- docvortex/analyzers/native/pdf/models.py +246 -0
- docvortex/analyzers/native/pdf/native_text.py +1004 -0
- docvortex/analyzers/native/pdf/pipeline.py +1390 -0
- docvortex/analyzers/native/pdf/script_geometry.py +636 -0
- docvortex/analyzers/native/pdf/shared.py +30 -0
- docvortex/analyzers/native/pdf/spatial_text.py +383 -0
- docvortex/analyzers/native/pdf/table_annotations.py +446 -0
- docvortex/analyzers/native/pdf/table_constants.py +48 -0
- docvortex/analyzers/native/pdf/table_detection.py +227 -0
- docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
- docvortex/analyzers/native/pdf/table_geometry.py +40 -0
- docvortex/analyzers/native/pdf/table_materialization.py +444 -0
- docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
- docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
- docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
- docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
- docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
- docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
- docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
- docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
- docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
- docvortex/analyzers/native/pdf/table_rows.py +34 -0
- docvortex/analyzers/native/pdf/table_rules.py +1129 -0
- docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
- docvortex/analyzers/native/pdf/tables.py +147 -0
- docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
- docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
- docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
- docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
- docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
- docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
- docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
- docvortex/analyzers/native/pdf/text_blocks.py +82 -0
- docvortex/analyzers/native/pdf/text_styles.py +55 -0
- docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
- docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
- docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
- docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
- docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
- docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
- docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
- docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
- docvortex/analyzers/native/pdf/titles.py +75 -0
- docvortex/analyzers/native/pdf/typography.py +19 -0
- docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
- docvortex/api.py +180 -0
- docvortex/assets/__init__.py +5 -0
- docvortex/assets/store.py +51 -0
- docvortex/cli.py +54 -0
- docvortex/codecs/__init__.py +3 -0
- docvortex/codecs/html/__init__.py +22 -0
- docvortex/codecs/html/contracts.py +236 -0
- docvortex/codecs/html/materializer.py +331 -0
- docvortex/codecs/html/parser.py +763 -0
- docvortex/codecs/html/resources.py +34 -0
- docvortex/codecs/json.py +17 -0
- docvortex/content/__init__.py +5 -0
- docvortex/content/inline.py +248 -0
- docvortex/content/markup/__init__.py +44 -0
- docvortex/content/markup/anchors.py +188 -0
- docvortex/content/markup/formula.py +280 -0
- docvortex/content/markup/projector.py +1237 -0
- docvortex/content/markup/styles.py +327 -0
- docvortex/content/mathml.py +167 -0
- docvortex/content/normalization.py +188 -0
- docvortex/content/spans.py +183 -0
- docvortex/content/table/__init__.py +18 -0
- docvortex/content/table/blocks.py +152 -0
- docvortex/content/table/content.py +425 -0
- docvortex/content/table/document.py +104 -0
- docvortex/content/table/html.py +399 -0
- docvortex/content/table/models.py +76 -0
- docvortex/content/table/rules.py +42 -0
- docvortex/content/table/structure.py +221 -0
- docvortex/content/tree.py +5 -0
- docvortex/document/__init__.py +3 -0
- docvortex/document/contracts.py +22 -0
- docvortex/document/detection.py +389 -0
- docvortex/document/filetypes.py +175 -0
- docvortex/document/page_range.py +167 -0
- docvortex/document/pdf/__init__.py +19 -0
- docvortex/document/pdf/classify.py +1138 -0
- docvortex/document/pdf/constants.py +132 -0
- docvortex/document/pdf/diagnostics.py +350 -0
- docvortex/document/pdf/document.py +582 -0
- docvortex/document/pdf/font_runtime.py +335 -0
- docvortex/document/pdf/geometry.py +31 -0
- docvortex/document/pdf/images.py +654 -0
- docvortex/document/pdf/native_annotations.py +367 -0
- docvortex/document/pdf/native_contracts.py +169 -0
- docvortex/document/pdf/native_coordinates.py +216 -0
- docvortex/document/pdf/native_lifecycle.py +16 -0
- docvortex/document/pdf/native_objects.py +902 -0
- docvortex/document/pdf/native_text_geometry.py +315 -0
- docvortex/document/pdf/pdfium.py +325 -0
- docvortex/document/pdf/raster.py +46 -0
- docvortex/document/pdf/text/__init__.py +62 -0
- docvortex/document/pdf/text/contracts.py +211 -0
- docvortex/document/pdf/text/extract.py +165 -0
- docvortex/document/pdf/text/geometry.py +16 -0
- docvortex/document/pdf/text/groups.py +162 -0
- docvortex/document/pdf/visual_geometry.py +201 -0
- docvortex/document/pdf/visuals.py +343 -0
- docvortex/document/source.py +92 -0
- docvortex/errors.py +21 -0
- docvortex/export/__init__.py +3 -0
- docvortex/export/bundle.py +98 -0
- docvortex/export/files.py +66 -0
- docvortex/export/middle.py +208 -0
- docvortex/foundation/__init__.py +3 -0
- docvortex/foundation/geometry.py +125 -0
- docvortex/foundation/hyperlink.py +65 -0
- docvortex/foundation/image.py +48 -0
- docvortex/foundation/image_encoding.py +30 -0
- docvortex/foundation/image_payload.py +280 -0
- docvortex/foundation/language.py +92 -0
- docvortex/foundation/platform.py +38 -0
- docvortex/foundation/text.py +153 -0
- docvortex/foundation/type_identity.py +20 -0
- docvortex/foundation/xml_names.py +20 -0
- docvortex/options.py +30 -0
- docvortex/postprocess/__init__.py +3 -0
- docvortex/postprocess/content.py +53 -0
- docvortex/postprocess/document.py +19 -0
- docvortex/postprocess/lists.py +236 -0
- docvortex/postprocess/page_blocks.py +214 -0
- docvortex/postprocess/pages.py +95 -0
- docvortex/postprocess/paragraphs.py +580 -0
- docvortex/postprocess/visual.py +715 -0
- docvortex/render/__init__.py +48 -0
- docvortex/render/_internal/__init__.py +3 -0
- docvortex/render/_internal/common/__init__.py +3 -0
- docvortex/render/_internal/common/context.py +43 -0
- docvortex/render/_internal/common/html_table.py +178 -0
- docvortex/render/_internal/common/index.py +33 -0
- docvortex/render/_internal/common/list_items.py +158 -0
- docvortex/render/_internal/common/planner.py +140 -0
- docvortex/render/_internal/docx/__init__.py +3 -0
- docvortex/render/_internal/docx/assets.py +202 -0
- docvortex/render/_internal/docx/inline.py +434 -0
- docvortex/render/_internal/docx/math.py +220 -0
- docvortex/render/_internal/docx/renderer.py +905 -0
- docvortex/render/_internal/docx/styles.py +195 -0
- docvortex/render/_internal/docx/table.py +442 -0
- docvortex/render/_internal/epub/__init__.py +5 -0
- docvortex/render/_internal/epub/assets.py +173 -0
- docvortex/render/_internal/epub/package.py +249 -0
- docvortex/render/_internal/epub/renderer.py +1156 -0
- docvortex/render/_internal/html/__init__.py +3 -0
- docvortex/render/_internal/html/inline.py +346 -0
- docvortex/render/_internal/html/renderer.py +1041 -0
- docvortex/render/_internal/html/sanitizer.py +478 -0
- docvortex/render/_internal/html/table.py +121 -0
- docvortex/render/_internal/latex/__init__.py +1 -0
- docvortex/render/_internal/latex/assets.py +85 -0
- docvortex/render/_internal/latex/inline.py +145 -0
- docvortex/render/_internal/latex/renderer.py +506 -0
- docvortex/render/_internal/latex/table.py +347 -0
- docvortex/render/_internal/markdown/__init__.py +3 -0
- docvortex/render/_internal/markdown/assets.py +78 -0
- docvortex/render/_internal/markdown/blocks.py +635 -0
- docvortex/render/_internal/markdown/escaping.py +51 -0
- docvortex/render/_internal/markdown/inline.py +260 -0
- docvortex/render/_internal/markdown/renderer.py +93 -0
- docvortex/render/_internal/markdown/table.py +281 -0
- docvortex/render/_internal/pdf/__init__.py +3 -0
- docvortex/render/_internal/pdf/assets.py +197 -0
- docvortex/render/_internal/pdf/formula.py +417 -0
- docvortex/render/_internal/pdf/inline.py +343 -0
- docvortex/render/_internal/pdf/renderer.py +734 -0
- docvortex/render/_internal/pdf/styles.py +206 -0
- docvortex/render/_internal/pdf/table.py +272 -0
- docvortex/render/_internal/structured_content/__init__.py +3 -0
- docvortex/render/_internal/structured_content/renderer.py +193 -0
- docvortex/render/api.py +199 -0
- docvortex/render/contracts.py +205 -0
- docvortex/render/docx.py +38 -0
- docvortex/render/epub.py +35 -0
- docvortex/render/fragments.py +70 -0
- docvortex/render/html.py +29 -0
- docvortex/render/latex.py +24 -0
- docvortex/render/markdown.py +50 -0
- docvortex/render/pdf.py +25 -0
- docvortex/render/structured_content.py +23 -0
- docvortex/resources/epub/docvortex.css +91 -0
- docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
- docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
- docvortex/resources/fonts/NOTICE +190 -0
- docvortex/resources/fonts/manifest.json +9 -0
- docvortex/resources/html/docvortex.css +601 -0
- docvortex/resources/html/docvortex.min.css +1 -0
- docvortex/result.py +91 -0
- docvortex/schema.py +1141 -0
- docvortex/version.py +3 -0
- docvortex-0.2.1.dist-info/METADATA +193 -0
- docvortex-0.2.1.dist-info/RECORD +364 -0
- docvortex-0.2.1.dist-info/WHEEL +5 -0
- docvortex-0.2.1.dist-info/entry_points.txt +2 -0
- docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
- docvortex-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"""编排 Native PDF 表格多候选生成、欠分割诊断和高置信仲裁。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .candidate import serialize_candidate_html
|
|
11
|
+
from .contracts import (
|
|
12
|
+
NativeTableCandidate,
|
|
13
|
+
NativeTableInput,
|
|
14
|
+
NativeTableRectangle,
|
|
15
|
+
NativeTableResult,
|
|
16
|
+
NativeTableRule,
|
|
17
|
+
NativeTableText,
|
|
18
|
+
)
|
|
19
|
+
from .geometry import bbox_intersection, normalize_bbox
|
|
20
|
+
from .sparse_hybrid import build_sparse_hybrid_candidates, diagnose_sparse_hybrid_candidate_builds
|
|
21
|
+
from .sparse_multiline import build_sparse_multiline_candidates, diagnose_sparse_multiline_candidate_builds
|
|
22
|
+
from .text import build_native_table_text
|
|
23
|
+
from .text_grid import build_text_candidates, diagnose_text_candidate_builds
|
|
24
|
+
from .vector import MAX_PRIMITIVES_PER_TABLE, build_vector_candidates, diagnose_vector_candidate_builds
|
|
25
|
+
|
|
26
|
+
MIN_TOPOLOGY_SCORE_GAP = 0.05
|
|
27
|
+
_SOURCE_PRIORITY = {
|
|
28
|
+
"vector_grid": 6,
|
|
29
|
+
"sparse_hybrid": 5,
|
|
30
|
+
"sparse_grid": 4,
|
|
31
|
+
"key_value": 3,
|
|
32
|
+
"text_grid": 2,
|
|
33
|
+
"sparse_multiline": 1,
|
|
34
|
+
}
|
|
35
|
+
_VERIFIED_SCORE_BY_SOURCE = {
|
|
36
|
+
"vector_grid": 0.95,
|
|
37
|
+
"sparse_hybrid": 0.98,
|
|
38
|
+
"sparse_multiline": 0.98,
|
|
39
|
+
"sparse_grid": 0.95,
|
|
40
|
+
"key_value": 0.95,
|
|
41
|
+
"text_grid": 0.95,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class _CandidateEvaluation:
|
|
47
|
+
"""保存一次生产决策及调试工具需要的中间候选。"""
|
|
48
|
+
|
|
49
|
+
primitive_count: int
|
|
50
|
+
text: NativeTableText | None
|
|
51
|
+
generated_candidates: tuple[NativeTableCandidate, ...]
|
|
52
|
+
candidates: tuple[NativeTableCandidate, ...]
|
|
53
|
+
selected: NativeTableCandidate | None
|
|
54
|
+
physical_topology_conflict: bool
|
|
55
|
+
first_rejection_gate: str | None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_verified_line_candidate(candidate: NativeTableCandidate) -> bool:
|
|
59
|
+
"""判断候选是否由无歧义 drawing 网格独立验证。"""
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
candidate.source == "vector_grid"
|
|
63
|
+
and candidate.score >= 0.95
|
|
64
|
+
and "evidence=line_grid" in candidate.issues
|
|
65
|
+
and "ambiguous_separator_ratio=0.0000" in candidate.issues
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_verified_rect_candidate(candidate: NativeTableCandidate) -> bool:
|
|
70
|
+
"""判断候选是否由无歧义矩形晶格独立验证。"""
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
candidate.source == "vector_grid"
|
|
74
|
+
and candidate.score >= 0.95
|
|
75
|
+
and "evidence=rect_grid" in candidate.issues
|
|
76
|
+
and "ambiguous_separator_ratio=0.0000" in candidate.issues
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _has_line_rect_topology_conflict(
|
|
81
|
+
candidates: Iterable[NativeTableCandidate],
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""判断两类独立物理证据是否给出不同拓扑。"""
|
|
84
|
+
|
|
85
|
+
materialized = list(candidates)
|
|
86
|
+
line_candidates = [candidate for candidate in materialized if _is_verified_line_candidate(candidate)]
|
|
87
|
+
rect_candidates = [candidate for candidate in materialized if _is_verified_rect_candidate(candidate)]
|
|
88
|
+
return any(line.topology != rect.topology for line in line_candidates for rect in rect_candidates)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _has_attempted_line_rect_grid_conflict(
|
|
92
|
+
attempts: Iterable[dict[str, Any]],
|
|
93
|
+
) -> bool:
|
|
94
|
+
"""判断已恢复轨道的 line/rect 物理假设是否在行列数上冲突。"""
|
|
95
|
+
|
|
96
|
+
materialized = list(attempts)
|
|
97
|
+
line_grids = [attempt.get("grid") for attempt in materialized if attempt.get("evidence") == "line_grid"]
|
|
98
|
+
rect_grids = [attempt.get("grid") for attempt in materialized if attempt.get("evidence") == "rect_grid"]
|
|
99
|
+
return any(
|
|
100
|
+
isinstance(line, dict)
|
|
101
|
+
and isinstance(rect, dict)
|
|
102
|
+
and (line.get("rows"), line.get("cols")) != (rect.get("rows"), rect.get("cols"))
|
|
103
|
+
for line in line_grids
|
|
104
|
+
for rect in rect_grids
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolved_rect_undercount_candidate(
|
|
109
|
+
candidates: Iterable[NativeTableCandidate],
|
|
110
|
+
attempts: Iterable[dict[str, Any]],
|
|
111
|
+
text: NativeTableText,
|
|
112
|
+
) -> NativeTableCandidate | None:
|
|
113
|
+
"""在线网格明确漏行且矩形晶格逐行吻合时允许 rect 独立胜出。"""
|
|
114
|
+
|
|
115
|
+
rect_candidates = [candidate for candidate in candidates if _is_verified_rect_candidate(candidate)]
|
|
116
|
+
if len(rect_candidates) != 1:
|
|
117
|
+
return None
|
|
118
|
+
rect_candidate = rect_candidates[0]
|
|
119
|
+
line_attempts = [attempt for attempt in attempts if attempt.get("evidence") == "line_grid"]
|
|
120
|
+
has_matching_undercount = any(
|
|
121
|
+
attempt.get("first_rejection_gate") == "physical_row_undercount"
|
|
122
|
+
and isinstance(attempt.get("grid"), dict)
|
|
123
|
+
and attempt["grid"].get("cols") == rect_candidate.cols
|
|
124
|
+
and attempt["grid"].get("rows", 0) < rect_candidate.rows
|
|
125
|
+
for attempt in line_attempts
|
|
126
|
+
)
|
|
127
|
+
if not has_matching_undercount:
|
|
128
|
+
return None
|
|
129
|
+
if rect_candidate.rows != len(text.rows) or rect_candidate.text_capture < 1.0 or rect_candidate.order_consistency < 1.0:
|
|
130
|
+
return None
|
|
131
|
+
return rect_candidate
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _passes_verified_threshold(candidate: NativeTableCandidate) -> bool:
|
|
135
|
+
"""按候选来源应用独立校准的 verified 可靠度门槛。"""
|
|
136
|
+
|
|
137
|
+
return candidate.score >= _VERIFIED_SCORE_BY_SOURCE[candidate.source]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _read_value(item: object, name: str, default: Any = None) -> Any:
|
|
141
|
+
"""同时读取普通对象属性和字典字段,供页面原语适配使用。"""
|
|
142
|
+
|
|
143
|
+
if isinstance(item, dict):
|
|
144
|
+
return item.get(name, default)
|
|
145
|
+
return getattr(item, name, default)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def coerce_native_table_rules(
|
|
149
|
+
drawing_lines: Iterable[object],
|
|
150
|
+
) -> tuple[NativeTableRule, ...]:
|
|
151
|
+
"""把 PDFDocument 或 Flash drawing 结果转换成共享横竖线契约。"""
|
|
152
|
+
|
|
153
|
+
rules: list[NativeTableRule] = []
|
|
154
|
+
for drawing_line in drawing_lines:
|
|
155
|
+
bbox = normalize_bbox(_read_value(drawing_line, "bbox"))
|
|
156
|
+
orientation = str(_read_value(drawing_line, "orientation", ""))
|
|
157
|
+
if bbox is None or orientation not in {"horizontal", "vertical"}:
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
width = max(0.0, float(_read_value(drawing_line, "width", 0.0) or 0.0))
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
width = 0.0
|
|
163
|
+
rules.append(
|
|
164
|
+
NativeTableRule(
|
|
165
|
+
bbox=bbox,
|
|
166
|
+
width=width,
|
|
167
|
+
orientation=orientation, # type: ignore[arg-type]
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return tuple(rules)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def coerce_native_table_rectangles(
|
|
174
|
+
path_infos: Iterable[object],
|
|
175
|
+
) -> tuple[NativeTableRectangle, ...]:
|
|
176
|
+
"""把 PDF Path 摘要转换成共享矩形路径契约。"""
|
|
177
|
+
|
|
178
|
+
rectangles: list[NativeTableRectangle] = []
|
|
179
|
+
for path_info in path_infos:
|
|
180
|
+
bbox = normalize_bbox(_read_value(path_info, "bbox"))
|
|
181
|
+
if bbox is None:
|
|
182
|
+
continue
|
|
183
|
+
try:
|
|
184
|
+
segment_count = int(_read_value(path_info, "segment_count", 0) or 0)
|
|
185
|
+
form_depth = int(_read_value(path_info, "form_depth", 0) or 0)
|
|
186
|
+
except (TypeError, ValueError):
|
|
187
|
+
continue
|
|
188
|
+
rectangles.append(
|
|
189
|
+
NativeTableRectangle(
|
|
190
|
+
bbox=bbox,
|
|
191
|
+
segment_count=segment_count,
|
|
192
|
+
fill_visible=bool(_read_value(path_info, "fill_visible", False)),
|
|
193
|
+
stroke_visible=bool(_read_value(path_info, "stroke_visible", False)),
|
|
194
|
+
form_depth=form_depth,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
return tuple(rectangles)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _remove_undercounted_vector_candidates(
|
|
201
|
+
candidates: list[NativeTableCandidate],
|
|
202
|
+
) -> list[NativeTableCandidate]:
|
|
203
|
+
"""当稳定文本候选显著多出行列时,剔除欠分割矢量候选。"""
|
|
204
|
+
|
|
205
|
+
text_candidates = [
|
|
206
|
+
candidate
|
|
207
|
+
for candidate in candidates
|
|
208
|
+
if candidate.source != "vector_grid" and candidate.row_stability >= 0.80 and candidate.column_stability >= 0.80
|
|
209
|
+
]
|
|
210
|
+
if not text_candidates:
|
|
211
|
+
return candidates
|
|
212
|
+
output: list[NativeTableCandidate] = []
|
|
213
|
+
for candidate in candidates:
|
|
214
|
+
if candidate.source != "vector_grid":
|
|
215
|
+
output.append(candidate)
|
|
216
|
+
continue
|
|
217
|
+
if _is_verified_line_candidate(candidate):
|
|
218
|
+
output.append(candidate)
|
|
219
|
+
continue
|
|
220
|
+
undercounted = any(
|
|
221
|
+
text_candidate.rows >= math.ceil(1.5 * candidate.rows) or text_candidate.cols >= math.ceil(1.3 * candidate.cols)
|
|
222
|
+
for text_candidate in text_candidates
|
|
223
|
+
)
|
|
224
|
+
if not undercounted:
|
|
225
|
+
output.append(candidate)
|
|
226
|
+
return output
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _has_alias_affected_physical_blank_row(
|
|
230
|
+
vector_attempts: tuple[dict[str, Any], ...],
|
|
231
|
+
) -> bool:
|
|
232
|
+
"""判断强线框空白行是否因自身 alias 风险而禁止文本候选绕过。"""
|
|
233
|
+
|
|
234
|
+
for attempt in vector_attempts:
|
|
235
|
+
if attempt.get("evidence") != "line_grid":
|
|
236
|
+
continue
|
|
237
|
+
hypotheses = [attempt, *attempt.get("track_hypotheses", [])]
|
|
238
|
+
for hypothesis in hypotheses:
|
|
239
|
+
if hypothesis.get("first_rejection_gate") != "empty_row":
|
|
240
|
+
continue
|
|
241
|
+
empty_rows = set(hypothesis.get("empty_rows", []))
|
|
242
|
+
affected_rows = set(hypothesis.get("alias_affected_rows", []))
|
|
243
|
+
if empty_rows.intersection(affected_rows):
|
|
244
|
+
return True
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _has_physical_row_undercount(
|
|
249
|
+
vector_attempts: tuple[dict[str, Any], ...],
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""判断 line-grid 及其有限轨道假设是否已发现物理行欠分割。"""
|
|
252
|
+
|
|
253
|
+
for attempt in vector_attempts:
|
|
254
|
+
if attempt.get("evidence") != "line_grid":
|
|
255
|
+
continue
|
|
256
|
+
hypotheses = [attempt, *attempt.get("track_hypotheses", [])]
|
|
257
|
+
if any(hypothesis.get("first_rejection_gate") == "physical_row_undercount" for hypothesis in hypotheses):
|
|
258
|
+
return True
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _table_primitive_count(table_input: NativeTableInput) -> int:
|
|
263
|
+
"""统计实际与目标表格相交的 drawing 和矩形数量。"""
|
|
264
|
+
|
|
265
|
+
table_bbox = normalize_bbox(table_input.table_bbox)
|
|
266
|
+
if table_bbox is None:
|
|
267
|
+
return 0
|
|
268
|
+
count = 0
|
|
269
|
+
for primitive in (*table_input.drawing_lines, *table_input.rectangles):
|
|
270
|
+
primitive_bbox = normalize_bbox(primitive.bbox)
|
|
271
|
+
if primitive_bbox is not None and bbox_intersection(primitive_bbox, table_bbox) is not None:
|
|
272
|
+
count += 1
|
|
273
|
+
return count
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _select_candidate(
|
|
277
|
+
candidates: list[NativeTableCandidate],
|
|
278
|
+
) -> NativeTableCandidate | None:
|
|
279
|
+
"""选择达到生产门槛且未与近分异构候选冲突的最佳结果。"""
|
|
280
|
+
|
|
281
|
+
accepted = [candidate for candidate in candidates if _passes_verified_threshold(candidate)]
|
|
282
|
+
if not accepted:
|
|
283
|
+
return None
|
|
284
|
+
if _has_line_rect_topology_conflict(accepted):
|
|
285
|
+
return None
|
|
286
|
+
verified_line_candidates = [candidate for candidate in accepted if _is_verified_line_candidate(candidate)]
|
|
287
|
+
if len(verified_line_candidates) == 1:
|
|
288
|
+
return verified_line_candidates[0]
|
|
289
|
+
accepted.sort(
|
|
290
|
+
key=lambda candidate: (
|
|
291
|
+
candidate.score,
|
|
292
|
+
_SOURCE_PRIORITY[candidate.source],
|
|
293
|
+
),
|
|
294
|
+
reverse=True,
|
|
295
|
+
)
|
|
296
|
+
best = accepted[0]
|
|
297
|
+
for competitor in accepted[1:]:
|
|
298
|
+
if competitor.topology == best.topology:
|
|
299
|
+
continue
|
|
300
|
+
if best.score - competitor.score < MIN_TOPOLOGY_SCORE_GAP:
|
|
301
|
+
return None
|
|
302
|
+
return best
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _evaluate_native_pdf_table(
|
|
306
|
+
table_input: NativeTableInput,
|
|
307
|
+
) -> _CandidateEvaluation:
|
|
308
|
+
"""执行共享生产判定,并保留候选生成到仲裁的完整阶段结果。"""
|
|
309
|
+
|
|
310
|
+
table_bbox = normalize_bbox(table_input.table_bbox)
|
|
311
|
+
page_width, page_height = table_input.page_size
|
|
312
|
+
primitive_count = _table_primitive_count(table_input)
|
|
313
|
+
if (
|
|
314
|
+
table_bbox is None
|
|
315
|
+
or page_width <= 0
|
|
316
|
+
or page_height <= 0
|
|
317
|
+
or table_bbox[0] < 0
|
|
318
|
+
or table_bbox[1] < 0
|
|
319
|
+
or table_bbox[2] > page_width
|
|
320
|
+
or table_bbox[3] > page_height
|
|
321
|
+
):
|
|
322
|
+
return _CandidateEvaluation(
|
|
323
|
+
primitive_count,
|
|
324
|
+
None,
|
|
325
|
+
(),
|
|
326
|
+
(),
|
|
327
|
+
None,
|
|
328
|
+
False,
|
|
329
|
+
"input_geometry",
|
|
330
|
+
)
|
|
331
|
+
if primitive_count > MAX_PRIMITIVES_PER_TABLE:
|
|
332
|
+
return _CandidateEvaluation(
|
|
333
|
+
primitive_count,
|
|
334
|
+
None,
|
|
335
|
+
(),
|
|
336
|
+
(),
|
|
337
|
+
None,
|
|
338
|
+
False,
|
|
339
|
+
"primitive_limit",
|
|
340
|
+
)
|
|
341
|
+
text = build_native_table_text(table_input)
|
|
342
|
+
if text is None:
|
|
343
|
+
return _CandidateEvaluation(
|
|
344
|
+
primitive_count,
|
|
345
|
+
None,
|
|
346
|
+
(),
|
|
347
|
+
(),
|
|
348
|
+
None,
|
|
349
|
+
False,
|
|
350
|
+
"native_text",
|
|
351
|
+
)
|
|
352
|
+
vector_attempt_records: list[dict[str, Any]] = []
|
|
353
|
+
vector_candidates = build_vector_candidates(
|
|
354
|
+
table_input,
|
|
355
|
+
text,
|
|
356
|
+
diagnostics=vector_attempt_records,
|
|
357
|
+
)
|
|
358
|
+
vector_attempts: tuple[dict[str, Any], ...] = tuple(vector_attempt_records)
|
|
359
|
+
physical_topology_conflict = False
|
|
360
|
+
if any(_is_verified_rect_candidate(candidate) for candidate in vector_candidates):
|
|
361
|
+
physical_topology_conflict = _has_attempted_line_rect_grid_conflict(vector_attempts)
|
|
362
|
+
if (
|
|
363
|
+
physical_topology_conflict
|
|
364
|
+
and _resolved_rect_undercount_candidate(
|
|
365
|
+
vector_candidates,
|
|
366
|
+
vector_attempts,
|
|
367
|
+
text,
|
|
368
|
+
)
|
|
369
|
+
is not None
|
|
370
|
+
):
|
|
371
|
+
physical_topology_conflict = False
|
|
372
|
+
vector_selection = None if physical_topology_conflict else _select_candidate(vector_candidates)
|
|
373
|
+
sparse_hybrid_allowed = len(text.rows) >= 2 and vector_selection is None and not physical_topology_conflict
|
|
374
|
+
if sparse_hybrid_allowed:
|
|
375
|
+
if not vector_attempts:
|
|
376
|
+
vector_attempts = diagnose_vector_candidate_builds(
|
|
377
|
+
table_input,
|
|
378
|
+
text,
|
|
379
|
+
)
|
|
380
|
+
if _has_alias_affected_physical_blank_row(vector_attempts):
|
|
381
|
+
sparse_hybrid_allowed = False
|
|
382
|
+
sparse_hybrid_candidates = build_sparse_hybrid_candidates(table_input, text) if sparse_hybrid_allowed else []
|
|
383
|
+
sparse_hybrid_selection = _select_candidate(sparse_hybrid_candidates)
|
|
384
|
+
text_candidates = (
|
|
385
|
+
build_text_candidates(table_input, text)
|
|
386
|
+
if len(text.rows) >= 2 and vector_selection is None and sparse_hybrid_selection is None
|
|
387
|
+
else []
|
|
388
|
+
)
|
|
389
|
+
if text_candidates and (not vector_candidates or physical_topology_conflict):
|
|
390
|
+
if not vector_attempts:
|
|
391
|
+
vector_attempts = diagnose_vector_candidate_builds(
|
|
392
|
+
table_input,
|
|
393
|
+
text,
|
|
394
|
+
)
|
|
395
|
+
if _has_alias_affected_physical_blank_row(vector_attempts) or _has_physical_row_undercount(vector_attempts):
|
|
396
|
+
text_candidates = []
|
|
397
|
+
existing_generated_candidates = [
|
|
398
|
+
*vector_candidates,
|
|
399
|
+
*sparse_hybrid_candidates,
|
|
400
|
+
*text_candidates,
|
|
401
|
+
]
|
|
402
|
+
existing_candidates = _remove_undercounted_vector_candidates(existing_generated_candidates)
|
|
403
|
+
existing_verified = any(_passes_verified_threshold(candidate) for candidate in existing_candidates)
|
|
404
|
+
sparse_multiline_allowed = len(text.rows) >= 2 and not physical_topology_conflict and not existing_verified
|
|
405
|
+
if sparse_multiline_allowed:
|
|
406
|
+
if not vector_attempts:
|
|
407
|
+
vector_attempts = diagnose_vector_candidate_builds(
|
|
408
|
+
table_input,
|
|
409
|
+
text,
|
|
410
|
+
)
|
|
411
|
+
if _has_alias_affected_physical_blank_row(vector_attempts):
|
|
412
|
+
sparse_multiline_allowed = False
|
|
413
|
+
sparse_multiline_candidates = build_sparse_multiline_candidates(table_input, text) if sparse_multiline_allowed else []
|
|
414
|
+
generated_candidates = [
|
|
415
|
+
*existing_generated_candidates,
|
|
416
|
+
*sparse_multiline_candidates,
|
|
417
|
+
]
|
|
418
|
+
candidates = _remove_undercounted_vector_candidates(generated_candidates)
|
|
419
|
+
selected = None if physical_topology_conflict else _select_candidate(candidates)
|
|
420
|
+
if selected is not None:
|
|
421
|
+
first_rejection_gate = None
|
|
422
|
+
elif not generated_candidates:
|
|
423
|
+
first_rejection_gate = "candidate_generation"
|
|
424
|
+
elif not candidates:
|
|
425
|
+
first_rejection_gate = "undercount_guard"
|
|
426
|
+
elif not any(_passes_verified_threshold(candidate) for candidate in candidates):
|
|
427
|
+
first_rejection_gate = "verified_threshold"
|
|
428
|
+
else:
|
|
429
|
+
first_rejection_gate = "topology_conflict"
|
|
430
|
+
return _CandidateEvaluation(
|
|
431
|
+
primitive_count,
|
|
432
|
+
text,
|
|
433
|
+
tuple(generated_candidates),
|
|
434
|
+
tuple(candidates),
|
|
435
|
+
selected,
|
|
436
|
+
physical_topology_conflict,
|
|
437
|
+
first_rejection_gate,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def diagnose_native_pdf_table(table_input: NativeTableInput) -> dict[str, Any]:
|
|
442
|
+
"""返回仅供测试评测使用、不会进入用户结果的候选诊断。"""
|
|
443
|
+
|
|
444
|
+
evaluation = _evaluate_native_pdf_table(table_input)
|
|
445
|
+
vector_attempts = (
|
|
446
|
+
diagnose_vector_candidate_builds(
|
|
447
|
+
table_input,
|
|
448
|
+
evaluation.text,
|
|
449
|
+
)
|
|
450
|
+
if evaluation.text is not None
|
|
451
|
+
else ()
|
|
452
|
+
)
|
|
453
|
+
text_attempts = (
|
|
454
|
+
diagnose_text_candidate_builds(
|
|
455
|
+
table_input,
|
|
456
|
+
evaluation.text,
|
|
457
|
+
)
|
|
458
|
+
if evaluation.text is not None
|
|
459
|
+
else ()
|
|
460
|
+
)
|
|
461
|
+
sparse_hybrid_attempts = (
|
|
462
|
+
diagnose_sparse_hybrid_candidate_builds(
|
|
463
|
+
table_input,
|
|
464
|
+
evaluation.text,
|
|
465
|
+
)
|
|
466
|
+
if evaluation.text is not None
|
|
467
|
+
else ()
|
|
468
|
+
)
|
|
469
|
+
sparse_multiline_attempts = (
|
|
470
|
+
diagnose_sparse_multiline_candidate_builds(
|
|
471
|
+
table_input,
|
|
472
|
+
evaluation.text,
|
|
473
|
+
)
|
|
474
|
+
if evaluation.text is not None
|
|
475
|
+
else ()
|
|
476
|
+
)
|
|
477
|
+
first_rejection_gate = evaluation.first_rejection_gate
|
|
478
|
+
if first_rejection_gate == "candidate_generation":
|
|
479
|
+
text_attempt = next(
|
|
480
|
+
(
|
|
481
|
+
attempt
|
|
482
|
+
for attempt in text_attempts
|
|
483
|
+
if attempt.get("first_rejection_gate")
|
|
484
|
+
in {
|
|
485
|
+
"dense_row_ambiguity",
|
|
486
|
+
"header_requires_rowspan",
|
|
487
|
+
"token_split",
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
None,
|
|
491
|
+
)
|
|
492
|
+
line_attempt = next(
|
|
493
|
+
(attempt for attempt in vector_attempts if attempt.get("evidence") == "line_grid"),
|
|
494
|
+
None,
|
|
495
|
+
)
|
|
496
|
+
if text_attempt is not None:
|
|
497
|
+
first_rejection_gate = "text_" + str(text_attempt["first_rejection_gate"])
|
|
498
|
+
elif sparse_attempt := next(
|
|
499
|
+
(attempt for attempt in sparse_hybrid_attempts if attempt.get("first_rejection_gate")),
|
|
500
|
+
None,
|
|
501
|
+
):
|
|
502
|
+
first_rejection_gate = "sparse_hybrid_" + str(sparse_attempt["first_rejection_gate"])
|
|
503
|
+
elif line_attempt is not None and line_attempt.get("first_rejection_gate"):
|
|
504
|
+
first_rejection_gate = "vector_" + str(line_attempt["first_rejection_gate"])
|
|
505
|
+
elif multiline_attempt := next(
|
|
506
|
+
(attempt for attempt in sparse_multiline_attempts if attempt.get("first_rejection_gate")),
|
|
507
|
+
None,
|
|
508
|
+
):
|
|
509
|
+
first_rejection_gate = "sparse_multiline_" + str(multiline_attempt["first_rejection_gate"])
|
|
510
|
+
|
|
511
|
+
def candidate_record(
|
|
512
|
+
candidate: NativeTableCandidate,
|
|
513
|
+
) -> dict[str, Any]:
|
|
514
|
+
"""把内部候选转换成稳定且不包含单元格全文的诊断记录。"""
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
"source": candidate.source,
|
|
518
|
+
"rows": candidate.rows,
|
|
519
|
+
"cols": candidate.cols,
|
|
520
|
+
"tracks": {
|
|
521
|
+
"x": candidate.cols + 1,
|
|
522
|
+
"y": candidate.rows + 1,
|
|
523
|
+
},
|
|
524
|
+
"score": candidate.score,
|
|
525
|
+
"verified": _passes_verified_threshold(candidate),
|
|
526
|
+
"score_components": {
|
|
527
|
+
"text_capture": candidate.text_capture,
|
|
528
|
+
"structure_support": candidate.structure_support,
|
|
529
|
+
"row_stability": candidate.row_stability,
|
|
530
|
+
"column_stability": candidate.column_stability,
|
|
531
|
+
"order_consistency": candidate.order_consistency,
|
|
532
|
+
},
|
|
533
|
+
"span_signature": [
|
|
534
|
+
[cell.row, cell.col, cell.rowspan, cell.colspan]
|
|
535
|
+
for cell in candidate.cells
|
|
536
|
+
if cell.rowspan > 1 or cell.colspan > 1
|
|
537
|
+
],
|
|
538
|
+
"issues": list(candidate.issues),
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
retained_ids = {id(candidate) for candidate in evaluation.candidates}
|
|
542
|
+
removed_candidates = [
|
|
543
|
+
candidate_record(candidate) for candidate in evaluation.generated_candidates if id(candidate) not in retained_ids
|
|
544
|
+
]
|
|
545
|
+
counterfactual_best = max(
|
|
546
|
+
evaluation.generated_candidates,
|
|
547
|
+
key=lambda candidate: (
|
|
548
|
+
candidate.score,
|
|
549
|
+
_SOURCE_PRIORITY[candidate.source],
|
|
550
|
+
),
|
|
551
|
+
default=None,
|
|
552
|
+
)
|
|
553
|
+
return {
|
|
554
|
+
"primitive_count": evaluation.primitive_count,
|
|
555
|
+
"glyph_count": len(evaluation.text.glyphs) if evaluation.text else 0,
|
|
556
|
+
"visual_text_rows": len(evaluation.text.rows) if evaluation.text else 0,
|
|
557
|
+
"first_rejection_gate": first_rejection_gate,
|
|
558
|
+
"line_rect_topology_conflict": (
|
|
559
|
+
evaluation.physical_topology_conflict or _has_line_rect_topology_conflict(evaluation.candidates)
|
|
560
|
+
),
|
|
561
|
+
"vector_attempts": list(vector_attempts),
|
|
562
|
+
"sparse_hybrid_attempts": list(sparse_hybrid_attempts),
|
|
563
|
+
"sparse_multiline_attempts": list(sparse_multiline_attempts),
|
|
564
|
+
"text_attempts": list(text_attempts),
|
|
565
|
+
"generated_candidates": [candidate_record(candidate) for candidate in evaluation.generated_candidates],
|
|
566
|
+
"removed_by_undercount": removed_candidates,
|
|
567
|
+
"counterfactual_best": (candidate_record(counterfactual_best) if counterfactual_best is not None else None),
|
|
568
|
+
"adopted": (candidate_record(evaluation.selected) if evaluation.selected is not None else None),
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def recover_native_pdf_table(
|
|
573
|
+
table_input: NativeTableInput,
|
|
574
|
+
) -> NativeTableResult | None:
|
|
575
|
+
"""对一个已知表格区域运行全部候选并返回高置信 HTML 结果。"""
|
|
576
|
+
|
|
577
|
+
evaluation = _evaluate_native_pdf_table(table_input)
|
|
578
|
+
selected = evaluation.selected
|
|
579
|
+
if selected is None or evaluation.text is None:
|
|
580
|
+
return None
|
|
581
|
+
diagnostics = tuple(
|
|
582
|
+
[
|
|
583
|
+
f"candidate={selected.source}",
|
|
584
|
+
f"score={selected.score:.4f}",
|
|
585
|
+
f"text_capture={selected.text_capture:.4f}",
|
|
586
|
+
f"structure_support={selected.structure_support:.4f}",
|
|
587
|
+
f"row_stability={selected.row_stability:.4f}",
|
|
588
|
+
f"column_stability={selected.column_stability:.4f}",
|
|
589
|
+
f"order_consistency={selected.order_consistency:.4f}",
|
|
590
|
+
]
|
|
591
|
+
+ list(selected.issues)
|
|
592
|
+
)
|
|
593
|
+
return NativeTableResult(
|
|
594
|
+
html=serialize_candidate_html(selected),
|
|
595
|
+
rows=selected.rows,
|
|
596
|
+
cols=selected.cols,
|
|
597
|
+
cells=selected.cells,
|
|
598
|
+
text=evaluation.text,
|
|
599
|
+
source=selected.source,
|
|
600
|
+
confidence=selected.score,
|
|
601
|
+
diagnostics=diagnostics,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
__all__ = [
|
|
606
|
+
"coerce_native_table_rectangles",
|
|
607
|
+
"coerce_native_table_rules",
|
|
608
|
+
"recover_native_pdf_table",
|
|
609
|
+
]
|