用40行代码搭建自己的无服务器OCR

0 阅读6分钟

用40行代码搭建自己的无服务器OCR,处理数学公式教科书只需2美元!

本文介绍如何使用 Modal 无服务器平台 + DeepSeek OCR 模型,低成本处理 PDF 教科书,让扫描版书籍变得可搜索、可复制。

📖 背景

几个月前,我想让我的 Gelman《贝叶斯数据分析》(Bayesian Data Analysis)副本变得可搜索,以便在一个专注于统计学的智能体中使用。

市面上确实有一些相当成熟的 OCR 工具,但它们要么有使用限制,要么在处理数千页文档时费用高得离谱。正好 DeepSeek 最近发布了一个开源 OCR 模型,能够很好地处理数学公式,这让我萌生了自己动手的想法。

但现实很骨感 —— 我的主力电脑还是十年前的 Titan Xp 显卡,早已不支持最新的 PyTorch 版本,根本跑不起来 DeepSeek OCR。

怎么办?答案是 Modal


🚀 什么是 Modal?

Modal 是一个无服务器计算平台,让你可以在云端运行 Python 代码,完全不用操心服务器运维。对于机器学习场景来说,它的杀手级特性是:可以按需挂载 GPU,只按实际运行秒数付费

来看看代码有多简洁:


import modal

  


image = modal.Image.from_registry(

    "nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04",

    add_python="3.11",

).pip_install("torch", "transformers", ...)

  


app = modal.App("my-gpu-app")

  


@app.function(image=image, gpu="A100")

def process_something():

    # 这段代码会在 A100 GPU 上运行,所有依赖都已就绪

    pass

装饰器模式让 Modal 用起来非常丝滑 —— 你只需要写普通的 Python 代码,在需要 GPU 的函数上加个装饰器,Modal 就会帮你搞定一切:构建容器、申请 GPU、路由请求。对于 OCR 这种任务来说,简直是绝配!


🔧 OCR 脚本实现

核心思路很简单:在 Modal 上部署一个 FastAPI 服务器,接收图片返回 Markdown 文本。下面逐步拆解关键代码。

1️⃣ 定义容器镜像

首先构建一个包含所有依赖的容器。DeepSeek OCR 需要 PyTorch、transformers 和一些图像处理库:


from pathlib import Path

import modal

  


APP_NAME = "deepseek-ocr-books-api-batch"

ROOT = Path(__file__).resolve().parents[1]

BOOKS_DIR = ROOT / "references" / "books"

PARSED_DIR = BOOKS_DIR / "parsed"

  


image = (

    modal.Image.from_registry(

        "nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04",

        add_python="3.11",

    )

    .apt_install("git", "libgl1", "libglib2.0-0")

    .pip_install(

        "torch==2.6.0",

        "torchvision==0.21.0",

        "transformers==4.46.3",

        "PyMuPDF",

        "Pillow",

        "numpy",

        extra_index_url="https://download.pytorch.org/whl/cu118",

    )

)

  


app = modal.App(APP_NAME)

路径配置放在顶部,方便定位 PDF 文件,保持配置和使用的紧密关联。

2️⃣ FastAPI 端点

接下来是核心部分 —— 将 FastAPI 服务器封装在 Modal 的 @modal.asgi_app() 装饰器中:


@app.function(image=image, gpu="A100", timeout=60 * 60 * 2# 2小时超时

@modal.asgi_app()

def fastapi_app():

    from fastapi import FastAPI, File, UploadFile

    from PIL import Image

    import torch

    from transformers import AutoModel, AutoTokenizer

  


    api = FastAPI()

  


    model_name = "deepseek-ai/DeepSeek-OCR"

    tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

    model = AutoModel.from_pretrained(model_name, trust_remote_code=True)

    model = model.cuda().to(torch.bfloat16).eval()

💡 关键点:模型在容器启动时只加载一次,后续请求会复用已加载的模型,这对于批量处理数百页文档来说至关重要,大幅提升吞吐量。

⚠️ 注意trust_remote_code=True 这个参数是必须的,因为 DeepSeek 模型在 HuggingFace 仓库中包含了自定义代码。

3️⃣ 批量推理加速

OCR 是典型的可并行化任务,每一页都是独立的。我们可以在单次模型推理中处理多个页面,比逐页处理快得多:


@api.post("/ocr_batch")

async def ocr_batch(files: list[UploadFile] = File(...)) -> dict[str, list[str]]:

    images = []

    for file in files:

        image_bytes = await file.read()

        images.append(Image.open(io.BytesIO(image_bytes)).convert("RGB"))

    batch_items = [prepare_inputs(image) for image in images]

    texts = run_batch(batch_items)

    return {"texts": texts}

run_batch 函数负责实际的模型推理 —— 将输入填充到相同长度,一次性跑完,再解码输出:


def run_batch(batch_items):

    # 将序列填充到相同长度

    lengths = [item[0].size(0) for item in batch_items]

    max_len = max(lengths)

  


    input_ids = torch.full((len(batch_items), max_len), pad_id, dtype=torch.long)

    # ... (填充逻辑)

  


    with torch.autocast("cuda", dtype=torch.bfloat16):

        with torch.no_grad():

            output_ids = model.generate(

                input_ids.cuda(),

                images=images,

                max_new_tokens=8192,

                temperature=0.0,

            )

  


    # 解码输出

    outputs = []

    for i, out_ids in enumerate(output_ids):

        token_ids = out_ids[lengths[i]:].tolist()

        text = tokenizer.decode(token_ids, skip_special_tokens=False)

        outputs.append(text.strip())

    return outputs

💡 小技巧:设置 temperature=0.0 让输出具有确定性,结果更稳定可复现。

4️⃣ 本地客户端

服务器部署好后,需要一个客户端来投喂页面。用 @app.local_entrypoint() 装饰器标记本地运行的入口函数:


@app.local_entrypoint()

def main(api_url: str, book: str = "", max_pages: int = None, batch_size: int = 1):

    import fitz  # PyMuPDF

  


    if book:

        pdf_paths = [BOOKS_DIR / book]

    else:

        pdf_paths = sorted(BOOKS_DIR.glob("*.pdf"))

  


    for pdf_path in pdf_paths:

        with fitz.open(pdf_path) as doc:

            batch_pages = []

            for page_index in range(doc.page_count):

                page = doc[page_index]

                pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))  # 2倍缩放

                batch_pages.append(pix.tobytes("png"))

  


                if len(batch_pages) >= batch_size:

                    # 发送批次到服务器

                    response = requests.post(f"{api_url}/ocr_batch", files=files)

                    texts = response.json()["texts"]

                    # 保存结果...

💡 重要技巧fitz.Matrix(2, 2) 这个 2 倍渲染技巧很关键!更高分辨率的输入意味着 OCR 模型能更准确地识别小号文字和数学下标。

5️⃣ 输出清理

DeepSeek OCR 输出会带有定位标签(grounding tags),标注文本在页面上的坐标位置。这些对某些场景有用,但纯文本搜索不需要:


tag_pattern = re.compile(

    r"<\|ref\|>(.*?)<\|/ref\|><\|det\|>.*?<\|/det\|>",

    flags=re.DOTALL,

)

  


for page_idx, text in zip(batch_page_indices, texts):

    text = tag_pattern.sub(r"\1", text)  # 保留文本,丢弃坐标

    page_path = pages_dir / f"page_{page_idx + 1:04d}.mmd"

    page_path.write_text(text, encoding="utf-8")

.mmd 扩展名代表 "多模态 markdown",表示来自 OCR 源可能带有些许瑕疵的 markdown 文件。


🏃‍♂️ 实际运行

第一步:部署服务器


modal deploy deepseek_ocr_modal.py

部署后会得到一个 URL,类似:

https://your-workspace--deepseek-ocr-books-api-batch-fastapi-app.modal.run

第二步:运行客户端


modal run deepseek_ocr_modal.py \

  --api-url "https://..." \

  --book "Gelman - Bayesian Data Analysis.pdf"


📊 实测效果

处理《贝叶斯数据分析》这本约 600 页的书:

| 指标 | 数值 |

|------|------|

| 处理时间 | 约 45 分钟 |

| GPU 型号 | A100 |

| 批处理大小 | 4 |

| 总花费 | 约 2 美元 |

输出结果:一个 markdown 文件目录(每页一个文件),加上合并后的 document.mmd 文件(带页码标记)。

OCR 质量展示

数学公式的识别质量相当惊艳!来看看实际效果:

OCR 输出示例


## 指数模型

  


指数分布通常用于建模"等待时间"和其他连续、正实值随机变量,通常在时间尺度上测量。给定参数θ,结果y的抽样分布为

  


p(y|θ) = θ exp(-yθ), 当y > 0,

  


其中θ = 1/E(y|θ)被称为"率"。从数学上讲,指数分布是伽马分布的一个特例,参数为(α,β) = (1,θ)。

  


指数分布具有"无记忆"特性,使其成为生存或寿命数据的自然模型;对象存活额外时间长度t的概率独立于到此时已过去的时间:Pr(y > t + s | y > s, θ) = Pr(y > t | θ) 对于任何s,t。

教科书原文截图

image.png

对比来看,OCR 效果相当不错!


🎯 后续玩法

有了可搜索的 markdown 文本,能做的事情就多了:

  • grep 全文搜索关键概念

  • ✅ 复制章节内容让 AI 解释符号含义

  • ✅ 构建向量索引,打造知识库问答

  • ✅ 批量处理课程笔记、论文、其他教材

从原本只是一堆图片的 PDF,变成了真正可用的数据资产!


📝 总结

这个方案的核心优势:

  1. 成本低 - 约 2 美元处理 600 页,比商业 OCR 服务便宜得多

  2. 质量高 - DeepSeek OCR 对数学公式识别效果出色

  3. 可复用 - 一次搭建,适用于各种 PDF 文档

  4. 易上手 - 核心代码不超过 40 行

如果你也有扫描版教科书或技术文档需要处理,花 5 分钟搭一套试试,绝对值!


🔗 参考链接


如果这篇文章对你有帮助,欢迎点赞收藏,有问题评论区见~ 👋