feat: add tail boundary score to DocumentStructure and health

This commit is contained in:
Research Assistant 2026-06-08 12:27:12 +08:00
parent 46937f2a0a
commit c8b4846998
4 changed files with 61 additions and 0 deletions

View file

@ -83,6 +83,7 @@ class DocumentStructure:
reference_zones: list[dict] | None = None
span_coverage: dict | None = None
layout_audit: dict | None = None
tail_boundary_score: dict = field(default_factory=dict)
@dataclass
@ -2794,6 +2795,15 @@ def normalize_document_structure(blocks: list[dict]) -> tuple[DocumentStructure,
layout_audit = _run_layout_audit(blocks)
doc_structure.layout_audit = layout_audit
# Compute tail boundary confidence score
from paperforge.worker.ocr_scores import score_tail_boundary
doc_structure.tail_boundary_score = score_tail_boundary(
forward_body_end=doc_structure.body_end_page,
backward_backmatter_start=doc_structure.backmatter_start.page if doc_structure.backmatter_start else None,
references_start={"page": doc_structure.references_start.page} if doc_structure.references_start else None,
)
# Rebuild tail reading order after role normalization and block reassignment.
# Without this, reading segment block_indices reference stale positions
# from before _promote_tail_body_candidates reassigned the blocks list.

View file

@ -68,6 +68,10 @@ def build_ocr_health(
spine = _detect_body_spine(structured_blocks, doc=doc_structure)
layout = _run_layout_audit(structured_blocks)
tail_score = {}
if doc_structure is not None and hasattr(doc_structure, "tail_boundary_score"):
tail_score = doc_structure.tail_boundary_score or {}
# Collect decision log summaries
from paperforge.worker.ocr_decisions import collect_decisions, summarize_decisions
@ -99,6 +103,7 @@ def build_ocr_health(
"layout_audit_status": layout.get("status", "unknown"),
"layout_anomaly_pages": layout.get("anomaly_pages", []),
"layout_anomaly_count": layout.get("anomaly_count", 0),
"tail_boundary_confidence": tail_score.get("score", 0.0),
}
report.update(decision_summary)
return report

View file

@ -1982,6 +1982,22 @@ def test_document_structure_json_serialization() -> None:
assert parsed["body_end_page"] == 70
assert parsed["page_layouts"]["71"]["column_count"] == 2
assert parsed["page_layouts"]["71"]["layout_type"] == "two_column"
assert "tail_boundary_score" in parsed
def test_document_structure_has_tail_boundary_score() -> None:
from paperforge.worker.ocr_document import normalize_document_structure
blocks = [
{"page": 1, "role": "body_paragraph", "text": "Body text.", "bbox": [100, 100, 500, 130]},
{"page": 2, "role": "section_heading", "text": "Discussion", "bbox": [100, 100, 500, 130]},
{"page": 2, "role": "body_paragraph", "text": "More body.", "bbox": [100, 140, 500, 170]},
{"page": 10, "role": "reference_heading", "text": "References", "bbox": [100, 100, 500, 130]},
{"page": 10, "role": "reference_item", "text": "1. Author. Title. Journal.", "bbox": [100, 140, 500, 170]},
]
doc, _ = normalize_document_structure(blocks)
assert doc.layout_audit is not None
assert doc.tail_boundary_score["score"] >= 0.0
def test_figure_caption_candidate_demoted_in_body() -> None:

View file

@ -152,3 +152,33 @@ def test_ocr_health_includes_decision_counts() -> None:
report = build_ocr_health(page_count=1, raw_blocks_count=1, structured_blocks=blocks, figure_inventory={}, table_inventory={})
assert report["role_mutation_count"] == 1
assert report["role_rescue_count"] == 1
def test_ocr_health_includes_tail_boundary_confidence() -> None:
from paperforge.worker.ocr_health import build_ocr_health
structured_blocks = [
{"role": "section_heading", "text": "1 Introduction"},
{"role": "body_paragraph", "text": "Body"},
{"role": "reference_heading", "text": "References"},
{"role": "reference_item", "text": "1. Author."},
]
figure_inventory = {
"matched_figures": [],
"unmatched_legends": [],
"unmatched_assets": [],
}
table_inventory = {
"tables": [],
"unmatched_captions": [],
"unmatched_assets": [],
}
report = build_ocr_health(
page_count=3,
raw_blocks_count=20,
structured_blocks=structured_blocks,
figure_inventory=figure_inventory,
table_inventory=table_inventory,
)
assert "tail_boundary_confidence" in report
assert isinstance(report["tail_boundary_confidence"], (int, float))