Spaces:
Build error
Build error
| """ | |
| 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"<!-- Page {idx + 1} -->\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"<h3 style='margin:8px 0'>Page {idx + 1}</h3>" | |
| + (tbl_html if tbl_html else "<p><em>No table detected.</em></p>") | |
| ) | |
| json_pages.append({"page": idx + 1, "rows": rows, "cols": cols, "html": tbl_html}) | |
| gr_progress(1.0, desc="Done!") | |
| return ( | |
| "<div style='font-family:sans-serif'>" + "\n".join(html_parts) + "</div>", | |
| json.dumps(json_pages, indent=2), | |
| ) | |
| # ββ JSON download helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_json(json_str: str): | |
| if not json_str or json_str in ("{}", ""): | |
| return None | |
| tmp = tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") | |
| tmp.write(json_str) | |
| tmp.close() | |
| return tmp.name | |
| def save_text(text: str): | |
| if not text: | |
| return None | |
| tmp = tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") | |
| tmp.write(text) | |
| tmp.close() | |
| return tmp.name | |
| # ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| #header { text-align: center; margin-bottom: 4px; } | |
| #header h1 { font-size: 2.2rem; font-weight: 700; } | |
| #header p { color: #6b7280; font-size: 1rem; } | |
| .warn-box { background:#fef3c7; border:1px solid #f59e0b; border-radius:8px; | |
| padding:10px 14px; color:#92400e; font-size:0.9rem; } | |
| """ | |
| INTRO_MD = """ | |
| <div id="header"> | |
| <h1>π Surya OCR 2</h1> | |
| <p>State-of-the-art document intelligence Β· 650M params Β· CPU inference</p> | |
| </div> | |
| """ | |
| WARN_MD = """ | |
| <div class="warn-box"> | |
| β³ <strong>CPU mode:</strong> The model downloads on first run (~400 MB GGUF). | |
| Inference is ~0.1 pages/second β a 5-page PDF takes ~50 s. | |
| Subsequent runs are faster once the model is cached. | |
| </div> | |
| """ | |
| with gr.Blocks(title="Surya OCR 2", css=CSS, theme=gr.themes.Soft()) as demo: | |
| gr.HTML(INTRO_MD) | |
| gr.HTML(WARN_MD) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| file_input = gr.File( | |
| label="Upload PDF or Image", | |
| file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp", ".webp"], | |
| ) | |
| page_range_input = gr.Textbox( | |
| label="Page range (PDF only, 0-indexed)", | |
| placeholder="e.g. 0,2-5 β leave blank for all pages", | |
| value="", | |
| ) | |
| with gr.Column(scale=2): | |
| with gr.Tabs(): | |
| # ββ Tab 1: Full OCR ββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Full OCR"): | |
| ocr_btn = gr.Button("Run Full OCR", variant="primary", size="lg") | |
| with gr.Tabs(): | |
| with gr.Tab("Text"): | |
| ocr_text_out = gr.Textbox( | |
| label="Extracted text", | |
| lines=20, | |
| show_copy_button=True, | |
| ) | |
| ocr_text_dl = gr.File(label="Download .txt", interactive=False) | |
| with gr.Tab("HTML"): | |
| ocr_html_out = gr.HTML(label="HTML output") | |
| with gr.Tab("JSON"): | |
| ocr_json_out = gr.Code( | |
| label="Structured JSON", language="json", lines=20 | |
| ) | |
| ocr_json_dl = gr.File(label="Download .json", interactive=False) | |
| with gr.Tab("Annotated pages"): | |
| ocr_gallery = gr.Gallery( | |
| label="Pages with detected blocks", | |
| columns=2, | |
| height="auto", | |
| ) | |
| def _ocr(file, pr, prog=gr.Progress()): | |
| text, html, js, imgs = run_ocr(file, pr, prog) | |
| return text, html, js, imgs, save_text(text), save_json(js) | |
| ocr_btn.click( | |
| _ocr, | |
| inputs=[file_input, page_range_input], | |
| outputs=[ | |
| ocr_text_out, ocr_html_out, ocr_json_out, | |
| ocr_gallery, ocr_text_dl, ocr_json_dl, | |
| ], | |
| ) | |
| # ββ Tab 2: Layout analysis βββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Layout Analysis"): | |
| lay_btn = gr.Button("Analyse Layout", variant="primary", size="lg") | |
| with gr.Tabs(): | |
| with gr.Tab("Summary"): | |
| lay_summary_out = gr.Textbox( | |
| label="Layout summary", | |
| lines=15, | |
| show_copy_button=True, | |
| ) | |
| with gr.Tab("JSON"): | |
| lay_json_out = gr.Code( | |
| label="Layout JSON", language="json", lines=20 | |
| ) | |
| lay_json_dl = gr.File(label="Download .json", interactive=False) | |
| with gr.Tab("Annotated pages"): | |
| lay_gallery = gr.Gallery( | |
| label="Pages with layout boxes", | |
| columns=2, | |
| height="auto", | |
| ) | |
| def _layout(file, pr, prog=gr.Progress()): | |
| summary, js, imgs = run_layout(file, pr, prog) | |
| return summary, js, imgs, save_json(js) | |
| lay_btn.click( | |
| _layout, | |
| inputs=[file_input, page_range_input], | |
| outputs=[lay_summary_out, lay_json_out, lay_gallery, lay_json_dl], | |
| ) | |
| # ββ Tab 3: Table recognition βββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Table Recognition"): | |
| tbl_btn = gr.Button("Recognise Tables", variant="primary", size="lg") | |
| with gr.Tabs(): | |
| with gr.Tab("HTML table"): | |
| tbl_html_out = gr.HTML(label="Extracted tables") | |
| with gr.Tab("JSON"): | |
| tbl_json_out = gr.Code( | |
| label="Table JSON", language="json", lines=15 | |
| ) | |
| tbl_json_dl = gr.File(label="Download .json", interactive=False) | |
| def _table(file, pr, prog=gr.Progress()): | |
| html, js = run_table_rec(file, pr, prog) | |
| return html, js, save_json(js) | |
| tbl_btn.click( | |
| _table, | |
| inputs=[file_input, page_range_input], | |
| outputs=[tbl_html_out, tbl_json_out, tbl_json_dl], | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| **Powered by [Surya OCR 2](https://huggingface.co/datalab-to/surya-ocr-2)** | |
| Β· 650 M params Β· Apache 2.0 code Β· OpenRail weights | |
| Β· Inference: llama.cpp (CPU) | |
| """, | |
| elem_id="footer", | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get("GRADIO_SERVER_PORT", 7860)), | |
| show_error=True, | |
| ) | |