2026年7月2日 Docker 容器化部署 PaddleOCR-VL 实战(OpenAI 协议兼容 HTTP 调用)

衔接上文:https://elowen.blog.csdn.net/article/details/162460343?spm=1011.2415.3001.5331

本文主要讲解PaddleOCR-VL-1.5/1.6 模型部署,并将模型封装成API,支持OpenAI

1、拉取镜像

bash 复制代码
# 拉取镜像
docker pull ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/paddle:3.3.1

拉取下来后镜像拉取后本地磁盘总占用 6.99GB,实际使用1.8G

2、启动容器并进入

bash 复制代码
# 启动容器,把当前目录挂载到容器的 /paddle
docker run --name paddle_vl -it \
  -p 8010:8000 \
  -v $PWD:/paddle \
  ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/paddle:3.3.1 \
  /bin/bash
  • --name paddle_vl:容器命名为 paddle_vl
  • -it:交互式终端,可进入容器操作
  • -p 8010:8000:宿主机 8010 端口映射容器 8000
  • -v $PWD:/paddle:当前目录挂载到容器 /paddle
  • 镜像:百度云 paddle 3.3.1 镜像
  • /bin/bash:启动后进入 bash 命令行

3、进入容器内后,安装paddleOCR

进入容器后,补装 PaddleOCR 开发套件([doc-parser]包含了运行 VLM 所需的全部依赖):

bash 复制代码
pip install -U "paddleocr[doc-parser]"

#如果下载慢可以用国内镜像
pip install -U "paddleocr[doc-parser]" python-multipart fastapi uvicorn pydantic pillow numpy -i https://pypi.tuna.tsinghua.edu.cn/simple

出现成功的提示即可

4、下载模型

模型下载在 /paddle/model 下

bash 复制代码
#看paddleocr的框架,如果是最新的话就只支持1.6的模型
#modelscope download --model PaddlePaddle/PaddleOCR-VL-1.5

modelscope download --model PaddlePaddle/PaddleOCR-VL-1.6

⚠️注意:这里有的同学可能会下载到 1.5 的模型,目前的 paddle 最新的框架是不支持1.5了,必须改用1.6

5、OpenAI 协议兼容 HTTP 推理服务

在目录 /paddle 下我们新建一个文件 paddleocr_vl_server.py 把以下内容写入到文件中

python 复制代码
"""
PaddleOCR-VL-1.5 OpenAI-Compatible HTTP Server
兼容 OpenAI Chat Completions API 协议,支持多模态图片识别
用法: python paddleocr_vl_server.py --model-path /path/to/PaddleOCR-VL-1.5 --port 8000
"""

import argparse
import base64
import json
import time
import uuid
from io import BytesIO
from typing import List, Optional, Dict, Any, Union

import uvicorn
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field


# ============================================================
# OpenAI API 兼容的数据模型
# ============================================================

class ChatMessageContentPart(BaseModel):
    """多模态消息内容部分"""
    type: str
    text: Optional[str] = None
    image_url: Optional[Dict[str, str]] = None


class ChatMessage(BaseModel):
    """聊天消息"""
    role: str
    content: Union[str, List[ChatMessageContentPart]]


class ChatCompletionRequest(BaseModel):
    """OpenAI Chat Completions 请求"""
    model: str = "PaddleOCR-VL-1.5"
    messages: List[ChatMessage]
    max_tokens: Optional[int] = 4096
    temperature: Optional[float] = 0.0
    top_p: Optional[float] = 1.0
    stream: Optional[bool] = False
    # PaddleOCR-VL 扩展参数
    use_layout_detection: Optional[bool] = True
    layout_model_dir: Optional[str] = None


class Choice(BaseModel):
    index: int = 0
    message: ChatMessage
    finish_reason: str = "stop"


class Usage(BaseModel):
    prompt_tokens: int = 0
    completion_tokens: int = 0
    total_tokens: int = 0


class ChatCompletionResponse(BaseModel):
    """OpenAI Chat Completions 响应"""
    id: str
    object: str = "chat.completion"
    created: int
    model: str
    choices: List[Choice]
    usage: Usage


class ModelInfo(BaseModel):
    id: str
    object: str = "model"
    created: int = 0
    owned_by: str = "PaddlePaddle"


class ModelsResponse(BaseModel):
    object: str = "list"
    data: List[ModelInfo]


# ============================================================
# 图片处理工具
# ============================================================

def decode_image_from_url(url: str) -> bytes:
    """从 data URL 或网络 URL 解码图片"""
    if url.startswith("data:image"):
        # data:image/png;base64,xxxxxx
        header, b64 = url.split(",", 1)
        return base64.b64decode(b64)
    else:
        # 普通 URL,需要下载
        import urllib.request
        with urllib.request.urlopen(url) as resp:
            return resp.read()


def extract_image_from_message(content: Union[str, List[ChatMessageContentPart]]) -> Optional[bytes]:
    """从消息中提取图片二进制数据"""
    if isinstance(content, str):
        return None

    for part in content:
        if part.type == "image_url" and part.image_url:
            url = part.image_url.get("url", "")
            if url:
                return decode_image_from_url(url)
    return None


def extract_text_from_message(content: Union[str, List[ChatMessageContentPart]]) -> str:
    """从消息中提取文本"""
    if isinstance(content, str):
        return content

    texts = []
    for part in content:
        if part.type == "text" and part.text:
            texts.append(part.text)
    return "\n".join(texts)


# ============================================================
# PaddleOCR-VL Pipeline 封装
# ============================================================

class PaddleOCRVLPipeline:
    """PaddleOCR-VL 推理管线封装"""

    def __init__(self, model_path: str, layout_model_path: Optional[str] = None):
        self.model_path = model_path
        self.layout_model_path = layout_model_path
        self.pipeline = None
        self._init_pipeline()

    def _init_pipeline(self):
        from paddleocr import PaddleOCRVL
        kwargs = {
            "vl_rec_model_dir": self.model_path,
            "vl_rec_backend": "native",
        }
        if self.layout_model_path:
            kwargs["layout_detection_model_dir"] = self.layout_model_path
        else:
            kwargs["use_layout_detection"] = False

        self.pipeline = PaddleOCRVL(**kwargs)
        print(f"✅ PaddleOCR-VL 模型加载完成: {self.model_path}")

    def predict(self, image_bytes: bytes, prompt: str = "描述这张图片",
                use_layout_detection: bool = True) -> str:
        """
        对图片进行 OCR 识别,返回 Markdown 格式文本
        """
        import tempfile
        import os
        from PIL import Image
        import numpy as np

        # 将 bytes 转为临时文件(PaddleOCR-VL 接受文件路径)
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
            f.write(image_bytes)
            img_path = f.name

        try:
            # 如果启用了版面检测,需要在 pipeline 层面控制
            if not use_layout_detection:
                self.pipeline.use_layout_detection = False

            output = self.pipeline.predict(img_path)

            # 组装结果
            results = []
            for res in output:
                # 优先获取 Markdown 格式输出,没有就转字符串
                if hasattr(res,'markdown'):
                    content = res.markdown
                else:
                    # fallback: 获取纯文本
                    content = str(res)

                if isinstance(content, dict):
                    content = str(content)

                results.append(content)

            return "\n\n".join(results) if results else "未识别到内容"
        finally:
            os.unlink(img_path)
            if not use_layout_detection:
                self.pipeline.use_layout_detection = True


# ============================================================
# FastAPI 应用
# ============================================================

app = FastAPI(
    title="PaddleOCR-VL-1.5 API",
    description="OpenAI-compatible API for PaddleOCR-VL-1.5 multimodal OCR",
    version="1.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 全局 pipeline 实例
pipeline: Optional[PaddleOCRVLPipeline] = None


@app.on_event("startup")
async def startup():
    global pipeline
    parser = argparse.ArgumentParser()
    parser.add_argument("--model-path", required=True, help="PaddleOCR-VL 模型目录")
    parser.add_argument("--layout-model-path", default=None, help="版面分析模型目录")
    parser.add_argument("--port", type=int, default=8000)
    args, _ = parser.parse_known_args()
    pipeline = PaddleOCRVLPipeline(args.model_path, args.layout_model_path)


# ---------- /v1/models ----------
@app.get("/v1/models", response_model=ModelsResponse)
async def list_models():
    return ModelsResponse(
        data=[
            ModelInfo(id="PaddleOCR-VL-1.5", owned_by="PaddlePaddle"),
        ]
    )


@app.get("/v1/models/{model_id}", response_model=ModelInfo)
async def get_model(model_id: str):
    return ModelInfo(id=model_id, owned_by="PaddlePaddle")


# ---------- /v1/chat/completions ----------
@app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
async def chat_completions(request: ChatCompletionRequest):
    global pipeline
    if pipeline is None:
        raise HTTPException(status_code=500, detail="模型未初始化")

    # 从 messages 中提取最后一张图片和配套文本
    image_bytes = None
    user_text = ""

    # 遍历消息,找最后一条含图片的用户消息
    for msg in reversed(request.messages):
        if msg.role == "user":
            img = extract_image_from_message(msg.content)
            txt = extract_text_from_message(msg.content)
            if img is not None:
                image_bytes = img
                user_text = txt
                break
            elif not user_text:
                user_text = txt

    if image_bytes is None:
        raise HTTPException(status_code=400, detail="请求中未包含图片,请使用 image_url 字段提供图片")

    # 执行推理
    try:
        result_text = pipeline.predict(
            image_bytes,
            prompt=user_text if user_text else "请识别图片中的内容",
            use_layout_detection=request.use_layout_detection,
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"推理失败: {str(e)}")

    # 组装 OpenAI 格式响应
    response_id = f"chatcmpl-{uuid.uuid4().hex[:12]}"
    now = int(time.time())

    return ChatCompletionResponse(
        id=response_id,
        created=now,
        model=request.model,
        choices=[
            Choice(
                message=ChatMessage(
                    role="assistant",
                    content=result_text,
                ),
                finish_reason="stop",
            )
        ],
        usage=Usage(
            prompt_tokens=0,
            completion_tokens=0,
            total_tokens=0,
        ),
    )


# ---------- /v1/ocr (PaddleOCR-VL 专属接口,额外功能) ----------
class OCRRequest(BaseModel):
    """直接 OCR 请求,接受 base64 图片"""
    image: str  # base64 编码的图片
    prompt: Optional[str] = "请识别图片中的内容"
    use_layout_detection: Optional[bool] = True
    output_format: Optional[str] = "markdown"  # markdown / json / text


@app.post("/v1/ocr")
async def ocr_direct(request: OCRRequest):
    """直接 OCR 接口,接受 base64 图片数据"""
    global pipeline
    if pipeline is None:
        raise HTTPException(status_code=500, detail="模型未初始化")

    try:
        image_bytes = base64.b64decode(request.image)
    except Exception:
        raise HTTPException(status_code=400, detail="图片 base64 解码失败")

    try:
        result_text = pipeline.predict(
            image_bytes,
            prompt=request.prompt,
            use_layout_detection=request.use_layout_detection,
        )
        return {
            "status": "success",
            "result": result_text,
            "format": request.output_format,
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"OCR 失败: {str(e)}")


# ---------- /health ----------
@app.get("/health")
async def health():
    return {"status": "ok", "model_loaded": pipeline is not None}


# ============================================================
# 入口
# ============================================================

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="PaddleOCR-VL-1.5 OpenAI-Compatible Server")
    parser.add_argument("--model-path", required=True, help="PaddleOCR-VL 模型目录路径")
    parser.add_argument("--layout-model-path", default=None, help="版面分析模型目录路径")
    parser.add_argument("--host", default="0.0.0.0", help="监听地址")
    parser.add_argument("--port", type=int, default=8000, help="监听端口")
    parser.add_argument("--workers", type=int, default=1, help="工作进程数(CPU 建议 1)")
    args = parser.parse_args()

    # 把参数注入到环境变量,供 startup 事件读取
    import os
    os.environ["PADDLEOCR_VL_MODEL_PATH"] = args.model_path
    if args.layout_model_path:
        os.environ["PADDLEOCR_VL_LAYOUT_PATH"] = args.layout_model_path

    # 重写 startup 让它从环境变量读
    @app.on_event("startup")
    async def startup_env():
        global pipeline
        mp = os.environ.get("PADDLEOCR_VL_MODEL_PATH")
        lp = os.environ.get("PADDLEOCR_VL_LAYOUT_PATH") or None
        if mp:
            pipeline = PaddleOCRVLPipeline(mp, lp)

    uvicorn.run(
        app,
        host=args.host,
        port=args.port,
        workers=args.workers,
    )

该脚本完整作用详解

这份代码是PaddleOCR-VL-1.5 多模态 OCR 大模型的 OpenAI 协议兼容 HTTP 推理服务,核心目标:把 Paddle 多模态图文识别模型封装成和 OpenAI ChatGPT 接口格式完全一致的 API,方便现有大模型客户端直接调用 OCR 能力。

一、核心定位
  1. 底层能力 :封装 PaddleOCR-VL-1.5 多模态 OCR 模型,支持图片文字识别、版面分析、文档转 Markdown;
  2. 上层兼容 :完全对齐 OpenAI /v1/chat/completions 标准接口,任何支持 OpenAI 协议的工具(LangChain、OpenAI SDK、各类 AI 客户端、RAG 系统)不用改代码就能接入 OCR;
  3. 额外扩展 :提供原生 /v1/ocr 简化接口、健康检查、模型列表接口,方便运维与直接传 Base64 图片调用。
二、分模块拆解功能
1. 数据模型层(OpenAI 协议对齐)

用 Pydantic 严格复刻 OpenAI Chat Completions 请求 / 响应结构:

  • 支持多模态入参:文本 + 图片 URL(base64 dataURL、网络图片链接);
  • 兼容 OpenAI 标准字段:modelmessagesstreammax_tokenstemperature
  • 自定义扩展参数:use_layout_detection 控制是否启用版面分析;
  • 输出标准 OpenAI 返回体:idchoicesassistant回答、usage字段。
2. 图片解析工具
  • 解析消息内的 image_url,支持两种图片来源:
    1. Base64 内嵌图片 data:image/png;base64,xxx
    2. 互联网远程图片 URL(自动下载);
  • 从对话 messages 中自动分离提示文本图片二进制流,自动检索最后一条带图片的用户消息作为识别输入。
3. PaddleOCR-VL 推理封装类 PaddleOCRVLPipeline
  • 启动时加载本地 VL 多模态 OCR 权重 + 可选版面分析模型;
  • 接收图片二进制,生成临时图片文件传给 PaddleOCR-VL 推理;
  • 识别完成自动删除临时文件,支持开关版面检测;
  • 优先输出结构化 Markdown 文档文本(表格、标题、段落自动排版),无 markdown 字段则返回原始识别文本。
4. FastAPI HTTP 服务接口
标准 OpenAI 兼容接口(给通用 AI 客户端用)
  1. GET /v1/models:列出当前可用模型(模拟 OpenAI 模型列表接口)
  2. GET /v1/models/{model_id}:查询模型信息
  3. POST /v1/chat/completions 核心接口
    • 接收 OpenAI 格式多模态对话(文字 + 图片);
    • 自动提取图片与提示词,调用 OCR 识别;
    • 把识别结果包装成 OpenAI 标准返回格式输出。
专属简化 OCR 接口(业务直接调用)
  • POST /v1/ocr:轻量化接口,直接传 Base64 图片字符串,无需套 chat 对话结构,适合内部业务系统快速调用,支持指定输出格式。
运维接口
  • GET /health:健康探测,返回模型是否加载成功,用于容器 / 集群存活检测。
5. 服务启动入口
  • 命令行参数:指定模型路径、版面模型路径、监听端口、主机、多进程;
  • 通过环境变量传递模型路径给服务启动钩子,避免 uvicorn 多进程下参数冲突;
  • 使用 uvicorn 启动高性能异步 Web 服务,默认单进程(Paddle 推理不支持多 worker 并发)。
三、实际使用场景
  1. RAG 知识库文档解析:LangChain/LlamaIndex 等框架直接调用 OpenAI 接口,上传 PDF 截图、票据、试卷、合同自动提取结构化文本;
  2. 通用 AI 客户端对接:Chatbox、OpenWebUI 等工具不用二次开发,填入该服务地址即可拥有图片 OCR 能力;
  3. 业务系统 OCR 服务 :后端微服务调用 /v1/ocr 识别发票、证件、手写文档;
  4. 私有化本地多模态 OCR 部署:离线环境部署,不依赖第三方云端 OCR,完全本地运行。
四、核心优势总结
  1. 协议兼容:复用现有 OpenAI 生态工具链,无需自定义请求逻辑;
  2. 多模态原生支持:遵循 GPT-4V 图片输入规范,图文混合提问;
  3. 结构化输出:版面分析后自动生成 Markdown,保留文档排版;
  4. 双接口设计:兼顾标准化 AI 调用与轻量化业务调用;
  5. 容器友好:提供健康检查,便于 Docker/K8s 集群运维;
  6. 离线私有化:全部推理在本地模型完成,无数据外传。

6、运行

进入到容器中启动

bash 复制代码
python /paddle/paddleocr_vl_server.py \
  --model-path /paddle/model/PaddleOCR-VL-1.6 \
  --host 0.0.0.0 \
  --port 8000

启动成功后到界面

7、调用

7.1 远程上传url

post http://IP:8010/v1/chat/completions

bash 复制代码
{
    "model": "PaddleOCR-VL-1.6",
    "messages": [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "提取图片全部文字,保留排版"
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": "<http://ip:10143/api/xinfang-intelligent-analysis/v1/material/preview/2071840161683677185?rj-token=***>"
                    }
                }
            ]
        }
    ]

返回的结果

javascript 复制代码
{
    "id": "chatcmpl-9b3d3ee3ebd0",
    "object": "chat.completion",
    "created": 1782870165,
    "model": "PaddleOCR-VL-1.6",
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "{'markdown_texts': '我是一个靓仔', 'markdown_images': {}, 'page_index': None, 'input_path': '/tmp/tmp75d07ys5.png'}"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 0,
        "completion_tokens": 0,
        "total_tokens": 0
    }
}

7.2 本地图片上传

post http://192.168.100.210:8010/v1/ocr/upload

END

如果觉得这份基础知识点总结清晰,别忘了动动小手点个赞👍,再关注一下呀~ 后续还会分享更多有关开发问题的干货技巧,同时一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟

相关推荐
ZXXstarstar2 小时前
2026年电池安全使用年限的量化决策:什么时间到期最需要替换?
linux·运维·服务器·电池
网络小白不怕黑2 小时前
httpd监控
linux·运维·服务器
2501_915716722 小时前
Nginx网关讲解_小白版
运维·nginx
胖胖雕2 小时前
利用阿里云与docker部署Certimate
阿里云·docker·云计算
kylin-运维3 小时前
k8s可视化看板嵌入AI助手(悬浮对话球)
人工智能·容器·kubernetes·aicoding
Amy187021118233 小时前
告别“爬楼抄表”时代:电梯计量自动化改造,让每一度电都“开口说话”
运维·自动化
dadaobusi3 小时前
在 Buildroot 中为 RISC-V 64 架构集成 numactl
运维·服务器
会飞的土拨鼠呀3 小时前
Docker 容器维护手册
docker·容器·eureka
慕伏白3 小时前
【慕伏白】Docker 常用指令
docker·容器·eureka