diff --git a/tests/test_ocr_figures.py b/tests/test_ocr_figures.py index 7a856ffd..f26990a0 100644 --- a/tests/test_ocr_figures.py +++ b/tests/test_ocr_figures.py @@ -74,3 +74,149 @@ def test_unmatched_assets_are_preserved() -> None: assert inventory["official_figure_count"] == 0 assert len(inventory["unmatched_assets"]) == 1 + + +def test_extract_figure_number_basic() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Figure 1. Caption") == 1 + + +def test_extract_figure_number_fig_dot() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Fig. 2. Test") == 2 + + +def test_extract_figure_number_supplementary() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Supplementary Fig. S3") == 3 + + +def test_extract_figure_number_extended_data() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Extended Data Fig. 4.") == 4 + + +def test_extract_figure_number_decimal_truncated() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + result = _extract_figure_number("Figure 1.2. Magnified view") + assert result == 1 or result == 1.2 + + +def test_extract_figure_number_none() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Some random text") is None + + +def test_extract_figure_number_multiline() -> None: + from paperforge.worker.ocr_figures import _extract_figure_number + assert _extract_figure_number("Figure 3.\nDescription continues") == 3 + + +def test_formal_legend_detection_explicit_figure_prefix() -> None: + from paperforge.worker.ocr_figures import build_figure_inventory + + structured_blocks = [ + { + "paper_id": "K001", + "page": 1, + "block_id": "p1_b1", + "role": "figure_caption", + "text": "Figure 1. This is a formal legend with plenty of descriptive text that explains the figure contents in detail.", + "bbox": [50, 400, 550, 450], + }, + { + "paper_id": "K001", + "page": 1, + "block_id": "p1_b2", + "role": "figure_asset", + "text": "", + "bbox": [50, 50, 550, 380], + }, + ] + + inventory = build_figure_inventory(structured_blocks) + + assert len(inventory["matched_figures"]) == 1 + assert inventory["matched_figures"][0]["figure_number"] == 1 + assert len(inventory["matched_figures"][0]["matched_assets"]) == 1 + assert inventory["matched_figures"][0]["confidence"] == 0.85 + + +def test_candidate_legend_geometry_match() -> None: + from paperforge.worker.ocr_figures import build_figure_inventory + + structured_blocks = [ + { + "paper_id": "K001", + "page": 1, + "block_id": "p1_b1", + "role": "figure_caption", + "text": "Figure 1. Formal legend that establishes a width profile.", + "bbox": [50, 420, 550, 460], + }, + { + "paper_id": "K001", + "page": 1, + "block_id": "p1_b2", + "role": "figure_caption", + "text": "No figure prefix but short and profile-matched", + "bbox": [60, 350, 540, 380], + }, + { + "paper_id": "K001", + "page": 1, + "block_id": "p1_b3", + "role": "figure_asset", + "text": "", + "bbox": [60, 50, 540, 330], + }, + ] + + inventory = build_figure_inventory(structured_blocks) + + assert len(inventory["matched_figures"]) == 2 + match_texts = [m["text"] for m in inventory["matched_figures"]] + assert any("No figure prefix" in t for t in match_texts) + + +def test_legend_only_figure_no_asset_match() -> None: + from paperforge.worker.ocr_figures import build_figure_inventory + + structured_blocks = [ + { + "paper_id": "K001", + "page": 2, + "block_id": "p2_b1", + "role": "figure_caption", + "text": "Figure 2. This caption has no matching asset on the same page.", + "bbox": [50, 700, 550, 750], + }, + ] + + inventory = build_figure_inventory(structured_blocks) + + assert len(inventory["matched_figures"]) == 1 + assert inventory["matched_figures"][0]["figure_number"] == 2 + assert len(inventory["matched_figures"][0]["matched_assets"]) == 0 + assert "legend_only" in inventory["matched_figures"][0]["flags"] + assert inventory["matched_figures"][0]["confidence"] == 0.4 + + +def test_unmatched_legends_populated() -> None: + from paperforge.worker.ocr_figures import build_figure_inventory + + structured_blocks = [ + { + "paper_id": "K001", + "page": 3, + "block_id": "p3_b1", + "role": "figure_caption", + "text": "Figure 3. Caption with no figure asset at all.", + "bbox": [50, 700, 550, 750], + }, + ] + + inventory = build_figure_inventory(structured_blocks) + + assert len(inventory["unmatched_legends"]) == 1 + assert inventory["unmatched_legends"][0]["block_id"] == "p3_b1" diff --git a/tests/test_ocr_tables.py b/tests/test_ocr_tables.py index 6edd493c..e108fe8d 100644 --- a/tests/test_ocr_tables.py +++ b/tests/test_ocr_tables.py @@ -1,8 +1,4 @@ -"""Phase 2 contract tests for table inventory. - -paperforge.worker.ocr_tables does not exist yet -- tests will fail until -Task 7 implements the module. -""" +"""Task 5 tests for hardened table matching.""" from __future__ import annotations @@ -63,3 +59,225 @@ def test_table_without_asset_is_tracked_as_unmatched_caption() -> None: assert inventory["official_table_count"] == 0 assert len(inventory["unmatched_captions"]) == 1 + + +def test_continuation_table_matches_same_page_asset() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 10, + "block_id": "p10_a1", + "role": "table_asset", + "text": "continued data", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 10, + "block_id": "p10_c1", + "role": "table_caption", + "text": "Table 1 (Continued)", + "bbox": [100, 420, 600, 460], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert len(inventory["tables"]) == 1 + t = inventory["tables"][0] + assert t["is_continuation"] is True + assert t["has_asset"] is True + assert t["asset_block_id"] == "p10_a1" + + +def test_continuation_does_not_increment_official_count() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 5, + "block_id": "p5_a1", + "role": "table_asset", + "text": "table data", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 5, + "block_id": "p5_c1", + "role": "table_caption", + "text": "Table 1. Main data", + "bbox": [100, 420, 600, 460], + }, + { + "paper_id": "KEY001", + "page": 6, + "block_id": "p6_a1", + "role": "table_asset", + "text": "continued data", + "bbox": [100, 100, 600, 300], + }, + { + "paper_id": "KEY001", + "page": 6, + "block_id": "p6_c1", + "role": "table_caption", + "text": "Table 1 (Continued)", + "bbox": [100, 320, 600, 360], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert inventory["official_table_count"] == 1 + normal = [t for t in inventory["tables"] if not t["is_continuation"]] + continued = [t for t in inventory["tables"] if t["is_continuation"]] + assert len(normal) == 1 + assert len(continued) == 1 + assert normal[0]["has_asset"] is True + assert continued[0]["has_asset"] is True + + +def test_continuation_without_asset_has_has_asset_false() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 10, + "block_id": "p10_c1", + "role": "table_caption", + "text": "Table 1 (Continued)", + "bbox": [100, 100, 600, 140], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert len(inventory["tables"]) == 1 + assert inventory["tables"][0]["is_continuation"] is True + assert inventory["tables"][0]["has_asset"] is False + assert len(inventory["unmatched_captions"]) == 1 + + +def test_multi_signal_scoring_prefers_better_asset() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 3, + "block_id": "p3_a1", + "role": "table_asset", + "text": "far table", + "bbox": [50, 600, 300, 800], + }, + { + "paper_id": "KEY001", + "page": 3, + "block_id": "p3_a2", + "role": "table_asset", + "text": "near table", + "bbox": [50, 50, 550, 300], + }, + { + "paper_id": "KEY001", + "page": 3, + "block_id": "p3_c1", + "role": "table_caption", + "text": "Table 3. Nearby data", + "bbox": [50, 310, 550, 350], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert inventory["official_table_count"] == 1 + t = inventory["tables"][0] + assert t["asset_block_id"] == "p3_a2" + + +def test_continuation_matches_only_same_page_not_adjacent() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 4, + "block_id": "p4_a1", + "role": "table_asset", + "text": "wrong page", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 5, + "block_id": "p5_a1", + "role": "table_asset", + "text": "same page", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 5, + "block_id": "p5_c1", + "role": "table_caption", + "text": "Table 2 (cont.)", + "bbox": [100, 420, 600, 460], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert len(inventory["tables"]) == 1 + t = inventory["tables"][0] + assert t["is_continuation"] is True + assert t["asset_block_id"] == "p5_a1" + + +def test_multiple_captions_match_correct_assets_in_order() -> None: + from paperforge.worker.ocr_tables import build_table_inventory + + structured_blocks = [ + { + "paper_id": "KEY001", + "page": 5, + "block_id": "a5b", + "role": "table_asset", + "text": "table 1 body", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 5, + "block_id": "c5a", + "role": "table_caption", + "text": "Table 1. First", + "bbox": [100, 420, 600, 460], + }, + { + "paper_id": "KEY001", + "page": 6, + "block_id": "a6b", + "role": "table_asset", + "text": "table 2 body", + "bbox": [100, 100, 600, 400], + }, + { + "paper_id": "KEY001", + "page": 6, + "block_id": "c6a", + "role": "table_caption", + "text": "Table 2. Second", + "bbox": [100, 420, 600, 460], + }, + ] + + inventory = build_table_inventory(structured_blocks) + + assert inventory["official_table_count"] == 2 + assert inventory["tables"][0]["asset_block_id"] == "a5b" + assert inventory["tables"][1]["asset_block_id"] == "a6b"