fix: gate weak boundary signals through backmatter_boundary_candidate

This commit is contained in:
Research Assistant 2026-06-07 00:59:22 +08:00
parent bf8c1be53a
commit 3e748dcb5d
4 changed files with 70 additions and 28 deletions

View file

@ -9,7 +9,6 @@ from paperforge.worker.ocr_roles import assign_block_role
_CANDIDATE_ROLES = frozenset({
"figure_caption_candidate",
"backmatter_heading_candidate",
"backmatter_boundary_candidate",
})

View file

@ -1704,27 +1704,7 @@ def _resolve_ambiguous_candidates(
block["role"] = "backmatter_heading"
# ---- 2.2 Resolve backmatter_boundary_candidate ----
if role == "backmatter_boundary_candidate":
is_in_tail = (body_end_page is not None and page >= body_end_page) or (
doc_structure.spread_start is not None and page >= doc_structure.spread_start
)
if not is_in_tail:
block["role"] = "section_heading"
continue
layout = page_layouts.get(page)
boundaries = layout.column_boundaries if layout else []
x_center = (bbox[0] + bbox[2]) / 2
col = _get_column_index_by_boundaries(x_center, boundaries)
child_count = _child_heading_count(i, page, col, boundaries)
if child_count >= 2.0:
block["role"] = "backmatter_boundary_heading"
else:
block["role"] = "section_heading"
# ---- 2.3 Resolve figure_caption_candidate ----
# ---- 2.2 Resolve figure_caption_candidate ----
if role == "figure_caption_candidate":
text = block.get("text", "") or ""
page_blocks = [b for b in blocks if b.get("page") == page]

View file

@ -459,10 +459,28 @@ def assign_block_role(
_page_num = block.get("page", 1) or 1
_total_pages = max((b.get("page", 1) or 1) for b in page_blocks) if page_blocks else _page_num
if _is_backmatter_boundary_heading(block, _page_num, _total_pages):
role_name = "backmatter_boundary_heading"
confidence = 0.7
lower_txt = text.lower()
has_container_words = any(
w in lower_txt
for w in ["additional information", "declaration", "supplementary"]
)
span_meta = block.get("span_metadata", {}) or {}
is_bold = False
if isinstance(span_meta, dict):
is_bold = "bold" in (span_meta.get("flags", "") or "").lower()
elif isinstance(span_meta, list):
is_bold = any(s.get("flags", 0) & 16 for s in span_meta)
if not (has_container_words and is_bold):
role_name = "backmatter_boundary_candidate"
confidence = 0.5
return RoleAssignment(
role="backmatter_boundary_heading",
confidence=0.7,
evidence=[f"backmatter boundary heading: {text[:60]}"],
role=role_name,
confidence=confidence,
evidence=[
f"backmatter {'boundary heading' if role_name == 'backmatter_boundary_heading' else 'boundary candidate'}: {text[:60]}"
],
)
if _has_heading_numbering(text):
depth = _heading_number_depth(text)
@ -732,10 +750,28 @@ def assign_block_role(
_pn2 = block.get("page", 1) or 1
_tp2 = max((b.get("page", 1) or 1) for b in page_blocks) if page_blocks else _pn2
if _is_backmatter_boundary_heading(block, _pn2, _tp2):
role_name = "backmatter_boundary_heading"
confidence = 0.7
lower_txt = text.lower()
has_container_words = any(
w in lower_txt
for w in ["additional information", "declaration", "supplementary"]
)
span_meta = block.get("span_metadata", {}) or {}
is_bold = False
if isinstance(span_meta, dict):
is_bold = "bold" in (span_meta.get("flags", "") or "").lower()
elif isinstance(span_meta, list):
is_bold = any(s.get("flags", 0) & 16 for s in span_meta)
if not (has_container_words and is_bold):
role_name = "backmatter_boundary_candidate"
confidence = 0.5
return RoleAssignment(
role="backmatter_boundary_heading",
confidence=0.7,
evidence=[f"backmatter boundary heading from text block: {text[:60]}"],
role=role_name,
confidence=confidence,
evidence=[
f"backmatter {'boundary heading' if role_name == 'backmatter_boundary_heading' else 'boundary candidate'} from text block: {text[:60]}"
],
)
if len(text) < 20:

View file

@ -463,6 +463,33 @@ def test_formal_figure_caption_still_direct() -> None:
)
def test_weak_backmatter_boundary_signal_emits_candidate():
"""A paragraph with backmatter boundary text but no bold span_metadata
should emit backmatter_boundary_candidate, not backmatter_boundary_heading."""
from paperforge.worker.ocr_roles import assign_block_role
block = {
"block_label": "paragraph_title",
"block_content": "ADDITIONAL INFORMATION",
"block_bbox": [100, 1200, 500, 1240],
"page": 8,
}
page_blocks = [block] + [
{
"block_label": "text",
"block_content": f"Some body text {i}",
"block_bbox": [100, 200 + i * 100, 500, 260 + i * 100],
"page": 8,
}
for i in range(5)
]
result = assign_block_role(
block, page_blocks=page_blocks, page_width=600, page_height=1600
)
assert result.role == "backmatter_boundary_candidate", (
f"Expected backmatter_boundary_candidate, got {result.role}"
)
def test_backmatter_boundary_detects_on_early_page() -> None:
"""Backmatter boundary should be detectable on papers with fewer
than 8 pages, without a hard page gate."""