华为昇腾服务器部署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()
相关推荐
AI人工智能+9 天前
CNN+CRNN+NER:如何实现食品经营许可证秒级结构化信息提取?
深度学习·ocr·食品经营许可证识别
摆烂小白敲代码10 天前
腾讯云智能结构化OCR在物流行业的应用
大数据·人工智能·经验分享·ocr·腾讯云
开开心心就好13 天前
免费音频转文字工具,绿色版离线多模型可用
人工智能·windows·计算机视觉·计算机外设·ocr·excel·语音识别
开开心心_Every14 天前
全屏程序切换工具,激活选中窗口快速切换
linux·运维·服务器·pdf·ocr·测试用例·模块测试
2401_8362358615 天前
名片识别产品:技术要点与应用场景深度解析
人工智能·科技·深度学习·ocr
njsgcs16 天前
glm-ocr ollama使用 python
ocr
开开心心就好16 天前
轻松鼠标连, 自定义区域模仿人手点击
人工智能·windows·物联网·计算机视觉·计算机外设·ocr·excel
littleshimmer16 天前
基于 C++ + Qt6 实现一款本地离线 OCR 工具(SnapOCR)
ocr
AI周红伟18 天前
周红伟:企业大模型微调和部署, DeepSeek-OCR v2技术原理和架构,部署案例实操。RAG+Agent智能体构建
大数据·人工智能·大模型·ocr·智能体·seedance
kongba00720 天前
如何在本地创建一个OCR工具,帮你识别文档,发票,合同等细碎的内容,并将结果给大模型整理格式输出。 经验工作流。给大模型生成代码就能直接跑。
大数据·ocr