Spaces:
Running on Zero
Running on Zero
| """ | |
| Chandra OCR 2 β Hugging Face Space demo. | |
| Model: datalab-to/chandra-ocr-2 (~10B, bf16) | |
| Docs: https://huggingface.co/datalab-to/chandra-ocr-2 | |
| Two pipeline stages: | |
| 1. Text & layout extraction β all text in reading order, with tables, math, | |
| forms and headings preserved as markdown (chandra's parse_markdown). | |
| 2. Visual understanding β locate figures/charts/diagrams/photos/stamps in the | |
| layout output, crop them at full resolution, and surface the model's | |
| caption + any structured data it read from them. | |
| Hardware: needs ZeroGPU (H200 slice) or a paid A100/L40S/RTX PRO 6000. | |
| The model is ~20 GB in bf16 and will NOT run on the free CPU tier. | |
| API facts verified against the installed `chandra` package (not guessed): | |
| - generate_hf(batch, model, max_output_tokens=...) -> list[GenerationResult] | |
| GenerationResult has .raw, .token_count, .error | |
| - PROMPT_MAPPING contains exactly: "ocr_layout", "ocr" | |
| - parse_markdown(raw, include_headers_footers=False, include_images=True) | |
| - parse_chunks(raw, image, bbox_scale=1000) -> list of | |
| {"bbox": [x0, y0, x1, y1] (pixels), "label": str, "content": str} | |
| - settings.BBOX_SCALE == 1000 (bbox is normalised per-axis, so it is | |
| resolution-independent and can be mapped onto the full-res source image) | |
| """ | |
| import inspect | |
| import json | |
| import os | |
| import time | |
| import zipfile | |
| from pathlib import Path | |
| import gradio as gr | |
| import torch | |
| from PIL import Image | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| #MODEL_ID = "datalab-to/chandra-ocr-2" | |
| MODEL_ID = "datalab-to/surya-ocr-2" | |
| PAGES_PER_GPU_CALL = 3 # keep each ZeroGPU allocation inside its duration budget | |
| GPU_DURATION = 180 # seconds requested per allocation | |
| MAX_PAGES = 20 # guard against someone uploading a 500-page PDF | |
| OUT_DIR = Path(os.environ.get("CHANDRA_OUT_DIR", "/tmp/chandra_out")) | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"} | |
| PDF_EXTS = {".pdf"} | |
| # Labels the layout model can attach to blocks that count as "figures" for | |
| # stage 2 (visual understanding). Kept lowercase for matching. | |
| FIGURE_LABELS = { | |
| "figure", "chart", "diagram", "photo", "image", "picture", "stamp", | |
| "graph", "plot", "illustration", "logo", "icon", "drawing", "map", | |
| } | |
| CAPTION_LABELS = {"caption", "figure-caption", "figcaption", "figure_caption"} | |
| # --------------------------------------------------------------------------- | |
| # ZeroGPU shim β lets the same file run locally without the `spaces` package | |
| # --------------------------------------------------------------------------- | |
| ON_ZERO = os.environ.get("SPACES_ZERO_GPU") == "true" | |
| try: | |
| import spaces | |
| gpu = spaces.GPU | |
| except ImportError: # local / non-ZeroGPU deploy | |
| def gpu(*args, **kwargs): | |
| if args and callable(args[0]): | |
| return args[0] | |
| def deco(fn): | |
| return fn | |
| return deco | |
| # --------------------------------------------------------------------------- | |
| # Chandra package β preferred path. Falls back to plain transformers. | |
| # --------------------------------------------------------------------------- | |
| CHANDRA_ERR = None | |
| USE_CHANDRA = False | |
| HAS_PARSE_CHUNKS = False | |
| PROMPT_TYPES = ["ocr_layout"] # safe default; replaced below if package present | |
| try: | |
| from chandra.model import generate_hf | |
| from chandra.model.schema import BatchInputItem | |
| from chandra.model import PROMPT_MAPPING # enumerate real prompt types | |
| try: | |
| from chandra.output import parse_markdown, parse_chunks | |
| except ImportError: | |
| from chandra.model.output import parse_markdown, parse_chunks | |
| USE_CHANDRA = True | |
| HAS_PARSE_CHUNKS = True | |
| PROMPT_TYPES = list(PROMPT_MAPPING.keys()) | |
| except Exception as e: # noqa: BLE001 | |
| CHANDRA_ERR = f"{type(e).__name__}: {e}" | |
| USE_CHANDRA = False | |
| HAS_PARSE_CHUNKS = False | |
| DEFAULT_PROMPT_TYPE = "ocr_layout" if "ocr_layout" in PROMPT_TYPES else PROMPT_TYPES[0] | |
| # --------------------------------------------------------------------------- | |
| # Model β loaded at module scope, guarded so a failure never kills the Space | |
| # --------------------------------------------------------------------------- | |
| MODEL = None | |
| PROCESSOR = None | |
| MODEL_ERROR = None | |
| def _load(): | |
| from transformers import AutoProcessor | |
| try: | |
| from transformers import AutoModelForImageTextToText as VLM | |
| except ImportError: | |
| from transformers import AutoModelForVision2Seq as VLM | |
| # On ZeroGPU there is no GPU visible at import time, so accelerate's | |
| # device_map="auto" would strand the model on CPU. ZeroGPU instead | |
| # intercepts .to("cuda") at global scope. Elsewhere, device_map is fine. | |
| # Blackwell (sm_120) -> SDPA, never flash-attn (no prebuilt wheels). | |
| kw = dict(low_cpu_mem_usage=True, attn_implementation="sdpa") | |
| if not ON_ZERO: | |
| kw["device_map"] = "auto" | |
| try: | |
| m = VLM.from_pretrained(MODEL_ID, dtype=torch.bfloat16, **kw) | |
| except TypeError: # transformers < 4.56 spelled it torch_dtype | |
| kw.pop("attn_implementation", None) | |
| m = VLM.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, **kw) | |
| if ON_ZERO: | |
| m = m.to("cuda") | |
| m.eval() | |
| p = AutoProcessor.from_pretrained(MODEL_ID) | |
| p.tokenizer.padding_side = "left" # required by chandra's batched generate | |
| m.processor = p | |
| return m, p | |
| try: | |
| print(f"Loading {MODEL_ID} ...") | |
| _t0 = time.time() | |
| MODEL, PROCESSOR = _load() | |
| print(f"Loaded in {time.time() - _t0:.0f}s | chandra pkg: {USE_CHANDRA} " | |
| f"({CHANDRA_ERR or 'ok'})") | |
| except Exception as e: # noqa: BLE001 | |
| MODEL_ERROR = f"{type(e).__name__}: {e}" | |
| print(f"MODEL LOAD FAILED: {MODEL_ERROR}") | |
| MODEL_LOADED = MODEL is not None | |
| # --------------------------------------------------------------------------- | |
| # Diagnostics β surfaced in the UI, not just logs | |
| # --------------------------------------------------------------------------- | |
| def _transformers_version(): | |
| try: | |
| import transformers | |
| return transformers.__version__ | |
| except Exception: # noqa: BLE001 | |
| return "n/a" | |
| def diagnostics_md() -> str: | |
| cuda = torch.cuda.is_available() | |
| gpu_name = torch.cuda.get_device_name(0) if cuda else "none visible" | |
| rows = [ | |
| ("torch", torch.__version__), | |
| ("transformers", _transformers_version()), | |
| ("gradio", gr.__version__), | |
| ("CUDA visible", str(cuda)), | |
| ("GPU", gpu_name), | |
| ("chandra package", "imported" if USE_CHANDRA else f"FAILED β {CHANDRA_ERR}"), | |
| ("prompt types", ", ".join(PROMPT_TYPES)), | |
| ("model loaded", "yes" if MODEL_LOADED else "NO"), | |
| ("model error", MODEL_ERROR or "β"), | |
| ] | |
| return "\n".join(f"- **{k}**: `{v}`" for k, v in rows) | |
| # --------------------------------------------------------------------------- | |
| # Page extraction | |
| # --------------------------------------------------------------------------- | |
| def as_path(f) -> Path: | |
| """Normalise str / os.PathLike / Gradio file object into a real Path. | |
| pathlib.Path also has a .name attribute, but there it is the *basename* β | |
| a hasattr(f, "name") check would silently drop the directory. Handle the | |
| types distinctly instead. | |
| """ | |
| if isinstance(f, (str, os.PathLike)): | |
| return Path(f) | |
| # Gradio file object (or tempfile.NamedTemporaryFile) exposes .name as a path | |
| return Path(getattr(f, "name", str(f))) | |
| def pdf_to_images(path: Path, dpi: int): | |
| try: | |
| import pymupdf | |
| except ImportError: | |
| import fitz as pymupdf | |
| doc = pymupdf.open(str(path)) | |
| pages = [] | |
| for i, page in enumerate(doc): | |
| pix = page.get_pixmap(dpi=dpi) | |
| pages.append((f"{path.stem}_p{i + 1:03d}", | |
| Image.frombytes("RGB", (pix.width, pix.height), pix.samples))) | |
| doc.close() | |
| return pages | |
| def collect_pages(files, dpi: int): | |
| """Return (pages, rejected) where pages is [(name, full_res_PIL)] and | |
| rejected is [(name, reason)]. One bad file never fails the run.""" | |
| pages, rejected = [], [] | |
| for f in files: | |
| p = as_path(f) | |
| ext = p.suffix.lower() | |
| try: | |
| if ext in PDF_EXTS: | |
| pages.extend(pdf_to_images(p, dpi)) | |
| elif ext in IMAGE_EXTS: | |
| pages.append((p.stem, Image.open(p).convert("RGB"))) | |
| else: | |
| rejected.append((p.name, f"unsupported type '{ext or 'none'}'")) | |
| except Exception as e: # noqa: BLE001 | |
| rejected.append((p.name, f"{type(e).__name__}: {e}")) | |
| return pages, rejected | |
| def fit(img: Image.Image, max_side: int) -> Image.Image: | |
| """Cap the long edge β visual token count scales with area, so this is the | |
| single biggest lever on VRAM and latency.""" | |
| img = img.convert("RGB") | |
| if max(img.size) > max_side: | |
| s = max_side / max(img.size) | |
| img = img.resize((max(1, int(img.width * s)), max(1, int(img.height * s))), | |
| Image.LANCZOS) | |
| return img | |
| # --------------------------------------------------------------------------- | |
| # Inference | |
| # --------------------------------------------------------------------------- | |
| def _infer_chunk(images, prompt_type: str, max_new_tokens: int): | |
| """OCR a small batch of PIL images. Returns a list of raw model strings.""" | |
| if MODEL is None: | |
| raise RuntimeError("model not loaded") | |
| if USE_CHANDRA: | |
| batch = [BatchInputItem(image=im, prompt_type=prompt_type) for im in images] | |
| results = generate_hf(batch, MODEL, max_output_tokens=max_new_tokens) | |
| return [getattr(r, "raw", None) or getattr(r, "markdown", "") or str(r) | |
| for r in results] | |
| # ---- fallback: drive the chat template directly (degraded) ---- | |
| outs = [] | |
| for im in images: | |
| msgs = [{"role": "user", "content": [ | |
| {"type": "image", "image": im}, | |
| {"type": "text", "text": prompt_type}, | |
| ]}] | |
| inputs = PROCESSOR.apply_chat_template( | |
| msgs, tokenize=True, add_generation_prompt=True, | |
| return_dict=True, return_tensors="pt", | |
| ).to(MODEL.device) | |
| if "pixel_values" in inputs: | |
| inputs["pixel_values"] = inputs["pixel_values"].to(MODEL.dtype) | |
| gen = MODEL.generate(**inputs, max_new_tokens=int(max_new_tokens), | |
| do_sample=False) | |
| trimmed = [o[len(i):] for i, o in zip(inputs["input_ids"], gen)] | |
| outs.append(PROCESSOR.batch_decode(trimmed, skip_special_tokens=True)[0].strip()) | |
| return outs | |
| def to_markdown(raw: str) -> str: | |
| if USE_CHANDRA: | |
| try: | |
| return parse_markdown(raw) | |
| except Exception: # noqa: BLE001 | |
| pass | |
| return raw | |
| # --------------------------------------------------------------------------- | |
| # Stage 2 β figure extraction (visual understanding) | |
| # --------------------------------------------------------------------------- | |
| def _find_caption(chunks, fig_idx: int): | |
| """Look for a caption chunk immediately after (or before) the figure.""" | |
| for j in (fig_idx + 1, fig_idx - 1): | |
| if 0 <= j < len(chunks): | |
| lbl = (chunks[j].get("label") or "").lower() | |
| if lbl in CAPTION_LABELS: | |
| return chunks[j].get("content") or "" | |
| return "" | |
| def extract_figures(raw: str, full_res: Image.Image): | |
| """Locate figure-like blocks in the layout output and crop them from the | |
| FULL-RESOLUTION source image (bbox is per-axis normalised, so it maps | |
| cleanly onto any resolution with the same aspect ratio). | |
| Returns (figures, degraded) where figures is a list of dicts and degraded | |
| is True when we could not use the real chandra path (so captions/data are | |
| NOT from the model's figure understanding). | |
| """ | |
| if not USE_CHANDRA or not HAS_PARSE_CHUNKS: | |
| return [], True | |
| try: | |
| chunks = parse_chunks(raw, full_res, bbox_scale=1000) | |
| except Exception: # noqa: BLE001 | |
| return [], True | |
| figures = [] | |
| for i, ch in enumerate(chunks): | |
| label = (ch.get("label") or "").lower() | |
| if label not in FIGURE_LABELS: | |
| continue | |
| bbox = ch.get("bbox") | |
| if not bbox or len(bbox) != 4: | |
| continue | |
| x0, y0, x1, y1 = (int(v) for v in bbox) | |
| w, h = full_res.size | |
| x0, y0 = max(0, x0), max(0, y0) | |
| x1, y1 = min(w, x1), min(h, y1) | |
| if x1 <= x0 or y1 <= y0: | |
| continue | |
| crop = full_res.crop((x0, y0, x1, y1)) | |
| figures.append({ | |
| "page": None, # filled by caller | |
| "label": label, | |
| "bbox": [x0, y0, x1, y1], | |
| "crop": crop, | |
| "caption": _find_caption(chunks, i), | |
| "structured": (ch.get("content") or "").strip(), | |
| }) | |
| return figures, False | |
| # --------------------------------------------------------------------------- | |
| # Orchestration β streaming generator | |
| # --------------------------------------------------------------------------- | |
| FIG_HEADERS = ["Page", "Label", "BBox (x0,y0,x1,y1)", "Caption", "Structured data"] | |
| def run(files, prompt_type, dpi, max_side, max_new_tokens, progress=gr.Progress()): | |
| def empty(status): | |
| return "", "", "", status, None, [], None, None | |
| if not MODEL_LOADED: | |
| yield empty(f"Model failed to load β see diagnostics.\n{MODEL_ERROR}") | |
| return | |
| if not files: | |
| yield empty("Upload a PDF or some images first.") | |
| return | |
| try: | |
| pages, rejected = collect_pages(files, int(dpi)) | |
| except Exception as e: # noqa: BLE001 | |
| yield empty(f"Could not read those files:\n{type(e).__name__}: {e}") | |
| return | |
| if not pages: | |
| yield empty("No readable PDF or image files found in that upload.") | |
| return | |
| skip_note = "" | |
| if rejected: | |
| skip_note = "\nSkipped:\n" + "\n".join(f" - {n}: {r}" for n, r in rejected) | |
| truncated = "" | |
| if len(pages) > MAX_PAGES: | |
| truncated = f" (truncated from {len(pages)})" | |
| pages = pages[:MAX_PAGES] | |
| md_parts, raw_parts = [], [] | |
| all_figs = [] # flattened figure records for the gallery/table | |
| per_page = [] # (name, md, raw, figs) | |
| t_start = time.time() | |
| for start in range(0, len(pages), PAGES_PER_GPU_CALL): | |
| chunk = pages[start:start + PAGES_PER_GPU_CALL] | |
| names = [n for n, _ in chunk] | |
| full_res = [im for _, im in chunk] | |
| imgs = [fit(im, int(max_side)) for im in full_res] | |
| progress(start / len(pages), | |
| desc=f"{names[0]} β¦ ({start + 1}-{start + len(chunk)}/{len(pages)})") | |
| try: | |
| raws = _infer_chunk(imgs, prompt_type, int(max_new_tokens)) | |
| except torch.cuda.OutOfMemoryError: | |
| torch.cuda.empty_cache() | |
| raws = ["[OUT OF MEMORY β lower 'Max image side']"] * len(chunk) | |
| except Exception as e: # noqa: BLE001 | |
| raws = [f"[FAILED: {type(e).__name__}: {e}]"] * len(chunk) | |
| for name, fimg, raw in zip(names, full_res, raws): | |
| md = to_markdown(raw) | |
| figs, degraded = extract_figures(raw, fimg) | |
| for fg in figs: | |
| fg["page"] = name | |
| fg["degraded"] = degraded | |
| all_figs.append(fg) | |
| md_parts.append(f"\n\n---\n\n## {name}\n\n{md}") | |
| raw_parts.append(f"===== {name} =====\n{raw}") | |
| per_page.append((name, md, raw, figs)) | |
| elapsed = time.time() - t_start | |
| joined = "\n".join(md_parts) | |
| yield (joined, joined, | |
| "\n\n".join(raw_parts), | |
| f"{len(per_page)}/{len(pages)} pages{truncated} Β· {elapsed:.0f}s " | |
| f"({elapsed / max(1, len(per_page)):.1f}s/page){skip_note}", | |
| _gallery(all_figs), _fig_rows(all_figs), | |
| None, None) | |
| # ---- artefacts ---- | |
| stamp = time.strftime("%Y%m%d_%H%M%S") | |
| md_path = OUT_DIR / f"chandra_{stamp}.md" | |
| md_path.write_text("\n".join(md_parts), encoding="utf-8") | |
| zip_path = OUT_DIR / f"chandra_{stamp}.zip" | |
| with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z: | |
| for name, md, raw, figs in per_page: | |
| safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in name) | |
| z.writestr(f"markdown/{safe}.md", md) | |
| z.writestr(f"raw/{safe}.txt", raw) | |
| for k, fg in enumerate(figs): | |
| buf = _to_png_bytes(fg["crop"]) | |
| if buf: | |
| z.writestr(f"figures/{safe}_{k:02d}_{fg['label']}.png", buf) | |
| z.writestr("manifest.json", json.dumps({ | |
| "model": MODEL_ID, | |
| "prompt_type": prompt_type, | |
| "dpi": dpi, | |
| "max_side": max_side, | |
| "pages": [n for n, _, _, _ in per_page], | |
| "figures": [ | |
| {"page": f["page"], "label": f["label"], "bbox": f["bbox"], | |
| "caption": f["caption"], "structured": f["structured"]} | |
| for f in all_figs | |
| ], | |
| }, indent=2)) | |
| total = time.time() - t_start | |
| joined = "\n".join(md_parts) | |
| yield (joined, joined, | |
| "\n\n".join(raw_parts), | |
| f"Done β {len(per_page)} page(s){truncated} in {total:.0f}s " | |
| f"({total / max(1, len(per_page)):.1f}s/page){skip_note}", | |
| _gallery(all_figs), _fig_rows(all_figs), | |
| str(md_path), str(zip_path)) | |
| def _to_png_bytes(img: Image.Image): | |
| import io | |
| buf = io.BytesIO() | |
| try: | |
| img.save(buf, format="PNG") | |
| return buf.getvalue() | |
| except Exception: # noqa: BLE001 | |
| return None | |
| def _gallery(figs): | |
| """Gallery entries: (crop, caption). Caption is clearly marked degraded when | |
| it did not come from the model's figure-understanding path.""" | |
| out = [] | |
| for f in figs: | |
| cap = f["caption"] or "(no caption from model)" | |
| if f.get("degraded"): | |
| cap = f"[degraded β not from model] {cap}" | |
| out.append((f["crop"], f"{f['page']} Β· {f['label']} Β· {cap}")) | |
| return out or None | |
| def _fig_rows(figs): | |
| rows = [] | |
| for f in figs: | |
| cap = f["caption"] or "" | |
| if f.get("degraded"): | |
| cap = f"[degraded] {cap}".strip() | |
| rows.append([f["page"], f["label"], | |
| f"({f['bbox'][0]},{f['bbox'][1]},{f['bbox'][2]},{f['bbox'][3]})", | |
| cap, f["structured"]]) | |
| return rows | |
| # --------------------------------------------------------------------------- | |
| # UI β Gradio 4/5/6 tolerant | |
| # --------------------------------------------------------------------------- | |
| GR_MAJOR = int(gr.__version__.split(".")[0]) | |
| def C(cls, **kw): | |
| """Build a component, dropping kwargs this Gradio version rejects. | |
| Gradio 6 removed Textbox.show_copy_button and moved theme/css from | |
| Blocks() to launch(). This keeps one file working across 4/5/6. | |
| """ | |
| try: | |
| allowed = set(inspect.signature(cls.__init__).parameters) | |
| if "kwargs" not in allowed: | |
| kw = {k: v for k, v in kw.items() if k in allowed} | |
| except (TypeError, ValueError): | |
| pass | |
| return cls(**kw) | |
| CSS = """ | |
| #raw_out textarea { font-family: ui-monospace, monospace; font-size: 12px; } | |
| #status textarea { font-family: ui-monospace, monospace; font-size: 12px; } | |
| .md_pane { max-height: 640px; overflow-y: auto; } | |
| """ | |
| _STYLE = dict(theme=gr.themes.Soft(), css=CSS) | |
| _BLOCKS_KW = {} if GR_MAJOR >= 6 else _STYLE | |
| _LAUNCH_KW = _STYLE if GR_MAJOR >= 6 else {} | |
| with gr.Blocks(title="Chandra OCR 2", **_BLOCKS_KW) as demo: | |
| gr.Markdown( | |
| f""" | |
| # Chandra OCR 2 β document β markdown / HTML / JSON + figures | |
| Layout-aware OCR from [Datalab](https://datalab.to). Handles tables, math, | |
| forms, handwriting and 90+ languages, preserving reading order and structure. | |
| Two outputs per page: | |
| 1. **Text & layout** β markdown with tables/math/forms/headings preserved. | |
| 2. **Figures** β charts, diagrams, photos and stamps cropped at full resolution, | |
| with the model's caption and any structured data it read from them. | |
| Upload **PDFs and/or images**; each page is processed separately and results | |
| stream in below. Capped at **{MAX_PAGES} pages** per run in this demo. | |
| Model: [`{MODEL_ID}`](https://huggingface.co/{MODEL_ID}) Β· weights are under a | |
| modified OpenRAIL-M licence (free for research, personal use, and companies | |
| under $2M funding/revenue β **not** for building a competitor to Datalab's API). | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # NOTE: no file_types filter β it silently rejects valid files on | |
| # some browser/version combinations. Validation happens in Python. | |
| files = C(gr.Files, label="PDFs / images", file_count="multiple") | |
| go = C(gr.Button, value="Run OCR", variant="primary") | |
| prompt_type = C(gr.Dropdown, label="Prompt type", choices=PROMPT_TYPES, | |
| value=DEFAULT_PROMPT_TYPE, allow_custom_value=True, | |
| info="Enumerated from the chandra package; editable " | |
| "so you can try undocumented values.") | |
| with gr.Accordion("Advanced", open=False): | |
| dpi = C(gr.Slider, minimum=100, maximum=400, value=200, step=25, | |
| label="PDF render DPI", info="200-300 suits most scans.") | |
| max_side = C(gr.Slider, minimum=768, maximum=2560, value=1540, step=64, | |
| label="Max image side (px)", | |
| info="Biggest lever on speed and VRAM.") | |
| max_new = C(gr.Slider, minimum=512, maximum=8192, value=4096, step=256, | |
| label="Max new tokens", | |
| info="Layout output is verbose β keep this high.") | |
| status = C(gr.Textbox, label="Status", lines=3, elem_id="status") | |
| # Diagnostics β auto-open when the model failed to load. | |
| diag = gr.Accordion("Diagnostics", open=not MODEL_LOADED) | |
| with diag: | |
| gr.Markdown(diagnostics_md()) | |
| with gr.Column(scale=2): | |
| with gr.Tabs(): | |
| with gr.Tab("Rendered"): | |
| md_view = C(gr.Markdown, value="", elem_classes=["md_pane"]) | |
| with gr.Tab("Markdown source"): | |
| md_src = C(gr.Textbox, label=None, lines=24, show_copy_button=True) | |
| with gr.Tab("Raw model output"): | |
| raw_view = C(gr.Textbox, label=None, lines=24, | |
| elem_id="raw_out", show_copy_button=True) | |
| with gr.Tab("Figures"): | |
| fig_gallery = C(gr.Gallery, label="Extracted figures", | |
| columns=3, height="auto", | |
| object_fit="contain") | |
| fig_table = C(gr.Dataframe, headers=FIG_HEADERS, | |
| label="Figure metadata", interactive=False) | |
| with gr.Row(): | |
| md_file = C(gr.File, label="Combined .md") | |
| zip_file = C(gr.File, label="All pages .zip") | |
| go.click( | |
| run, | |
| inputs=[files, prompt_type, dpi, max_side, max_new], | |
| outputs=[md_view, md_src, raw_view, status, fig_gallery, fig_table, | |
| md_file, zip_file], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=12).launch(show_error=True, **_LAUNCH_KW) |