File size: 6,153 Bytes
6068584
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
186
187
188
189
190
191
192
193
194
import os
import sys
import base64
import tempfile
import json
import traceback

# Install pinned runtime deps at startup (before importing torch/transformers)
_RUNTIME_PKGS = [
    "torch==2.10.0",
    "torchvision==0.25.0",
    "transformers==4.57.1",
    "PyMuPDF==1.26.1",
    "Pillow>=10.0.0",
]

print("Installing pinned runtime dependencies...", flush=True)
import subprocess
subprocess.run(
    [sys.executable, "-m", "pip", "install", "--quiet", "--no-cache-dir"] + _RUNTIME_PKGS,
    check=True,
)
print("Runtime deps installed.", flush=True)

import torch
from transformers import AutoModel, AutoTokenizer

MODEL_NAME = "baidu/Unlimited-OCR"

print("=== Pre-loading Baidu Unlimited-OCR model at startup ===", flush=True)
print("Loading tokenizer...", flush=True)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
print("Loading model (CPU)...", flush=True)
model = AutoModel.from_pretrained(
    MODEL_NAME,
    trust_remote_code=True,
    use_safetensors=True,
    torch_dtype=torch.float32,
).eval()
print("OCR model ready (CPU).", flush=True)
print("=== OCR model pre-load complete ===", flush=True)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("unlimited-ocr")


def _collect_output(out_dir: str) -> str:
    result = ""
    for fname in sorted(os.listdir(out_dir)):
        if fname.endswith((".txt", ".md")):
            with open(os.path.join(out_dir, fname), "r", encoding="utf-8") as f:
                result += f.read() + "\n"
    if not result:
        for fname in sorted(os.listdir(out_dir)):
            fpath = os.path.join(out_dir, fname)
            if os.path.isfile(fpath):
                try:
                    with open(fpath, "r", encoding="utf-8") as f:
                        result += f.read() + "\n"
                except Exception:
                    pass
    return result.strip()


def _run_ocr_internal(image_path: str, mode: str, prompt: str) -> str:
    out_dir = tempfile.mkdtemp(prefix="ocr_out_")

    if mode == "gundam":
        base_size, image_size, crop_mode, ngram_window = 1024, 640, True, 128
    else:
        base_size, image_size, crop_mode, ngram_window = 1024, 1024, False, 128

    model.infer(
        tokenizer,
        prompt=f"<image>{prompt}",
        image_file=image_path,
        output_path=out_dir,
        base_size=base_size,
        image_size=image_size,
        crop_mode=crop_mode,
        max_length=8192,
        no_repeat_ngram_size=35,
        ngram_window=ngram_window,
        save_results=True,
    )

    return _collect_output(out_dir)


@mcp.tool()
def run_ocr(
    image_base64: str = "",
    image_path: str = "",
    mode: str = "gundam",
    prompt: str = "document parsing.",
) -> str:
    """Run OCR on an image using Baidu Unlimited-OCR (CPU inference).

    Extracts text from images of documents, screenshots, signs, handwriting, etc.
    Supports multilingual text extraction with high accuracy.

    Args:
        image_base64: Base64-encoded image data (PNG/JPEG). Takes precedence if provided.
        image_path: File path to the image. Used if image_base64 is empty.
        mode: 'gundam' for fast mode (640px crop), 'base' for accurate mode (1024px).
        prompt: Instruction for the OCR model (e.g. "document parsing.", "read all text.").

    Returns:
        Extracted text from the image.
    """
    if image_base64:
        try:
            image_bytes = base64.b64decode(image_base64)
        except Exception as e:
            return f"Error decoding base64 image: {e}"
        tmp_path = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
        with open(tmp_path, "wb") as f:
            f.write(image_bytes)
    elif image_path:
        if not os.path.exists(image_path):
            return f"Error: Image file not found: {image_path}"
        tmp_path = image_path
    else:
        return "Error: Provide either image_base64 or image_path."

    try:
        result = _run_ocr_internal(tmp_path, mode, prompt)
        return result if result else "No text detected in image."
    except Exception as e:
        return f"OCR error: {e}"
    finally:
        if image_base64 and os.path.exists(tmp_path):
            os.unlink(tmp_path)


@mcp.tool()
def run_ocr_pdf(
    pdf_base64: str = "",
    pdf_path: str = "",
    mode: str = "gundam",
    prompt: str = "document parsing.",
) -> str:
    """Run OCR on a PDF document. Converts each page to an image and runs OCR.

    Args:
        pdf_base64: Base64-encoded PDF data. Takes precedence if provided.
        pdf_path: File path to the PDF. Used if pdf_base64 is empty.
        mode: 'gundam' for fast mode, 'base' for accurate mode.
        prompt: Instruction for the OCR model.

    Returns:
        Concatenated text from all pages.
    """
    import fitz

    if pdf_base64:
        try:
            pdf_bytes = base64.b64decode(pdf_base64)
        except Exception as e:
            return f"Error decoding base64 PDF: {e}"
        tmp_pdf = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name
        with open(tmp_pdf, "wb") as f:
            f.write(pdf_bytes)
    elif pdf_path:
        if not os.path.exists(pdf_path):
            return f"Error: PDF file not found: {pdf_path}"
        tmp_pdf = pdf_path
    else:
        return "Error: Provide either pdf_base64 or pdf_path."

    try:
        doc = fitz.open(tmp_pdf)
        tmp_dir = tempfile.mkdtemp(prefix="pdf_ocr_")
        mat = fitz.Matrix(200 / 72, 200 / 72)
        all_text = []
        for i, page in enumerate(doc):
            out = os.path.join(tmp_dir, f"page_{i + 1:04d}.png")
            page.get_pixmap(matrix=mat).save(out)
            text = _run_ocr_internal(out, mode, prompt)
            all_text.append(f"--- Page {i + 1} ---\n{text}")
            os.unlink(out)
        doc.close()
        return "\n\n".join(all_text) if all_text else "No text detected in PDF."
    except Exception as e:
        return f"PDF OCR error: {e}"
    finally:
        if pdf_base64 and os.path.exists(tmp_pdf):
            os.unlink(tmp_pdf)


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=7860)