From 0a2988b89f7cb4a2b4ceb3d4ccb8603ac8d0771a Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Fri, 5 Jun 2026 15:36:59 +0800 Subject: [PATCH] revert: restore original Phase 2 figure/table matching logic --- paperforge/worker/ocr_figures.py | 139 +++++++------------ paperforge/worker/ocr_tables.py | 108 ++++----------- tests/test_ocr_figures.py | 146 -------------------- tests/test_ocr_tables.py | 228 +------------------------------ 4 files changed, 78 insertions(+), 543 deletions(-) diff --git a/paperforge/worker/ocr_figures.py b/paperforge/worker/ocr_figures.py index 24ae187d..855725d0 100644 --- a/paperforge/worker/ocr_figures.py +++ b/paperforge/worker/ocr_figures.py @@ -51,31 +51,11 @@ def _centroid_y(bbox: list[float]) -> float: return (bbox[1] + bbox[3]) / 2 -def _compute_text_confidence(text: str, bbox: list[float], profile_legends: list[dict]) -> bool: - """Check if a block's text/geometry is similar to known formal legends.""" - if not text or not profile_legends: - return False - if len(text) > 200: - return False - if any(w in text.lower() for w in [" is ", " are ", " was ", " were "]): - return False - if profile_legends: - widths = [ - l.get("bbox", [0, 0, 0, 0])[2] - l.get("bbox", [0, 0, 0, 0])[0] - for l in profile_legends - if l.get("bbox") - ] - if widths: - avg_width = sum(widths) / len(widths) - bbox_width = bbox[2] - bbox[0] - if abs(bbox_width - avg_width) / max(1, avg_width) < 0.5: - return True - return False - - def build_figure_inventory(structured_blocks: list[dict]) -> dict[str, Any]: legends: list[dict] = [] assets: list[dict] = [] + unmatched_assets: list[dict] = [] + matched_figures: list[dict] = [] for block in structured_blocks: role = block.get("role", "") @@ -88,109 +68,84 @@ def build_figure_inventory(structured_blocks: list[dict]) -> dict[str, Any]: if raw_label in {"image", "chart", "figure_title", "figure"} or not raw_label: assets.append(block) - # --- Step 1: Classify each legend with confidence level --- - # All legends are included; the classification affects confidence scoring - legend_classification: dict[int, str] = {} # legend index -> "formal" | "candidate" | "fallback" - for i, leg in enumerate(legends): - text = leg.get("text", "") - bbox = leg.get("bbox", [0, 0, 0, 0]) - if _extract_figure_number(text) is not None: - legend_classification[i] = "formal" - elif _compute_text_confidence(text, bbox, [legends[j] for j in range(i) if legend_classification.get(j) == "formal"]): - legend_classification[i] = "candidate" - else: - legend_classification[i] = "fallback" - - # --- Step 2: Multi-signal matching --- used_asset_indices: set[int] = set() - matched_figures: list[dict] = [] - unmatched_legends: list[dict] = [] - - for i_leg, legend in enumerate(legends): + for legend in legends: legend_page = legend.get("page", 0) legend_bbox = legend.get("bbox", [0, 0, 0, 0]) legend_text = legend.get("text", "") fig_num = _extract_figure_number(legend_text) - cls = legend_classification.get(i_leg, "fallback") - is_formal = cls == "formal" candidate_pages = [legend_page, legend_page + 1, legend_page - 1] - matched_assets = [] + matched_assets = [] for page in candidate_pages: if page < 1: continue page_assets = [ - (i, a) for i, a in enumerate(assets) - if a.get("page", 0) == page and i not in used_asset_indices + (i, a) for i, a in enumerate(assets) if a.get("page", 0) == page and i not in used_asset_indices ] if not page_assets: continue - scored = [] + candidates_for_page: list[dict] = [] for i, asset in page_assets: asset_bbox = asset.get("bbox", [0, 0, 0, 0]) overlap = _compute_overlap_score(legend_bbox, asset_bbox) dist_y = abs(_centroid_y(legend_bbox) - _centroid_y(asset_bbox)) - dir_bonus = 1.0 if _centroid_y(asset_bbox) < _centroid_y(legend_bbox) else 0.5 - same_page = 2.0 if page == legend_page else 0.0 - # Horizontal alignment - legend_cx = (legend_bbox[0] + legend_bbox[2]) / 2 - asset_cx = (asset_bbox[0] + asset_bbox[2]) / 2 - h_align = max(0, 1.0 - abs(legend_cx - asset_cx) / max(1, legend_bbox[2] - legend_bbox[0])) - # Asset size bonus - a_w = asset_bbox[2] - asset_bbox[0] - a_h = asset_bbox[3] - asset_bbox[1] - size_score = min(2.0, (a_w * a_h) / 50000) - score = overlap * 10 + dir_bonus + same_page + h_align * 2 + size_score * 0.5 - dist_y * 0.01 - scored.append({"asset_index": i, "score": score, "overlap": overlap}) + direction_bonus = 1.0 if _centroid_y(asset_bbox) < _centroid_y(legend_bbox) else 0.5 + same_page_bonus = 2.0 if page == legend_page else 0.0 + score = overlap * 10 + direction_bonus + same_page_bonus - dist_y * 0.01 + candidates_for_page.append( + { + "asset_index": i, + "asset": asset, + "score": score, + "overlap": overlap, + "distance_y": dist_y, + "same_page": page == legend_page, + } + ) - scored.sort(key=lambda c: c["score"], reverse=True) - for c in scored: - if c["score"] <= -5: - continue + candidates_for_page.sort(key=lambda c: c["score"], reverse=True) + + for candidate in candidates_for_page: if len(matched_assets) >= 3: break - matched_assets.append(assets[c["asset_index"]]) - used_asset_indices.add(c["asset_index"]) + if candidate["score"] > -5: + matched_assets.append(candidate["asset"]) + used_asset_indices.add(candidate["asset_index"]) if matched_assets: break - # --- Step 3: Classify outcome --- - flags: list[str] = [] - confidence = 0.85 if is_formal else 0.5 - if not matched_assets: - flags.append("legend_only") - confidence = 0.4 if is_formal else 0.2 - unmatched_legends.append(legend) - elif confidence < 0.6: - flags.append("low_confidence_match") - confidence = 0.4 + matched_figures.append( + { + "legend_block_id": legend.get("block_id", ""), + "page": legend_page, + "text": legend_text, + "figure_number": fig_num, + "matched_assets": [ + { + "block_id": a.get("block_id", ""), + "bbox": a.get("bbox", [0, 0, 0, 0]), + } + for a in matched_assets + ], + "confidence": 0.85 if matched_assets else 0.4, + "flags": [] if matched_assets else ["legend_only"], + } + ) - matched_figures.append({ - "legend_block_id": legend.get("block_id", ""), - "page": legend_page, - "text": legend_text, - "figure_number": fig_num, - "matched_assets": [ - {"block_id": a.get("block_id", ""), "bbox": a.get("bbox", [0, 0, 0, 0])} - for a in matched_assets - ], - "confidence": confidence, - "flags": flags, - }) - - unmatched_assets_list = [ - asset for i, asset in enumerate(assets) if i not in used_asset_indices - ] + for i, asset in enumerate(assets): + if i not in used_asset_indices: + unmatched_assets.append(asset) return { "figure_legends": legends, "figure_assets": assets, "matched_figures": matched_figures, - "unmatched_legends": unmatched_legends, - "unmatched_assets": unmatched_assets_list, + "unmatched_legends": [], + "unmatched_assets": unmatched_assets, "official_figure_count": len(matched_figures), } diff --git a/paperforge/worker/ocr_tables.py b/paperforge/worker/ocr_tables.py index 5cf8e810..3f44d05c 100644 --- a/paperforge/worker/ocr_tables.py +++ b/paperforge/worker/ocr_tables.py @@ -6,13 +6,12 @@ from typing import Any from paperforge.core.io import write_json + _TABLE_PREFIX_PATTERN = re.compile( r"^(?:Table|Supplementary\s+Table|Extended\s+Data\s+Table)\s+(\d+(?:\.\d+)?)", flags=re.IGNORECASE, ) -_CONTINUATION_PATTERN = re.compile(r"\(Continued\)|\(cont\.\)|\(cont\)", re.IGNORECASE) - def _extract_table_number(text: str) -> int | None: m = _TABLE_PREFIX_PATTERN.search(text) @@ -24,31 +23,12 @@ def _extract_table_number(text: str) -> int | None: return None -def _compute_overlap_score(a_bbox: list[float], b_bbox: list[float]) -> float: - if not a_bbox or not b_bbox or len(a_bbox) < 4 or len(b_bbox) < 4: - return 0.0 - ax1, ay1, ax2, ay2 = a_bbox - bx1, by1, bx2, by2 = b_bbox - ix1 = max(ax1, bx1) - iy1 = max(ay1, by1) - ix2 = min(ax2, bx2) - iy2 = min(ay2, by2) - iw = max(0, ix2 - ix1) - ih = max(0, iy2 - iy1) - intersection = iw * ih - if intersection == 0: - return 0.0 - a_area = (ax2 - ax1) * (ay2 - ay1) - b_area = (bx2 - bx1) * (by2 - by1) - union = a_area + b_area - intersection - if union == 0: - return 0.0 - return intersection / union - - def build_table_inventory(structured_blocks: list[dict]) -> dict[str, Any]: + tables: list[dict] = [] captions: list[dict] = [] assets: list[dict] = [] + unmatched_captions: list[dict] = [] + unmatched_assets: list[dict] = [] for block in structured_blocks: role = block.get("role", "") @@ -61,72 +41,30 @@ def build_table_inventory(structured_blocks: list[dict]) -> dict[str, Any]: assets.append(block) used_asset_indices: set[int] = set() - tables: list[dict] = [] - unmatched_captions: list[dict] = [] - for caption in captions: caption_page = caption.get("page", 0) - caption_text = caption.get("text", "") or "" - caption_bbox = caption.get("bbox", [0, 0, 0, 0]) + caption_text = caption.get("text", "") table_num = _extract_table_number(caption_text) - is_continuation = bool(_CONTINUATION_PATTERN.search(caption_text)) - # Continuations should only match same-page assets - candidate_pages = [caption_page] - if not is_continuation: - candidate_pages.extend([caption_page + 1, caption_page - 1]) + candidate_pages = [caption_page, caption_page + 1, caption_page - 1] + matched_asset = None - best_match: tuple[int | None, float] = (None, -999) for page in candidate_pages: if page < 1: continue - page_assets = [ - (i, a) for i, a in enumerate(assets) - if a.get("page", 0) == page and i not in used_asset_indices - ] - for i, asset in page_assets: - asset_bbox = asset.get("bbox", [0, 0, 0, 0]) - overlap = _compute_overlap_score(caption_bbox, asset_bbox) - if is_continuation: - same_page = 3.0 - h_align = 0.0 - if len(caption_bbox) >= 4 and len(asset_bbox) >= 4: - ccx = (caption_bbox[0] + caption_bbox[2]) / 2 - acx = (asset_bbox[0] + asset_bbox[2]) / 2 - h_align = max(0, 1.0 - abs(ccx - acx) / max(1, caption_bbox[2] - caption_bbox[0])) - score = same_page + h_align * 3 + overlap * 5 - else: - dist_y = 0.0 - if len(caption_bbox) >= 4 and len(asset_bbox) >= 4: - dist_y = abs((caption_bbox[1] + caption_bbox[3]) / 2 - (asset_bbox[1] + asset_bbox[3]) / 2) - same_page = 2.0 if page == caption_page else 0.0 - direction = 1.0 if (len(asset_bbox) >= 4 and len(caption_bbox) >= 4 - and (asset_bbox[1] + asset_bbox[3]) / 2 < (caption_bbox[1] + caption_bbox[3]) / 2) else 0.5 - h_align = 0.0 - if len(caption_bbox) >= 4 and len(asset_bbox) >= 4: - ccx = (caption_bbox[0] + caption_bbox[2]) / 2 - acx = (asset_bbox[0] + asset_bbox[2]) / 2 - h_align = max(0, 1.0 - abs(ccx - acx) / max(1, caption_bbox[2] - caption_bbox[0])) - a_w = asset_bbox[2] - asset_bbox[0] if len(asset_bbox) >= 4 else 0 - a_h = asset_bbox[3] - asset_bbox[1] if len(asset_bbox) >= 4 else 0 - size_score = min(2.0, (a_w * a_h) / 50000) - score = overlap * 10 + same_page + direction + h_align * 2 + size_score * 0.5 - dist_y * 0.01 - if score > best_match[1]: - best_match = (i, score) - - matched_asset = assets[best_match[0]] if best_match[0] is not None else None - if best_match[0] is not None: - used_asset_indices.add(best_match[0]) - - if matched_asset is None: - unmatched_captions.append(caption) + for i, asset in enumerate(assets): + if asset.get("page", 0) == page and i not in used_asset_indices: + matched_asset = asset + used_asset_indices.add(i) + break + if matched_asset: + break tables.append({ "caption_block_id": caption.get("block_id", ""), "page": caption_page, "caption_text": caption_text, "table_number": table_num, - "is_continuation": is_continuation, "asset_block_id": matched_asset.get("block_id", "") if matched_asset else "", "asset_bbox": matched_asset.get("bbox", [0, 0, 0, 0]) if matched_asset else [], "assistive_text": (matched_asset.get("text", "") or "") if matched_asset else "", @@ -134,17 +72,23 @@ def build_table_inventory(structured_blocks: list[dict]) -> dict[str, Any]: "has_asset": matched_asset is not None, }) - unmatched_assets_list = [ - a for i, a in enumerate(assets) if i not in used_asset_indices - ] + for caption in captions: + if not any( + t["caption_block_id"] == caption.get("block_id", "") + for t in tables + if t["has_asset"] + ): + unmatched_captions.append(caption) - official_count = len([t for t in tables if t["has_asset"] and not t["is_continuation"]]) + for i, asset in enumerate(assets): + if i not in used_asset_indices: + unmatched_assets.append(asset) return { "tables": tables, "unmatched_captions": unmatched_captions, - "unmatched_assets": unmatched_assets_list, - "official_table_count": official_count, + "unmatched_assets": unmatched_assets, + "official_table_count": len([t for t in tables if t["has_asset"]]), } diff --git a/tests/test_ocr_figures.py b/tests/test_ocr_figures.py index f26990a0..7a856ffd 100644 --- a/tests/test_ocr_figures.py +++ b/tests/test_ocr_figures.py @@ -74,149 +74,3 @@ 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 e108fe8d..6edd493c 100644 --- a/tests/test_ocr_tables.py +++ b/tests/test_ocr_tables.py @@ -1,4 +1,8 @@ -"""Task 5 tests for hardened table matching.""" +"""Phase 2 contract tests for table inventory. + +paperforge.worker.ocr_tables does not exist yet -- tests will fail until +Task 7 implements the module. +""" from __future__ import annotations @@ -59,225 +63,3 @@ 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"