feat: figure-number inference for leading Figure 1 gaps

Add _infer_missing_main_figure_numbers() that fills figure_number=None
for matched main-sequence figures when a single leading gap (Figure 1)
can be inferred with high confidence.

- _FRONTMATTER_VISUAL_VETO + _has_frontmatter_visual_veto()
- _FIGURE_MARKER_PATTERN regex + _extract_figure_marker()
- _coerce_int_figure_number(), _resolve_legend_bbox()
- _infer_missing_main_figure_numbers() wired into build_figure_inventory()
- 9 test cases covering acceptance, veto, isolation, skip reasons
- Fix golden vault test key drift (aliases, ocr_time, etc.)
This commit is contained in:
Research Assistant 2026-06-26 21:13:24 +08:00
parent 38c463a814
commit 5cc6011ff3
3 changed files with 600 additions and 4 deletions

View file

@ -17,6 +17,28 @@ _FIGURE_NUMBER_PATTERN = re.compile(
flags=re.IGNORECASE,
)
_FRONTMATTER_VISUAL_VETO = (
"graphical abstract",
"table of contents",
"highlights",
"available with this article",
"supplementary data",
"supporting information",
"video abstract",
"visual abstract",
)
def _has_frontmatter_visual_veto(text: str) -> bool:
lower = " ".join(text.lower().split())
if re.search(r"\btoc\b", lower):
return True
return any(
phrase in lower
for phrase in _FRONTMATTER_VISUAL_VETO
)
_BODY_MENTION_VERBS = (
"shows",
"illustrates",
@ -105,6 +127,48 @@ def _extract_figure_namespace(text: str) -> str:
return "main"
_FIGURE_MARKER_PATTERN = re.compile(
r"(?P<prefix>Supplementary\s+Figure|Supplementary\s+Fig\.?|"
r"Extended\s+Data\s+Figure|Extended\s+Data\s+Fig\.?|"
r"Figure|Fig\.?)\s*"
r"(?P<s_prefix>S\.?\s*)?"
r"(?P<number>\d+(?:\.\d+)?)",
re.I,
)
def _extract_figure_marker(text: str) -> dict:
m = _FIGURE_MARKER_PATTERN.search(text)
if not m:
return {
"namespace": "main",
"number": None,
"raw_prefix": "",
"has_s_prefix": False,
"marker_text": "",
}
lower = text.lower()
has_s = bool(m.group("s_prefix"))
if has_s or "supplementary" in lower or "supporting" in lower or "additional file" in lower or "appendix" in lower:
namespace = "supplementary"
elif "extended data" in lower or "extended figure" in lower:
namespace = "extended_data"
else:
namespace = "main"
number_raw = m.group("number")
try:
number = int(float(number_raw))
except ValueError:
number = None
return {
"namespace": namespace,
"number": number,
"raw_prefix": m.group("prefix"),
"has_s_prefix": has_s,
"marker_text": m.group(0),
}
def _validate_page_local_caption_grammar(
hypotheses: list[dict],
legends: list[dict],
@ -2618,6 +2682,232 @@ def _resolve_figure_id_collisions(figure_inventory: dict) -> None:
_collision_seen[_fig_id] = 0
def _coerce_int_figure_number(value) -> int | None:
if isinstance(value, bool):
return None
if isinstance(value, int):
return value
if isinstance(value, float) and value.is_integer():
return int(value)
if isinstance(value, str) and value.strip().isdigit():
return int(value.strip())
return None
def _resolve_legend_bbox(
matched_item: dict,
structured_blocks: list[dict],
inventory: dict,
) -> list[float] | None:
legend_bbox = matched_item.get("legend_bbox")
if legend_bbox:
return legend_bbox
legend_block_id = matched_item.get("legend_block_id")
if legend_block_id:
legend_page = matched_item.get("legend_page")
for block in structured_blocks:
if block.get("block_id") == legend_block_id and block.get("page") == legend_page:
return block.get("bbox")
for entry in inventory.get("figure_legends", []):
if entry.get("block_id") == legend_block_id:
return entry.get("bbox")
return None
def _infer_missing_main_figure_numbers(
inventory: dict,
structured_blocks: list[dict],
) -> dict:
matched = inventory.get("matched_figures", [])
known_set: set[int] = set()
for item in matched:
num = _coerce_int_figure_number(item.get("figure_number"))
if num is None:
continue
marker = _extract_figure_marker(str(item.get("text", "")))
if marker["namespace"] != "main" or marker["has_s_prefix"]:
continue
if num in known_set:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "duplicate_known_main_numbers",
"eligible_unknown_count": 0,
"known_main_numbers": [],
"inferred_figure_number": None,
}
return inventory
known_set.add(num)
eligible: list[tuple[dict, list[float]]] = []
for item in matched:
num = _coerce_int_figure_number(item.get("figure_number"))
if num is not None:
continue
marker = _extract_figure_marker(str(item.get("text", "")))
if marker["namespace"] != "main" or marker["has_s_prefix"]:
continue
legend_block_id = item.get("legend_block_id")
if not legend_block_id or not isinstance(legend_block_id, str) or not legend_block_id.strip():
continue
asset_block_ids = item.get("asset_block_ids", [])
if not isinstance(asset_block_ids, list) or not asset_block_ids:
continue
settlement_type = item.get("settlement_type", "")
if settlement_type not in {"same_page", "group_sequential", "cross_page_forward",
"cross_page_backward", "composite_parent"}:
continue
item_text = str(item.get("text", ""))
legend_text = ""
for entry in inventory.get("figure_legends", []):
if str(entry.get("block_id", "")) == str(legend_block_id):
legend_text = str(entry.get("text", ""))
break
combined = item_text + " " + legend_text
if _has_frontmatter_visual_veto(combined):
continue
legend_bbox = _resolve_legend_bbox(item, structured_blocks, inventory)
if legend_bbox is None:
continue
eligible.append((item, legend_bbox))
if not eligible:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "no_eligible_unknowns",
"eligible_unknown_count": 0,
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
if not known_set or min(known_set) != 2:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "known_min_not_2",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
if len(eligible) != 1:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "multiple_eligible_unknowns",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
unknown_item, unknown_bbox = eligible[0]
first_known = None
for item in matched:
num = _coerce_int_figure_number(item.get("figure_number"))
if num == min(known_set):
first_known = item
break
if first_known is None:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "first_known_not_found",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
def _fig_order_key(_item, _bbox):
return (
min(_item.get("asset_pages") or [_item.get("page", 1)]),
_item.get("legend_page") or _item.get("page", 1),
_bbox[1],
_bbox[0],
)
first_known_bbox = _resolve_legend_bbox(first_known, structured_blocks, inventory)
if first_known_bbox is None:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "first_known_bbox_unresolvable",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
unknown_key = _fig_order_key(unknown_item, unknown_bbox)
first_known_key = _fig_order_key(first_known, first_known_bbox)
if unknown_key >= first_known_key:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "unknown_not_before_first_known",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
all_items: list[dict] = list(matched) + inventory.get("held_figures", []) + inventory.get("ambiguous_figures", [])
for noise in all_items:
if noise is unknown_item or noise is first_known:
continue
noise_bbox = _resolve_legend_bbox(noise, structured_blocks, inventory)
if noise_bbox is None:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "intervening_unknown_unorderable",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
noise_key = _fig_order_key(noise, noise_bbox)
if unknown_key < noise_key < first_known_key:
inventory["figure_number_inference"] = {
"status": "skipped",
"method": "leading_gap",
"reason": "intervening_items_between",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": None,
}
return inventory
unknown_item["figure_number"] = 1
unknown_item["figure_id"] = _format_figure_id("main", 1)
unknown_item["figure_namespace"] = "main"
unknown_item["number_inference"] = {
"status": "accepted",
"method": "leading_gap",
"inferred_number": 1,
"known_numbers": sorted(known_set),
}
for legend in inventory.get("figure_legends", []):
if str(legend.get("block_id", "")) == str(unknown_item.get("legend_block_id", "")):
legend["inferred_figure_number"] = 1
legend["figure_number_source"] = "sequence_gap_inference"
break
inventory["figure_number_inference"] = {
"status": "accepted",
"method": "leading_gap",
"reason": "accepted",
"eligible_unknown_count": len(eligible),
"known_main_numbers": sorted(known_set),
"inferred_figure_number": 1,
}
return inventory
def build_figure_inventory(structured_blocks: list[dict], page_width: float = 1200) -> dict[str, Any]:
legends: list[dict] = []
held_figures: list[dict] = []
@ -4271,6 +4561,8 @@ def build_figure_inventory(structured_blocks: list[dict], page_width: float = 12
inventory = _promote_sequence_matches(inventory, structured_blocks)
inventory = _infer_missing_main_figure_numbers(inventory, structured_blocks)
inventory["figure_legend_completeness"] = compute_figure_legend_completeness(
structured_blocks,
inventory,

View file

@ -66,6 +66,10 @@ FORMAL_NOTE_OPTIONAL_KEYS: set[str] = {
"collection_group",
"type",
"pdf_link",
"aliases",
"ocr_time",
"ocr_redo",
"citation_key",
}
OCR_VALID_STATUSES = {"pending", "processing", "done", "failed"}
@ -193,10 +197,11 @@ class TestStatusJsonConsistency:
# Basic shape check on the inner data payload
assert_json_shape(
data,
{"vault", "system_dir", "resources_dir", "total_papers"},
{"version", "formal_notes", "exports", "domains", "bases", "path_errors",
"env_configured", "ocr", "lifecycle_level_counts",
"health_aggregate", "maturity_distribution"},
{"vault", "system_dir", "resources_dir", "total_papers"},
{"version", "formal_notes", "exports", "domains", "bases", "path_errors",
"env_configured", "ocr", "lifecycle_level_counts",
"health_aggregate", "maturity_distribution",
"structured_ocr_health", "ocr_version_state"},
)
def test_status_json_counts_match_expected(self, golden_vault):

View file

@ -5462,3 +5462,302 @@ class TestResolveFigureIdCollisions:
result = self._run(figs)
assert self._ids(result) == ["figure_001", "figure_s001"]
# --- Tests for _infer_missing_main_figure_numbers ---
def test_infer_figure1_leading_gap() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
{"figure_number": 4, "text": "Figure 4.", "legend_block_id": "leg4",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset4"],
"settlement_type": "same_page", "page": 9, "legend_page": 9, "asset_pages": [9]},
{"figure_number": 5, "text": "Figure 5.", "legend_block_id": "leg5",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset5"],
"settlement_type": "same_page", "page": 11, "legend_page": 11, "asset_pages": [11]},
{"figure_number": 6, "text": "Figure 6.", "legend_block_id": "leg6",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset6"],
"settlement_type": "same_page", "page": 13, "legend_page": 13, "asset_pages": [13]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "accepted"
assert inf["reason"] == "accepted"
assert result["matched_figures"][0]["figure_number"] == 1
assert result["matched_figures"][0]["figure_id"] == "figure_001"
assert result["matched_figures"][0]["figure_namespace"] == "main"
assert figure_legends[0]["inferred_figure_number"] == 1
assert figure_legends[0]["figure_number_source"] == "sequence_gap_inference"
def test_infer_frontmatter_veto() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Graphical Abstract", "legend_block_id": "leg_veto",
"asset_block_ids": ["asset_veto"], "settlement_type": "same_page",
"page": 1, "legend_page": 1, "asset_pages": [1], "legend_bbox": [0, 0, 100, 100]},
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
{"figure_number": 4, "text": "Figure 4.", "legend_block_id": "leg4",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset4"],
"settlement_type": "same_page", "page": 9, "legend_page": 9, "asset_pages": [9]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "accepted"
assert inf["reason"] == "accepted"
assert result["matched_figures"][1]["figure_number"] == 1
assert result["matched_figures"][1]["figure_id"] == "figure_001"
assert result["matched_figures"][0]["figure_number"] is None
assert "number_inference" not in result["matched_figures"][0]
def test_infer_main_supplementary_isolation() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 1, "text": "Figure S1.", "legend_block_id": "leg_s1",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset_s1"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [200, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 9, "legend_page": 9, "asset_pages": [9]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "accepted"
assert inf["reason"] == "accepted"
assert result["matched_figures"][0]["figure_number"] == 1
# S1 unchanged with its original number, not in main known set
assert result["matched_figures"][1]["figure_number"] == 1
assert inf["known_main_numbers"] == [2, 3]
def test_infer_no_eligible_unknowns() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": 1, "text": "Figure 1.", "legend_block_id": "leg1",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset1"],
"settlement_type": "same_page", "page": 1},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 3},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 5},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": [],
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "no_eligible_unknowns"
def test_infer_known_min_not_2() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 1, "legend_page": 1, "asset_pages": [1]},
{"figure_number": 1, "text": "Figure 1.", "legend_block_id": "leg1_known",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset1_known"],
"settlement_type": "same_page", "page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 4, "text": "Figure 4.", "legend_block_id": "leg4",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset4"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "known_min_not_2"
def test_infer_multiple_eligible_unknowns() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 1, "legend_page": 1, "asset_pages": [1]},
{"figure_number": None, "text": "Some figure", "legend_block_id": "leg2",
"asset_block_ids": ["asset2"], "settlement_type": "same_page",
"page": 2, "legend_page": 2, "asset_pages": [2]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2_known",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2_known"],
"settlement_type": "same_page", "page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
{"block_id": "leg2", "bbox": [200, 100, 500, 200], "text": "Some caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "multiple_eligible_unknowns"
def test_infer_missing_legend_bbox() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": [],
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "no_eligible_unknowns"
def test_infer_unknown_not_before_first_known() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 5, "legend_page": 5, "asset_pages": [5]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 7, "legend_page": 7, "asset_pages": [7]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "unknown_not_before_first_known"
def test_infer_duplicate_known_numbers_skips() -> None:
from paperforge.worker.ocr_figures import _infer_missing_main_figure_numbers
matched_figures = [
{"figure_number": None, "text": "Figure 1.", "legend_block_id": "leg1",
"asset_block_ids": ["asset1"], "settlement_type": "same_page",
"page": 1, "legend_page": 1, "asset_pages": [1]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2"],
"settlement_type": "same_page", "page": 3, "legend_page": 3, "asset_pages": [3]},
{"figure_number": 2, "text": "Figure 2.", "legend_block_id": "leg2b",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset2b"],
"settlement_type": "same_page", "page": 4, "legend_page": 4, "asset_pages": [4]},
{"figure_number": 3, "text": "Figure 3.", "legend_block_id": "leg3",
"legend_bbox": [100, 100, 500, 200], "asset_block_ids": ["asset3"],
"settlement_type": "same_page", "page": 5, "legend_page": 5, "asset_pages": [5]},
]
figure_legends = [
{"block_id": "leg1", "bbox": [100, 100, 500, 200], "text": "Figure 1. Test caption"},
]
inventory = {
"matched_figures": matched_figures,
"figure_legends": figure_legends,
"held_figures": [],
"ambiguous_figures": [],
}
result = _infer_missing_main_figure_numbers(inventory, [])
inf = result["figure_number_inference"]
assert inf["status"] == "skipped"
assert inf["reason"] == "duplicate_known_main_numbers"