parse-bench 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parse_bench/__init__.py +3 -0
- parse_bench/analysis/__init__.py +6 -0
- parse_bench/analysis/aggregation_report.py +582 -0
- parse_bench/analysis/cli.py +472 -0
- parse_bench/analysis/comparison.py +382 -0
- parse_bench/analysis/comparison_core.py +357 -0
- parse_bench/analysis/comparison_report.py +2066 -0
- parse_bench/analysis/detailed_report.py +2254 -0
- parse_bench/analysis/leaderboard_report.py +852 -0
- parse_bench/analysis/metric_definitions.py +771 -0
- parse_bench/cli.py +267 -0
- parse_bench/data/__init__.py +1 -0
- parse_bench/data/cli.py +118 -0
- parse_bench/data/download.py +127 -0
- parse_bench/evaluation/__init__.py +11 -0
- parse_bench/evaluation/cli.py +435 -0
- parse_bench/evaluation/evaluators/__init__.py +17 -0
- parse_bench/evaluation/evaluators/base.py +34 -0
- parse_bench/evaluation/evaluators/extract.py +429 -0
- parse_bench/evaluation/evaluators/layoutdet.py +1682 -0
- parse_bench/evaluation/evaluators/parse.py +1353 -0
- parse_bench/evaluation/evaluators/qa.py +199 -0
- parse_bench/evaluation/layout_adapters/__init__.py +21 -0
- parse_bench/evaluation/layout_adapters/adapters.py +3180 -0
- parse_bench/evaluation/layout_adapters/base.py +105 -0
- parse_bench/evaluation/layout_adapters/registry.py +109 -0
- parse_bench/evaluation/layout_label_mappers/__init__.py +22 -0
- parse_bench/evaluation/layout_label_mappers/base.py +66 -0
- parse_bench/evaluation/layout_label_mappers/mappers.py +332 -0
- parse_bench/evaluation/layout_label_mappers/projection.py +74 -0
- parse_bench/evaluation/layout_label_mappers/registry.py +119 -0
- parse_bench/evaluation/metric_aggregation.py +56 -0
- parse_bench/evaluation/metrics/__init__.py +5 -0
- parse_bench/evaluation/metrics/attribution/__init__.py +35 -0
- parse_bench/evaluation/metrics/attribution/constants.py +12 -0
- parse_bench/evaluation/metrics/attribution/core.py +1108 -0
- parse_bench/evaluation/metrics/attribution/evaluate.py +446 -0
- parse_bench/evaluation/metrics/attribution/geometry.py +161 -0
- parse_bench/evaluation/metrics/attribution/text_utils.py +233 -0
- parse_bench/evaluation/metrics/base.py +33 -0
- parse_bench/evaluation/metrics/downstream/__init__.py +0 -0
- parse_bench/evaluation/metrics/extract/__init__.py +29 -0
- parse_bench/evaluation/metrics/extract/json_subset_match.py +473 -0
- parse_bench/evaluation/metrics/extract/json_subset_match_metric.py +81 -0
- parse_bench/evaluation/metrics/extract/list_unwrap.py +340 -0
- parse_bench/evaluation/metrics/extract/rule_based_metric.py +90 -0
- parse_bench/evaluation/metrics/extract/test_rules.py +409 -0
- parse_bench/evaluation/metrics/extract/test_types.py +11 -0
- parse_bench/evaluation/metrics/field_grounding/__init__.py +21 -0
- parse_bench/evaluation/metrics/field_grounding/core.py +437 -0
- parse_bench/evaluation/metrics/field_grounding/extract_adapter.py +1224 -0
- parse_bench/evaluation/metrics/field_grounding/parse_adapter.py +697 -0
- parse_bench/evaluation/metrics/field_grounding/rule_filters.py +19 -0
- parse_bench/evaluation/metrics/field_grounding/value_compare.py +190 -0
- parse_bench/evaluation/metrics/layoutdet/__init__.py +17 -0
- parse_bench/evaluation/metrics/layoutdet/classification_utils.py +300 -0
- parse_bench/evaluation/metrics/layoutdet/iou.py +76 -0
- parse_bench/evaluation/metrics/parse/__init__.py +5 -0
- parse_bench/evaluation/metrics/parse/_vendor_grits_reference.py +531 -0
- parse_bench/evaluation/metrics/parse/cross_page_table_consistency.py +165 -0
- parse_bench/evaluation/metrics/parse/emphasis_spans.py +242 -0
- parse_bench/evaluation/metrics/parse/fast_tree_edit.py +282 -0
- parse_bench/evaluation/metrics/parse/grits_metric.py +1125 -0
- parse_bench/evaluation/metrics/parse/grits_reference_metric.py +142 -0
- parse_bench/evaluation/metrics/parse/header_accuracy_metric.py +1662 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/__init__.py +51 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/base.py +125 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/config.py +44 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/postprocess.py +322 -0
- parse_bench/evaluation/metrics/parse/llm_normalization/strategy_judge.py +541 -0
- parse_bench/evaluation/metrics/parse/mermaid_graph.py +682 -0
- parse_bench/evaluation/metrics/parse/rule_based_judge_metric.py +56 -0
- parse_bench/evaluation/metrics/parse/rule_based_metric.py +434 -0
- parse_bench/evaluation/metrics/parse/rules_bag.py +1161 -0
- parse_bench/evaluation/metrics/parse/rules_base.py +751 -0
- parse_bench/evaluation/metrics/parse/rules_chart.py +1556 -0
- parse_bench/evaluation/metrics/parse/rules_diagram.py +591 -0
- parse_bench/evaluation/metrics/parse/rules_form.py +2274 -0
- parse_bench/evaluation/metrics/parse/rules_formatting.py +1500 -0
- parse_bench/evaluation/metrics/parse/rules_heading.py +228 -0
- parse_bench/evaluation/metrics/parse/rules_list.py +226 -0
- parse_bench/evaluation/metrics/parse/rules_page_decoration.py +276 -0
- parse_bench/evaluation/metrics/parse/rules_table.py +1666 -0
- parse_bench/evaluation/metrics/parse/rules_text.py +340 -0
- parse_bench/evaluation/metrics/parse/rules_watermark.py +105 -0
- parse_bench/evaluation/metrics/parse/structural_consistency_metric.py +251 -0
- parse_bench/evaluation/metrics/parse/table_extraction.py +152 -0
- parse_bench/evaluation/metrics/parse/table_merging.py +195 -0
- parse_bench/evaluation/metrics/parse/table_pairing.py +87 -0
- parse_bench/evaluation/metrics/parse/table_parsing.py +955 -0
- parse_bench/evaluation/metrics/parse/table_record_match_metric.py +1453 -0
- parse_bench/evaluation/metrics/parse/table_splitting.py +301 -0
- parse_bench/evaluation/metrics/parse/table_title_stripping.py +530 -0
- parse_bench/evaluation/metrics/parse/teds_metric.py +600 -0
- parse_bench/evaluation/metrics/parse/test_rules.py +120 -0
- parse_bench/evaluation/metrics/parse/test_types.py +103 -0
- parse_bench/evaluation/metrics/parse/text_content_projection.py +175 -0
- parse_bench/evaluation/metrics/parse/text_similarity_metric.py +61 -0
- parse_bench/evaluation/metrics/parse/utils.py +885 -0
- parse_bench/evaluation/metrics/qa/__init__.py +5 -0
- parse_bench/evaluation/metrics/qa/answer_comparison.py +380 -0
- parse_bench/evaluation/qa/__init__.py +5 -0
- parse_bench/evaluation/qa/llm_service.py +335 -0
- parse_bench/evaluation/reports/__init__.py +8 -0
- parse_bench/evaluation/reports/csv.py +64 -0
- parse_bench/evaluation/reports/html.py +338 -0
- parse_bench/evaluation/reports/markdown.py +98 -0
- parse_bench/evaluation/reports/rule_csv.py +22 -0
- parse_bench/evaluation/runner.py +1864 -0
- parse_bench/evaluation/stats.py +104 -0
- parse_bench/extensions.py +72 -0
- parse_bench/inference/__init__.py +33 -0
- parse_bench/inference/chunkr_layout_extraction.py +160 -0
- parse_bench/inference/cli.py +484 -0
- parse_bench/inference/layout_extraction.py +422 -0
- parse_bench/inference/pipelines/__init__.py +59 -0
- parse_bench/inference/pipelines/extract.py +39 -0
- parse_bench/inference/pipelines/layout.py +142 -0
- parse_bench/inference/pipelines/parse.py +2603 -0
- parse_bench/inference/pipelines.py +0 -0
- parse_bench/inference/providers/__init__.py +28 -0
- parse_bench/inference/providers/base.py +196 -0
- parse_bench/inference/providers/cancellation.py +137 -0
- parse_bench/inference/providers/extract/__init__.py +22 -0
- parse_bench/inference/providers/extract/citations.py +549 -0
- parse_bench/inference/providers/extract/extend.py +851 -0
- parse_bench/inference/providers/extract/llamaextract_v2_api.py +583 -0
- parse_bench/inference/providers/layoutdet/__init__.py +25 -0
- parse_bench/inference/providers/layoutdet/adapters.py +946 -0
- parse_bench/inference/providers/layoutdet/base.py +203 -0
- parse_bench/inference/providers/layoutdet/chandra.py +449 -0
- parse_bench/inference/providers/layoutdet/docling.py +125 -0
- parse_bench/inference/providers/layoutdet/dots_ocr.py +606 -0
- parse_bench/inference/providers/layoutdet/layout_v3.py +137 -0
- parse_bench/inference/providers/layoutdet/layout_v3_byoc.py +204 -0
- parse_bench/inference/providers/layoutdet/paddle.py +117 -0
- parse_bench/inference/providers/layoutdet/qwen3vl.py +360 -0
- parse_bench/inference/providers/layoutdet/surya.py +250 -0
- parse_bench/inference/providers/layoutdet/yolo.py +109 -0
- parse_bench/inference/providers/parse/__init__.py +64 -0
- parse_bench/inference/providers/parse/_docling_common.py +233 -0
- parse_bench/inference/providers/parse/_layout_utils.py +611 -0
- parse_bench/inference/providers/parse/amazon_nova.py +515 -0
- parse_bench/inference/providers/parse/anthropic.py +882 -0
- parse_bench/inference/providers/parse/azure_document_intelligence.py +700 -0
- parse_bench/inference/providers/parse/chandra2.py +633 -0
- parse_bench/inference/providers/parse/chunkr.py +268 -0
- parse_bench/inference/providers/parse/databricks_ai_parse.py +724 -0
- parse_bench/inference/providers/parse/datalab.py +370 -0
- parse_bench/inference/providers/parse/deepseekocr2.py +382 -0
- parse_bench/inference/providers/parse/docling.py +281 -0
- parse_bench/inference/providers/parse/docling_serve.py +289 -0
- parse_bench/inference/providers/parse/dots_ocr.py +574 -0
- parse_bench/inference/providers/parse/extend_parse.py +710 -0
- parse_bench/inference/providers/parse/falconocr.py +436 -0
- parse_bench/inference/providers/parse/florin_parser_nano.py +559 -0
- parse_bench/inference/providers/parse/gemma4.py +472 -0
- parse_bench/inference/providers/parse/glm_zai.py +229 -0
- parse_bench/inference/providers/parse/google.py +1125 -0
- parse_bench/inference/providers/parse/google_agentic_vision.py +819 -0
- parse_bench/inference/providers/parse/google_docai.py +776 -0
- parse_bench/inference/providers/parse/google_docai_layout_normalization.py +573 -0
- parse_bench/inference/providers/parse/granite_vision.py +515 -0
- parse_bench/inference/providers/parse/infinity_parser2.py +704 -0
- parse_bench/inference/providers/parse/kdl_frontier_nano.py +3327 -0
- parse_bench/inference/providers/parse/landingai.py +452 -0
- parse_bench/inference/providers/parse/liteparse.py +350 -0
- parse_bench/inference/providers/parse/llamaparse.py +677 -0
- parse_bench/inference/providers/parse/llamaparse_v2_normalization.py +1013 -0
- parse_bench/inference/providers/parse/markitdown.py +138 -0
- parse_bench/inference/providers/parse/mineru25.py +405 -0
- parse_bench/inference/providers/parse/mineru2605pro.py +432 -0
- parse_bench/inference/providers/parse/mineru_diffusion.py +371 -0
- parse_bench/inference/providers/parse/mistral_ocr.py +546 -0
- parse_bench/inference/providers/parse/nemotron_omni.py +473 -0
- parse_bench/inference/providers/parse/oi_parser.py +222 -0
- parse_bench/inference/providers/parse/openai.py +740 -0
- parse_bench/inference/providers/parse/opendataloader.py +152 -0
- parse_bench/inference/providers/parse/paddleocr.py +624 -0
- parse_bench/inference/providers/parse/pdf_inspector.py +142 -0
- parse_bench/inference/providers/parse/pulse.py +785 -0
- parse_bench/inference/providers/parse/pymupdf.py +207 -0
- parse_bench/inference/providers/parse/pymupdf4llm.py +356 -0
- parse_bench/inference/providers/parse/pypdf.py +179 -0
- parse_bench/inference/providers/parse/qwen.py +678 -0
- parse_bench/inference/providers/parse/rakedoc_nano.py +70 -0
- parse_bench/inference/providers/parse/reducto.py +546 -0
- parse_bench/inference/providers/parse/surya2.py +372 -0
- parse_bench/inference/providers/parse/tesseract.py +301 -0
- parse_bench/inference/providers/parse/textract.py +694 -0
- parse_bench/inference/providers/parse/unlimitedocr.py +346 -0
- parse_bench/inference/providers/parse/unstructured.py +485 -0
- parse_bench/inference/providers/parse/warp_ingest.py +199 -0
- parse_bench/inference/providers/registry.py +49 -0
- parse_bench/inference/renormalize.py +170 -0
- parse_bench/inference/runner.py +2023 -0
- parse_bench/layout_label_mapping.py +424 -0
- parse_bench/layout_projection.py +179 -0
- parse_bench/pipeline/__init__.py +1 -0
- parse_bench/pipeline/cli.py +549 -0
- parse_bench/schemas/__init__.py +33 -0
- parse_bench/schemas/evaluation.py +93 -0
- parse_bench/schemas/extract_output.py +36 -0
- parse_bench/schemas/layout_detection_output.py +545 -0
- parse_bench/schemas/layout_ontology.py +315 -0
- parse_bench/schemas/metrics.py +69 -0
- parse_bench/schemas/parse_output.py +152 -0
- parse_bench/schemas/pipeline.py +22 -0
- parse_bench/schemas/pipeline_io.py +106 -0
- parse_bench/schemas/product.py +97 -0
- parse_bench/test_cases/__init__.py +25 -0
- parse_bench/test_cases/bbox_value_strict_comparator.py +880 -0
- parse_bench/test_cases/extract_field_paths.py +164 -0
- parse_bench/test_cases/layout_attribution_generation.py +287 -0
- parse_bench/test_cases/loader.py +652 -0
- parse_bench/test_cases/parse_rule_schemas.py +1071 -0
- parse_bench/test_cases/rule_filters.py +32 -0
- parse_bench/test_cases/rule_ids.py +107 -0
- parse_bench/test_cases/schema.py +427 -0
- parse_bench/utils/__init__.py +15 -0
- parse_bench/utils/gemini_layout_utils.py +670 -0
- parse_bench/utils/text_aggregation.py +100 -0
- parse_bench-1.0.0.dist-info/METADATA +476 -0
- parse_bench-1.0.0.dist-info/RECORD +227 -0
- parse_bench-1.0.0.dist-info/WHEEL +4 -0
- parse_bench-1.0.0.dist-info/entry_points.txt +2 -0
- parse_bench-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""florin-parser-nano — ParseBench provider.
|
|
2
|
+
|
|
3
|
+
`florin-inc/florin-parser-nano <https://huggingface.co/florin-inc/florin-parser-nano>`_
|
|
4
|
+
is a LoRA fine-tune of `KDLAI/KDL-Frontier-Parser-nano
|
|
5
|
+
<https://huggingface.co/KDLAI/KDL-Frontier-Parser-nano>`_ (KoreaDeep, 1.2B, Qwen2-VL
|
|
6
|
+
architecture) that teaches the model to emit inline formatting — ``**bold**``,
|
|
7
|
+
``~~strikethrough~~``, ``<sup>``/``<sub>`` — during text recognition, with the
|
|
8
|
+
production prompt unchanged. Weights are AGPL-3.0, inherited from the base model.
|
|
9
|
+
Full attribution to KoreaDeep for the base model and the pipeline design.
|
|
10
|
+
|
|
11
|
+
This provider inherits every inference stage of the in-repo ``kdl_frontier_nano``
|
|
12
|
+
provider unchanged — page rendering, layout detection, crop/bucket, all four
|
|
13
|
+
recognition passes, retry and error handling — and differs in exactly two ways:
|
|
14
|
+
|
|
15
|
+
1. **The served weights** are the fine-tune instead of the base model.
|
|
16
|
+
2. **Markdown emission**: the function that turns the finished element list into
|
|
17
|
+
markdown is replaced by the four emission fixes implemented in this file
|
|
18
|
+
(see "THE FOUR EMISSION CHANGES" below). The ``pages`` payload (each element's
|
|
19
|
+
category, bounding box and recognised text) is left exactly as the inherited
|
|
20
|
+
pipeline produced it, so the Visual Grounding dimension is computed from
|
|
21
|
+
unmodified pipeline output.
|
|
22
|
+
|
|
23
|
+
Serve the weights exactly as the base model is served:
|
|
24
|
+
|
|
25
|
+
vllm serve florin-inc/florin-parser-nano \\
|
|
26
|
+
--served-model-name florin-parser-nano \\
|
|
27
|
+
--max-model-len 8192 --gpu-memory-utilization 0.85 \\
|
|
28
|
+
--max-num-seqs 24 --trust-remote-code \\
|
|
29
|
+
--limit-mm-per-prompt '{"image":1}'
|
|
30
|
+
|
|
31
|
+
Then:
|
|
32
|
+
|
|
33
|
+
FLORIN_NANO_ENDPOINT_URL=http://localhost:8000/v1 \\
|
|
34
|
+
uv run parse-bench run florin_parser_nano --input_dir data ...
|
|
35
|
+
|
|
36
|
+
Config (env):
|
|
37
|
+
FLORIN_NANO_ENDPOINT_URL vLLM base URL ending in /v1 (required)
|
|
38
|
+
FLORIN_NANO_MODEL served model name (default florin-parser-nano)
|
|
39
|
+
All other knobs (per-document request concurrency, per-stage token budgets, DPI)
|
|
40
|
+
are inherited from the ``kdl_frontier_nano`` provider and keep their ``KDL_NANO_*``
|
|
41
|
+
environment variables and defaults, because the fine-tune is served and driven
|
|
42
|
+
identically to the base model.
|
|
43
|
+
|
|
44
|
+
THE FOUR EMISSION CHANGES
|
|
45
|
+
-------------------------
|
|
46
|
+
Each is a defect fix in the emission stage, not a change to recognition output:
|
|
47
|
+
|
|
48
|
+
(a) **section_header layout-label mapping.** ``NATIVE_LAYOUT_CATEGORY_MAP`` in
|
|
49
|
+
``kdl_frontier_nano.py`` has no ``section_header`` key, so that raw layout label
|
|
50
|
+
falls through to ``Text`` and the formatter's ``## `` branch (the only source of
|
|
51
|
+
``## `` headings) is unreachable. One key is added.
|
|
52
|
+
|
|
53
|
+
(b) **Heading depth from bounding-box height.** The inherited pipeline emits every
|
|
54
|
+
``Title`` element as ``# ``, which makes any parent/child heading-hierarchy
|
|
55
|
+
check unsatisfiable by construction. Depth 1..4 is assigned from the rank of
|
|
56
|
+
each Title's bounding-box height within the document (taller box = shallower
|
|
57
|
+
heading). Ranks are computed from geometry the layout stage already produced;
|
|
58
|
+
element dicts are not modified.
|
|
59
|
+
|
|
60
|
+
(c) **Bold run-in labels.** A paragraph- or list-item-leading ``Label:`` run (e.g.
|
|
61
|
+
"**AGENCY:** Department of Transport") is bold in the source documents; the
|
|
62
|
+
inherited emission drops the markup. The leading label run is wrapped in
|
|
63
|
+
``**...**``, never inside tables, headings, code fences, HTML blocks, or lines
|
|
64
|
+
that already contain ``**``. For a list item the marker stays outside the span
|
|
65
|
+
(``- Note: x`` becomes ``- **Note:** x``), and fence state is tracked across
|
|
66
|
+
lines so fence interiors are never touched. (Corrected 2026-08-21 after
|
|
67
|
+
maintainer review of PR #99: the original implementation swallowed list
|
|
68
|
+
markers into the span and, lacking fence state, bolded lines inside fences.)
|
|
69
|
+
|
|
70
|
+
(d) **Relaxed standalone-heading gate.** The inherited ``_is_titleish`` vetoes any
|
|
71
|
+
candidate line ending in ``.!?:;,`` and any line matching ``^.{1,40}:\\s``,
|
|
72
|
+
which rejects genuine headings such as "Notes:". The label/value veto is
|
|
73
|
+
dropped; the terminal-punctuation veto is kept for every character except the
|
|
74
|
+
colon (``.!?;,``), so "Notes:" promotes but a sentence ending in ``.`` does
|
|
75
|
+
not; every other guard is kept and promoted lines are capped at 20 words.
|
|
76
|
+
(Corrected 2026-08-21 after maintainer review of PR #99: the original
|
|
77
|
+
implementation dropped the terminal-punctuation veto entirely, which promoted
|
|
78
|
+
ordinary short sentences ending in ``.`` to headings.)
|
|
79
|
+
|
|
80
|
+
IMPLEMENTATION NOTE — why two module attributes are rebound during inference
|
|
81
|
+
----------------------------------------------------------------------------
|
|
82
|
+
``kdl_frontier_nano.py`` is not modified. Two things the emission changes need are
|
|
83
|
+
looked up as module globals by module-level functions in that file, where no subclass
|
|
84
|
+
hook can reach them: ``NATIVE_LAYOUT_CATEGORY_MAP`` (read by ``_category_for_item``
|
|
85
|
+
during layout parsing) and ``_NanoEngine`` (instantiated by name inside
|
|
86
|
+
``KdlFrontierNanoProvider.run_inference``). ``_patched_bindings()`` below rebinds
|
|
87
|
+
those two attributes while a florin document is in flight and restores them when the
|
|
88
|
+
last in-flight document finishes (re-entrant, so concurrent documents are safe).
|
|
89
|
+
Consequence: do not run the unpatched ``kdl_frontier_nano`` pipeline concurrently in
|
|
90
|
+
the same process as ``florin_parser_nano``; run them as separate invocations.
|
|
91
|
+
|
|
92
|
+
All emission code in this file is a pure function of the element list — no network,
|
|
93
|
+
no randomness — so the same elements always produce the same markdown.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
from __future__ import annotations
|
|
97
|
+
|
|
98
|
+
import asyncio
|
|
99
|
+
import contextlib
|
|
100
|
+
import os
|
|
101
|
+
import re
|
|
102
|
+
import threading
|
|
103
|
+
from typing import Any, Dict, Iterator, List, Tuple
|
|
104
|
+
|
|
105
|
+
from PIL import Image
|
|
106
|
+
|
|
107
|
+
from parse_bench.inference.providers.base import ProviderConfigError
|
|
108
|
+
from parse_bench.inference.providers.parse import kdl_frontier_nano as K
|
|
109
|
+
from parse_bench.inference.providers.registry import register_provider
|
|
110
|
+
from parse_bench.schemas.pipeline import PipelineSpec
|
|
111
|
+
from parse_bench.schemas.pipeline_io import InferenceRequest, RawInferenceResult
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# (a) layout label map fix — one added key. "Section-header" is the pipeline's
|
|
115
|
+
# own canonical category name and the only category rendered as "## ".
|
|
116
|
+
# =============================================================================
|
|
117
|
+
FLORIN_NATIVE_LAYOUT_CATEGORY_MAP: Dict[str, str] = {
|
|
118
|
+
**K.NATIVE_LAYOUT_CATEGORY_MAP,
|
|
119
|
+
"section_header": "Section-header",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#: Fixed emission parameters for this provider (the submitted configuration).
|
|
123
|
+
_MAX_HEADING_DEPTH = 4 # change (b): heading depths 1..4
|
|
124
|
+
_TITLE_GATE_WORD_CAP = 20 # change (d): word cap alongside the narrowed veto
|
|
125
|
+
#: change (d): the vendored ``_TERMINAL_PUNCT`` with ``:`` removed — a line ending
|
|
126
|
+
#: in sentence punctuation is body text; a line ending in ``:`` may be a heading.
|
|
127
|
+
_TERMINAL_PUNCT_NO_COLON = tuple(".!?;,")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# =============================================================================
|
|
131
|
+
# (b) heading depth from bounding-box height
|
|
132
|
+
# =============================================================================
|
|
133
|
+
def _heading_levels_by_bbox(
|
|
134
|
+
elements: List[Dict[str, Any]], max_level: int = _MAX_HEADING_DEPTH
|
|
135
|
+
) -> Dict[int, int]:
|
|
136
|
+
"""Assign a heading depth 1..``max_level`` to every ``Title`` element.
|
|
137
|
+
|
|
138
|
+
Taller bounding box = more prominent heading = shallower depth. Depths come
|
|
139
|
+
from the *rank* of the distinct rounded box heights within the document, not
|
|
140
|
+
from absolute sizes, so the result does not depend on page size or render
|
|
141
|
+
DPI. Ties share a depth. Returns ``{index in elements -> depth}`` without
|
|
142
|
+
writing to the element dicts, which keeps the elements (and therefore the
|
|
143
|
+
Visual Grounding payload) bit-identical.
|
|
144
|
+
"""
|
|
145
|
+
heights: List[Tuple[int, float]] = []
|
|
146
|
+
for i, el in enumerate(elements):
|
|
147
|
+
if el.get("category") != "Title":
|
|
148
|
+
continue
|
|
149
|
+
bb = el.get("bbox")
|
|
150
|
+
if not bb or len(bb) < 4:
|
|
151
|
+
continue
|
|
152
|
+
heights.append((i, abs(float(bb[3]) - float(bb[1]))))
|
|
153
|
+
if not heights:
|
|
154
|
+
return {}
|
|
155
|
+
distinct = sorted({round(h, 4) for _, h in heights}, reverse=True)
|
|
156
|
+
rank = {h: min(r + 1, max_level) for r, h in enumerate(distinct)}
|
|
157
|
+
return {i: rank[round(h, 4)] for i, h in heights}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# element -> markdown
|
|
162
|
+
# =============================================================================
|
|
163
|
+
def _format_element(el: Dict[str, Any], level: int | None) -> str:
|
|
164
|
+
"""Render one element, delegating everything unchanged to the inherited
|
|
165
|
+
formatter. The single differing branch: a ``Title`` element with an assigned
|
|
166
|
+
depth gets that many ``#`` characters instead of always one."""
|
|
167
|
+
if el.get("category") == "Title" and level:
|
|
168
|
+
body = K._preserve_inline_markup(
|
|
169
|
+
K._strip_leading_heading_marker(el.get("content") or "")
|
|
170
|
+
)
|
|
171
|
+
return f"{'#' * int(level)} {body}"
|
|
172
|
+
return K._nano_format_element(el)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _assemble_markdown(
|
|
176
|
+
elements: List[Dict[str, Any]],
|
|
177
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
178
|
+
"""Port of ``K._nano_assemble_markdown`` with the per-element formatter
|
|
179
|
+
swapped for :func:`_format_element`, so heading depths from change (b) can be
|
|
180
|
+
applied without editing the vendored file.
|
|
181
|
+
|
|
182
|
+
Behaviour copied verbatim: sort by (page, layout_order); drop elements with
|
|
183
|
+
an invalid page number; group runs of contiguous same-page ``List-item``
|
|
184
|
+
elements into a single block; drop empty blocks; insert
|
|
185
|
+
``---\\n\\n**Page N**`` separators between pages of the whole-document string
|
|
186
|
+
only.
|
|
187
|
+
"""
|
|
188
|
+
levels = _heading_levels_by_bbox(elements)
|
|
189
|
+
|
|
190
|
+
valid = [
|
|
191
|
+
(i, e)
|
|
192
|
+
for i, e in sorted(
|
|
193
|
+
enumerate(elements),
|
|
194
|
+
key=lambda p: (p[1].get("page_number", 1), p[1].get("layout_order", 0)),
|
|
195
|
+
)
|
|
196
|
+
if isinstance(e.get("page_number"), int)
|
|
197
|
+
and not isinstance(e.get("page_number"), bool)
|
|
198
|
+
and e.get("page_number", 0) >= 1
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
blocks: List[Tuple[int, str]] = []
|
|
202
|
+
index = 0
|
|
203
|
+
while index < len(valid):
|
|
204
|
+
idx, el = valid[index]
|
|
205
|
+
page = el["page_number"]
|
|
206
|
+
if el.get("category") == "List-item":
|
|
207
|
+
items = []
|
|
208
|
+
while (
|
|
209
|
+
index < len(valid)
|
|
210
|
+
and valid[index][1].get("category") == "List-item"
|
|
211
|
+
and valid[index][1]["page_number"] == page
|
|
212
|
+
):
|
|
213
|
+
j, item = valid[index]
|
|
214
|
+
formatted = _format_element(item, levels.get(j))
|
|
215
|
+
if formatted:
|
|
216
|
+
items.append(formatted)
|
|
217
|
+
index += 1
|
|
218
|
+
content = "\n".join(items).strip()
|
|
219
|
+
else:
|
|
220
|
+
content = _format_element(el, levels.get(idx)).strip()
|
|
221
|
+
index += 1
|
|
222
|
+
if content:
|
|
223
|
+
blocks.append((page, content))
|
|
224
|
+
|
|
225
|
+
md_parts: List[str] = []
|
|
226
|
+
current_page: int | None = None
|
|
227
|
+
for page, content in blocks:
|
|
228
|
+
if current_page is not None and page != current_page:
|
|
229
|
+
md_parts.append(f"---\n\n**Page {page}**")
|
|
230
|
+
md_parts.append(content)
|
|
231
|
+
current_page = page
|
|
232
|
+
|
|
233
|
+
pages_md: Dict[int, List[str]] = {}
|
|
234
|
+
for page, content in blocks:
|
|
235
|
+
pages_md.setdefault(page, []).append(content)
|
|
236
|
+
markdown_pages = [
|
|
237
|
+
{"page_number": page, "content": "\n\n".join(parts)}
|
|
238
|
+
for page, parts in sorted(pages_md.items())
|
|
239
|
+
]
|
|
240
|
+
return "\n\n".join(md_parts), markdown_pages
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# =============================================================================
|
|
244
|
+
# (d) relaxed standalone-heading gate
|
|
245
|
+
# =============================================================================
|
|
246
|
+
def _is_titleish_relaxed(
|
|
247
|
+
text: str, max_words: int, caps_ratio: float, require_all_caps: bool
|
|
248
|
+
) -> bool:
|
|
249
|
+
"""Replacement for ``K._is_titleish`` with one veto removed and one narrowed.
|
|
250
|
+
|
|
251
|
+
Kept, unchanged from the vendored gate: refuse a line that is already a
|
|
252
|
+
heading, a bullet, a numbered item or a table row; refuse a line with no
|
|
253
|
+
ASCII letters; and require either a leading capital letter or a
|
|
254
|
+
capitalisation ratio above ``caps_ratio``.
|
|
255
|
+
|
|
256
|
+
Removed: the "``^.{1,40}:\\s`` looks like a label/value pair" veto, which
|
|
257
|
+
rejects genuine headings.
|
|
258
|
+
|
|
259
|
+
Narrowed (2026-08-21, maintainer review of PR #99): the vendored
|
|
260
|
+
terminal-punctuation veto is restored for every character EXCEPT the colon
|
|
261
|
+
(``_TERMINAL_PUNCT_NO_COLON``). Dropping it entirely had promoted ordinary
|
|
262
|
+
short sentences ending in ``.`` to ``# `` headings; keeping ``:`` out of the
|
|
263
|
+
veto set is the point of this change ("Notes:" is a genuine heading the
|
|
264
|
+
vendored gate wrongly rejects).
|
|
265
|
+
|
|
266
|
+
Replaced: the shipped 12-word cap becomes ``_TITLE_GATE_WORD_CAP`` (20).
|
|
267
|
+
The ``max_words`` argument is accepted and deliberately ignored so the
|
|
268
|
+
signature matches the vendored gate, which ``title_promote`` calls
|
|
269
|
+
positionally.
|
|
270
|
+
"""
|
|
271
|
+
s = text.strip()
|
|
272
|
+
if not s:
|
|
273
|
+
return False
|
|
274
|
+
if (
|
|
275
|
+
K._HEADING_RE.match(s)
|
|
276
|
+
or K._LIST_RE.match(s)
|
|
277
|
+
or K._NUMLIST_RE.match(s)
|
|
278
|
+
or K._TABLEROW_RE.match(s)
|
|
279
|
+
):
|
|
280
|
+
return False
|
|
281
|
+
if len(s.split()) > _TITLE_GATE_WORD_CAP:
|
|
282
|
+
return False
|
|
283
|
+
if s.endswith(_TERMINAL_PUNCT_NO_COLON):
|
|
284
|
+
return False
|
|
285
|
+
letters = K._LETTER_RE.findall(s)
|
|
286
|
+
if not letters:
|
|
287
|
+
return False
|
|
288
|
+
caps_frac = len(K._UPPER_RE.findall(s)) / len(letters)
|
|
289
|
+
if require_all_caps:
|
|
290
|
+
return caps_frac >= caps_ratio
|
|
291
|
+
first_alpha = next((c for c in s if c.isalpha()), "")
|
|
292
|
+
return first_alpha.isupper() or caps_frac > caps_ratio
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _title_promote(md: str, variant: str = "aggressive") -> str:
|
|
296
|
+
"""Port of ``K.title_promote`` with the accept/reject gate swapped for
|
|
297
|
+
:func:`_is_titleish_relaxed`.
|
|
298
|
+
|
|
299
|
+
Verbatim behaviour: skip fenced code blocks; consider only lines that are
|
|
300
|
+
"standalone" (blank line, or document edge, both above and below); never
|
|
301
|
+
promote the ``**Page N**`` separator; unwrap a ``> `` blockquote and promote
|
|
302
|
+
its inner text if the gate accepts it.
|
|
303
|
+
"""
|
|
304
|
+
if not md:
|
|
305
|
+
return md
|
|
306
|
+
max_words, caps_ratio, require_all_caps = K._TITLE_VARIANTS[variant]
|
|
307
|
+
do_promote = variant != "deblockquote_only"
|
|
308
|
+
|
|
309
|
+
lines = md.split("\n")
|
|
310
|
+
n = len(lines)
|
|
311
|
+
out = list(lines)
|
|
312
|
+
in_fence = False
|
|
313
|
+
for i, raw in enumerate(lines):
|
|
314
|
+
if K._FENCE_RE.match(raw):
|
|
315
|
+
in_fence = not in_fence
|
|
316
|
+
continue
|
|
317
|
+
if in_fence:
|
|
318
|
+
continue
|
|
319
|
+
above = lines[i - 1] if i > 0 else ""
|
|
320
|
+
below = lines[i + 1] if i + 1 < n else ""
|
|
321
|
+
standalone = (i == 0 or above.strip() == "") and (
|
|
322
|
+
i + 1 >= n or below.strip() == ""
|
|
323
|
+
)
|
|
324
|
+
if not standalone:
|
|
325
|
+
continue
|
|
326
|
+
if K._PAGE_MARKER_RE.match(raw.strip()):
|
|
327
|
+
continue
|
|
328
|
+
bq = K._BLOCKQUOTE_RE.match(raw)
|
|
329
|
+
if bq:
|
|
330
|
+
inner = bq.group(1)
|
|
331
|
+
if _is_titleish_relaxed(inner, max_words, caps_ratio, require_all_caps):
|
|
332
|
+
out[i] = ("# " + inner.strip()) if do_promote else inner.strip()
|
|
333
|
+
continue
|
|
334
|
+
if do_promote and _is_titleish_relaxed(
|
|
335
|
+
raw, max_words, caps_ratio, require_all_caps
|
|
336
|
+
):
|
|
337
|
+
out[i] = "# " + raw.strip()
|
|
338
|
+
return "\n".join(out)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# =============================================================================
|
|
342
|
+
# (c) bold run-in `Label:` prefixes
|
|
343
|
+
# =============================================================================
|
|
344
|
+
# A line whose first token run is a short label ending in a colon, followed by
|
|
345
|
+
# whitespace and then the value. Deliberately narrow: at most 60 characters and
|
|
346
|
+
# at most 6 words in the label, no colon inside it, and the line may not begin
|
|
347
|
+
# with a markdown structural character.
|
|
348
|
+
_LABEL_RE = re.compile(r"^([^\s#>|*<][^:\n]{0,60}?:)(\s)")
|
|
349
|
+
_HAS_BOLD = re.compile(r"\*\*")
|
|
350
|
+
_HTML_TABLE_SPAN = re.compile(r"<table\b.*?</table>", re.S)
|
|
351
|
+
_PAGE_MARKER = re.compile(r"^\*\*Page\s+\d+\*\*$")
|
|
352
|
+
#: Lines never touched: heading, pipe-table row, HTML, code fence, image, page rule.
|
|
353
|
+
_SKIP_PREFIXES = ("#", "|", "<", "`", "!", "---")
|
|
354
|
+
#: A code/LaTeX fence marker line — same pattern as the vendored ``_FENCE_RE``.
|
|
355
|
+
_FENCE_LINE = re.compile(r"^\s*(```|~~~)")
|
|
356
|
+
#: A markdown list-item marker: optional indent of at most 3 spaces (4+ is an
|
|
357
|
+
#: indented code block), then a bullet (``-``, ``+``, ``*``) or a number with
|
|
358
|
+
#: ``.``/``)``, then whitespace.
|
|
359
|
+
_LIST_PREFIX = re.compile(r"^(\s{0,3}(?:[-+*]|\d{1,3}[.)])\s+)")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _table_line_mask(md: str) -> List[bool]:
|
|
363
|
+
"""True for each line that lies inside an HTML ``<table>...</table>`` block."""
|
|
364
|
+
spans = [(m.start(), m.end()) for m in _HTML_TABLE_SPAN.finditer(md)]
|
|
365
|
+
mask: List[bool] = []
|
|
366
|
+
pos = 0
|
|
367
|
+
for line in md.split("\n"):
|
|
368
|
+
mask.append(any(a <= pos < b for a, b in spans))
|
|
369
|
+
pos += len(line) + 1
|
|
370
|
+
return mask
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _skippable(line: str) -> bool:
|
|
374
|
+
s = line.strip()
|
|
375
|
+
if not s:
|
|
376
|
+
return True
|
|
377
|
+
if _PAGE_MARKER.match(s):
|
|
378
|
+
return True
|
|
379
|
+
return s.startswith(_SKIP_PREFIXES)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _bold_run_in_labels(md: str) -> str:
|
|
383
|
+
"""Wrap a leading ``Label:`` run of each paragraph or list item in ``**...**``.
|
|
384
|
+
|
|
385
|
+
Never touches a line inside a table, a heading, a code fence, an HTML block,
|
|
386
|
+
or a line that already contains ``**`` (nesting or splitting an existing bold
|
|
387
|
+
span would break the markdown).
|
|
388
|
+
|
|
389
|
+
Corrected 2026-08-21 after the maintainer's review of PR #99 found two
|
|
390
|
+
defects in the original implementation:
|
|
391
|
+
|
|
392
|
+
1. **List structure is preserved.** The label pattern used to match against
|
|
393
|
+
the whole line, so ``- Note: x`` became ``**- Note:** x`` — the bullet was
|
|
394
|
+
swallowed into the bold span, breaking the list. The list marker is now
|
|
395
|
+
split off first and the label bolded inside the item: ``- **Note:** x``,
|
|
396
|
+
which is valid markdown and matches the benchmark's bold patterns.
|
|
397
|
+
2. **Fence interiors are never touched.** The skip test used to look at each
|
|
398
|
+
line in isolation, so it skipped the fence marker itself but bolded
|
|
399
|
+
label-shaped lines INSIDE code/LaTeX fences. Fence state is now tracked
|
|
400
|
+
across lines with the same marker pattern as the vendored ``_FENCE_RE``.
|
|
401
|
+
"""
|
|
402
|
+
tbl = _table_line_mask(md)
|
|
403
|
+
out: List[str] = []
|
|
404
|
+
in_fence = False
|
|
405
|
+
for i, line in enumerate(md.split("\n")):
|
|
406
|
+
if _FENCE_LINE.match(line):
|
|
407
|
+
in_fence = not in_fence
|
|
408
|
+
out.append(line)
|
|
409
|
+
continue
|
|
410
|
+
if in_fence or tbl[i] or _HAS_BOLD.search(line):
|
|
411
|
+
out.append(line)
|
|
412
|
+
continue
|
|
413
|
+
prefix = ""
|
|
414
|
+
body = line
|
|
415
|
+
lp = _LIST_PREFIX.match(line)
|
|
416
|
+
if lp:
|
|
417
|
+
prefix = lp.group(1)
|
|
418
|
+
body = line[lp.end() :]
|
|
419
|
+
if _skippable(body):
|
|
420
|
+
out.append(line)
|
|
421
|
+
continue
|
|
422
|
+
m = _LABEL_RE.match(body)
|
|
423
|
+
if m and len(m.group(1).split()) <= 6:
|
|
424
|
+
body = f"**{m.group(1)}**{m.group(2)}{body[m.end():]}"
|
|
425
|
+
out.append(prefix + body)
|
|
426
|
+
return "\n".join(out)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# =============================================================================
|
|
430
|
+
# document-level post-processing
|
|
431
|
+
# =============================================================================
|
|
432
|
+
def _postprocess_markdown(md: str, *, title_variant: str = "aggressive") -> str:
|
|
433
|
+
"""Port of ``K.postprocess_markdown``: ``header_mark`` -> ``quote_fold`` ->
|
|
434
|
+
``title_promote``, each wrapped so one failing rule never discards the
|
|
435
|
+
document, and all rules skipped on a runaway document.
|
|
436
|
+
|
|
437
|
+
Two seams: ``title_promote`` uses the relaxed gate (change (d)), and change
|
|
438
|
+
(c) is appended as a final rule. (c) runs last so it sees the promoted
|
|
439
|
+
headings and leaves them alone.
|
|
440
|
+
"""
|
|
441
|
+
if not md or K._looks_runaway(md):
|
|
442
|
+
return md
|
|
443
|
+
try:
|
|
444
|
+
md = K.header_mark(md)
|
|
445
|
+
except Exception: # noqa: BLE001 — never fail a document on a post-processing rule
|
|
446
|
+
pass
|
|
447
|
+
try:
|
|
448
|
+
md = K.quote_fold(md)
|
|
449
|
+
md = _title_promote(md, variant=title_variant)
|
|
450
|
+
except Exception: # noqa: BLE001
|
|
451
|
+
pass
|
|
452
|
+
try:
|
|
453
|
+
md = _bold_run_in_labels(md)
|
|
454
|
+
except Exception: # noqa: BLE001
|
|
455
|
+
pass
|
|
456
|
+
return md
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def build_markdown(
|
|
460
|
+
elements: List[Dict[str, Any]],
|
|
461
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
462
|
+
"""Finished element list -> (whole-document markdown, per-page records)."""
|
|
463
|
+
full_md, markdown_pages = _assemble_markdown(elements)
|
|
464
|
+
full_md = _postprocess_markdown(full_md)
|
|
465
|
+
for page in markdown_pages:
|
|
466
|
+
page["content"] = _postprocess_markdown(page["content"])
|
|
467
|
+
return full_md, markdown_pages
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# =============================================================================
|
|
471
|
+
# engine + provider
|
|
472
|
+
# =============================================================================
|
|
473
|
+
class _FlorinNanoEngine(K._NanoEngine):
|
|
474
|
+
"""The inherited per-document engine with this module's markdown emission.
|
|
475
|
+
|
|
476
|
+
``_parse_page`` is overridden only to keep a reference to the element
|
|
477
|
+
dictionaries the inherited engine produces; the inherited implementation
|
|
478
|
+
still does all the work. After ``parse_pages`` returns, those same
|
|
479
|
+
dictionaries have been through the inherited post-processing in place, so
|
|
480
|
+
they are the finished elements — including the ``picture_path`` field, which
|
|
481
|
+
the ``pages`` payload drops and which the image markdown needs.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
485
|
+
super().__init__(*args, **kwargs)
|
|
486
|
+
self._captured: List[Dict[str, Any]] = []
|
|
487
|
+
|
|
488
|
+
async def _parse_page( # type: ignore[override]
|
|
489
|
+
self,
|
|
490
|
+
client: Any,
|
|
491
|
+
semaphore: asyncio.Semaphore,
|
|
492
|
+
image: Image.Image,
|
|
493
|
+
page_no: int,
|
|
494
|
+
) -> List[Dict[str, Any]]:
|
|
495
|
+
elements = await super()._parse_page(client, semaphore, image, page_no)
|
|
496
|
+
self._captured.extend(elements)
|
|
497
|
+
return elements
|
|
498
|
+
|
|
499
|
+
async def parse_pages(self, page_images: List[Image.Image]) -> dict:
|
|
500
|
+
self._captured = []
|
|
501
|
+
raw = await super().parse_pages(page_images)
|
|
502
|
+
full_md, markdown_pages = build_markdown(self._captured)
|
|
503
|
+
raw.update({"markdown": full_md, "markdown_pages": markdown_pages})
|
|
504
|
+
return raw
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
_BINDING_LOCK = threading.Lock()
|
|
508
|
+
_BINDING_DEPTH = 0
|
|
509
|
+
_SAVED_BINDINGS: tuple | None = None
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@contextlib.contextmanager
|
|
513
|
+
def _patched_bindings() -> Iterator[None]:
|
|
514
|
+
"""Scoped, re-entrant rebinding of the two module globals described in the
|
|
515
|
+
module docstring. The first entering document installs the bindings; the
|
|
516
|
+
last exiting document restores the originals, including on exception."""
|
|
517
|
+
global _BINDING_DEPTH, _SAVED_BINDINGS
|
|
518
|
+
with _BINDING_LOCK:
|
|
519
|
+
if _BINDING_DEPTH == 0:
|
|
520
|
+
_SAVED_BINDINGS = (K.NATIVE_LAYOUT_CATEGORY_MAP, K._NanoEngine)
|
|
521
|
+
K.NATIVE_LAYOUT_CATEGORY_MAP = FLORIN_NATIVE_LAYOUT_CATEGORY_MAP
|
|
522
|
+
K._NanoEngine = _FlorinNanoEngine # type: ignore[misc]
|
|
523
|
+
_BINDING_DEPTH += 1
|
|
524
|
+
try:
|
|
525
|
+
yield
|
|
526
|
+
finally:
|
|
527
|
+
with _BINDING_LOCK:
|
|
528
|
+
_BINDING_DEPTH -= 1
|
|
529
|
+
if _BINDING_DEPTH == 0 and _SAVED_BINDINGS is not None:
|
|
530
|
+
K.NATIVE_LAYOUT_CATEGORY_MAP, K._NanoEngine = _SAVED_BINDINGS # type: ignore[misc]
|
|
531
|
+
_SAVED_BINDINGS = None
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@register_provider("florin_parser_nano")
|
|
535
|
+
class FlorinParserNanoProvider(K.KdlFrontierNanoProvider):
|
|
536
|
+
"""florin-inc/florin-parser-nano: the ``kdl_frontier_nano`` provider with the
|
|
537
|
+
fine-tuned weights and this module's markdown emission. Serving requirements
|
|
538
|
+
and every inference stage are inherited unchanged."""
|
|
539
|
+
|
|
540
|
+
def __init__(self, provider_name: str, base_config: dict[str, Any] | None = None):
|
|
541
|
+
cfg = dict(base_config or {})
|
|
542
|
+
cfg["endpoint_url"] = (
|
|
543
|
+
cfg.get("endpoint_url") or os.getenv("FLORIN_NANO_ENDPOINT_URL") or ""
|
|
544
|
+
)
|
|
545
|
+
if not cfg["endpoint_url"]:
|
|
546
|
+
raise ProviderConfigError(
|
|
547
|
+
"FLORIN_NANO_ENDPOINT_URL is required (vLLM OpenAI-compatible base "
|
|
548
|
+
"URL ending in /v1, serving florin-inc/florin-parser-nano)."
|
|
549
|
+
)
|
|
550
|
+
cfg["model"] = (
|
|
551
|
+
cfg.get("model") or os.getenv("FLORIN_NANO_MODEL") or "florin-parser-nano"
|
|
552
|
+
)
|
|
553
|
+
super().__init__(provider_name, cfg)
|
|
554
|
+
|
|
555
|
+
def run_inference(
|
|
556
|
+
self, pipeline: PipelineSpec, request: InferenceRequest
|
|
557
|
+
) -> RawInferenceResult:
|
|
558
|
+
with _patched_bindings():
|
|
559
|
+
return super().run_inference(pipeline, request)
|