PaddleOCRv5离线识别全场景多语种验证码:TaoToken统一Key接入与onnxruntime本地推理验证
发布时间:2026/10/24 7:25:52 锦皓数字建站

1. 为什么要在离线环境跑 PaddleOCRv5 验证码识别验证码识别这个需求很多人第一反应是调云端 OCR 接口。但真到落地环节问题就来了内网系统不能出网、批量任务对延迟敏感、每天几万张图按次计费成本压不住。这时候离线推理就成了刚需。PaddleOCRv5 相比 v4 官方口径精度提升约 13 个百分点字典扩充到一万多字符对生僻字、手写体、混合字符的覆盖明显更好用来处理中英数字混合、字母数字粘连这类典型验证码场景是合适的。但官方那套安装流程对离线场景不太友好。paddlepaddle 本体加依赖动辄几个 G装完还要处理各种动态库版本冲突在一台不能联网的机器上折腾这些非常痛苦。我试过更轻的路子把官方模型导出成 ONNX运行时只依赖 onnxruntime 加 numpy、opencv 这几个基础库整个环境压到几百兆拷贝到离线机器上解压即用。模型文件本身是静态的换 v4、v5 或者其它语种模型只要同步替换字典文件同一份推理代码不用改。那 TaoToken 在这里扮演什么角色离线推理本身不需要联网但工程上还有两件事绕不开一是模型和字典文件的版本管理、下载分发需要鉴权二是团队里有人要在线做效果对比、跑小批量验证时需要一个统一的 Key 和配额入口而不是每人各自申请一堆账号。TaoToken 提供的就是这个统一通道一个 Key 管住模型对话、API 调用和配额离线机器上的推理脚本通过环境变量读取同一个 Key在线验证和离线批量走同一套鉴权逻辑省去多头管理的麻烦。这篇内容适合两类人一类是手里有内网 OCR 需求、想把 PaddleOCRv5 跑起来的工程师另一类是想先在线验证识别效果、再决定要不要落地离线方案的开发者。下面从环境准备、模型导出、可复制配置、请求验证到报错排查一步步给到能直接抄的代码和参数。2. TaoToken 前置准备统一 Key 与配额管理在动手写推理代码之前先把鉴权通道理顺。TaoToken 的定位是统一入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api这个地址不加 UTM 参数配置里直接用它。你需要做的第一件事是拿到一个 API Key然后把它写进环境变量而不是硬编码在脚本里。具体操作路径登录后进入控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面创建一个新 Key页面地址 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建时建议按用途命名比如ocr-offline-batch和ocr-online-test分开方便后续在配额面板里区分统计。Key 只显示一次复制后立刻存到本地密码管理器或者直接写进 shell 配置。拿到 Key 之后在离线机器的终端里设置环境变量。Linux/macOS 用export TAOTOKEN_API_KEYsk-你的实际Key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiWindows PowerShell 用$env:TAOTOKEN_API_KEYsk-你的实际Key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api这里有个细节要注意离线推理脚本本身不发起网络请求但如果你要做在线效果对比或者用统一 Key 去拉取模型清单、查询配额就需要这个环境变量。把 Key 放在环境变量里而不是配置文件里好处是离线机器打包分发时不会把密钥带出去每台机器各自注入即可。配额管理这块TaoToken 的控制台能看到每个 Key 的调用量和剩余额度。对于 OCR 项目我的建议是给在线验证单独建一个 Key设一个较低的日限额防止调试脚本跑飞离线批量任务用的 Key 只用于拉取模型和字典调用频率很低额度基本不会成为瓶颈。这样即使某个 Key 泄露影响范围也可控。模型文件从哪来PaddleOCR 官方在 HuggingFace 和 GitHub Release 上提供推理模型onnx 版本可以用 Paddle2ONNX 转换也可以直接找社区已经转好的。转换命令大致是paddle2onnx --model_dir ./ch_PP-OCRv5_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./ppocrv5_rec.onnx \ --opset_version 11 \ --enable_onnx_checker True转换完得到ppocrv5_rec.onnx和配套的字典文件ppocr_keys_v1.txt。字典文件很关键模型输出的是索引要靠字典映射回字符换模型必须同步换字典否则识别结果全是乱码。把 onnx 和字典放到离线机器的同一个目录下比如/opt/ocr/models/后面配置里统一引用这个路径。3. 可复制配置onnxruntime 会话参数与预处理脚本这一节是核心直接给能跑的配置和代码。先看目录结构建议这样组织/opt/ocr/ ├── models/ │ ├── ppocrv5_rec.onnx │ └── ppocr_keys_v1.txt ├── config/ │ └── ocr_settings.json └── scripts/ └── rec_offline.py配置文件ocr_settings.json内容如下路径和参数都写死在这里脚本读取后初始化会话{ model_path: /opt/ocr/models/ppocrv5_rec.onnx, dict_path: /opt/ocr/models/ppocr_keys_v1.txt, session: { intra_op_num_threads: 4, inter_op_num_threads: 2, execution_mode: sequential, graph_optimization_level: ORT_ENABLE_ALL }, preprocess: { rec_image_shape: [3, 48, 320], rec_batch_num: 6, max_text_length: 25 }, taotoken: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY } }intra_op_num_threads控制单算子内部并行线程数验证码图片小设 4 足够inter_op_num_threads是算子间并行设 2 避免线程争抢。graph_optimization_level开ORT_ENABLE_ALL让 onnxruntime 做算子融合实测下来比默认级别快 15% 左右。rec_image_shape是 PaddleOCR 识别模型的标准输入尺寸高度固定 48宽度 320 对应最长 25 个字符左右验证码一般 4 到 6 位这个宽度绰绰有余。推理脚本rec_offline.py的关键部分import json import os import cv2 import numpy as np import onnxruntime as ort with open(/opt/ocr/config/ocr_settings.json, r, encodingutf-8) as f: cfg json.load(f) sess_opts ort.SessionOptions() sess_opts.intra_op_num_threads cfg[session][intra_op_num_threads] sess_opts.inter_op_num_threads cfg[session][inter_op_num_threads] sess_opts.execution_mode ort.ExecutionMode.ORT_SEQUENTIAL sess_opts.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL providers [CPUExecutionProvider] if CUDAExecutionProvider in ort.get_available_providers(): providers.insert(0, CUDAExecutionProvider) session ort.InferenceSession(cfg[model_path], sess_opts, providersproviders) with open(cfg[dict_path], r, encodingutf-8) as f: char_list [line.strip() for line in f if line.strip()] char_list.insert(0, blank) char_list.append( ) def preprocess(img_path): img cv2.imread(img_path) h, w img.shape[:2] target_h, target_w cfg[preprocess][rec_image_shape][1], cfg[preprocess][rec_image_shape][2] ratio w / h resized_w min(target_w, int(target_h * ratio)) resized cv2.resize(img, (resized_w, target_h)) padded np.zeros((target_h, target_w, 3), dtypenp.float32) padded[:, :resized_w, :] resized padded padded.transpose(2, 0, 1) / 255.0 mean np.array([0.485, 0.456, 0.406]).reshape(3, 1, 1) std np.array([0.229, 0.224, 0.225]).reshape(3, 1, 1) padded (padded - mean) / std return padded[np.newaxis, :].astype(np.float32) def decode(output): preds output[0] preds_idx preds.argmax(axis2) preds_prob preds.max(axis2) result [] for i, idx in enumerate(preds_idx[0]): if idx ! 0 and (i 0 or idx ! preds_idx[0][i - 1]): result.append((char_list[idx], float(preds_prob[0][i]))) return result预处理里的归一化参数是 ImageNet 的均值和方差PaddleOCR 训练时用的就是这套别改成 0.5 那套否则精度会掉。decode函数做了去重和去 blank这是 CTC 解码的标准逻辑验证码字符之间没有分隔符靠这个逻辑把重复预测合并。如果要用 GPU 加速把providers顺序调整后还需要确认 onnxruntime-gpu 版本和 CUDA 版本匹配。离线机器上装onnxruntime-gpu1.17.0配 CUDA 11.8 是实测比较稳的组合装完用ort.get_available_providers()确认CUDAExecutionProvider在列表里。CPU 场景下onnxruntime基础包就够体积小很多。4. 验证请求与成功结果准确率与耗时实测配置写完得验证它真的能跑、跑得准、跑得快。准备一批测试图覆盖中英数字混合、纯数字、字母数字粘连、轻微扭曲四类每类 20 张共 80 张。验证脚本这样写import time import glob test_images glob.glob(/opt/ocr/testset/*.png) correct 0 total_time 0.0 for img_path in test_images: input_tensor preprocess(img_path) start time.perf_counter() outputs session.run(None, {session.get_inputs()[0].name: input_tensor}) elapsed time.perf_counter() - start total_time elapsed chars decode(outputs) pred .join([c for c, p in chars]) gt os.path.basename(img_path).split(_)[0] if pred gt: correct 1 print(f{img_path} - pred{pred} gt{gt} time{elapsed*1000:.1f}ms) print(faccuracy{correct/len(test_images):.3f} avg_time{total_time/len(test_images)*1000:.1f}ms)实测结果CPU 4 线程Intel i7-12700纯数字验证码准确率 98%字母数字混合 94%带轻微扭曲的 89%平均单张耗时 12 到 18 毫秒。GPU 场景下平均耗时降到 3 到 5 毫秒批量 6 张一起推理时吞吐还能再提一截。这个精度对大多数验证码场景够用个别识别错的集中在字符粘连严重或者背景干扰强的图上可以通过加检测模型先裁切再识别来改善。在线验证这块用 TaoToken 的统一 Key 跑一次模型对话接口确认鉴权通道是通的。请求示例curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 回复 ok 即可}], max_tokens: 10 }返回里能看到choices数组和usage字段说明 Key 有效、配额正常扣减。这一步的意义在于离线推理脚本虽然不联网但团队里做效果对比、拉取模型清单、查询剩余额度都走这个通道提前验证通后面批量任务出问题能快速排除是鉴权还是推理本身的锅。成功结果的判断标准有三条推理脚本输出accuracy在 0.85 以上、avg_time在 50ms 以内、curl 请求返回 200 且choices非空。三条都满足说明离线推理和在线鉴权两条链路都通了。如果准确率明显偏低先检查字典文件是否和模型匹配如果耗时异常高检查intra_op_num_threads是否设得过大导致线程切换开销。5. 常见报错排查401、local proxy failed、reading choices踩过的坑集中在这几类对照报错直接定位。401 Unauthorized。curl 请求返回{error: invalid api key}或者 401 状态码。原因通常是环境变量没生效或者 Key 复制时带了空格。排查步骤先echo $TAOTOKEN_API_KEY确认变量有值且没有前后空格再检查请求头里Bearer后面是否紧跟 Key。如果是在 Docker 容器里跑注意export只在当前 shell 生效容器启动时要通过-e TAOTOKEN_API_KEYxxx注入。另外 Key 如果被控制台删除或过期也会返回 401去 API Keys 页面确认状态。local proxy failed。这个报错一般出现在请求发不出去的场景提示连接本地代理失败。先检查环境里有没有残留的HTTP_PROXY、HTTPS_PROXY变量离线机器上这些变量如果指向一个不存在的代理请求就会卡住然后报这个错。用env | grep -i proxy查一下有的话unset掉。还有一种情况是 DNS 解析失败curl -v https://taotoken.net/api看解析到哪个 IP如果解析不出来检查/etc/resolv.conf。注意这里说的是正常的网络配置排查不涉及任何绕过网络管理的手段纯粹是确认本机网络栈是否正常。reading choices 报错。解析返回 JSON 时抛KeyError: choices或者reading choices失败。说明返回体结构和你预期的不一样通常是请求本身失败了返回的是错误对象而不是正常的 completion 结构。打印完整返回体看error字段常见的是model not found模型名写错或者insufficient quota配额用完。模型名要以文档里列出的为准地址是 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 别自己猜。配额问题去控制台看剩余额度或者换一个 Key 试。OAuth 相关报错。如果你用 Claude Code 这类工具接入可能会遇到 OAuth 回调失败。这类工具走的是 Anthropic 兼容通道配置时 Base URL 填https://taotoken.net/apiKey 填 TaoToken 的 KeyModel ID 填文档里对应的模型标识。三件套缺一不可只填 Key 不填 Base URL 会走到默认端点然后 OAuth 失败。Claude Code 的配置在~/.claude/settings.jsonCline 的 MCP 配置在扩展设置里Codex 的auth.json在用户目录下三者的共同点是都要显式写全 Base URL、Key、Model ID 三个字段少一个就报鉴权或路由错误。onnxruntime 加载模型报错。InvalidGraph或者No Op registered for XXX。这是 opset 版本不匹配转换时--opset_version 11是兼容性比较好的选择如果模型用了更高 opset 的算子把 onnxruntime 升级到对应版本。还有一种input name mismatch用session.get_inputs()[0].name动态取输入名别硬编码x。识别结果全是乱码或空。字典文件路径错了或者字典和模型不是同一版本。PaddleOCRv5 的字典有一万多行v4 的字典行数不同混用会导致索引错位。确认dict_path指向的文件行数和模型输出维度匹配输出维度一般是字典长度加 1blank再加 1空格。6. 落地建议与统一入口离线推理跑通之后工程上还有几个优化点值得做。批量推理时把rec_batch_num调到 6 到 8把多张图拼成一个 batch 送进 session吞吐能提升 2 到 3 倍但注意 batch 内图片要 padding 到同一尺寸脚本里已经按rec_image_shape统一处理了。模型量化方面onnxruntime 支持动态量化quantize_dynamic把权重转成 int8模型体积减半CPU 推理速度提升 30% 左右精度损失在 1 个百分点以内验证码场景可以接受。模型和字典的版本管理建议用清单文件记录每次更新模型时同步更新清单里的哈希值离线机器启动时校验避免模型和字典版本错配。团队协作场景下在线验证和离线批量共用同一个 TaoToken Key 的环境变量注入方式配额在控制台统一看谁跑了多少一目了然。需要进一步查接入细节的文档入口在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 模型对话的在线调试入口是 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 长期跑编码和 Agent 任务的可以看 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 管理和配额面板在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 创建页 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。把这些入口存进书签后面调参和排障能省不少翻找时间。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。