From bd3f3b63e96c7fcd843e491de37570f91035e65b Mon Sep 17 00:00:00 2001 From: LLLin000 <809867916@qq.com> Date: Thu, 2 Jul 2026 14:42:59 +0800 Subject: [PATCH] fix: sidecar figure caption preservation + rotated table caption matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 37LK5T97 had two bugs: 1. Figure 1 sidecar demotion (ocr_document.py): - caption in left column (246px wide), image in right column - _is_near_figure_media uses h_overlap which fails for adjacent columns - candidate_resolution demoted the caption as 'narrative prose' - Fix: add _is_sidecar_candidate check — if figure_caption_candidate has vertical overlap with a media_asset but no horizontal overlap (adjacent columns), skip the prose-based demotion. - New narrow helper, does not change _is_near_figure_media (avoids regression in vision_footnote routing) 2. Rotated table caption matching (ocr_scores.py): - Tables 1-3 had rotated captions (dir=[0,-1], vertical text) - score_table_match only checked x_overlap + asset_below_caption - Rotated captions are beside the table body, not above it - Fix: when caption has rotated text and x_overlap < 0.5, use adjacent-column gap check + y_overlap ratio instead - Non-rotated captions unaffected (elif only fires for rotated) Result: 37LK5T97 Figure 1 matched (figure_001, asset=block_id=9). All 6 tables (3 rotated + 1 continuation x2 + 1 normal) matched. 428 regression tests pass. --- paperforge/worker/ocr_document.py | 25 +++++++++++++++++++++++++ paperforge/worker/ocr_scores.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/paperforge/worker/ocr_document.py b/paperforge/worker/ocr_document.py index 6b1132f7..1d931533 100644 --- a/paperforge/worker/ocr_document.py +++ b/paperforge/worker/ocr_document.py @@ -4332,6 +4332,27 @@ def _check_caption_style_match(block: dict, blocks: list[dict]) -> bool: return size_match and font_match +def _is_sidecar_candidate(block: dict, page_blocks: list[dict]) -> bool: + """Check if a figure_caption_candidate is a sidecar: no x_overlap but y_overlap + with a media_asset on the same page (adjacent columns layout).""" + bb = block.get("bbox") or block.get("block_bbox") or [0, 0, 0, 0] + if len(bb) < 4: + return False + bx1, by1, bx2, by2 = bb[:4] + for other in page_blocks: + label = str(other.get("block_label") or other.get("raw_label") or "").strip() + if label not in {"image", "chart", "figure"}: + continue + ob = other.get("bbox") or other.get("block_bbox") or [] + if len(ob) < 4: + continue + ox1, oy1, ox2, oy2 = ob[:4] + # Adjacent columns: no horizontal overlap but vertical overlap + if not (bx1 < ox2 and ox1 < bx2) and (by1 < oy2 and oy1 < by2): + return True + return False + + def _resolve_ambiguous_candidates( blocks: list[dict], doc_structure: DocumentStructure, @@ -4535,6 +4556,7 @@ def _resolve_ambiguous_candidates( reason="backmatter heading candidate in backmatter region, promoted to backmatter heading", ) + # ---- 2.2 Resolve figure_caption_candidate ---- if role == "figure_caption_candidate": text = block.get("text", "") or "" @@ -4556,6 +4578,9 @@ def _resolve_ambiguous_candidates( if ( in_body_spine and is_prose + # Sidecar figure captions (caption in one column, image adjacent) + # must survive prose detection — they are long by nature. + and not _is_sidecar_candidate(block, page_blocks) and not (zone == "display_zone" and style_family == "legend_like" and raw_label == "figure_title") # Vision-footnote figure descriptions already validated by dedicated rescue and not (raw_label == "vision_footnote" and _looks_like_figure_description_opening(text)) diff --git a/paperforge/worker/ocr_scores.py b/paperforge/worker/ocr_scores.py index 77a808ae..5c71ebf8 100644 --- a/paperforge/worker/ocr_scores.py +++ b/paperforge/worker/ocr_scores.py @@ -65,6 +65,37 @@ def score_table_match(caption: dict, asset: dict, *, is_continuation: bool = Fal if _bbox_x_overlap_ratio(caption_bbox, asset_bbox) >= 0.5: score += 0.25 evidence.append("x_overlap") + elif len(caption_bbox) >= 4 and len(asset_bbox) >= 4: + # Check if caption has rotated text (dir != [1,0]) — vertical caption + # placed beside the table body, not above it. + span = caption.get("span_metadata") or [] + if isinstance(span, dict): + span = [span] + _is_rotated = False + if isinstance(span, list): + for _entry in span: + if not isinstance(_entry, dict): + continue + _dir = _entry.get("dir") + if isinstance(_dir, (list, tuple)) and len(_dir) == 2: + if abs(float(_dir[1])) > abs(float(_dir[0])): + _is_rotated = True + break + if _is_rotated: + # Adjacent-column check: caption beside table, no x_overlap + _gap = max(0.0, asset_bbox[0] - caption_bbox[2], caption_bbox[0] - asset_bbox[2]) + _narrow_w = min(caption_bbox[2] - caption_bbox[0], asset_bbox[2] - asset_bbox[0]) + if _gap < _narrow_w * 0.3 and _gap < 80: + score += 0.20 + evidence.append("adjacent_x") + # y_overlap: caption and table share vertical band + _by1, _by2 = caption_bbox[1], caption_bbox[3] + _oy1, _oy2 = asset_bbox[1], asset_bbox[3] + _y_overlap = max(0.0, min(_by2, _oy2) - max(_by1, _oy1)) + _min_span = min(_by2 - _by1, _oy2 - _oy1) + if _min_span > 0 and _y_overlap / _min_span >= 0.3: + score += 0.15 + evidence.append("y_overlap_with_asset") if len(caption_bbox) >= 4 and len(asset_bbox) >= 4: if same_page and asset_bbox[1] >= caption_bbox[3]: score += 0.25