""" Surya OCR 2 — Gradio UI CPU inference via llama.cpp backend. """ import os import json import re import tempfile from pathlib import Path # Must be set before importing surya os.environ.setdefault("SURYA_INFERENCE_BACKEND", "llamacpp") os.environ.setdefault("HF_HOME", "/tmp/hf_cache") import gradio as gr from PIL import Image, ImageDraw, ImageFont # ── Label → colour map ──────────────────────────────────────────────────────── LABEL_COLORS: dict[str, str] = { "Text": "#2196F3", "SectionHeader": "#9C27B0", "Table": "#FF9800", "Equation": "#F44336", "Picture": "#4CAF50", "Figure": "#4CAF50", "Form": "#00BCD4", "PageHeader": "#607D8B", "PageFooter": "#607D8B", "ListGroup": "#8BC34A", "Caption": "#795548", "Footnote": "#9E9E9E", "Code": "#FF5722", "TableOfContents": "#3F51B5", "Bibliography": "#795548", } # ── Lazy-loaded inference manager (spawns llama-server once) ───────────────── _manager = None def get_manager(): global _manager if _manager is None: from surya.inference import SuryaInferenceManager # noqa: PLC0415 _manager = SuryaInferenceManager() return _manager # ── PDF / image helpers ─────────────────────────────────────────────────────── def pdf_to_images(pdf_path: str, dpi: int = 150) -> list[Image.Image]: import fitz # PyMuPDF doc = fitz.open(pdf_path) scale = dpi / 72.0 mat = fitz.Matrix(scale, scale) images = [] for page in doc: pix = page.get_pixmap(matrix=mat, alpha=False) img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) images.append(img) doc.close() return images def parse_page_range(spec: str, total: int) -> list[int]: """Convert a page-range string like '0,2-5,8' into a sorted list of indices.""" if not spec.strip(): return list(range(total)) indices: set[int] = set() for part in spec.split(","): part = part.strip() m = re.match(r"^(\d+)-(\d+)$", part) if m: indices.update(range(int(m.group(1)), int(m.group(2)) + 1)) elif part.isdigit(): indices.add(int(part)) return sorted(i for i in indices if 0 <= i < total) def load_images(file_path: str, page_range_str: str) -> list[Image.Image]: if not file_path: return [] suffix = Path(file_path).suffix.lower() if suffix == ".pdf": all_pages = pdf_to_images(file_path) indices = parse_page_range(page_range_str, len(all_pages)) return [all_pages[i] for i in indices] else: return [Image.open(file_path).convert("RGB")] # ── Drawing helpers ─────────────────────────────────────────────────────────── def draw_boxes(img: Image.Image, boxes: list[dict], show_labels: bool = True) -> Image.Image: out = img.copy() draw = ImageDraw.Draw(out, "RGBA") try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 14) except Exception: font = ImageFont.load_default() for box in boxes: bbox = box.get("bbox") label = box.get("label", "?") order = box.get("reading_order") if not bbox: continue color = LABEL_COLORS.get(label, "#888888") r, g, b = int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16) draw.rectangle(bbox, outline=(r, g, b, 255), width=2) draw.rectangle(bbox, fill=(r, g, b, 25)) if show_labels: tag = f"{order}:{label}" if order is not None else label tx, ty = bbox[0] + 2, max(0, bbox[1] - 16) draw.rectangle([tx - 1, ty - 1, tx + len(tag) * 8, ty + 16], fill=(r, g, b, 200)) draw.text((tx, ty), tag, fill="white", font=font) return out # ── Strip HTML to plain text ────────────────────────────────────────────────── def html_to_text(html: str) -> str: text = re.sub(r"<[^>]+>", " ", html or "") return re.sub(r"\s+", " ", text).strip() # ── OCR ─────────────────────────────────────────────────────────────────────── def run_ocr(file_obj, page_range: str, gr_progress=gr.Progress()): if file_obj is None: return "Upload a file first.", "", "{}", [] file_path = file_obj.name if hasattr(file_obj, "name") else file_obj gr_progress(0.05, desc="Loading pages…") images = load_images(file_path, page_range) if not images: return "No pages found.", "", "{}", [] gr_progress(0.15, desc=f"Starting inference on {len(images)} page(s)…") manager = get_manager() from surya.recognition import RecognitionPredictor # noqa: PLC0415 predictor = RecognitionPredictor(manager) text_pages, html_pages, json_pages, annotated = [], [], [], [] for idx, img in enumerate(images): gr_progress( 0.15 + 0.80 * (idx / len(images)), desc=f"OCR: page {idx + 1}/{len(images)}…", ) preds = predictor([img]) pred = preds[0] blocks_raw = [] text_chunks, html_chunks = [], [] for blk in getattr(pred, "blocks", []): if getattr(blk, "skipped", False): continue blk_html = getattr(blk, "html", "") or "" blk_text = html_to_text(blk_html) blk_label = getattr(blk, "label", "Text") blk_bbox = getattr(blk, "bbox", None) blk_conf = getattr(blk, "confidence", 1.0) blk_order = getattr(blk, "reading_order", None) if blk_text: text_chunks.append(blk_text) if blk_html: html_chunks.append(blk_html) blocks_raw.append({ "label": blk_label, "reading_order": blk_order, "text": blk_text, "html": blk_html, "bbox": blk_bbox, "confidence": round(float(blk_conf), 3), }) text_pages.append(f"═══ Page {idx + 1} ═══\n" + "\n\n".join(text_chunks)) html_pages.append(f"\n" + "\n".join(html_chunks)) json_pages.append({"page": idx + 1, "blocks": blocks_raw}) annotated.append(draw_boxes(img, blocks_raw)) gr_progress(1.0, desc="Done!") return ( "\n\n".join(text_pages), "\n\n".join(html_pages), json.dumps(json_pages, indent=2, ensure_ascii=False), annotated, ) # ── Layout analysis ─────────────────────────────────────────────────────────── def run_layout(file_obj, page_range: str, gr_progress=gr.Progress()): if file_obj is None: return "Upload a file first.", "{}", [] file_path = file_obj.name if hasattr(file_obj, "name") else file_obj gr_progress(0.05, desc="Loading pages…") images = load_images(file_path, page_range) if not images: return "No pages found.", "{}", [] gr_progress(0.15, desc="Running layout analysis…") manager = get_manager() from surya.layout import LayoutPredictor # noqa: PLC0415 predictor = LayoutPredictor(manager) predictions = predictor(images) summary_parts, json_pages, annotated = [], [], [] for idx, (img, pred) in enumerate(zip(images, predictions)): bboxes = getattr(pred, "bboxes", []) counts: dict[str, int] = {} boxes_raw = [] for box in bboxes: label = getattr(box, "label", "Unknown") bbox = getattr(box, "bbox", None) position = getattr(box, "position", None) confidence = getattr(box, "confidence", 1.0) counts[label] = counts.get(label, 0) + 1 boxes_raw.append({ "label": label, "reading_order": position, "bbox": bbox, "confidence": round(float(confidence), 3), }) lines = [f" {k}: {v}" for k, v in sorted(counts.items())] summary_parts.append(f"═══ Page {idx + 1} ═══\n" + "\n".join(lines)) json_pages.append({"page": idx + 1, "layout": boxes_raw}) annotated.append(draw_boxes(img, boxes_raw)) gr_progress(1.0, desc="Done!") return ( "\n\n".join(summary_parts), json.dumps(json_pages, indent=2), annotated, ) # ── Table recognition ───────────────────────────────────────────────────────── def run_table_rec(file_obj, page_range: str, gr_progress=gr.Progress()): if file_obj is None: return "Upload a file first.", "{}" file_path = file_obj.name if hasattr(file_obj, "name") else file_obj gr_progress(0.05, desc="Loading pages…") images = load_images(file_path, page_range) if not images: return "No pages found.", "{}" gr_progress(0.15, desc="Running table recognition…") manager = get_manager() from surya.table_rec import TableRecPredictor # noqa: PLC0415 predictor = TableRecPredictor(manager) predictions = predictor(images) html_parts, json_pages = [], [] for idx, pred in enumerate(predictions): tbl_html = getattr(pred, "html", "") or "" rows = len(getattr(pred, "rows", [])) cols = len(getattr(pred, "cols", [])) html_parts.append( f"
No table detected.
") ) json_pages.append({"page": idx + 1, "rows": rows, "cols": cols, "html": tbl_html}) gr_progress(1.0, desc="Done!") return ( "State-of-the-art document intelligence · 650M params · CPU inference