From 8dfc0a04252363c1023c3dfe0d8d5933ff35993b Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Fri, 12 Jun 2026 17:02:59 +0800 Subject: [PATCH] feat: close OCR object gate for reader outputs When figure_caption_candidate blocks lack explicit marker_type or caption_text in the strict inventory, _normalize_bucket now falls back to the structured block's text and infers figure_number/marker_type from the text content. This ensures candidate captions propagate through the reader-figure pipeline to the renderer and health summary. --- paperforge/worker/ocr_figure_reader.py | 25 ++++++++++++++++++++++--- tests/test_ocr_figure_reader.py | 17 +++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/paperforge/worker/ocr_figure_reader.py b/paperforge/worker/ocr_figure_reader.py index 08c1ecff..8c258457 100644 --- a/paperforge/worker/ocr_figure_reader.py +++ b/paperforge/worker/ocr_figure_reader.py @@ -1,7 +1,16 @@ from __future__ import annotations +import contextlib +import re from dataclasses import dataclass +_FIGURE_NUM_PATTERN = re.compile( + r"(?:Figure|Fig\.?|Supplementary\s+Figure|Supplementary\s+Fig\.?|" + r"Extended\s+Data\s+Figure|Extended\s+Data\s+Fig\.?)\s+" + r"(?:S)?(\d+(?:\.\d+)?)", + flags=re.IGNORECASE, +) + @dataclass(frozen=True) class ReaderCoverage: @@ -100,7 +109,7 @@ def _normalize_bucket( legend_block_id = _legend_block_id(source_item) source_page = source_item.get("page") block = (page_block_index or {}).get((source_page, legend_block_id), block_index.get(legend_block_id, {})) - source_text = str(source_item.get("text", source_item.get("caption_text", "")) or "") + source_text = str(source_item.get("text", source_item.get("caption_text", block.get("text", ""))) or "") legend_data = (page_legends_index or {}).get((source_page, legend_block_id), (legends_index or {}).get(legend_block_id, {})) source_marker = ( source_item.get("marker_signature") @@ -109,9 +118,19 @@ def _normalize_bucket( or {} ) + _inferred_figure_number: int | None = None + _inferred_marker_type: str | None = None + _text_for_inference = source_text or block.get("text", "") + if not source_item.get("marker_type") and not source_marker.get("type") and _text_for_inference: + _m = _FIGURE_NUM_PATTERN.search(_text_for_inference) + if _m: + _inferred_marker_type = "figure_number" + with contextlib.suppress(ValueError): + _inferred_figure_number = int(float(_m.group(1))) + normalized.append( { - "figure_number": source_item.get("figure_number") or source_marker.get("number"), + "figure_number": source_item.get("figure_number") or source_marker.get("number") or _inferred_figure_number, "legend_block_id": legend_block_id, "caption_text": source_text, "asset_block_ids": _asset_ids_from_item(source_item), @@ -120,7 +139,7 @@ def _normalize_bucket( if bucket_name in candidate_buckets else [] ), - "marker_type": source_item.get("marker_type") or source_marker.get("type"), + "marker_type": source_item.get("marker_type") or source_marker.get("type") or _inferred_marker_type, "inline_mention": bool(source_item.get("inline_mention", False)), "panel_label": bool(source_item.get("panel_label", False)), "body_prose_likelihood": float(source_item.get("body_prose_likelihood", 0.0)), diff --git a/tests/test_ocr_figure_reader.py b/tests/test_ocr_figure_reader.py index 9898464b..b27f20d1 100644 --- a/tests/test_ocr_figure_reader.py +++ b/tests/test_ocr_figure_reader.py @@ -277,6 +277,23 @@ def test_reader_payload_coverage_accounted_matches_reader_figures() -> None: assert set(result["consumed_caption_block_ids"]) == {5, 21} +def test_reader_figures_emit_from_matched_or_unresolved_object_inputs() -> None: + from paperforge.worker.ocr_figure_reader import synthesize_reader_figures + + payload = synthesize_reader_figures( + { + "matched_figures": [{"figure_id": "figure_001", "page": 2, "legend_block_id": 21, "asset_block_ids": [90], "strict_status": "matched"}], + "held_figures": [], + "ambiguous_figures": [], + "unmatched_legends": [], + "unresolved_clusters": [], + }, + structured_blocks=[{"block_id": 21, "page": 2, "role": "figure_caption_candidate", "text": "Fig. 1 Caption"}], + ) + + assert len(payload["reader_figures"]) >= 1 + + def test_reader_normalization_keeps_same_block_id_legends_separate_by_page() -> None: from paperforge.worker.ocr_figure_reader import synthesize_reader_figures