Files
wehub-resource-sync 925e56bb5f
Unit tests / build (t4_gpu) (push) Has been cancelled
Unit tests / build (ubuntu-latest) (push) Has been cancelled
Unit tests / build (windows-latest) (push) Has been cancelled
Test CLI scripts / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:56 +08:00

138 lines
5.2 KiB
Python

from __future__ import annotations
from typing import List, Optional
from PIL import Image
from surya.common.blank import is_blank_region
from surya.inference import SuryaInferenceManager, get_default_manager
from surya.inference.parsers import denorm_bbox, parse_layout
from surya.inference.prompts import LAYOUT_JSON_SCHEMA, PROMPT_TYPE_LAYOUT
from surya.inference.schema import BatchInputItem
from surya.layout.label import LAYOUT_PRED_RELABEL, TEXT_LABELS
from surya.layout.schema import LayoutBox, LayoutResult
from surya.logging import get_logger
from surya.settings import settings
logger = get_logger()
class LayoutPredictor:
"""Run LAYOUT_PROMPT on full pages, parse JSON, return LayoutResult per image."""
def __init__(self, manager: Optional[SuryaInferenceManager] = None):
self.manager = manager # If None, get_default_manager() is used at call time
self._disable_tqdm = settings.DISABLE_TQDM
@property
def disable_tqdm(self) -> bool:
return self._disable_tqdm
@disable_tqdm.setter
def disable_tqdm(self, value: bool) -> None:
self._disable_tqdm = bool(value)
def to(self, *args, **kwargs):
# Manager-backed; .to() is a no-op for compatibility with BasePredictor callers.
return
def __call__(
self,
images: List[Image.Image],
target_image_sizes: Optional[List[tuple]] = None,
max_tokens: Optional[int] = None,
) -> List[LayoutResult]:
"""Run layout on a batch of images.
target_image_sizes: optional list of (width, height) tuples — if
provided, bboxes are denormalized to these sizes instead of each
input image's size. Useful when layout runs on a low-DPI render but
you want bboxes in the OCR image's coordinate space.
"""
if not images:
return []
manager = self.manager or get_default_manager()
max_tokens = max_tokens or settings.SURYA_MAX_TOKENS_LAYOUT
guided = LAYOUT_JSON_SCHEMA if settings.SURYA_GUIDED_LAYOUT else None
batch = [
BatchInputItem(
image=img,
prompt_type=PROMPT_TYPE_LAYOUT,
max_tokens=max_tokens,
guided_json=guided,
)
for img in images
]
outputs = manager.generate(batch)
if target_image_sizes is not None and len(target_image_sizes) != len(images):
raise ValueError("target_image_sizes must match images length")
results: List[LayoutResult] = []
for idx, (img, out) in enumerate(zip(images, outputs)):
if target_image_sizes is not None:
w, h = target_image_sizes[idx]
else:
w, h = img.size
page_bbox = [0, 0, float(w), float(h)]
if out.error or not out.raw:
results.append(
LayoutResult(
bboxes=[], image_bbox=page_bbox, raw=out.raw, error=True
)
)
continue
try:
parsed = parse_layout(out.raw)
except Exception as e:
logger.warning(f"Layout parse failed: {e}; raw[:300]={out.raw[:300]!r}")
results.append(
LayoutResult(
bboxes=[], image_bbox=page_bbox, raw=out.raw, error=True
)
)
continue
confidence = out.mean_token_prob if out.mean_token_prob is not None else 1.0
img_w, img_h = img.size
boxes: List[LayoutBox] = []
dropped_blank = 0
for blk in parsed:
canon = LAYOUT_PRED_RELABEL.get(blk.label, blk.label)
# Drop text-labeled blocks the model hallucinated over an
# essentially-blank region (mostly white OR near-uniform
# color). Visual blocks (Picture / Figure / Table / etc.)
# are allowed to be uniform — that's normal content.
if canon in TEXT_LABELS:
img_bbox = denorm_bbox(
blk.bbox, img_w, img_h, scale=settings.BBOX_SCALE
)
x0, y0, x1, y1 = (max(0, int(v)) for v in img_bbox)
if x1 > x0 and y1 > y0:
if is_blank_region(img.crop((x0, y0, x1, y1))):
dropped_blank += 1
continue
pixel_bbox = denorm_bbox(blk.bbox, w, h, scale=settings.BBOX_SCALE)
boxes.append(
LayoutBox(
polygon=list(pixel_bbox),
label=canon,
raw_label=blk.label,
position=len(boxes),
count=blk.count,
confidence=confidence,
)
)
if dropped_blank:
logger.info(
f"dropped {dropped_blank} text-labeled layout block(s) over "
f"blank/uniform regions"
)
results.append(
LayoutResult(
bboxes=boxes, image_bbox=page_bbox, raw=out.raw, error=False
)
)
return results