Suya OCR API — vLLM-backed, OpenAI-compatible OCR service
FastAPI service wrapping the Surya-OCR-2 model (datalab-to) served through vLLM: legacy /v1/api/ai/* endpoints, an OpenAI-compatible /v1/chat/completions endpoint, a coalescing request batcher, a local OCR CLI, Docker packaging, multilingual example outputs, and quantization/concurrency benchmarks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import os
|
||||
import click
|
||||
import copy
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from surya.common.util import expand_bbox
|
||||
from surya.debug.draw import draw_bboxes_on_image
|
||||
from surya.inference import SuryaInferenceManager
|
||||
from surya.layout import LayoutPredictor
|
||||
from surya.logging import configure_logging, get_logger
|
||||
from surya.scripts.config import CLILoader
|
||||
from surya.table_rec import TableRecPredictor
|
||||
|
||||
configure_logging()
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
@click.command(help="Run table recognition on an input file or folder.")
|
||||
@CLILoader.common_options
|
||||
@click.option(
|
||||
"--skip_table_detection",
|
||||
is_flag=True,
|
||||
help="Tables are already cropped, so don't re-detect tables.",
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"--mode",
|
||||
type=click.Choice(["simple", "full"]),
|
||||
default="simple",
|
||||
help="simple: rows+cols only (geometric cells). full: full HTML (BLOCK_PROMPT).",
|
||||
)
|
||||
def table_recognition_cli(
|
||||
input_path: str, skip_table_detection: bool, mode: str, **kwargs
|
||||
):
|
||||
# Layout runs on the low-DPI render; table crops come from the high-DPI
|
||||
# image so the table_rec model sees readable cell content.
|
||||
loader = CLILoader(input_path, kwargs, highres=True)
|
||||
|
||||
manager = SuryaInferenceManager()
|
||||
layout_predictor = LayoutPredictor(manager)
|
||||
table_rec_predictor = TableRecPredictor(manager)
|
||||
|
||||
pnums = []
|
||||
prev_name = None
|
||||
for name in loader.names:
|
||||
if prev_name is None or prev_name != name:
|
||||
pnums.append(0)
|
||||
else:
|
||||
pnums.append(pnums[-1] + 1)
|
||||
prev_name = name
|
||||
|
||||
table_imgs = []
|
||||
table_counts = []
|
||||
table_counts_per_img = []
|
||||
|
||||
if skip_table_detection:
|
||||
for img in loader.highres_images:
|
||||
table_imgs.append(img)
|
||||
table_counts.append(1)
|
||||
table_counts_per_img.append(0)
|
||||
else:
|
||||
layout_predictions = layout_predictor(
|
||||
loader.images,
|
||||
target_image_sizes=[img.size for img in loader.highres_images],
|
||||
)
|
||||
for layout_pred, img in zip(layout_predictions, loader.highres_images):
|
||||
tables_on_page = [
|
||||
line
|
||||
for line in layout_pred.bboxes
|
||||
if line.label in ("Table", "TableOfContents")
|
||||
]
|
||||
table_counts.append(len(tables_on_page))
|
||||
for line in tables_on_page:
|
||||
bbox = expand_bbox(line.bbox)
|
||||
table_imgs.append(img.crop(bbox))
|
||||
table_counts_per_img.append(line.count)
|
||||
|
||||
table_preds = table_rec_predictor(table_imgs, mode=mode)
|
||||
|
||||
img_idx = 0
|
||||
prev_count = 0
|
||||
table_predictions = defaultdict(list)
|
||||
for i in range(sum(table_counts)):
|
||||
while i >= prev_count + table_counts[img_idx]:
|
||||
prev_count += table_counts[img_idx]
|
||||
img_idx += 1
|
||||
|
||||
pred = table_preds[i]
|
||||
orig_name = loader.names[img_idx]
|
||||
pnum = pnums[img_idx]
|
||||
table_img = table_imgs[i]
|
||||
|
||||
out_pred = pred.model_dump()
|
||||
out_pred["page"] = pnum + 1
|
||||
table_idx = i - prev_count
|
||||
out_pred["table_idx"] = table_idx
|
||||
table_predictions[orig_name].append(out_pred)
|
||||
|
||||
if loader.save_images and pred.rows:
|
||||
rows = [line.bbox for line in pred.rows]
|
||||
cols = [line.bbox for line in pred.cols]
|
||||
row_labels = [f"Row {line.row_id}" for line in pred.rows]
|
||||
col_labels = [f"Col {line.col_id}" for line in pred.cols]
|
||||
cells = [line.bbox for line in pred.cells]
|
||||
|
||||
rc_image = copy.deepcopy(table_img)
|
||||
rc_image = draw_bboxes_on_image(
|
||||
rows, rc_image, labels=row_labels, label_font_size=20, color="blue"
|
||||
)
|
||||
rc_image = draw_bboxes_on_image(
|
||||
cols, rc_image, labels=col_labels, label_font_size=20, color="red"
|
||||
)
|
||||
rc_image.save(
|
||||
os.path.join(
|
||||
loader.result_path,
|
||||
f"{orig_name}_page{pnum + 1}_table{table_idx}_rc.png",
|
||||
)
|
||||
)
|
||||
|
||||
cell_image = copy.deepcopy(table_img)
|
||||
cell_image = draw_bboxes_on_image(cells, cell_image, color="green")
|
||||
cell_image.save(
|
||||
os.path.join(
|
||||
loader.result_path,
|
||||
f"{orig_name}_page{pnum + 1}_table{table_idx}_cells.png",
|
||||
)
|
||||
)
|
||||
|
||||
with open(
|
||||
os.path.join(loader.result_path, "results.json"), "w+", encoding="utf-8"
|
||||
) as f:
|
||||
json.dump(table_predictions, f, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Wrote results to {loader.result_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
table_recognition_cli()
|
||||
Reference in New Issue
Block a user