mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 17:00:23 +00:00
327 lines
14 KiB
Python
327 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
def test_required_seed_without_verifier_holds_not_accepts() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
block = {"block_id": "a1", "seed_role": "authors", "text": "Author One Author Two"}
|
|
|
|
decision = resolve_verified_role(block, RoleGateContext())
|
|
|
|
assert decision.role == "unknown_structural"
|
|
assert decision.status == "HOLD"
|
|
assert decision.seed_role == "authors"
|
|
assert decision.source == "structural_gate"
|
|
|
|
|
|
def test_non_required_seed_can_accept_as_non_structural() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
block = {"block_id": "b1", "seed_role": "body_paragraph", "text": "Regular body paragraph."}
|
|
|
|
decision = resolve_verified_role(block, RoleGateContext())
|
|
|
|
assert decision.role == "body_paragraph"
|
|
assert decision.status == "ACCEPT"
|
|
assert decision.source == "non_structural_seed"
|
|
|
|
|
|
def test_hold_preserves_candidate_and_hides_from_render_by_default() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
block = {"block_id": "r1", "seed_role": "reference_item", "text": "[1] Not actually in references."}
|
|
|
|
decision = resolve_verified_role(block, RoleGateContext())
|
|
fields = decision.as_block_fields()
|
|
|
|
assert fields["role"] == "unknown_structural"
|
|
assert fields["role_candidate"] == "reference_item"
|
|
assert fields["role_verification_status"] == "HOLD"
|
|
assert fields["render_default"] is False
|
|
|
|
|
|
def test_gate_preserves_pre_normalized_non_structural_role() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
block = {"block_id": "i1", "role": "structured_insert", "seed_role": "body_paragraph", "text": "Side note."}
|
|
|
|
decision = resolve_verified_role(block, RoleGateContext())
|
|
|
|
assert decision.role == "structured_insert"
|
|
assert decision.seed_role == "body_paragraph"
|
|
assert decision.source == "non_structural_normalized_role"
|
|
|
|
|
|
def test_gate_preserves_pre_normalized_safe_role_even_if_seed_is_required() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
block = {
|
|
"block_id": "x",
|
|
"role": "frontmatter_noise",
|
|
"seed_role": "authors",
|
|
"text": "Author-like sidebar",
|
|
}
|
|
|
|
decision = resolve_verified_role(block, RoleGateContext())
|
|
|
|
assert decision.role == "frontmatter_noise"
|
|
assert decision.source == "non_structural_normalized_role"
|
|
|
|
|
|
def test_document_abstract_span_includes_structured_subheadings_and_excludes_support() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_document_abstract_span
|
|
|
|
blocks = [
|
|
{"block_id": "h", "seed_role": "abstract_heading", "text": "Abstract"},
|
|
{"block_id": "q", "seed_role": "section_heading", "text": "Questions/purposes"},
|
|
{"block_id": "a1", "seed_role": "abstract_body", "text": "First abstract sentence."},
|
|
{"block_id": "authors", "seed_role": "authors", "text": "Author One Author Two"},
|
|
{"block_id": "m", "seed_role": "section_heading", "text": "Methods"},
|
|
{"block_id": "a2", "seed_role": "body_paragraph", "text": "Second abstract sentence."},
|
|
{"block_id": "intro", "seed_role": "section_heading", "text": "Introduction"},
|
|
{"block_id": "bad", "seed_role": "abstract_body", "text": "Mislabeled body."},
|
|
]
|
|
context = {
|
|
"body_start_block_id": "intro",
|
|
"frontmatter_main_zone_ids": {"h", "q", "a1", "m", "a2"},
|
|
"frontmatter_support_zone_ids": {"authors"},
|
|
"publisher_sidebar_zone_ids": set(),
|
|
"correspondence_zone_ids": set(),
|
|
"affiliation_zone_ids": set(),
|
|
}
|
|
|
|
span = build_document_abstract_span(blocks, context)
|
|
|
|
assert span["status"] == "ACCEPT"
|
|
assert span["heading_block_id"] == "h"
|
|
assert span["body_block_ids"] == ["q", "a1", "m", "a2"]
|
|
assert span["excluded_support_block_ids"] == ["authors"]
|
|
assert span["stop_reason"] == "body_start"
|
|
|
|
|
|
def test_abstract_span_stops_before_intro_even_when_later_block_has_abstract_seed() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_document_abstract_span
|
|
|
|
blocks = [
|
|
{"block_id": "h", "seed_role": "abstract_heading", "text": "Abstract"},
|
|
{"block_id": "a", "seed_role": "abstract_body", "text": "Actual abstract."},
|
|
{"block_id": "intro", "seed_role": "section_heading", "text": "Introduction"},
|
|
{"block_id": "bad", "seed_role": "abstract_body", "text": "Mislabeled body result."},
|
|
]
|
|
span = build_document_abstract_span(blocks, {"body_start_block_id": "intro"})
|
|
|
|
assert span["body_block_ids"] == ["a"]
|
|
assert span["stop_reason"] == "body_start"
|
|
|
|
|
|
def test_abstract_span_stops_at_intro_like_heading_without_body_start_id() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_document_abstract_span
|
|
|
|
blocks = [
|
|
{"block_id": "h", "seed_role": "abstract_heading", "text": "Abstract"},
|
|
{"block_id": "a", "seed_role": "abstract_body", "text": "Actual abstract."},
|
|
{"block_id": "intro", "seed_role": "section_heading", "text": "1. Introduction"},
|
|
{"block_id": "bad", "seed_role": "abstract_body", "text": "Mislabeled body result."},
|
|
]
|
|
span = build_document_abstract_span(blocks, {})
|
|
|
|
assert span["body_block_ids"] == ["a"]
|
|
assert span["stop_reason"] == "intro_like_heading"
|
|
|
|
|
|
def test_reference_zone_adapter_excludes_tail_after_boundary() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"block_id": "body", "seed_role": "reference_item", "text": "[1] body parameter"},
|
|
{"block_id": "refs", "seed_role": "reference_heading", "text": "References"},
|
|
{"block_id": "r1", "seed_role": "reference_item", "text": "[1] Smith J. 2020."},
|
|
{"block_id": "bio", "seed_role": "section_heading", "text": "Biography"},
|
|
{"block_id": "bad", "seed_role": "reference_item", "text": "Biography text mislabeled."},
|
|
]
|
|
artifacts = {
|
|
"reference_family_anchor": {"heading_block_id": "refs", "item_block_ids": ["r1", "bad"]},
|
|
"region_bus": {"reference_zone_ids": {"refs", "r1"}},
|
|
"tail_spread": {"reference_end_before_block_id": "bio"},
|
|
"reference_numbering_family": {"accepted_item_ids": {"r1"}},
|
|
}
|
|
|
|
zone = build_verified_reference_zone_from_artifacts(blocks, artifacts)
|
|
|
|
assert zone["status"] == "ACCEPT"
|
|
assert zone["heading_block_id"] == "refs"
|
|
assert zone["item_block_ids"] == ["r1"]
|
|
|
|
|
|
def test_reference_zone_adapter_keeps_multicolumn_reference_items_inside_region() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"block_id": "refs", "seed_role": "reference_heading", "text": "References", "column": 1},
|
|
{"block_id": "r1", "seed_role": "reference_item", "text": "[1] Left column reference.", "column": 1},
|
|
{"block_id": "r2", "seed_role": "reference_item", "text": "[2] Right column reference.", "column": 2},
|
|
{"block_id": "appendix", "seed_role": "section_heading", "text": "Appendix", "column": 1},
|
|
]
|
|
artifacts = {
|
|
"reference_family_anchor": {"heading_block_id": "refs", "item_block_ids": ["r1", "r2"]},
|
|
"region_bus": {"reference_zone_ids": {"refs", "r1", "r2"}},
|
|
"tail_spread": {"reference_end_before_block_id": "appendix"},
|
|
"reference_numbering_family": {"accepted_item_ids": {"r1", "r2"}},
|
|
}
|
|
|
|
zone = build_verified_reference_zone_from_artifacts(blocks, artifacts)
|
|
|
|
assert zone["status"] == "ACCEPT"
|
|
assert zone["item_block_ids"] == ["r1", "r2"]
|
|
|
|
|
|
def test_reference_zone_adapter_accepts_without_tail_end_boundary_when_region_is_present() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"block_id": "refs", "seed_role": "reference_heading", "text": "References"},
|
|
{"block_id": "r1", "seed_role": "reference_item", "text": "[1] Smith J. 2020."},
|
|
]
|
|
artifacts = {
|
|
"reference_family_anchor": {"heading_block_id": "refs", "item_block_ids": ["r1"]},
|
|
"region_bus": {"reference_zone_ids": {"refs", "r1"}},
|
|
"tail_spread": {},
|
|
}
|
|
|
|
zone = build_verified_reference_zone_from_artifacts(blocks, artifacts)
|
|
|
|
assert zone["status"] == "ACCEPT"
|
|
assert zone["item_block_ids"] == ["r1"]
|
|
|
|
|
|
def test_reference_zone_accepts_heading_when_region_ids_are_plain_block_ids() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"page": 13, "block_id": 10, "text": "References", "seed_role": "reference_heading"},
|
|
{"page": 13, "block_id": 11, "text": "[1] Ref item", "seed_role": "reference_item"},
|
|
]
|
|
zone = build_verified_reference_zone_from_artifacts(
|
|
blocks,
|
|
{
|
|
"reference_family_anchor": {},
|
|
"region_bus": {"reference_zone_ids": {10, 11}},
|
|
"tail_spread": {},
|
|
},
|
|
)
|
|
|
|
assert zone["status"] == "ACCEPT"
|
|
assert zone["heading_block_id"] == 10
|
|
assert zone["item_block_ids"] == [11]
|
|
|
|
|
|
def test_ordered_reference_anchors_do_not_swallow_non_ref_blocks_between_anchors() -> None:
|
|
from paperforge.worker.ocr_structural_gate import _build_ordered_reference_items
|
|
|
|
blocks = [
|
|
{"page": 14, "block_id": 11, "seed_role": "reference_item", "bbox": [620, 500, 1100, 520], "text": "Smith J. 2020."},
|
|
{"page": 14, "block_id": 12, "seed_role": "body_paragraph", "bbox": [620, 530, 1100, 600], "text": "A long body paragraph that does not belong in references."},
|
|
{"page": 14, "block_id": 13, "seed_role": "reference_item", "bbox": [620, 610, 1100, 630], "text": "Brown T. 2021."},
|
|
]
|
|
anchor_ids = {11, 13}
|
|
result = _build_ordered_reference_items(blocks, anchor_ids, set())
|
|
assert 11 in result
|
|
assert 13 in result
|
|
assert 12 not in result
|
|
|
|
|
|
def test_ordered_reference_anchors_fill_reference_like_gap_but_not_intrusion() -> None:
|
|
from paperforge.worker.ocr_structural_gate import _build_ordered_reference_items
|
|
|
|
blocks = [
|
|
{"page": 20, "block_id": 1, "seed_role": "reference_item", "bbox": [80, 500, 520, 520], "text": "Ref [1]."},
|
|
{"page": 20, "block_id": 2, "seed_role": "reference_item", "bbox": [80, 525, 520, 545], "text": "Continuation line of ref [1]."},
|
|
{"page": 20, "block_id": 3, "seed_role": "reference_item", "bbox": [80, 550, 520, 570], "text": "Ref [2]."},
|
|
]
|
|
anchor_ids = {1, 3}
|
|
result = _build_ordered_reference_items(blocks, anchor_ids, set())
|
|
assert 1 in result
|
|
assert 3 in result
|
|
assert 2 in result
|
|
|
|
|
|
def test_ordered_reference_anchors_stop_at_section_heading() -> None:
|
|
from paperforge.worker.ocr_structural_gate import _build_ordered_reference_items
|
|
|
|
blocks = [
|
|
{"page": 14, "block_id": 11, "seed_role": "reference_item", "bbox": [620, 500, 1100, 520], "text": "Smith J. 2020."},
|
|
{"page": 14, "block_id": 12, "seed_role": "section_heading", "bbox": [620, 530, 1100, 550], "text": "Data Availability"},
|
|
{"page": 14, "block_id": 13, "seed_role": "reference_item", "bbox": [620, 560, 1100, 580], "text": "Brown T. 2021."},
|
|
]
|
|
anchor_ids = {11, 13}
|
|
result = _build_ordered_reference_items(blocks, anchor_ids, set())
|
|
assert 11 in result
|
|
assert 13 in result
|
|
assert 12 not in result
|
|
|
|
|
|
def test_ordered_reference_anchors_rejects_acknowledgement_swallowed_as_gap() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"page": 14, "block_id": "refs", "seed_role": "reference_heading", "text": "References"},
|
|
{"page": 14, "block_id": "r1", "seed_role": "reference_item", "bbox": [80, 120, 520, 140], "text": "[1] Smith J. 2020."},
|
|
{"page": 14, "block_id": "ack", "seed_role": "backmatter_body", "bbox": [80, 160, 520, 200], "text": "Acknowledgements We thank the funding agency."},
|
|
{"page": 14, "block_id": "r2", "seed_role": "reference_item", "bbox": [80, 220, 520, 240], "text": "[2] Brown T. 2021."},
|
|
]
|
|
zone = build_verified_reference_zone_from_artifacts(
|
|
blocks,
|
|
{
|
|
"reference_family_anchor": {"heading_block_id": "refs", "item_block_ids": ["r1", "r2"]},
|
|
"region_bus": {"reference_zone_ids": {"refs", "r1", "r2"}},
|
|
"tail_spread": {},
|
|
},
|
|
)
|
|
assert zone["status"] == "ACCEPT"
|
|
assert "r1" in zone["item_block_ids"]
|
|
assert "r2" in zone["item_block_ids"]
|
|
assert "ack" not in zone["item_block_ids"]
|
|
|
|
|
|
def test_abstract_body_without_span_is_not_verified_accept() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
decision = resolve_verified_role(
|
|
{"block_id": "a", "role": "unassigned", "seed_role": "abstract_body", "text": "Abstract sentence."},
|
|
RoleGateContext(abstract_span={"status": "MISSING"}),
|
|
)
|
|
|
|
assert decision.status == "ACCEPT"
|
|
assert decision.role == "abstract_body"
|
|
|
|
|
|
def test_section_heading_without_heading_artifact_is_not_verified_accept() -> None:
|
|
from paperforge.worker.ocr_structural_gate import RoleGateContext, resolve_verified_role
|
|
|
|
decision = resolve_verified_role(
|
|
{"block_id": "h1", "role": "unassigned", "seed_role": "section_heading", "text": "Methods"},
|
|
RoleGateContext(accepted_heading_block_ids=set()),
|
|
)
|
|
|
|
assert decision.source != "section_heading_fallback"
|
|
|
|
|
|
def test_reference_zone_accepts_heading_when_region_ids_are_page_prefixed() -> None:
|
|
from paperforge.worker.ocr_structural_gate import build_verified_reference_zone_from_artifacts
|
|
|
|
blocks = [
|
|
{"page": 13, "block_id": 10, "text": "References", "seed_role": "reference_heading"},
|
|
{"page": 13, "block_id": 11, "text": "[1] Ref item", "seed_role": "reference_item"},
|
|
]
|
|
zone = build_verified_reference_zone_from_artifacts(
|
|
blocks,
|
|
{
|
|
"reference_family_anchor": {},
|
|
"region_bus": {"reference_zone_ids": {"p13:10", "p13:11"}},
|
|
"tail_spread": {},
|
|
},
|
|
)
|
|
|
|
assert zone["status"] == "ACCEPT"
|
|
assert zone["heading_block_id"] == 10
|
|
assert zone["item_block_ids"] == [11]
|