mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 17:00:23 +00:00
240 lines
9.6 KiB
Python
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
|