华为昇腾服务器部署Paddle OCR VL模型及推理服务

Paddle OCR VL 1.5是百度飞桨推出的0.9B超紧凑多模态文档解析模型,其核心特色包括:全球首个支持异形框定位,能精准处理倾斜、弯折、拍照畸变等真实场景;在OmniDocBench v1.5评测中达到94.5%高精度;集成文本定位、印章识别、表格解析等多任务一体化能力。

我在昇腾服务器上部署了这个模型,并开启了推理服务,使得用户可以上传图像或者多页PDF文件,并自动对多页文件进行整合后输出。

按照VLLM Ascend官网的介绍,通过以下命令启动Paddle OCR VL的服务

bash 复制代码
docker run --rm \
    --name paddleocr_vl \
    --shm-size=64g \
    --net=host \
    --device /dev/davinci6 \
    --device /dev/davinci_manager \
    --device /dev/devmm_svm \
    --device /dev/hisi_hdc \
    -v /usr/local/dcmi:/usr/local/dcmi \
    -v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
    -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
    -v /usr/local/Ascend/driver/lib64:/usr/local/Ascend/driver/lib64 \
    -v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
    -v /etc/ascend_install.info:/etc/ascend_install.info \
    -v /Public_dir/PaddleOCR-VL-1.5:/model \
    -itd m.daocloud.io/quay.io/ascend/vllm-ascend:v0.14.0rc1 bash

然后进入容器,通过以下命令启动。因为这个模型的参数量很小,所以可以把gpu-memory-utilization的值设小一点

bash 复制代码
nohup vllm serve /model --host 123.123.123.123 --port 9999 --served-model-name PaddleOCR-VL-1.5-0.9B --mm-processor-cache-gb 0 --compilation-config '{"cudagraph_mode": "FULL_DECODE_ONLY"}'  --trust-remote-code --gpu-memory-utilization 0.4 &

启动之后即可即可通过以下的程序进行调用。

python 复制代码
from openai import OpenAI

client = OpenAI(
    api_key="EMPTY",
    base_url="http://123.123.123.123:9999/v1",
    timeout=3600
)

# Task-specific base prompts
TASKS = {
    "ocr": "OCR:",
    "table": "Table Recognition:",
    "formula": "Formula Recognition:",
    "chart": "Chart Recognition:",
}

messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "image_url",
                "image_url": {
                    "url": "https://ofasys-multimodal-wlcb-3-toshanghai.oss-accelerate.aliyuncs.com/wpf272043/keepme/image/receipt.png"
                }
            },
            {
                "type": "text",
                "text": TASKS["ocr"]
            }
        ]
    }
]

response = client.chat.completions.create(
    model="PaddleOCR-VL-0.9B",
    messages=messages,
    temperature=0.0,
)
print(f"Generated text: {response.choices[0].message.content}")

为了更好的提升OCR识别的效果,还可以搭配Paddle的PP-DocLayoutV2模型,这是百度飞桨推出的文档版面分析模型,基于RT-DETR检测器扩展了指针网络,能精准检测标题、文本、表格、公式等23种版面元素并预测阅读顺序进行版面识别。

按照官方文档的介绍准备镜像,PaddleOCR-VL --- vllm-ascend

然后通过以下命令启动

bash 复制代码
docker run -itd \
    --name paddle-ocr-doclayout \
    -v /Public_dir:/work \
    --privileged \
    --network=host \
    --shm-size=32G \
    -w=/work \
    -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
    -v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
    -v /usr/local/dcmi:/usr/local/dcmi \
    -e Ascend_RT_VISIBLE_DEVICES="6" \
    paddle-npu:v1.2 bash

然后编写一个API服务的程序,提供用户上传图像或者PDF文件,然后进行OCR提取的功能。

python 复制代码
from fastapi import FastAPI, UploadFile, File, Depends, HTTPException, Form
from fastapi.responses import JSONResponse, StreamingResponse
from contextlib import asynccontextmanager
import asyncio
import aiofiles
import os
import uuid
import logging
from typing import List
from paddleocr import PaddleOCRVL
import io
import zipfile
import shutil

#os.environ['PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK'] = True
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 模型管理器(单例模式)
class ModelManager:
    _instance = None
    pipeline = None
    
    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance
    
    async def initialize_model(self):
        if self.pipeline is not None:
            return self.pipeline
            
        try:
            from paddleocr import PaddleOCRVL
            doclayout_model_path = "/work/PP-DocLayoutV2/"
            
            self.pipeline = PaddleOCRVL(
                vl_rec_backend="vllm-server", 
                vl_rec_server_url="http://10.134.252.253:9999/v1",
                vl_rec_model_name="PaddleOCR-VL-1.5-0.9B",
                layout_detection_model_name="PP-DocLayoutV2",  
                layout_detection_model_dir=doclayout_model_path,
                use_layout_detection=True,
                device="npu:6"
            )
            logger.info("✅ PP-DocLayoutV2模型初始化完成")
            return self.pipeline
        except Exception as e:
            logger.error(f"❌ 模型初始化失败: {e}")
            raise
    
    def get_model(self):
        if self.pipeline is None:
            raise RuntimeError("模型未初始化")
        return self.pipeline

# 应用生命周期
@asynccontextmanager
async def app_lifespan(app: FastAPI):
    # 启动时初始化
    logger.info("🚀 启动FastAPI服务...")
    model_manager = ModelManager.get_instance()
    await model_manager.initialize_model()
    yield
    # 关闭时清理
    logger.info("🛑 关闭服务,清理资源...")
    if model_manager.pipeline is not None:
        del model_manager.pipeline
        model_manager.pipeline = None

app = FastAPI(
    title="OCR高并发服务",
    description="基于PP-DocLayoutV2的高性能OCR服务",
    version="1.0.0",
    lifespan=app_lifespan
)

# 依赖注入
async def get_ocr_pipeline():
    return ModelManager.get_instance().get_model()

@app.get("/")
async def root():
    return {"message": "OCR服务运行中", "status": "healthy"}

@app.post("/predict")
async def predict_ocr(
    file: UploadFile = File(..., description="支持JPG,PNG或PDF"),
    merge: bool = Form(False, description="是否对多页PDF识别结果进行合并(true/false)"),
    pipeline: PaddleOCRVL = Depends(get_ocr_pipeline)
):
    """高性能OCR预测接口"""
    start_time = asyncio.get_event_loop().time()
    # 文件验证
    if not file.content_type.startswith('image/') and not file.filename.lower().endswith('.pdf'):
        raise HTTPException(400, "请上传图片文件或PDF文件")
    
    # 处理文件
    temp_dir = "temp"
    os.makedirs(temp_dir, exist_ok=True)
    temp_id = uuid.uuid4()
    temp_session_dir = f"{temp_dir}/{temp_id}"
    os.makedirs(temp_session_dir, exist_ok=True)
    temp_path = os.path.join(temp_session_dir, f"{file.filename}")
    
    try:
        # 保存文件
        async with aiofiles.open(temp_path, 'wb') as f:
            await f.write(await file.read())
        
        # 异步推理
        loop = asyncio.get_event_loop()
        output = await loop.run_in_executor(
            None, 
            lambda: pipeline.predict(temp_path)
        )
        
        # 处理结果
        results = []
        pages_res = list(output)
        if len(pages_res) > 1 and merge:
            output = pipeline.restructure_pages(
                pages_res,
                merge_tables=True,
                relevel_titles=True,
                concatenate_pages=True
            )
        for i, res in enumerate(output):
            save_path = f"{temp_session_dir}/output_{i}.json"
            res.save_to_json(save_path=save_path)
            save_path = f"{temp_session_dir}/output_{i}.md"
            res.save_to_markdown(save_path=save_path)

        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files in os.walk(temp_session_dir):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, temp_session_dir)
                    zipf.write(file_path, arcname)
        zip_buffer.seek(0)
        #results.append({"index": i, "save_path": save_path})

        '''
        with open(results[0]['save_path'], 'r', encoding='utf-8') as f:
            json_text = file.read()
        string_stream = io.StringIO(json_text)
        string_stream.seek(0)

        processing_time = asyncio.get_event_loop().time() - start_time
        logger.info(f"✅ 处理完成: {file.filename}, 耗时: {processing_time:.2f}s")
        
        return {
            "filename": file.filename,
            "processing_time": f"{processing_time:.2f}s",
            "results": results
        }
        '''
        response = StreamingResponse(
            zip_buffer,
            media_type="application/x-zip-compressed",
            headers={
                "Content-Disposition": f"attachment; filename={temp_id}",
                "Content-Type": "application/zip"
            }
        )
        shutil.rmtree(temp_session_dir)
        return response
        
    except Exception as e:
        logger.error(f"❌ 处理失败: {file.filename}, 错误: {e}")
        raise HTTPException(500, f"处理失败: {str(e)}")
    finally:
        if os.path.exists(temp_path):
            os.unlink(temp_path)

@app.get("/health")
async def health_check():
    """健康检查端点"""
    try:
        manager = ModelManager.get_instance()
        pipeline = manager.get_model()
        return {
            "status": "healthy",
            "model_loaded": pipeline is not None,
            "message": "服务正常运行"
        }
    except Exception as e:
        raise HTTPException(500, f"服务异常: {str(e)}")

该程序构建了一个基于FastAPI的高性能OCR服务后端,主要功能是调用百度飞桨的PaddleOCR VL 1.5和PP-DocLayoutV2模型,对上传的图像或PDF文件进行版面分析、文本识别和结构化输出。其功能如下:

  1. 采用单例模式管理模型实例,确保服务启动时仅加载一次模型,提高资源利用率;

  2. 完全异步化处理,从文件上传、模型推理到结果打包均使用异步IO,支持高并发请求;

  3. 支持多页PDF处理,提供页面合并选项,可自动合并跨页表格和重组标题层级;

  4. 输出结果多样化,同时生成JSON和Markdown两种格式,并通过ZIP压缩包返回;

  5. 包含完善的错误处理、临时文件管理和健康检查机制,确保服务稳定性。

最后运行以下命令,通过Gunicorn来启动多个进程,提供API服务。

bash 复制代码
export PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK='True'
nohup gunicorn -w 16 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 main:app &

通过以下Python程序可以调用这个OCR服务,测试效果

python 复制代码
import requests
import argparse
import os
import sys
from typing import Optional
import time
import base64

def get_mime_type(file_path):
    extension = os.path.splitext(file_path)[1].lower()
    mime_map = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        'pdf': 'application/pdf'
    }
    return mime_map.get(extension, 'application/octet-stream')

def upload_file_to_api(api_url: str, file_path: str, merge: bool, output_path: str) -> bool:
    """
    上传文件到 API 并保存返回的 ZIP 文件
    
    Args:
        api_url: API 地址
        file_path: 要上传的文件路径
        merge: 是否合并的布尔值
        output_path: 保存返回 ZIP 文件的路径
        
    Returns:
        bool: 是否成功
    """
    # 检查文件是否存在
    if not os.path.exists(file_path):
        print(f"错误: 文件 '{file_path}' 不存在")
        return False
    
    # 检查文件是否可读
    if not os.access(file_path, os.R_OK):
        print(f"错误: 文件 '{file_path}' 不可读")
        return False
    
    # 准备目录(如果输出路径包含目录)
    output_dir = os.path.dirname(output_path)
    if output_dir and not os.path.exists(output_dir):
        try:
            os.makedirs(output_dir, exist_ok=True)
        except Exception as e:
            print(f"错误: 无法创建输出目录 '{output_dir}': {e}")
            return False
    
    try:
        # 打开文件
        with open(file_path, 'rb') as f:
            # 准备 multipart/form-data 数据
            files = {
                'file': (os.path.basename(file_path), f, get_mime_type(file_path))
            }
            data = {
                'merge': str(merge).lower(),  # 转换为 'true' 或 'false'
                'componentCode': '04350939'
            }
            
            cur_date, auth = generate_signature()
            headers = {
                "x-date": cur_date,
                "Authorization": auth
            }

            print(f"正在上传文件: {file_path}")
            print(f"API 地址: {api_url}")
            print(f"Merge 参数: {merge}")
            print(f"输出文件: {output_path}")
            
            # 发送 POST 请求
            response = requests.post(api_url, files=files, data=data, headers=headers, verify=False)
            
            # 检查响应状态
            if response.status_code != 200:
                print(f"错误: API 返回状态码 {response.status_code}")
                if response.text:
                    print(f"响应内容: {response.text[:500]}")
                return False
            
            # 检查响应内容类型
            content_type = response.headers.get('Content-Type', '').lower()
            
            # 保存文件
            zip_content = response.content
            with open(output_path, 'wb') as out_file:
                out_file.write(zip_content)
            # 验证文件是否成功保存
            if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
                file_size = os.path.getsize(output_path)
                print(f"✓ 文件上传成功!")
                print(f"  保存路径: {output_path}")
                print(f"  文件大小: {file_size:,} 字节")
                
                return True
            else:
                print("错误: 保存的文件为空或不存在")
                return False
                
    except requests.exceptions.ConnectionError:
        print(f"错误: 无法连接到服务器 '{api_url}'。请检查网络连接和服务器状态。")
        return False
    except requests.exceptions.Timeout:
        print("错误: 请求超时。请检查网络连接或稍后重试。")
        return False
    except requests.exceptions.RequestException as e:
        print(f"错误: 网络请求异常: {e}")
        return False
    except IOError as e:
        print(f"错误: 文件读写异常: {e}")
        return False
    except Exception as e:
        print(f"错误: 未知异常: {type(e).__name__}: {e}")
        return False

def main():
    parser = argparse.ArgumentParser(
        description='上传图片或PDF并接收 ZIP 文件',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用示例:
  %(prog)s -f document.pdf -m true -o result.zip
  %(prog)s -f image.jpg -m false -o ./output/result.zip --url http://localhost:8000/upload/
  %(prog)s --file data.json --merge true --output /tmp/output.zip
        """
    )
    
    # 必需参数
    parser.add_argument(
        '-f', '--file',
        required=True,
        help='要上传的文件路径'
    )
    
    parser.add_argument(
        '-m', '--merge',
        required=True,
        type=str.lower,
        choices=['true', 'false', 't', 'f', '1', '0', 'yes', 'no'],
        help='是否合并处理 (true/false)'
    )
    
    parser.add_argument(
        '-o', '--output',
        required=True,
        help='保存返回 ZIP 文件的路径'
    )
    
    # 可选参数
    parser.add_argument(
        '--url',
        default='https://123.123.123.123:8000/predict',
        help='API地址 (默认: https://123.123.123.123:8000/predict)'
    )
    
    parser.add_argument(
        '--timeout',
        type=int,
        default=60,
        help='请求超时时间(秒)(默认: 30)'
    )
    
    # 解析参数
    args = parser.parse_args()
    
    # 转换 merge 参数
    merge_map = {
        'true': True, 't': True, '1': True, 'yes': True,
        'false': False, 'f': False, '0': False, 'no': False
    }
    merge = merge_map[args.merge]
    
    # 设置全局超时
    requests_timeout = args.timeout
    
    # 上传文件
    success = upload_file_to_api(args.url, args.file, merge, args.output)
    
    # 根据结果返回适当的退出码
    sys.exit(0 if success else 1)

if __name__ == "__main__":
    main()
相关推荐
石榴树下的七彩鱼17 小时前
OCR 识别不准确怎么办?模糊 / 倾斜 / 反光图片优化实战(附完整解决方案 + 代码示例)
图像处理·人工智能·后端·ocr·api·文字识别·图片识别
开开心心_Every1 天前
安卓图片压缩工具,无损缩放尺寸免费好用
人工智能·pdf·计算机外设·ocr·语音识别·团队开发·规格说明书
Klong.k1 天前
话不多说 直接上glm-ocr的工具类
ocr
Daydream.V1 天前
PaddleOCR入门到实战教程
paddle·paddleocr·paddleocr安装·paddleocr实战
无心水1 天前
13、云端OCR终极指南|百度/阿里/腾讯API高精度文字提取实战
百度·架构·pdf·ocr·dubbo·pdf解析·pdf抽取
Chef_Chen1 天前
论文解读:不需要OCR,不需要描述生成——ColPali重构了文档检索的底层逻辑
重构·ocr
新缸中之脑2 天前
Chandra:商业OCR的终结者
ocr
MarkHD3 天前
RPA进阶实战:从零打造智能票据处理机器人——OCR识别、Excel自动填报与邮件通知全流程
机器人·ocr·rpa
叫我黎大侠3 天前
.NET 实战:调用千问视觉模型实现 OCR(车票识别完整教程)
阿里云·ai·c#·ocr·asp.net·.net·.netcore
石榴树下的七彩鱼3 天前
OCR 识别接口哪个好?2026 年主流 OCR API 对比评测(附免费在线体验)
图像处理·人工智能·后端·计算机视觉·ocr·api·文字识别