From c86700b6f2ab7de955dbbaeec6b327fa521f2c7d Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Wed, 10 Jun 2026 20:43:54 +0800 Subject: [PATCH] feat: persist OCR reader figure artifacts and health metrics --- paperforge/worker/ocr.py | 9 ++++++++ paperforge/worker/ocr_health.py | 11 ++++++++++ paperforge/worker/ocr_rebuild.py | 9 ++++++++ tests/test_ocr_figure_reader.py | 36 ++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/paperforge/worker/ocr.py b/paperforge/worker/ocr.py index 4d8ac00f..21e9ed51 100644 --- a/paperforge/worker/ocr.py +++ b/paperforge/worker/ocr.py @@ -1846,6 +1846,14 @@ def postprocess_ocr_result(vault: Path, key: str, all_results: list[dict]) -> tu figure_inventory, ) + # --- Phase 2a: reader figure synthesis --- + from paperforge.worker.ocr_figure_reader import synthesize_reader_figures + + reader_payload = synthesize_reader_figures(figure_inventory, structured_blocks=structured) + reader_figures_dir = ocr_root / "structure" + reader_figures_dir.mkdir(parents=True, exist_ok=True) + write_json(reader_figures_dir / "reader_figures.json", reader_payload) + # --- Phase 2: table inventory --- table_inventory = build_table_inventory(structured) write_table_inventory( @@ -1903,6 +1911,7 @@ def postprocess_ocr_result(vault: Path, key: str, all_results: list[dict]) -> tu figure_inventory=figure_inventory, table_inventory=table_inventory, doc_structure=doc_structure, + reader_payload=reader_payload, ) write_ocr_health(ocr_root / "health", health_report) meta["ocr_health_overall"] = health_report["overall"] diff --git a/paperforge/worker/ocr_health.py b/paperforge/worker/ocr_health.py index f602ba6d..1e586ea3 100644 --- a/paperforge/worker/ocr_health.py +++ b/paperforge/worker/ocr_health.py @@ -57,6 +57,7 @@ def build_ocr_health( figure_inventory: dict, table_inventory: dict, doc_structure: Any = None, + reader_payload: dict | None = None, ) -> dict[str, Any]: section_heading_count = sum(1 for b in structured_blocks if b.get("role") == "section_heading") abstract_found = any( @@ -224,6 +225,14 @@ def build_ocr_health( else 1.0 ), } + + if reader_payload is not None: + rc = reader_payload.get("reader_coverage", {}) + report["figure_reader_coverage_total"] = rc.get("total", 0) + report["figure_reader_coverage_accounted"] = rc.get("accounted", 0) + report["figure_reader_coverage_gap_count"] = rc.get("gap_count", 0) + report["figure_reader_coverage_ratio"] = rc.get("ratio", 1.0) + report.update(decision_summary) degraded_reasons = [] @@ -278,6 +287,8 @@ def build_ocr_health( degraded_reasons.append( f"figure legend completeness gap ({formal_legend_gaps} numbered legends unaccounted for)" ) + if reader_payload is not None and reader_payload.get("reader_coverage", {}).get("gap_count", 0) > 0: + degraded_reasons.append("reader_figure_coverage_gap") return report diff --git a/paperforge/worker/ocr_rebuild.py b/paperforge/worker/ocr_rebuild.py index 1ae28da5..d6d9edc0 100644 --- a/paperforge/worker/ocr_rebuild.py +++ b/paperforge/worker/ocr_rebuild.py @@ -130,6 +130,14 @@ def run_derived_rebuild_for_keys(vault: Path, keys: list[str]) -> dict: figure_inventory = build_figure_inventory(structured) write_figure_inventory(artifacts.blocks_structured.parent / "figure_inventory.json", figure_inventory) + # Rebuild reader figures + from paperforge.worker.ocr_figure_reader import synthesize_reader_figures + + reader_payload = synthesize_reader_figures(figure_inventory, structured_blocks=structured) + reader_figures_dir = paper_root / "structure" + reader_figures_dir.mkdir(parents=True, exist_ok=True) + write_json(reader_figures_dir / "reader_figures.json", reader_payload) + # Rebuild table inventory from paperforge.worker.ocr_tables import build_table_inventory, write_table_inventory @@ -189,6 +197,7 @@ def run_derived_rebuild_for_keys(vault: Path, keys: list[str]) -> dict: figure_inventory=figure_inventory, table_inventory=table_inventory, doc_structure=doc_structure, + reader_payload=reader_payload, ) write_ocr_health(paper_root / "health", health_report) diff --git a/tests/test_ocr_figure_reader.py b/tests/test_ocr_figure_reader.py index f7414813..3f8946da 100644 --- a/tests/test_ocr_figure_reader.py +++ b/tests/test_ocr_figure_reader.py @@ -202,3 +202,39 @@ def test_legend_only_consumes_caption_when_rendered() -> None: assert result["consumed_caption_block_ids"] == [21] assert result["reader_figures"][0]["reader_status"] == "LEGEND_ONLY" + + +def test_reader_payload_coverage_accounted_matches_reader_figures() -> None: + from paperforge.worker.ocr_figure_reader import synthesize_reader_figures + + strict_inventory = { + "matched_figures": [ + { + "figure_number": 1, + "legend_block_id": 5, + "text": "Fig. 1 Overview of the system...", + "matched_assets": [{"block_id": 10, "bbox": [1, 2, 3, 4]}], + "match_score": 0.91, + "marker_type": "figure_number", + } + ], + "held_figures": [], + "ambiguous_figures": [], + "unmatched_legends": [ + { + "figure_number": 2, + "legend_block_id": 21, + "text": "FIGURE 2 | Treadmill exercise protocols...", + "marker_type": "figure_number", + } + ], + "unresolved_clusters": [], + } + + result = synthesize_reader_figures(strict_inventory, structured_blocks=[]) + + assert result["reader_coverage"]["total"] == 2 + assert result["reader_coverage"]["accounted"] == 2 + assert result["reader_coverage"]["gap_count"] == 0 + assert result["reader_coverage"]["ratio"] == 1.0 + assert set(result["consumed_caption_block_ids"]) == {5, 21}