Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| import json | |
| import logging | |
| import os | |
| import subprocess | |
| import time | |
| from typing import Any, List | |
| import gradio as gr | |
| import httpx | |
| import yaml | |
| log = logging.getLogger("glmocr_selfhosted_space") | |
| logging.basicConfig(level=logging.INFO) | |
| MODEL_ID = os.getenv("GLMOCR_MODEL_ID", "zai-org/GLM-OCR") | |
| OCR_API_PORT = int(os.getenv("OCR_API_PORT", "8080")) | |
| OCR_API_BASE = f"http://127.0.0.1:{OCR_API_PORT}" | |
| VLLM_MAX_MODEL_LEN = os.getenv("VLLM_MAX_MODEL_LEN", "8192") | |
| VLLM_GPU_MEMORY_UTIL = os.getenv("VLLM_GPU_MEMORY_UTIL", "0.92") | |
| VLLM_EXTRA_ARGS = os.getenv("VLLM_EXTRA_ARGS", "").strip() | |
| _parser = None | |
| _vllm_proc = None | |
| def _start_vllm_if_needed() -> None: | |
| global _vllm_proc | |
| if _vllm_proc is not None and _vllm_proc.poll() is None: | |
| return | |
| cmd: List[str] = [ | |
| "python", | |
| "-m", | |
| "vllm.entrypoints.openai.api_server", | |
| "--model", | |
| MODEL_ID, | |
| "--served-model-name", | |
| "glm-ocr", | |
| "--port", | |
| str(OCR_API_PORT), | |
| "--max-model-len", | |
| VLLM_MAX_MODEL_LEN, | |
| "--gpu-memory-utilization", | |
| VLLM_GPU_MEMORY_UTIL, | |
| ] | |
| if VLLM_EXTRA_ARGS: | |
| cmd.extend(VLLM_EXTRA_ARGS.split()) | |
| log.info("Starting vLLM server: %s", " ".join(cmd)) | |
| _vllm_proc = subprocess.Popen(cmd) | |
| deadline = time.time() + 420 | |
| last_err: str = "" | |
| while time.time() < deadline: | |
| if _vllm_proc.poll() is not None: | |
| raise RuntimeError("vLLM server exited early. Check Space logs for details.") | |
| try: | |
| resp = httpx.get(f"{OCR_API_BASE}/v1/models", timeout=8.0) | |
| if resp.status_code == 200: | |
| log.info("vLLM is ready.") | |
| return | |
| except Exception as e: | |
| last_err = str(e) | |
| time.sleep(3) | |
| raise RuntimeError(f"Timed out waiting for vLLM startup. Last error: {last_err}") | |
| def _configure_glmocr_to_keep_header_footer() -> None: | |
| import glmocr | |
| config_path = os.path.join(os.path.dirname(glmocr.__file__), "config.yaml") | |
| with open(config_path, "r", encoding="utf-8") as f: | |
| cfg = yaml.safe_load(f) or {} | |
| pipeline = cfg.setdefault("pipeline", {}) | |
| maas = pipeline.setdefault("maas", {}) | |
| maas["enabled"] = False | |
| ocr_api = pipeline.setdefault("ocr_api", {}) | |
| ocr_api["api_host"] = "127.0.0.1" | |
| ocr_api["api_port"] = OCR_API_PORT | |
| ocr_api["model"] = "glm-ocr" | |
| ocr_api["api_path"] = "/v1/chat/completions" | |
| ocr_api["api_mode"] = "openai" | |
| ocr_api["verify_ssl"] = False | |
| layout = pipeline.setdefault("layout", {}) | |
| label_task_mapping = layout.setdefault("label_task_mapping", {}) | |
| text_labels = set(label_task_mapping.get("text", []) or []) | |
| abandon_labels = set(label_task_mapping.get("abandon", []) or []) | |
| skip_labels = set(label_task_mapping.get("skip", []) or []) | |
| text_labels.update({"header", "footer"}) | |
| abandon_labels.discard("header") | |
| abandon_labels.discard("footer") | |
| # Keep image-only decorative zones out of OCR by default. | |
| # If you want logo OCR too, move these to text_labels. | |
| skip_labels.update({"header_image", "footer_image"}) | |
| abandon_labels.discard("header_image") | |
| abandon_labels.discard("footer_image") | |
| label_task_mapping["text"] = sorted(text_labels) | |
| label_task_mapping["abandon"] = sorted(abandon_labels) | |
| label_task_mapping["skip"] = sorted(skip_labels) | |
| with open(config_path, "w", encoding="utf-8") as f: | |
| yaml.safe_dump(cfg, f, sort_keys=False) | |
| def get_parser(): | |
| global _parser | |
| if _parser is None: | |
| _start_vllm_if_needed() | |
| _configure_glmocr_to_keep_header_footer() | |
| from glmocr import GlmOcr | |
| _parser = GlmOcr() | |
| return _parser | |
| def _extract_md(result: Any) -> str: | |
| if result is None: | |
| return "" | |
| if isinstance(result, list): | |
| chunks = [] | |
| for item in result: | |
| md = getattr(item, "markdown_result", "") | |
| if md: | |
| chunks.append(str(md).strip()) | |
| return "\n\n---page-separator---\n\n".join([c for c in chunks if c]).strip() | |
| md = getattr(result, "markdown_result", "") | |
| return str(md).strip() if md else "" | |
| def _extract_json(result: Any) -> str: | |
| if result is None: | |
| return "{}" | |
| if isinstance(result, list): | |
| payload = [getattr(item, "json_result", None) for item in result] | |
| return json.dumps(payload, ensure_ascii=False, indent=2) | |
| return json.dumps(getattr(result, "json_result", None), ensure_ascii=False, indent=2) | |
| def run_ocr(file_obj): | |
| if file_obj is None: | |
| return "Please upload a file.", "{}" | |
| path = file_obj.name if hasattr(file_obj, "name") else str(file_obj) | |
| try: | |
| parser = get_parser() | |
| result = parser.parse(path) | |
| md = _extract_md(result) or "(No content)" | |
| jr = _extract_json(result) | |
| return md, jr | |
| except Exception as e: | |
| import traceback | |
| log.exception("run_ocr failed: %s", e) | |
| return f"Error: {e}\n\n{traceback.format_exc()}", "{}" | |
| def build_demo(): | |
| with gr.Blocks(title="GLM-OCR Self-hosted (Header/Footer enabled)") as demo: | |
| gr.Markdown("# GLM-OCR Self-hosted (Header/Footer enabled)") | |
| gr.Markdown( | |
| "Runs a local vLLM server inside the Space and configures GLM-OCR " | |
| "to include `header` and `footer` regions in OCR output." | |
| ) | |
| file_in = gr.File( | |
| label="Upload PDF or image", | |
| file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"], | |
| ) | |
| run_btn = gr.Button("Run OCR", variant="primary") | |
| out_md = gr.Textbox(label="Markdown", lines=30) | |
| out_json = gr.Textbox(label="JSON (layout details)", lines=20) | |
| run_btn.click(fn=run_ocr, inputs=file_in, outputs=[out_md, out_json]) | |
| return demo | |
| if __name__ == "__main__": | |
| build_demo().launch() | |