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,66 @@
|
||||
from PIL import ImageDraw, ImageFont
|
||||
|
||||
from surya.debug.fonts import get_font_path
|
||||
from surya.debug.text import get_text_size
|
||||
|
||||
|
||||
def draw_bboxes_on_image(
|
||||
bboxes, image, labels=None, label_font_size=10, color: str | list = "red"
|
||||
):
|
||||
polys = []
|
||||
for bb in bboxes:
|
||||
# Clockwise polygon
|
||||
poly = [[bb[0], bb[1]], [bb[2], bb[1]], [bb[2], bb[3]], [bb[0], bb[3]]]
|
||||
polys.append(poly)
|
||||
|
||||
return draw_polys_on_image(
|
||||
polys, image, labels, label_font_size=label_font_size, color=color
|
||||
)
|
||||
|
||||
|
||||
def draw_polys_on_image(
|
||||
corners,
|
||||
image,
|
||||
labels=None,
|
||||
box_padding=-1,
|
||||
label_offset=1,
|
||||
label_font_size=10,
|
||||
color: str | list = "red",
|
||||
):
|
||||
draw = ImageDraw.Draw(image)
|
||||
font_path = get_font_path()
|
||||
label_font = ImageFont.truetype(font_path, label_font_size)
|
||||
|
||||
for i in range(len(corners)):
|
||||
poly = corners[i]
|
||||
poly = [(int(p[0]), int(p[1])) for p in poly]
|
||||
draw.polygon(
|
||||
poly, outline=color[i] if isinstance(color, list) else color, width=1
|
||||
)
|
||||
|
||||
if labels is not None:
|
||||
label = labels[i]
|
||||
text_position = (
|
||||
min([p[0] for p in poly]) + label_offset,
|
||||
min([p[1] for p in poly]) + label_offset,
|
||||
)
|
||||
text_size = get_text_size(label, label_font)
|
||||
box_position = (
|
||||
text_position[0] - box_padding + label_offset,
|
||||
text_position[1] - box_padding + label_offset,
|
||||
text_position[0] + text_size[0] + box_padding + label_offset,
|
||||
text_position[1] + text_size[1] + box_padding + label_offset,
|
||||
)
|
||||
try:
|
||||
draw.rectangle(box_position, fill="white")
|
||||
except Exception as e:
|
||||
print(f"Error drawing rectangle at {box_position}: {e}")
|
||||
continue
|
||||
draw.text(
|
||||
text_position,
|
||||
label,
|
||||
fill=color[i] if isinstance(color, list) else color,
|
||||
font=label_font,
|
||||
)
|
||||
|
||||
return image
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import requests
|
||||
|
||||
from surya.settings import settings
|
||||
|
||||
|
||||
def get_font_path(langs: Optional[List[str]] = None) -> str:
|
||||
font_path = settings.RECOGNITION_RENDER_FONTS["all"]
|
||||
if langs is not None:
|
||||
for k in settings.RECOGNITION_RENDER_FONTS:
|
||||
if k in langs and len(langs) == 1:
|
||||
font_path = settings.RECOGNITION_RENDER_FONTS[k]
|
||||
break
|
||||
|
||||
if not os.path.exists(font_path):
|
||||
os.makedirs(os.path.dirname(font_path), exist_ok=True)
|
||||
font_dl_path = f"{settings.RECOGNITION_FONT_DL_BASE}/{os.path.basename(font_path)}"
|
||||
with requests.get(font_dl_path, stream=True) as r, open(font_path, 'wb') as f:
|
||||
r.raise_for_status()
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
return font_path
|
||||
@@ -0,0 +1,64 @@
|
||||
<style>
|
||||
.katex-display-container {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.katex-inline-container {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js" onload="setTimeout(function() {renderMath()})" async></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
|
||||
<script>
|
||||
function htmlUnescape(escapedText) {
|
||||
const htmlEntities = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' '
|
||||
};
|
||||
|
||||
return escapedText.replace(/&|<|>|"|'| /g, match => htmlEntities[match]);
|
||||
}
|
||||
|
||||
const renderMath = (function() {
|
||||
try {
|
||||
const mathElements = document.querySelectorAll('math');
|
||||
|
||||
mathElements.forEach(function(element) {
|
||||
let mathContent = element.innerHTML.trim();
|
||||
mathContent = htmlUnescape(mathContent);
|
||||
const isDisplay = element.getAttribute('display') === 'block';
|
||||
|
||||
const container = document.createElement('span');
|
||||
container.className = isDisplay ? 'katex-display-container' : 'katex-inline-container';
|
||||
element.parentNode.insertBefore(container, element);
|
||||
|
||||
try {
|
||||
katex.render(mathContent, container, {
|
||||
displayMode: isDisplay,
|
||||
throwOnError: false
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('KaTeX rendering error:', err);
|
||||
container.textContent = mathContent; // Fallback to raw text
|
||||
}
|
||||
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
|
||||
console.log('Math rendering complete with', mathElements.length, 'expressions');
|
||||
} catch (err) {
|
||||
console.error('Error in renderMath function:', err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
import html as htmllib
|
||||
import os.path
|
||||
import re
|
||||
|
||||
filepath = os.path.abspath(__file__)
|
||||
|
||||
def render_text_as_html(
|
||||
bboxes: list[list[int]],
|
||||
texts: list[str],
|
||||
image_size: tuple[int, int],
|
||||
base_font_size: int = 16,
|
||||
scaler: int = 2
|
||||
):
|
||||
katex_path = os.path.join(os.path.dirname(filepath), "katex.js")
|
||||
with open(katex_path, "r") as f:
|
||||
katex_script = f.read()
|
||||
|
||||
html_content = []
|
||||
image_size = tuple([int(s * scaler) for s in image_size])
|
||||
width, height = image_size
|
||||
|
||||
|
||||
html_content.append(f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: {width}px;
|
||||
height: {height}px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
color: black;
|
||||
}}
|
||||
.text-box {{
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
font-family: Arial, sans-serif;
|
||||
white-space: pre-wrap;
|
||||
}}
|
||||
.vertical-text {{
|
||||
writing-mode: vertical-rl; /* Top to bottom, right to left */
|
||||
}}
|
||||
</style>
|
||||
{katex_script}
|
||||
</head>
|
||||
<body>
|
||||
""")
|
||||
|
||||
for i, (bbox, text) in enumerate(zip(bboxes, texts)):
|
||||
bbox = bbox.copy()
|
||||
bbox = [int(bb * scaler) for bb in bbox]
|
||||
x1, y1, x2, y2 = bbox
|
||||
width = x2 - x1
|
||||
height = y2 - y1
|
||||
min_dim = min(width, height)
|
||||
|
||||
# Scale font size based on box height
|
||||
font_size = min(int(min_dim * 0.75), base_font_size)
|
||||
|
||||
# Create div with absolute positioning
|
||||
div_style = (
|
||||
f"left: {x1}px; "
|
||||
f"top: {y1}px; "
|
||||
f"width: {width}px; "
|
||||
f"height: {height}px; "
|
||||
f"font-size: {font_size}px;"
|
||||
)
|
||||
|
||||
class_ = "text-box"
|
||||
if height > width * 2:
|
||||
class_ += " vertical-text"
|
||||
|
||||
# Determine if content is HTML/MathML or plain text
|
||||
if "<" in text and ">" in text and re.search(r"<(html|math|div|sub|sup|i|u|mark|small|del|b|br|code)\b", text.lower()):
|
||||
# Content is already HTML/MathML, include as-is
|
||||
html_content.append(f'<span class="{class_}" id="box-{i}" style="{div_style}">{text}</span>')
|
||||
else:
|
||||
# Plain text, escape it
|
||||
escaped_text = htmllib.escape(text)
|
||||
html_content.append(f'<span class="{class_}" id="box-{i}" style="{div_style}">{escaped_text}</span>')
|
||||
|
||||
html_content.append("</body></html>")
|
||||
|
||||
return "\n".join(html_content), image_size
|
||||
@@ -0,0 +1,8 @@
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def get_text_size(text, font):
|
||||
im = Image.new(mode="P", size=(0, 0))
|
||||
draw = ImageDraw.Draw(im)
|
||||
_, _, width, height = draw.textbbox((0, 0), text=text, font=font)
|
||||
return width, height
|
||||
Reference in New Issue
Block a user