lllin000_PaperForge/tests/test_ocr_profiles.py
2026-06-09 17:49:52 +08:00

240 lines
9.6 KiB
Python

"""Tests for ocr_profiles.py profile infrastructure."""
from __future__ import annotations
def test_extract_block_span_profile_list_format() -> None:
from paperforge.worker.ocr_profiles import extract_block_span_profile
block = {
"span_metadata": [
{"size": 14.0, "font": "Times-Bold", "flags": 16, "color": 0},
{"size": 14.0, "font": "Times-Bold", "flags": 16, "color": 0},
]
}
profile = extract_block_span_profile(block)
assert profile is not None
assert profile["mean_size"] == 14.0
assert profile["max_size"] == 14.0
assert profile["is_bold"] is True
assert profile["is_italic"] is False
assert profile["is_colored"] is False
def test_extract_block_span_profile_dict_format() -> None:
from paperforge.worker.ocr_profiles import extract_block_span_profile
block = {
"span_metadata": {"size": 12.0, "flags": "bold"}
}
profile = extract_block_span_profile(block)
assert profile is not None
assert profile["mean_size"] == 12.0
assert profile["is_bold"] is True
def test_extract_block_span_profile_no_data() -> None:
from paperforge.worker.ocr_profiles import extract_block_span_profile
block = {}
profile = extract_block_span_profile(block)
assert profile is None
def test_extract_block_span_profile_empty_list() -> None:
from paperforge.worker.ocr_profiles import extract_block_span_profile
block = {"span_metadata": []}
profile = extract_block_span_profile(block)
assert profile is None
def test_build_role_span_profiles_aggregates_by_role() -> None:
from paperforge.worker.ocr_profiles import build_role_span_profiles
blocks = [
{"role": "section_heading", "span_metadata": {"size": 16.0, "flags": "bold"}},
{"role": "section_heading", "span_metadata": {"size": 15.5, "flags": "bold"}},
{"role": "body_paragraph", "span_metadata": {"size": 10.0, "flags": "normal"}},
{"role": "body_paragraph", "span_metadata": {"size": 10.5, "flags": "normal"}},
]
profiles = build_role_span_profiles(blocks)
assert "section_heading" in profiles
assert "body_paragraph" in profiles
assert profiles["section_heading"]["block_count"] == 2
assert profiles["body_paragraph"]["mean_size"] == 10.25
def test_build_role_span_profiles_profile_quality() -> None:
from paperforge.worker.ocr_profiles import build_role_span_profiles
blocks = [
{"role": "body_paragraph", "span_metadata": {"size": 10.0, "flags": 0}}
]
profiles = build_role_span_profiles(blocks)
assert profiles["body_paragraph"]["quality"] == "no_data"
def test_build_role_span_profiles_empty_input() -> None:
from paperforge.worker.ocr_profiles import build_role_span_profiles
assert build_role_span_profiles([]) == {}
def test_cross_validate_with_span_no_profile() -> None:
from paperforge.worker.ocr_profiles import cross_validate_with_span
block = {"span_metadata": {"size": 10.0, "flags": 0}}
result = cross_validate_with_span(block, "body_paragraph", {})
assert result["role"] == "body_paragraph"
assert result["adjustment"] == 0.0
def test_cross_validate_with_span_mismatch() -> None:
from paperforge.worker.ocr_profiles import cross_validate_with_span
block = {"span_metadata": {"size": 16.0, "flags": "bold"}}
profiles = {
"body_paragraph": {
"block_count": 5, "mean_size": 10.0, "max_size": 10.5, "min_size": 9.5,
"dispersion": 0.05, "quality": "strong", "bold_ratio": 0.0,
"italic_ratio": 0.0, "font_families": ["TimesNewRomanPSMT"],
},
}
result = cross_validate_with_span(block, "body_paragraph", profiles)
assert result["role"] == "body_paragraph"
assert result["adjustment"] < 0
def test_cross_validate_with_span_match() -> None:
from paperforge.worker.ocr_profiles import cross_validate_with_span
block = {"span_metadata": {"size": 10.0, "flags": 0}}
profiles = {
"body_paragraph": {
"block_count": 5, "mean_size": 10.0, "max_size": 10.5, "min_size": 9.5,
"dispersion": 0.05, "quality": "strong", "bold_ratio": 0.0,
"italic_ratio": 0.0, "font_families": ["TimesNewRomanPSMT"],
},
}
result = cross_validate_with_span(block, "body_paragraph", profiles)
assert result["role"] == "body_paragraph"
assert result["adjustment"] > 0
def test_body_family_from_central_pages() -> None:
"""Body family profile should be built from anchor (central) pages,
not from contaminated frontmatter or tail pages.
Creates blocks where contaminated pages have different font profiles
than clean central pages. Verifies that build_family_profiles
correctly builds a body_family matching central pages."""
from paperforge.worker.ocr_profiles import (
build_family_profiles,
extract_block_span_profile,
compare_against_family,
)
blocks = []
# Page 1: contaminated — body blocks with different font (TimesNewRoman, 8pt)
for i in range(3):
blocks.append({
"page": 1, "role": "body_paragraph",
"span_metadata": {"size": 8, "font": "TimesNewRoman", "flags": "italic"},
})
# Central pages 4-8: clean body paragraphs (Helvetica, 10pt)
for pg in range(4, 9):
for i in range(3):
blocks.append({
"page": pg, "role": "body_paragraph",
"span_metadata": {"size": 10, "font": "Helvetica", "flags": "normal"},
})
# Tail: backmatter body with different font
blocks.append({
"page": 10, "role": "backmatter_body",
"span_metadata": {"size": 9, "font": "Arial", "flags": "normal"},
})
families = build_family_profiles(blocks)
assert "body_family" in families, f"body_family missing from {list(families.keys())}"
bf = families["body_family"]
# The body_family should be dominated by central pages (Helvetica, 10pt)
# Since all body_paragraph blocks from ALL pages are pooled, contaminated
# blocks (3 at 8pt italic) skew the result away from the true central profile.
# This test asserts that the central profile match is stronger — if the
# family is contaminated, this assertion may fail.
central_profile = {"mean_size": 10.0, "max_size": 10.0, "font_families": {"Helvetica"},
"is_bold": False, "is_italic": False, "is_colored": False}
contaminated_profile = {"mean_size": 8.0, "max_size": 8.0, "font_families": {"TimesNewRoman"},
"is_bold": False, "is_italic": True, "is_colored": False}
central_match = compare_against_family(central_profile, bf)
contaminated_match = compare_against_family(contaminated_profile, bf)
# If body_family were built from central pages only, central match would
# be better. Currently all blocks are pooled, so contaminated blocks
# (3 of 18 total) may reduce match quality but central should still
# dominate by count. The key insight: if contaminated count ≥ 1/3 of
# central, the pooled profile would be skewed.
contaminated_ratio = 3 / 18 # 3 contaminated body + 15 central
assert contaminated_ratio < 0.3, (
"Test setup: contaminated blocks should not dominate central by count"
)
# Even with count arithmetic, contaminated font (italic) distorts bold_ratio
# and font_families. With 3 italic out of 18 total, bold_ratio = 0.17,
# which makes the family partially "not compatible" with non-italic blocks.
assert bf["italic_ratio"] < 0.3, (
f"Body family italic_ratio should be low (central is normal), got {bf['italic_ratio']}"
)
# Verify that a pure central block matches better than a pure contaminated block.
# This is the core assertion: central-dominant profile → central block matches well.
assert central_match["match_score"] >= 0.5, (
f"Central block should match body_family (score={central_match['match_score']})"
)
def test_cross_validate_with_span_suggests_alternative() -> None:
from paperforge.worker.ocr_profiles import cross_validate_with_span
block = {"span_metadata": {"size": 16.0, "flags": "bold"}}
profiles = {
"body_paragraph": {
"block_count": 5, "mean_size": 10.0, "max_size": 10.5, "min_size": 9.5,
"dispersion": 0.05, "quality": "strong", "bold_ratio": 0.0,
"italic_ratio": 0.0, "font_families": ["TimesNewRomanPSMT"],
},
"section_heading": {
"block_count": 3, "mean_size": 16.0, "max_size": 16.5, "min_size": 15.5,
"dispersion": 0.03, "quality": "strong", "bold_ratio": 1.0,
"italic_ratio": 0.0, "font_families": ["TimesNewRomanPS-BoldMT"],
},
}
result = cross_validate_with_span(block, "body_paragraph", profiles)
assert "section_heading" in result["suggested_roles"]
assert result["adjustment"] < 0
def test_build_family_profiles_prefers_style_family_partition_artifacts() -> None:
from paperforge.worker.ocr_profiles import build_family_profiles
blocks = [
{
"role": "body_paragraph",
"style_family": "body_like",
"span_metadata": {"size": 10.0, "font": "Times", "flags": "normal"},
},
{
"role": "body_paragraph",
"style_family": "legend_like",
"span_metadata": {"size": 8.0, "font": "Times", "flags": "normal"},
},
]
families = build_family_profiles(blocks)
assert families["body_family"]["block_count"] == 1
assert families["body_family"]["mean_size"] == 10.0
assert families["legend_family"]["block_count"] == 1
assert families["legend_family"]["mean_size"] == 8.0