
衔接上文: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 能力。
一、核心定位
- 底层能力 :封装
PaddleOCR-VL-1.5多模态 OCR 模型,支持图片文字识别、版面分析、文档转 Markdown; - 上层兼容 :完全对齐 OpenAI
/v1/chat/completions标准接口,任何支持 OpenAI 协议的工具(LangChain、OpenAI SDK、各类 AI 客户端、RAG 系统)不用改代码就能接入 OCR; - 额外扩展 :提供原生
/v1/ocr简化接口、健康检查、模型列表接口,方便运维与直接传 Base64 图片调用。
二、分模块拆解功能
1. 数据模型层(OpenAI 协议对齐)
用 Pydantic 严格复刻 OpenAI Chat Completions 请求 / 响应结构:
- 支持多模态入参:文本 + 图片 URL(base64 dataURL、网络图片链接);
- 兼容 OpenAI 标准字段:
model、messages、stream、max_tokens、temperature; - 自定义扩展参数:
use_layout_detection控制是否启用版面分析; - 输出标准 OpenAI 返回体:
id、choices、assistant回答、usage字段。
2. 图片解析工具
- 解析消息内的
image_url,支持两种图片来源:- Base64 内嵌图片
data:image/png;base64,xxx; - 互联网远程图片 URL(自动下载);
- Base64 内嵌图片
- 从对话 messages 中自动分离提示文本 和图片二进制流,自动检索最后一条带图片的用户消息作为识别输入。
3. PaddleOCR-VL 推理封装类 PaddleOCRVLPipeline
- 启动时加载本地 VL 多模态 OCR 权重 + 可选版面分析模型;
- 接收图片二进制,生成临时图片文件传给 PaddleOCR-VL 推理;
- 识别完成自动删除临时文件,支持开关版面检测;
- 优先输出结构化 Markdown 文档文本(表格、标题、段落自动排版),无 markdown 字段则返回原始识别文本。
4. FastAPI HTTP 服务接口
标准 OpenAI 兼容接口(给通用 AI 客户端用)
GET /v1/models:列出当前可用模型(模拟 OpenAI 模型列表接口)GET /v1/models/{model_id}:查询模型信息POST /v1/chat/completions核心接口- 接收 OpenAI 格式多模态对话(文字 + 图片);
- 自动提取图片与提示词,调用 OCR 识别;
- 把识别结果包装成 OpenAI 标准返回格式输出。
专属简化 OCR 接口(业务直接调用)
POST /v1/ocr:轻量化接口,直接传 Base64 图片字符串,无需套 chat 对话结构,适合内部业务系统快速调用,支持指定输出格式。
运维接口
GET /health:健康探测,返回模型是否加载成功,用于容器 / 集群存活检测。
5. 服务启动入口
- 命令行参数:指定模型路径、版面模型路径、监听端口、主机、多进程;
- 通过环境变量传递模型路径给服务启动钩子,避免 uvicorn 多进程下参数冲突;
- 使用 uvicorn 启动高性能异步 Web 服务,默认单进程(Paddle 推理不支持多 worker 并发)。
三、实际使用场景
- RAG 知识库文档解析:LangChain/LlamaIndex 等框架直接调用 OpenAI 接口,上传 PDF 截图、票据、试卷、合同自动提取结构化文本;
- 通用 AI 客户端对接:Chatbox、OpenWebUI 等工具不用二次开发,填入该服务地址即可拥有图片 OCR 能力;
- 业务系统 OCR 服务 :后端微服务调用
/v1/ocr识别发票、证件、手写文档; - 私有化本地多模态 OCR 部署:离线环境部署,不依赖第三方云端 OCR,完全本地运行。
四、核心优势总结
- 协议兼容:复用现有 OpenAI 生态工具链,无需自定义请求逻辑;
- 多模态原生支持:遵循 GPT-4V 图片输入规范,图文混合提问;
- 结构化输出:版面分析后自动生成 Markdown,保留文档排版;
- 双接口设计:兼顾标准化 AI 调用与轻量化业务调用;
- 容器友好:提供健康检查,便于 Docker/K8s 集群运维;
- 离线私有化:全部推理在本地模型完成,无数据外传。
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
如果觉得这份基础知识点总结清晰,别忘了动动小手点个赞👍,再关注一下呀~ 后续还会分享更多有关开发问题的干货技巧,同时一起解锁更多好用的功能,少踩坑多提效!🥰 你的支持就是我更新的最大动力,咱们下次分享再见呀~🌟