fix: make non-body families authoritative against body flow

This commit is contained in:
Research Assistant 2026-06-10 12:56:14 +08:00
parent b9b661dbad
commit f35470fb70
3 changed files with 104 additions and 2 deletions

View file

@ -515,6 +515,42 @@ def resolve_final_role(
],
)
if current_role == "body_paragraph" and style_family:
_NON_BODY_FAMILIES = {"reference_like", "table_caption_like", "legend_like", "heading_like", "support_like"}
_STRONG_AUTHORITIES = {
"figure_marker",
"figure_family_anchor",
"reference_marker",
"reference_family_anchor",
"table_marker",
"heading_marker",
"body_family_anchor",
}
if style_family in _NON_BODY_FAMILIES and style_family_authority in _STRONG_AUTHORITIES:
if (
style_family == "legend_like"
and _looks_like_late_figure_narrative_prose(str(block.get("text") or ""))
):
pass # narrative prose stays body_paragraph
else:
_FAMILY_ROLE_MAP = {
"reference_like": "reference_item",
"table_caption_like": "table_caption_candidate",
"legend_like": "figure_caption_candidate",
"heading_like": "section_heading",
}
mapped_role = _FAMILY_ROLE_MAP.get(style_family)
if mapped_role:
return RoleAssignment(
role=mapped_role,
confidence=max(current_confidence, 0.78),
evidence=[
f"late role resolution: non-body family '{style_family}' overrides body_paragraph",
f"style_family_authority={style_family_authority or 'none'}",
f"context_source={context_source}",
],
)
return RoleAssignment(
role=current_role,
confidence=current_confidence,

View file

@ -248,8 +248,9 @@ def test_normalize_document_structure_keeps_reference_family_anchor_anchor_first
assert doc.reference_family_anchor is not None
assert doc.reference_family_anchor["status"] == "ACCEPT"
assert normalized_blocks[3]["role"] == "body_paragraph"
assert normalized_blocks[4]["role"] == "body_paragraph"
# Non-body family override: reference_like + reference_family_anchor → reference_item
assert normalized_blocks[3]["role"] == "reference_item"
assert normalized_blocks[4]["role"] == "reference_item"
def test_normalize_document_structure_wires_style_family_artifacts_into_blocks() -> None:

View file

@ -1353,3 +1353,68 @@ def test_pipeline_does_not_commit_final_semantic_role_before_zone_and_family_par
for row in rows:
assert "seed_role" in row, f"block {row['block_id']} missing seed_role"
assert row["seed_role"] != "unassigned", f"seed_role should never be 'unassigned', got {row['seed_role']}"
def test_body_paragraph_overridden_to_reference_item_when_style_family_is_reference_like():
from paperforge.worker.ocr_roles import resolve_final_role
block = {
"role": "body_paragraph",
"role_confidence": 0.6,
"style_family": "reference_like",
"style_family_authority": "reference_marker",
"zone": "body_zone",
"marker_signature": {"type": "reference_pattern"},
}
result = resolve_final_role(block)
assert result.role == "reference_item"
assert result.confidence >= 0.78
def test_body_paragraph_overridden_to_figure_caption_when_style_family_is_legend_like():
from paperforge.worker.ocr_roles import resolve_final_role
block = {
"role": "body_paragraph",
"role_confidence": 0.6,
"style_family": "legend_like",
"style_family_authority": "figure_marker",
"zone": "body_zone",
"marker_signature": {"type": "figure_number"},
"text": "Figure 3.",
}
result = resolve_final_role(block)
assert result.role == "figure_caption_candidate"
assert result.confidence >= 0.78
def test_body_paragraph_overridden_to_table_caption_when_style_family_is_table_caption_like():
from paperforge.worker.ocr_roles import resolve_final_role
block = {
"role": "body_paragraph",
"role_confidence": 0.6,
"style_family": "table_caption_like",
"style_family_authority": "table_marker",
"zone": "body_zone",
"marker_signature": {"type": "table_number"},
"text": "Table 2.",
}
result = resolve_final_role(block)
assert result.role == "table_caption_candidate"
assert result.confidence >= 0.78
def test_body_paragraph_survives_when_style_family_authority_is_weak():
from paperforge.worker.ocr_roles import resolve_final_role
block = {
"role": "body_paragraph",
"role_confidence": 0.6,
"style_family": "reference_like",
"style_family_authority": "fallback",
"zone": "body_zone",
"marker_signature": {"type": "none"},
}
result = resolve_final_role(block)
assert result.role == "body_paragraph"