Classify whether a caption-matched crop is visually usable. This is intentionally conservative. A label/caption match proves identity, but not that the rendered crop contains the figure or table body.
(
*,
kind: str,
page_coverage_ratio: float,
visual_rect_count: int,
visual_body_ratio: float,
paragraph_text_chars: int,
table_body_rows: int,
caption_text_chars: int,
other_caption_labels: list[str] | None = None,
)
| 134 | |
| 135 | |
| 136 | def _classify_visual_quality( |
| 137 | *, |
| 138 | kind: str, |
| 139 | page_coverage_ratio: float, |
| 140 | visual_rect_count: int, |
| 141 | visual_body_ratio: float, |
| 142 | paragraph_text_chars: int, |
| 143 | table_body_rows: int, |
| 144 | caption_text_chars: int, |
| 145 | other_caption_labels: list[str] | None = None, |
| 146 | ) -> dict: |
| 147 | """Classify whether a caption-matched crop is visually usable. |
| 148 | |
| 149 | This is intentionally conservative. A label/caption match proves identity, |
| 150 | but not that the rendered crop contains the figure or table body. |
| 151 | """ |
| 152 | normalized_kind = kind.strip().lower() |
| 153 | other_caption_labels = list(other_caption_labels or []) |
| 154 | reasons: list[str] = [] |
| 155 | |
| 156 | if normalized_kind == "table": |
| 157 | text_per_table_row = paragraph_text_chars / max(1, table_body_rows) |
| 158 | if table_body_rows <= 0: |
| 159 | reasons.append("table_body_missing") |
| 160 | if table_body_rows <= 1 and visual_body_ratio < 0.03 and caption_text_chars >= 40: |
| 161 | reasons.append("caption_only_suspected") |
| 162 | # Dense tables naturally contain many text spans. Treat prose-like text |
| 163 | # as contamination when table structure is weak or the text density is |
| 164 | # far higher than the detected table body can explain. |
| 165 | if paragraph_text_chars >= 450 and ( |
| 166 | table_body_rows <= 2 |
| 167 | or (paragraph_text_chars >= 900 and visual_body_ratio <= 0.03 and text_per_table_row > 90) |
| 168 | or text_per_table_row > 140 |
| 169 | ): |
| 170 | reasons.append("table_text_contamination_suspected") |
| 171 | if other_caption_labels: |
| 172 | reasons.append("multiple_caption_regions_suspected") |
| 173 | status = "reject" if reasons else "usable" |
| 174 | else: |
| 175 | if other_caption_labels: |
| 176 | reasons.append("multiple_caption_regions_suspected") |
| 177 | visual_dominant = visual_rect_count >= 3 and visual_body_ratio >= 0.18 |
| 178 | if paragraph_text_chars >= 450 and not visual_dominant: |
| 179 | reasons.append("large_text_block_suspected") |
| 180 | if page_coverage_ratio >= 0.70 and paragraph_text_chars >= 250: |
| 181 | reasons.append("oversized_page_crop") |
| 182 | if visual_rect_count <= 1 and visual_body_ratio < 0.03: |
| 183 | reasons.append("low_visual_body_ratio") |
| 184 | if any( |
| 185 | code in reasons |
| 186 | for code in ( |
| 187 | "multiple_caption_regions_suspected", |
| 188 | "large_text_block_suspected", |
| 189 | "oversized_page_crop", |
| 190 | "low_visual_body_ratio", |
| 191 | ) |
| 192 | ): |
| 193 | status = "reject" |
no outgoing calls