1. 这不是“云上点点鼠标”的故事而是一次真实模型落地的全链路复盘我用ModelArts跑通第一个可对外提供API服务的OCR模型时卡在模型转换环节整整三天——不是因为不会写代码而是因为没搞懂华为云ModelArts里“推理镜像”和“自定义镜像”的本质区别。很多人把ModelArts当成一个“AI版的PPT制作工具”点几下就能出模型、点几下就能上线结果部署后接口返回503、日志里全是Failed to load model最后发现连模型输入张量的shape都没对齐。这根本不是平台的问题而是我们跳过了最该深挖的底层逻辑ModelArts不是黑盒它是一套有明确设计契约的工程化流水线。它把数据准备、训练调度、模型打包、服务编排这些原本分散在不同角色手里的活整合成一条可追溯、可审计、可复用的工业级路径。你看到的“一键部署”背后是容器镜像构建规范、Ascend NPU算子兼容性检查、模型服务框架MindX SDK的版本绑定、以及华为云内部服务网格Service Mesh对流量的精细控制。本文不讲“如何注册账号→创建Notebook→上传数据→点击训练”的操作手册式流程而是带你拆开ModelArts的外壳看清楚每个按钮按下后底层到底发生了什么、为什么必须这样发生、以及当你遇到“训练卡在99%”或“部署后请求超时”时该去哪个日志文件里找真正的线索。适合三类人刚从本地PyTorch环境迁移到云平台的算法工程师、需要把团队模型快速交付给业务方的AI项目经理、以及正在为华为ICT大赛云赛道做备赛的技术骨干——你们要的不是“能跑”而是“跑得稳、查得清、扩得快”。2. 整体架构设计为什么ModelArts的“训练-部署”不是两步而是五层契约2.1 模型生命周期的五个不可绕过的契约层ModelArts的训练与部署表面看是两个独立动作实则被五层强约束的契约环环相扣。跳过任何一层后续都会出现“看似成功、实则脆弱”的问题。这五层不是华为云的营销话术而是由底层硬件昇腾AI芯片、中间件MindSpore Runtime、云基础设施CCE容器引擎共同决定的硬性边界。第一层是数据契约ModelArts要求训练数据必须符合特定的存储协议与元数据格式。比如使用obs://bucket-name/dataset/路径时系统会自动扫描dataset/下的train/、val/子目录并期望其中的图片按类别分文件夹存放train/cat/xxx.jpg,train/dog/xxx.jpg。如果你把所有图片混在一个文件夹里再用--data-dir参数强行指定训练脚本可能跑通但验证集划分会失效——因为ModelArts的分布式训练调度器MoE Scheduler在启动Worker节点前会预读取OBS桶的目录结构生成分片索引这个索引一旦生成就不可更改。我曾因此导致验证集准确率虚高3.7%因为80%的验证样本实际来自训练集。第二层是训练契约核心在于train.py入口脚本的签名规范。ModelArts不是简单地python train.py而是通过/home/work/run_train.sh脚本注入一系列环境变量如ASCEND_HOME、DEVICE_ID并强制要求你的train.py必须接受--data_url、--train_url、--num_gpus等参数。更关键的是它要求模型保存路径必须指向/home/work/model/而非任意路径因为训练任务结束后系统会自动将此目录打包为.tar.gz归档到OBS的train_url位置。如果你在代码里写死torch.save(model, model.pth)模型文件根本不会被上传后续部署时就会报错No such file or directory: model.pth。第三层是模型契约这是最容易被忽视的致命层。ModelArts不接受原始的.pth或.h5文件直接部署它要求模型必须经过“固化”Freeze和“序列化”Serialize两道工序。以PyTorch为例你需要调用torch.jit.script(model)或torch.jit.trace(model, example_input)生成TorchScript模型再用model.save(model.ts)保存。这是因为ModelArts的推理服务MindX SDK底层使用C加载TorchScript它无法解析Python解释器的动态特性如if __name__ __main__:。我见过太多人直接上传.pth部署界面显示“成功”但调用API时返回RuntimeError: Expected object of scalar type Float but got scalar type Half——根源就是未做类型固化FP16权重在加载时被错误解析。第四层是服务契约部署时选择的“推理引擎”决定了整个服务的骨架。ModelArts提供三种模式在线服务Real-time Service基于CANNCompute Architecture for Neural Networks MindX SDK专为昇腾芯片优化延迟最低实测ResNet50单图15ms但仅支持华为认证的模型格式.om或.mindir批量服务Batch Service走Spark集群适合处理TB级离线数据但不提供HTTP API自定义镜像Custom Image允许你用Dockerfile构建任意环境灵活性最高但需自行处理GPU驱动、CUDA版本、模型服务框架如FastAPI Uvicorn的集成且无法享受华为云的自动扩缩容。选错引擎等于在高速公路上开拖拉机——不是不能动而是永远达不到设计性能。第五层是网络契约ModelArts服务默认绑定VPC内网对外暴露的Endpoint是私有IP。如果你的应用服务器不在同一VPC必须配置VPC对等连接或NAT网关。更隐蔽的坑是安全组规则默认只放行80/443端口而ModelArts在线服务监听的是8000端口非标准HTTP端口若不手动添加入站规则外部请求永远超时。这个细节在文档里藏得很深却让无数人以为是模型问题。提示这五层契约不是华为云的“限制”而是工业级AI平台的必然设计。就像汽车发动机必须遵循热力学定律一样AI模型在云上规模化运行就必须遵守数据流、计算流、服务流的物理约束。理解它们才能把ModelArts从“玩具”变成“产线”。2.2 为什么放弃“Notebook直连训练”转向“训练作业Training Job”模式新手常犯的错误是把ModelArts Notebook当成Jupyter Lab来用上传数据集→写train.py→!python train.py。这种方式在小数据集上可行但一旦进入真实项目就会暴露出三个致命缺陷资源不可控Notebook实例的GPU显存是固定的如p100 16GB而训练过程中的batch size、梯度累积步数、混合精度策略会动态影响显存占用。当训练突然OOMOut of Memory时整个Notebook会崩溃所有中间状态如optimizer state、learning rate scheduler全部丢失只能从头开始。而训练作业模式下你可以指定resource_spec如resource_spec: gpu_2x系统会为你分配独占的2卡环境并在OOM时自动触发checkpoint恢复机制。过程不可审计Notebook里的每一次!pip install、!wget、!python都是孤立命令没有统一的任务ID。当模型效果异常时你无法回溯“到底是哪次git pull引入了bug还是哪个pip install升级了不兼容的库”。训练作业则为每次执行生成唯一的job_id所有stdout/stderr日志、资源监控曲线GPU利用率、显存占用、甚至代码快照Git commit hash都绑定在此ID下审计时只需输入ID即可调取全量证据链。交付不可复现Notebook环境是“状态机”你今天装的torch1.12.0cu113明天可能被系统更新覆盖。而训练作业强制要求你提供requirements.txt和train.py的完整代码包zip平台会在干净的Docker镜像中解压执行确保“所见即所得”。我在一次客户交付中用Notebook训好的模型在客户环境复现时精度下降2.1%最后发现是因为客户环境的opencv-python版本比我的低而Notebook里没锁定版本号。所以从第一天起就该建立“训练作业优先”的思维。哪怕只是跑一个Hello World也要走完整流程① 将代码、依赖、数据路径配置全部放入一个code/目录② 编写train.py严格遵循--data_url等参数规范③ 打包为code.zip④ 在ModelArts控制台创建训练作业上传zip包设置OBS路径。这多花的10分钟换来的是未来三个月不踩坑的底气。2.3 部署方案选型在线服务 vs 自定义镜像一场关于“可控性”与“效率”的权衡当模型训练完成面对“在线服务”和“自定义镜像”两个选项很多人的直觉是选前者——毕竟标着“一键部署”。但我的经验是90%的生产级项目最终都走向了自定义镜像。这不是否定ModelArts的能力而是因为真实业务场景的复杂性远超平台预设的模板。在线服务的优势极其鲜明极致简单上传模型文件.om/.mindir填几个参数输入shape、输出label点击部署5分钟内获得HTTPS Endpoint极致稳定底层由华为云SRE团队维护自动处理GPU驱动更新、安全补丁、服务健康检查极致弹性基于CCE集群支持秒级扩缩容QPS从10到10000无需人工干预。但它有三个无法妥协的硬伤模型格式锁死只认华为认证的.omAscend或.mindirMindSpore格式。如果你用PyTorch训练必须先用torch.onnx.export()转ONNX再用atc工具转.om。而atc对算子的支持有严格列表如不支持torch.nn.MultiheadAttention的某些变体转模型失败率高达35%基于我2023年跟踪的127个案例统计预处理逻辑剥离在线服务只负责“模型推理”图像解码、归一化、resize等前处理必须由客户端完成。这意味着你的APP或Web前端要嵌入一套与训练时完全一致的OpenCV/PIL代码版本稍有差异结果就天差地别后处理能力缺失输出是原始logits你得自己写softmax、argmax、NMS非极大值抑制等逻辑。YOLOv5这类模型如果后处理写错一行检测框就全飘了。自定义镜像则把所有控制权交还给你你可以用Dockerfile安装任意版本的CUDA、cuDNN、PyTorch把preprocess.py、inference.py、postprocess.py全部打包进镜像形成端到端的黑盒服务用FastAPI暴露RESTful API用Swagger UI自动生成文档用Prometheus暴露metrics指标。代价是什么部署时间增加构建镜像、推送OBS、创建服务全程约8-12分钟运维责任转移GPU驱动兼容性、镜像安全漏洞扫描、服务健康探针liveness/readiness都要自己写成本略高自定义镜像服务按Pod实例计费而在线服务按调用量计费小流量场景后者更省。我的决策树很清晰如果是POC验证、内部工具、QPS100的轻量应用 → 用在线服务赢在速度如果是客户交付、金融/医疗等强合规场景、需要与现有K8s集群集成 → 用自定义镜像赢在可控。去年帮一家银行部署票据识别模型他们要求所有组件必须通过等保三级测评最终我们用自定义镜像在镜像里嵌入了国密SM4加密模块对传输中的图像数据进行端到端加密——这种深度定制在线服务根本做不到。3. 核心细节解析从数据准备到模型导出每一步的“为什么”与“怎么做”3.1 数据准备OBS桶不是网盘而是分布式文件系统的入口把数据上传到OBSObject Storage Service绝不是“找个地方存文件”那么简单。OBS是华为云的底层存储基石其设计哲学是“对象不可变、元数据可编程”。这意味着你上传的每一个文件都会被赋予唯一的ETag内容哈希并生成丰富的元数据如x-obs-meta-creator、x-obs-meta-version。ModelArts正是利用这些元数据实现数据版本管理与血缘追踪。正确姿势创建专用OBS桶如ai-model-data-2024开启“版本控制”Versioning——这能防止误删且每次上传同名文件会生成新版本ID按项目-阶段-数据集三级目录组织obs://ai-model-data-2024/ ├── project_finance/ # 项目名 │ ├── v1.0/ # 版本号 │ │ ├── train/ # 训练集 │ │ │ ├── invoice/ # 类别 │ │ │ │ ├── 001.jpg │ │ │ │ └── ... │ │ │ └── receipt/ │ │ ├── val/ # 验证集必须与train同结构 │ │ └── test/ # 测试集 │ └── v1.1/ # 新版本修复标注错误 └── project_medical/上传时启用“服务器端加密”SSE-KMS密钥由华为云KMS托管满足金融行业合规要求为桶配置生命周期规则自动将30天未访问的对象转为低频访问IA存储节省50%成本。常见错误与后果❌ 错误把数据集直接放在桶根目录obs://bucket/dataset.zip→ 后果ModelArts无法识别目录结构训练时报错Dataset not found in OBS path❌ 错误用浏览器上传大文件100MB→ 后果上传中断后无法断点续传且OBS会生成大量碎片对象影响后续List操作性能❌ 错误未开启版本控制反复覆盖同名文件→ 后果某次训练用的train_v2.zip被覆盖导致线上模型效果突降却无法回滚。实操心得我习惯用obsutil命令行工具替代网页上传。obsutil cp -r local_data/ obs://bucket/project_finance/v1.0/ --parallel1010线程并发10GB数据2分钟搞定且自动校验MD5。更重要的是obsutil会记录详细日志包括每个文件的ETag这是后续做数据血缘分析的唯一依据。3.2 训练脚本编写超越“能跑”追求“可审计、可复现”一个合格的ModelArts训练脚本不是train.py而是一个微型工程。它必须包含四个核心模块① 参数解析模块Argparse必须严格支持ModelArts注入的环境变量并提供合理默认值import argparse import os def parse_args(): parser argparse.ArgumentParser() # ModelArts强制参数 parser.add_argument(--data_url, typestr, defaultos.getenv(DATA_URL, /home/work/data)) parser.add_argument(--train_url, typestr, defaultos.getenv(TRAIN_URL, /home/work/model)) parser.add_argument(--num_gpus, typeint, defaultint(os.getenv(NUM_GPUS, 1))) # 业务参数可选但强烈建议 parser.add_argument(--batch_size, typeint, default32) parser.add_argument(--lr, typefloat, default0.001) parser.add_argument(--epochs, typeint, default50) return parser.parse_args() args parse_args()注意os.getenv()是兜底确保本地调试时也能运行。ModelArts会自动设置DATA_URL为OBS路径如obs://bucket/project_finance/v1.0/train/但你的代码必须能处理本地路径如./data/train/。② 数据加载模块DataLoader关键在于OBS路径的透明适配。不要用open()直接读OBS而要用华为云提供的obsutil或boto3需配置AK/SKfrom obs import ObsClient def load_data_from_obs(obs_path): # 解析obs://bucket/path/to/data/ bucket_name obs_path.split(/)[2] prefix /.join(obs_path.split(/)[3:]) obs_client ObsClient( access_key_idYOUR_AK, secret_access_keyYOUR_SK, serverhttps://obs.cn-north-4.myhuaweicloud.com ) # 列出所有对象 resp obs_client.listObjects(bucket_name, prefixprefix) image_paths [obj.key for obj in resp.body.objectList if obj.key.endswith((.jpg, .png))] # 下载到本地临时目录ModelArts会自动清理 local_dir /home/work/data/ os.makedirs(local_dir, exist_okTrue) for key in image_paths[:100]: # 示例只下载前100张 obs_client.getObject(bucket_name, key, downloadPathf{local_dir}/{os.path.basename(key)}) return local_dir提示生产环境建议用obsutil预下载整个数据集到/cache目录ModelArts预留的高速缓存区避免训练时IO瓶颈。obsutil cp -r ${args.data_url} /cache/ --update比边训练边下载快3倍。③ 模型保存模块Save Logic必须遵循ModelArts的“模型契约”生成可部署的格式import torch import torch.jit def save_model(model, save_path): # 1. 转为eval模式关闭dropout/batchnorm model.eval() # 2. 构造示例输入shape必须与训练一致 example_input torch.randn(1, 3, 224, 224) # ResNet50输入 # 3. 生成TorchScript模型 traced_script_module torch.jit.trace(model, example_input) # 4. 保存为.pt格式ModelArts在线服务支持 traced_script_module.save(f{save_path}/model.pt) # 5. 同时保存label映射部署时必需 with open(f{save_path}/labels.txt, w) as f: f.write(invoice\nreceipt\nbank_slip) # 调用 save_model(model, args.train_url) # train_url即/home/work/model/④ 日志与监控模块LoggingModelArts会自动收集/home/work/log/下的日志但你需要主动埋点import logging from datetime import datetime logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(/home/work/log/train.log), logging.StreamHandler() # 同时输出到stdout供ModelArts捕获 ] ) logger logging.getLogger(__name__) logger.info(fTraining started at {datetime.now()}) logger.info(fArgs: {args}) logger.info(fGPU count: {torch.cuda.device_count()}) # 训练循环中 for epoch in range(args.epochs): logger.info(fEpoch {epoch1}/{args.epochs} - Loss: {loss:.4f})实操心得我在每个epoch结束时用torch.save()保存完整的checkpoint含optimizer、scheduler而不是只存模型权重。这样即使训练中断也能从epoch_XX.pth继续避免重复劳动。ModelArts的训练作业会自动将/home/work/model/下的所有文件上传到OBS所以checkpoint也一并备份。3.3 模型导出与格式转换避开ATC工具的“算子陷阱”当你的PyTorch模型训练完成想部署到昇腾芯片如Atlas 300I必须经历PyTorch → ONNX → OM的转换。这一步是雷区密集带90%的失败源于对ATCAscend Tensor Compiler工具的理解偏差。第一步PyTorch → ONNX相对安全关键在于torch.onnx.export()的参数# 正确写法 torch.onnx.export( model, example_input, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, # 支持动态batch opset_version11, # 必须≤11ATC对opset12支持不全 verboseFalse )注意dynamic_axes必须声明否则ATC会报错Unsupported dynamic shape。opset_version11是黄金版本兼容性最好。第二步ONNX → OMATC转换高危区ATC命令看似简单但参数组合决定成败atc \ --modelmodel.onnx \ --framework5 \ # 5ONNX --outputmodel \ --soc_versionAscend310 \ # 必须与目标设备一致 --input_shapeinput:1,3,224,224 \ # 严格匹配ONNX的input name和shape --logerror \ --enable_small_tensor1 \ --precision_modeallow_mix_precision致命陷阱与破解❌ 陷阱1--soc_version填错如用Ascend910转310的模型→ 后果转换成功但部署后报错Invalid device id因为算子编译目标不匹配❌ 陷阱2--input_shape的name与ONNX不一致ONNX里叫x你写input→ 后果ATC静默失败生成空.om文件部署时才暴露❌ 陷阱3模型含不支持算子如torch.nn.functional.interpolate的modebicubic→ 后果ATC报错Operator interpolate is not supported解决方案是重写插值逻辑用F.upsample替代。终极避坑方案用ModelArts内置的“模型转换”功能在ModelArts控制台找到已训练的模型点击“转换”按钮。它会自动检测ONNX算子兼容性高亮不支持的节点提供替换建议如将bicubic改为bilinear生成转换后的.om文件并附带详细的兼容性报告。这比手动敲ATC命令可靠10倍尤其适合不熟悉昇腾生态的开发者。4. 实操过程从零开始完成一个OCR模型的端到端部署4.1 环境准备与账号配置最小化起步的5个必做动作在ModelArts上动手前这5件事必须做完否则后续所有操作都是空中楼阁开通ModelArts服务并绑定计费登录华为云控制台 → 搜索“ModelArts” → 点击“立即使用”。注意免费试用额度500小时GPU仅限新用户且需实名认证。我建议直接购买“按需计费”套餐避免额度用尽后服务中断。创建OBS桶并配置权限创建桶名modelarts-workspace-2024全局唯一在桶的“权限管理”中添加ModelArts服务的委托权限{ Statement: [ { Effect: Allow, Action: [obs:GetObject, obs:PutObject], Resource: [arn:aws:iam::your-account-id:obs:modelarts-workspace-2024/*] } ] }提示不要给ModelArts“桶管理员”权限最小权限原则是安全底线。配置VPC与子网ModelArts训练作业和在线服务都运行在VPC内。在“虚拟私有云”服务中创建VPC如modelarts-vpc网段192.168.0.0/16创建子网如modelarts-subnet网段192.168.1.0/24并勾选“启用DHCP”为子网配置路由表指向互联网网关IGW确保训练节点能访问PyPI。申请GPU资源配额华为云默认GPU配额为0。在“配额管理”中申请服务弹性云服务器ECS区域华北-北京四cn-north-4资源gpus数量2建议起步用途ModelArts训练作业。注意配额审批通常2小时内完成但周末可能延迟务必提前申请。安装ModelArts CLI工具可选但推荐pip install huaweicloud-sdk-modelarts # 配置AK/SK export HUAWEICLOUD_SDK_AKYOUR_AK export HUAWEICLOUD_SDK_SKYOUR_SK export HUAWEICLOUD_SDK_REGIONcn-north-4CLI工具让你摆脱网页控制台用脚本批量管理训练作业效率提升300%。4.2 数据上传与标注用EasyOCR训练自己的模型的真实案例本次实操我们以“票据文字识别”为场景目标是识别发票、收据上的关键字段金额、日期、商户名。数据来自公开数据集如SROIE但需二次清洗。步骤1数据清洗与格式统一原始SROIE数据是*.txt标注文件*.jpg图片但ModelArts的OCR训练要求train.txt格式image_001.jpg\t{text: [北京某某科技有限公司, 2023-05-20, ¥1,234.56]} image_002.jpg\t{text: [上海某某贸易公司, 2023-06-15, ¥876.90]}我写了一个Python脚本自动转换import json import os def convert_sroie_to_modelarts_format(sroie_root, output_txt): with open(output_txt, w) as f: for img_name in os.listdir(f{sroie_root}/images): if not img_name.endswith(.jpg): continue txt_name img_name.replace(.jpg, .txt) with open(f{sroie_root}/annotations/{txt_name}) as ann_f: lines ann_f.readlines() # 解析SROIE的4点坐标文本 texts [line.strip().split(\t)[1] for line in lines if len(line.strip().split(\t)) 1] label {text: texts} f.write(f{img_name}\t{json.dumps(label)}\n) convert_sroie_to_modelarts_format(./sroie, ./train.txt)步骤2上传到OBS# 创建OBS目录 obsutil mkdir obs://modelarts-workspace-2024/ocr-finance/train/ # 上传图片和标注文件 obsutil cp -r ./images/ obs://modelarts-workspace-2024/ocr-finance/train/images/ obsutil cp ./train.txt obs://modelarts-workspace-2024/ocr-finance/train/train.txt步骤3在ModelArts创建数据集控制台 → “数据管理” → “创建数据集”名称finance-ocr-v1数据源OBS路径obs://modelarts-workspace-2024/ocr-finance/train/数据格式选择“OCR文本检测与识别”标注文件train.txt点击“创建”。提示创建后ModelArts会自动解析train.txt生成可视化预览确认图片和文本是否对齐。如有错位立即修正train.txt并重新导入。4.3 模型训练基于PaddleOCR的微调实战ModelArts内置了PaddleOCR算法我们直接调用避免从零写训练逻辑。步骤1创建训练作业控制台 → “训练管理” → “创建训练作业”作业名称ocr-finance-train-v1算法来源选择“AI市场” → 搜索“PaddleOCR” → 选择最新版如paddleocr_v2.6代码来源使用算法自带的代码无需上传数据集选择刚创建的finance-ocr-v1训练参数{ Global: { use_gpu: true, epoch_num: 200, save_epoch_step: 10, save_model_dir: /home/work/model/, log_smooth_window: 20 }, Architecture: { model_type: rec, algorithm: CRNN, Transform: null, Backbone: {name: ResNet34_vd}, Head: {name: CTCHead} }, Optimizer: {name: Adam, lr: {name: Cosine, learning_rate: 0.001}} }注意save_model_dir必须是/home/work/model/这是ModelArts的契约。步骤2启动训练并监控点击“提交”后作业状态变为“运行中”。在“日志”页签实时查看train.log显示loss下降曲线正常应平滑收敛nvidia-smiGPU利用率应持续80%显存占用稳定若出现CUDA out of memory立即暂停作业调小batch_size在参数里改Train: {loader: {batch_size_per_card: 8}}。步骤3模型评估与导出训练完成后ModelArts自动生成评估报告准确率Accuracy字符级准确率目标95%编辑距离Edit Distance衡量识别错误程度越小越好推理速度FPS在昇腾芯片上的实测吞吐。我这次训练得到Accuracy: 96.2%Edit Distance: 0.82达标。然后点击“导出模型”选择“ONNX格式”生成model.onnx。4.4 模型部署自定义镜像的完整构建与发布由于PaddleOCR的ONNX模型含自定义算子ATC转换失败我们采用自定义镜像方案。步骤1编写DockerfileFROM registry.cn-north-4.huawei.com/modelarts/runtime-pytorch-1.12-cuda11.3:latest # 安装PaddleOCR依赖 RUN pip install paddlepaddle-gpu2.4.2.post112 -i https://pypi.tuna.tsinghua.edu.cn/simple RUN pip install paddleocr2.6.1 -i https://pypi.tuna.tsinghua.edu.cn/simple # 复制模型和代码 COPY ./model/ /app/model/ COPY ./src/ /app/src/ # 设置工作目录 WORKDIR /app/src # 暴露端口 EXPOSE 8000 # 启动服务 CMD [gunicorn, --bind, 0.0.0.0:8000, --workers, 4, app:app]步骤2编写FastAPI服务app.pyfrom fastapi import FastAPI, UploadFile, File from paddleocr import PaddleOCR import numpy as np from PIL import Image import io app FastAPI() # 初始化OCR模型全局单例避免重复加载 ocr PaddleOCR( use_angle_clsTrue, langch, use_gpuTrue, det_model_dir/app/model/det/, rec_model_dir/app/model/rec/, cls_model_dir/app/model/cls/ ) app.post(/ocr) async def ocr_service(file: UploadFile File(...)): try: # 读取图片 contents await file.read() image Image.open(io.BytesIO(contents)).convert(RGB) # OCR识别 result ocr.ocr(np.array(image), clsTrue) #