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

218 lines
7.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""TableRecPredictor: dual-path table structure recognition.
- predict_simple: TABLE_REC_PROMPT → rows + columns only, cells derived
geometrically (row × column intersections).
- predict_full: BLOCK_PROMPT on the table crop → full <table> HTML with
colspan / rowspan / <th>. The HTML lives on TableResult.html for marker to
consume directly.
"""
from __future__ import annotations
from typing import List, Optional
from PIL import Image
from surya.inference import SuryaInferenceManager, get_default_manager
from surya.inference.parsers import clean_block_html, denorm_bbox, parse_table_rec
from surya.inference.prompts import (
PROMPT_TYPE_BLOCK,
PROMPT_TYPE_TABLE_REC,
TABLE_REC_JSON_SCHEMA,
)
from surya.inference.schema import BatchInputItem
from surya.inference.util import image_token_budget
from surya.logging import get_logger
from surya.settings import settings
from surya.table_rec.schema import TableCell, TableCol, TableResult, TableRow
logger = get_logger()
def _polygon_from_bbox(bbox):
x0, y0, x1, y1 = bbox
return [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
def _intersect_bbox(a, b):
x0 = max(a[0], b[0])
y0 = max(a[1], b[1])
x1 = min(a[2], b[2])
y1 = min(a[3], b[3])
if x1 <= x0 or y1 <= y0:
return None
return (x0, y0, x1, y1)
class TableRecPredictor:
def __init__(self, manager: Optional[SuryaInferenceManager] = None):
self.manager = manager
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):
return
def __call__(
self, images: List[Image.Image], mode: str = "simple"
) -> List[TableResult]:
if mode == "full":
return self.predict_full(images)
return self.predict_simple(images)
def predict_simple(self, images: List[Image.Image]) -> List[TableResult]:
if not images:
return []
manager = self.manager or get_default_manager()
guided = TABLE_REC_JSON_SCHEMA if settings.SURYA_GUIDED_TABLE_REC else None
batch = [
BatchInputItem(
image=img,
prompt_type=PROMPT_TYPE_TABLE_REC,
max_tokens=settings.SURYA_MAX_TOKENS_TABLE_REC,
guided_json=guided,
)
for img in images
]
outputs = manager.generate(batch)
results: List[TableResult] = []
for img, out in zip(images, outputs):
w, h = img.size
page_bbox = [0, 0, float(w), float(h)]
if out.error or not out.raw:
results.append(
TableResult(
rows=[],
cols=[],
cells=[],
image_bbox=page_bbox,
raw=out.raw,
mode="simple",
error=True,
)
)
continue
try:
elements = parse_table_rec(out.raw)
except Exception as e:
logger.warning(
f"Table rec parse failed: {e}; raw[:200]={out.raw[:200]!r}"
)
results.append(
TableResult(
rows=[],
cols=[],
cells=[],
image_bbox=page_bbox,
raw=out.raw,
mode="simple",
error=True,
)
)
continue
rows: List[TableRow] = []
cols: List[TableCol] = []
for el in elements:
pixel_bbox = denorm_bbox(el.bbox, w, h, scale=settings.BBOX_SCALE)
poly = _polygon_from_bbox(pixel_bbox)
if el.label == "Row":
rows.append(TableRow(polygon=poly, row_id=len(rows)))
else:
cols.append(TableCol(polygon=poly, col_id=len(cols)))
# Derive cells geometrically (row × column intersections)
cells: List[TableCell] = []
cell_id = 0
for row in rows:
for col in cols:
inter = _intersect_bbox(row.bbox, col.bbox)
if inter is None:
continue
cells.append(
TableCell(
polygon=_polygon_from_bbox(inter),
row_id=row.row_id,
col_id=col.col_id,
cell_id=cell_id,
)
)
cell_id += 1
results.append(
TableResult(
rows=rows,
cols=cols,
cells=cells,
image_bbox=page_bbox,
raw=out.raw,
mode="simple",
error=False,
)
)
return results
def predict_full(
self, images: List[Image.Image], counts: Optional[List[int]] = None
) -> List[TableResult]:
"""Full-HTML path: BLOCK_PROMPT on table crops. Use when complex
structure (spanning cells, headers) matters and ground-truth-style
HTML is preferred. `counts` (one per image) shapes max_tokens."""
if not images:
return []
manager = self.manager or get_default_manager()
if counts is None:
counts = [0] * len(images)
batch = []
for img, count in zip(images, counts):
batch.append(
BatchInputItem(
image=img,
prompt_type=PROMPT_TYPE_BLOCK,
max_tokens=image_token_budget(
count,
ceiling=settings.SURYA_MAX_TOKENS_BLOCK_CEILING,
floor=1024,
),
)
)
outputs = manager.generate(batch)
results: List[TableResult] = []
for img, out in zip(images, outputs):
w, h = img.size
page_bbox = [0, 0, float(w), float(h)]
if out.error:
results.append(
TableResult(
rows=[],
cols=[],
cells=[],
image_bbox=page_bbox,
raw=out.raw,
mode="full",
error=True,
)
)
continue
html = clean_block_html(out.raw)
results.append(
TableResult(
rows=[],
cols=[],
cells=[],
image_bbox=page_bbox,
raw=out.raw,
html=html,
mode="full",
error=False,
)
)
return results