File size: 7,080 Bytes
f56f44c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
DeepSeek-OCR Gradio Space (CPU)
-------------------------------
Upload an image and run it through deepseek-ai/DeepSeek-OCR using the
built-in "Free OCR" prompt mode (plain text extraction, no markdown/
grounding boxes). A "Markdown" mode is also included since it's a
one-line prompt swap.

This version runs on CPU only (no CUDA, no flash-attention). The model
stays in bfloat16, matching hardcoded dtype casts inside its own custom
code — PyTorch supports bfloat16 tensors on CPU, just without hardware
acceleration, so it works but is slow. DeepSeek-OCR is a vision-language
model, so expect anywhere from ~30s to a few minutes per image depending
on resolution preset and CPU. Use the "Tiny" or "Small" resolution
presets for faster (if lower-fidelity) results.
"""

import os
import re
import shutil
import tempfile

import gradio as gr
import torch
from transformers import AutoModel, AutoTokenizer

MODEL_NAME = "deepseek-ai/DeepSeek-OCR"

# Force CPU regardless of what's visible to torch.
os.environ["CUDA_VISIBLE_DEVICES"] = ""
DEVICE = "cpu"

# ---------------------------------------------------------------------------
# Model loading (once, at startup)
# ---------------------------------------------------------------------------
print("Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)

print("Loading model on CPU (this can take a while, and first run also "
      "downloads several GB of weights)...")
model = AutoModel.from_pretrained(
    MODEL_NAME,
    trust_remote_code=True,
    use_safetensors=True,
)
# NOTE: don't cast to float32 here. DeepSeek-OCR's own forward pass hardcodes
# casting image inputs to bfloat16 internally (independent of the model's
# actual dtype), so the model's weights must also be bfloat16 or you get a
# "Input type (BFloat16) and bias type (float) should be the same" crash.
# PyTorch CPU does support bfloat16 tensors/ops (just without hardware
# acceleration), so this works, it's just slow.
model = model.eval().to(DEVICE).to(torch.bfloat16)

# ---------------------------------------------------------------------------
# DeepSeek-OCR's own `model.infer()` method has `.cuda()` calls hardcoded
# directly in its custom modeling code (independent of what device the model
# itself was loaded on), so it crashes on CPU-only hardware with
# "Found no NVIDIA driver". Since the model and all its tensors already live
# on CPU here, we monkey-patch `.cuda()` into a no-op that just returns the
# tensor/module unchanged — this makes those hardcoded calls harmless instead
# of failing.
if not torch.cuda.is_available():
    torch.Tensor.cuda = lambda self, *args, **kwargs: self
    torch.nn.Module.cuda = lambda self, *args, **kwargs: self

# Preset resolution modes exposed by the model's own infer() signature
SIZE_PRESETS = {
    "Tiny (512, fastest)": dict(base_size=512, image_size=512, crop_mode=False),
    "Small (640)": dict(base_size=640, image_size=640, crop_mode=False),
    "Base (1024, high quality)": dict(base_size=1024, image_size=1024, crop_mode=False),
    "Large (1280, best quality)": dict(base_size=1280, image_size=1280, crop_mode=False),
    "Gundam (1024/640 crop, best for documents)": dict(base_size=1024, image_size=640, crop_mode=True),
}

TASK_PROMPTS = {
    "Free OCR (plain text)": "<image>\nFree OCR. ",
    "Markdown (structure + tables)": "<image>\n<|grounding|>Convert the document to markdown. ",
}


def run_ocr(image, task_choice, size_choice, progress=gr.Progress(track_tqdm=True)):
    if image is None:
        return "Please upload an image first."

    prompt = TASK_PROMPTS[task_choice]
    preset = SIZE_PRESETS[size_choice]

    # model.infer() reads the image from a file path and writes its result to
    # output_path. Point both at a temp dir.
    tmp_dir = tempfile.mkdtemp(prefix="deepseek_ocr_")
    image_path = os.path.join(tmp_dir, "input.png")
    image.convert("RGB").save(image_path)

    try:
        with torch.no_grad():
            # IMPORTANT: model.infer()'s own source code only `return`s the
            # extracted text when save_results=True (or test_compress/
            # eval_mode=True); with all three False/default it computes
            # everything but returns None. So we must pass save_results=True
            # and then read the text back from the file it writes
            # (output_path/result.mmd) rather than trusting the return value.
            model.infer(
                tokenizer,
                prompt=prompt,
                image_file=image_path,
                output_path=tmp_dir,
                base_size=preset["base_size"],
                image_size=preset["image_size"],
                crop_mode=preset["crop_mode"],
                save_results=True,
            )

        result_file = os.path.join(tmp_dir, "result.mmd")
        if not os.path.exists(result_file):
            return ("No output file was produced. Try a different resolution "
                    "preset, or check the Space logs for errors.")

        with open(result_file, "r", encoding="utf-8") as f:
            text = f.read()

        # Free OCR output is plain text already; Markdown/grounding output
        # can contain <|ref|>...<|/ref|> and <|det|>[[...]]<|/det|> tags —
        # strip those so the box shows clean readable text either way.
        text = re.sub(r"<\|ref\|>.*?<\|/ref\|>", "", text, flags=re.DOTALL)
        text = re.sub(r"<\|det\|>\[\[.*?\]\]<\|/det\|>", "", text, flags=re.DOTALL)
        text = text.strip()

        return text if text else "No text was detected in this image."
    finally:
        shutil.rmtree(tmp_dir, ignore_errors=True)


with gr.Blocks(title="DeepSeek-OCR (CPU)") as demo:
    gr.Markdown(
        """
        # DeepSeek-OCR — Free OCR (CPU)
        Upload an image (a photo, screenshot, scanned page, etc.) and extract
        its text using [deepseek-ai/DeepSeek-OCR](https://huggingface.co/deepseek-ai/DeepSeek-OCR).

        ⚠️ Running on **CPU** — this is noticeably slower than GPU. Start with
        the "Tiny" or "Small" resolution preset if you want faster results.
        """
    )

    with gr.Row():
        with gr.Column():
            image_input = gr.Image(type="pil", label="Upload image")
            task_dropdown = gr.Dropdown(
                choices=list(TASK_PROMPTS.keys()),
                value="Free OCR (plain text)",
                label="Task",
            )
            size_dropdown = gr.Dropdown(
                choices=list(SIZE_PRESETS.keys()),
                value="Gundam (1024/640 crop, best for documents)",
                label="Resolution mode (lower = faster on CPU)",
            )
            run_button = gr.Button("Run OCR", variant="primary")

        with gr.Column():
            output_text = gr.Textbox(
                label="Extracted text", lines=25
            )

    run_button.click(
        fn=run_ocr,
        inputs=[image_input, task_dropdown, size_dropdown],
        outputs=output_text,
    )

if __name__ == "__main__":
    demo.queue().launch()