File size: 5,882 Bytes
5625297
55cb0af
14a7432
5625297
 
14a7432
 
 
b1c7862
14a7432
 
 
177ec96
14a7432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55cb0af
14a7432
 
 
 
 
 
 
 
55cb0af
 
14a7432
 
594145e
14a7432
 
 
37508d7
14a7432
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47c1654
 
721c9d9
 
 
14a7432
 
721c9d9
37508d7
14a7432
721c9d9
37508d7
5625297
14a7432
721c9d9
 
 
14a7432
721c9d9
 
 
14a7432
 
721c9d9
 
194f537
 
14a7432
 
 
 
 
 
 
 
 
 
 
 
 
7fbf37f
721c9d9
 
14a7432
 
 
7fbf37f
 
31f684f
14a7432
 
31f684f
5625297
14a7432
 
 
 
 
 
 
5625297
 
 
 
11bf437
14a7432
 
 
11bf437
 
5cc96b8
bcc62dd
14a7432
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/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()