# 宝可梦卡牌识别服务文档接口 > 三合一文档,取代原 `后端对接文档.md`(8010) / `8000接口文档.md` / `接口启动与验证.md`。 > 模型版本 **`card_seg_v0904`**(双区级联 DINOv2-large,8.19 权重 + 91,116 张图库)。 > 部署机 `192.168.77.249`,框架根 `~/顾工交接/wzj`。 ## 总览 | 端口 | 脚本 | 环境 | 谁调用 | 功能 | |---|---|---|---|---| | **8010** | `serve_recognition_api.py` | pytorch | **后端服务**(主用) | 图片 URL → Top-K `card_id` + 匹配分,结构化错误码契约 | | **8000** | `serve_card_match.py` | pytorch | 后端服务 / 小程序前端 | 图片 → 单张卡的业务字段(卡名/系列/卡号/稀有度…) | | **8100** | `serve_lang_judge.py` | paddleocr | **仅 8010 内部调用**(监听 127.0.0.1) | 卡面图 → 语种 + 方向角(PaddleOCR sidecar) | - 8010 与 8000 **是两套独立契约**,不是同一接口的两个版本:8010 返回"候选列表 + 匹配分",8000 返回"一张卡的业务字段"。 - 8100 是内部件,**不对后端暴露**;它挂掉时 8010 自动降级(仍可用,只是 language 不进日志)。 - 三个服务共用同一份权重与图库,识别结果同源。 --- # 一、8010 主识别接口(后端对接主用) ## 1.1 功能 `POST http://192.168.77.249:8010/recognize` —— 给一张卡牌图片 URL,返回最像的若干张卡的 `card_id` 及匹配分。后端拿 `card_id` 去 ES 表 `ptcg_cards`(`card_id` 字段)查卡详情做展示。 **后端要做的 3 件事**:① 发请求带 `task_id` + `image_url`;② 取 `data.matches[].card_id`(通常取 `rank=1`);③ 判 `code==200 && data.status=="success"`,失败时按 `data.error_code` 枚举分支处理。 > ⚠️ **不要**把 `algorithm_card_name` / `algorithm_series_name` 展示给终端用户——那是算法侧调试信息,可能不准。展示一律以后端用 `card_id` 查 ES 的结果为准。 ## 1.2 请求头 | Header | 必填 | 说明 | |---|---|---| | `Content-Type: application/json` | 是 | JSON 请求 | | `X-Request-Id` | 否 | 后端生成的追踪 ID,算法侧原样写日志 | | `Authorization: Bearer ` | 否 | **当前未开启鉴权**,不用传(仅当服务带 `API_TOKEN` 环境变量启动时才校验) | ## 1.3 请求参数(JSON body) | 参数 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `task_id` | string | **是** | - | 后端任务 ID,原样回写到响应 | | `image_url` | string | **是** | - | 待识别图片 URL,**算法侧自行下载,必须公网可访问** | | `top_k` | int | 否 | 5 | 返回候选数,上限 20 | | `min_match_rate` | int | 否 | 0 | 最低匹配分 0-100。**默认 0 = 不过滤,始终返回 top_k**(哪怕最高分 <60)。只要高置信可自行传 80 | | `return_embedding` | bool | 否 | false | 调试/入库用,见下 | ## 1.4 成功响应字段(`code == 200`) | 字段 | 类型 | 说明 | |---|---|---| | `code` | int | 固定 `200` | | `message` | string | 固定 `success` | | `data.task_id` | string | 回写请求的 task_id | | `data.status` | string | `success` | | `data.model_version` | string | 算法版本,当前固定 **`card_seg_v0904`** | | `data.processing_time_ms` | int | 算法处理耗时(毫秒) | | `data.matches` | array | 候选列表,已按 `match_rate` **降序**排好(**可能为空数组**) | | `data.matches[].card_id` | string | **★后端主要用这个**,对应 ES `ptcg_cards.card_id` | | `data.matches[].match_rate` | int | 匹配分 0-100,越高越像 | | `data.matches[].rank` | int | 排名,从 1 开始 | | `data.matches[].algorithm_card_name` | string | 算法识别的卡名,**仅调试用,勿展示** | | `data.matches[].algorithm_series_name` | string | 算法识别的系列名,同上 | | `data.embedding` | array | 仅 `return_embedding=true` 时出现,**1024 个 float(上半区特征)**;同时附带 `data.embedding_lower`(下半区 1024 维)与 `data.embedding_dim=1024` | **`match_rate` 语义**:100 ≈ 完全一致;实拍图 top1 一般 75-95;**<60 属低置信**。是否采纳由后端决定,算法侧默认不卡门槛、全部返回。 ```json { "code": 200, "message": "success", "data": { "task_id": "9988776655", "status": "success", "model_version": "card_seg_v0904", "processing_time_ms": 486, "matches": [ { "algorithm_card_name": "亚克诺姆", "algorithm_series_name": "补充包 勇魅群星 魅", "card_id": "0110001", "match_rate": 100, "rank": 1 }, { "algorithm_card_name": "迷布莉姆", "algorithm_series_name": "强化包 怒炎灼天", "card_id": "017526", "match_rate": 71, "rank": 2 } ] } } ``` ## 1.5 失败响应(★重点) **只要 `code != 200`,响应结构就变**:`data` 只保留 4 个字段,不再有 `matches` / `model_version` / `processing_time_ms`。 ```json { "code": 422, "message": "no card detected in image", "data": { "task_id": "9988776655", "status": "failed", "error_code": "NO_CARD_DETECTED", "error_message": "no card detected in image" } } ``` - **HTTP 状态码 = `code`**(400/415/422/500 直接体现在 HTTP status 上)。 - `message` 与 `data.error_message` 内容一致,展示提示用 `error_message`。 - **`error_code` 是稳定枚举,后端用它判分支**(不要用 `message` 文案判断)。 | HTTP / code | error_code | 含义 | 典型触发 & 建议处理 | |---|---|---|---| | **400** | `INVALID_REQUEST` | 请求参数错误 | 缺 `task_id`/`image_url`;检查参数 | | **400** | `INVALID_IMAGE_URL` | 图片 URL 非法或不可访问 | 下载失败;检查 URL 是否公网可达 | | **415** | `UNSUPPORTED_IMAGE_TYPE` | 不支持的图片格式 | 下载到了但不是有效图像;提示用户换图 | | **422** | `IMAGE_TOO_BLURRY` | 图片过于模糊 | 预留枚举,**当前版本不会主动触发** | | **422** | `NO_CARD_DETECTED` | 未检测到卡牌主体 | YOLO 检不出卡;提示用户重拍/更清晰 | | **500** | `MODEL_INFERENCE_ERROR` | 模型推理异常 | 特征提取失败或未分类异常;可重试 | | **504** | `MODEL_TIMEOUT` | 模型处理超时 | 预留枚举,**当前版本不会主动触发** | > **后端约定**:上面这些"业务性失败"建议后端转成前端 HTTP 200 + `data.status=failed`;而**算法服务超时 / HTTP 5xx / 网络失败**等基础设施异常,转成 **HTTP 502** 报警。 ## 1.6 调用示例 ```bash curl -X POST http://192.168.77.249:8010/recognize \ -H 'Content-Type: application/json' \ -H 'X-Request-Id: order-20260707-001' \ -d '{"task_id":"9988776655","image_url":"https://cdn.yourdomain.com/card-001.jpg","top_k":5,"min_match_rate":0}' ``` --- # 二、8000 卡牌字段接口(后端服务 + 小程序) ## 2.1 功能 传一张图片,返回**最匹配那一张卡的业务字段**。两个入口输入完全一样,只是返回结构不同: | 接口 | 方法 | 用途 | |---|---|---| | `http://192.168.77.249:8000/match` | GET / POST | **单对象**返回,给后端服务调用 | | `http://192.168.77.249:8000/match_fields` | GET / POST | **数组**返回(多一个图片字段),给小程序前端调用 | ## 2.2 请求头 | 场景 | Header | |---|---| | GET 传参 | 无需特殊头 | | JSON POST | `Content-Type: application/json` | | 上传文件 | `Content-Type: multipart/form-data`(curl 用 `-F` 自动带) | 无鉴权。 ## 2.3 请求参数(图片来源三选一) | 方式 | 参数 | 说明 | |---|---|---| | 上传文件 | multipart 字段 `image` | 前端本机图片,`-F "image=@xxx.jpg"` | | 图片 URL | `image_url` | 必须公网可下载 | | 服务器本地路径 | `image_url` | 服务器上已有文件的绝对路径,一般用不到 | 可选 `top_k`(默认 5)。注意:**`top_k` 只影响内部召回,两个入口都只返回 Top-1 那张卡**,不返回候选列表;要候选列表请用 8010。 ## 2.4 响应字段 `/match` 返回单对象 9 字段;`/match_fields` 返回**数组**(内含 1 个对象),多一个 `frontImageUrl`: | 字段 | 说明 | |---|---| | `card_id` | 卡牌唯一编号,后端主键 | | `card_name_ch` | 中文名 | | `pg_label` | 卡组 / 系列 | | `year` | 年份 | | `card_no` | 卡号 | | `rarity` | 稀有度(来自 `data/card_master_fields.csv`) | | `language` | 语种:`tcg us` / `tcg jp` / `简中` / `繁中` | | `card_type` | 固定 `1` | | `trace_id` | 本次调用随机 ID,仅供追踪,**不代表卡牌信息** | | `frontImageUrl` | 标准图地址,**仅 `/match_fields` 有** | ```json // GET /match { "card_id": "04101495", "card_name_ch": "褪色小镇", "pg_label": "XY - Ancient Origins", "year": "2015", "card_no": "73/98", "rarity": "Uncommon", "language": "tcg us", "card_type": 1, "trace_id": "923de4f4c4e146ccae5b5988452f7134" } // GET /match_fields [ { "card_id": "04101495", "card_name_ch": "褪色小镇", "...": "...", "frontImageUrl": "https://.../04101495/xxx.jpg", "trace_id": "22bf456a..." } ] ``` > 内部推理用 fusion/upper/lower 相似度(与入库同源),但**对外 9 字段契约不变**。 > `card_id` 之外的字段来自算法侧图库快照,如与业务库有出入,**以业务库为准**。 ## 2.5 失败响应 与 8010 不同,**8000 返回的是裸 `error` 文本,没有 error_code 枚举**,靠 HTTP 状态码区分: ```json { "error": "缺少图片:请上传 image 文件或传 image_url" } // 400 { "error": "上传文件为空" } // 400 { "error": "图片下载失败(公网 URL 访问不到): https://..." } // 500 { "error": "未检出卡牌区域或特征提取失败" } // 500 ``` | 状态码 | 含义 | 处理 | |---|---|---| | **400** | 请求参数问题 | 检查是否传了 `image` 或 `image_url` | | **500** | 图片下载失败 / 未识别出卡牌 | 换图重试;无需额外特殊处理 | ## 2.6 调用示例 ```bash curl "http://192.168.77.249:8000/match?image_url=https://xxx.jpg" curl "http://192.168.77.249:8000/match_fields?image_url=https://xxx.jpg" curl -X POST http://192.168.77.249:8000/match -F "image=@D:/某张卡.jpg" curl -X POST http://192.168.77.249:8000/match -H "Content-Type: application/json" -d '{"image_url":"https://xxx.jpg"}' ``` --- # 三、8100 语种/方向 sidecar(内部,不对后端暴露) ## 3.1 功能 `POST http://127.0.0.1:8100/lang_judge` —— 卡面裁剪图 → **语种 + 方向角**。 **为什么要单独一个进程**:主接口跑在 `pytorch` 环境,而 PaddleOCR 需要 `paddleocr==3.2.0 + paddlepaddle 3.2.0`,两套包**互不兼容、无法共存于同一环境**,故拆成独立进程由主接口 HTTP 调用(PaddleOCR 常驻内存,单图延迟最低)。 **监听 `127.0.0.1`,仅本机可达**,后端不要直接调。 判定规则(`ocr_judge.py` 原样逻辑): 1. 假名 ≥ 10 → `tcg jp`(日文铁证,最高优先级) 2. 汉字×3 > 拉丁字母 → 中文卡,再用 `hanzidentifier` 投票判 `简中` / `繁中` 3. 否则 → `tcg us` > 卡面英文元素(HP/Weakness/版权/编号)天然拉高 latin,故中文阈值放宽到 25%。 ## 3.2 请求头 | Header | 说明 | |---|---| | `Content-Type: multipart/form-data` | 必须。multipart 字段名 **`image`**,值为卡面裁剪图 | 无鉴权,无其它参数。 ## 3.3 响应字段 | 字段 | 类型 | 说明 | |---|---|---| | `language` | string / null | `tcg us` / `tcg jp` / `简中` / `繁中`;**异常时为 `null`** | | `angle` | int | 方向角 `0` / `90` / `180` / `270`(输入相对正立的顺时针旋转),来自 `doc_preprocessor_res.angle` | | `detail` | object | 字符统计明细,随语种分支不同:日文 `{kana,cjk,latin}`、中文 `{kana,s,t,cjk}`、英文 `{kana,latin,cjk}`;异常时为 `{err: "..."}` | | `text` | string | OCR 文本,**截断到 500 字符** | ```json { "language": "tcg us", "angle": 0, "detail": {"kana":0,"latin":86,"cjk":2}, "text": "Fading Town..." } ``` ## 3.4 失败响应 | 情况 | 响应 | 说明 | |---|---|---| | 没传图 / 字段名不对 | `400` `{"error":"缺少图片:请以 multipart 字段 image 上传"}` | | | 上传文件为空 | `400` `{"error":"上传文件为空"}` | | | **OCR 内部异常** | `200` `{"language":null,"angle":0,"detail":{"err":"..."},"text":""}` | **★注意:不是 HTTP 错误**,而是降级返回 `language=null`,调用方需判空 | ## 3.5 它挂掉的影响 8010 主接口**自动降级仍可用**(走全库检索、`angle=0`),日志出现 `[warn] OCR sidecar 不可达`。可选环境变量 `LANG_JUDGE_URL`(默认 `http://127.0.0.1:8100/lang_judge`)、`LANG_JUDGE_TIMEOUT`(默认 8 秒,超时即降级)。 验证 sidecar 是否被真实调用: ```bash grep -a 'POST /lang_judge' logs/serve_8100.log | tail -3 # 有 200 行 = 真被调用了 ``` --- # 四、健康检查(三个服务都有 `GET /health`) | 服务 | 预期返回 | |---|---| | 8010 | `{"status":"ok","gallery_size":91116,"model_version":"card_seg_v0904"}` | | 8000 | `{"status":"ok","gallery_size":91116,"model":"card_seg_v0904","feature_dim":1024,"alpha":0.5,"top_k_recall":30}` | | 8100 | `{"status":"ok"}` | **两条硬性判据:** 1. `gallery_size` **必须是 91116**。若是 78272,说明特征库是旧版、**权重与图库混版了**,立即停服(新老下半区权重向量余弦仅 0.058,检索会全废)。 2. ★ **`/health` 会"假 ok"**(模型没真正就绪也可能返回 ok),**必须再打一个真实请求**才算验过。 --- # 五、失败响应差异速查 | | 8010 | 8000 | 8100 | |---|---|---|---| | 错误结构 | `{code,message,data{task_id,status,error_code,error_message}}` | `{"error":"文本"}` | `{"error":"文本"}` | | 稳定枚举 | ✅ `error_code` 7 种 | ❌ 只有 400/500 | ❌ 只有 400 | | 判分支依据 | **`data.error_code`** | HTTP 状态码 | HTTP 状态码 | | 推理失败时 | `422 NO_CARD_DETECTED` | `500 {"error":"未检出卡牌区域…"}` | `200` + `language=null`(降级,非报错) | --- # 附:部署与运维(后端可忽略) **环境**:服务器 `192.168.77.249`(用户 `martin`),框架根 `~/顾工交接/wzj`,GPU V100。 **重要**:必须 `CUDA_VISIBLE_DEVICES=1`(GPU0 是 GTX1060,cu128 不支持)。 ## 启动 ```bash cd ~/顾工交接/wzj # ① 先起 OCR sidecar(paddleocr 环境,8010 依赖它) CUDA_VISIBLE_DEVICES=1 setsid nohup \ ~/miniconda3/envs/paddleocr/bin/python serve_lang_judge.py --port 8100 \ logs/serve_8100.log 2>&1 & # ② 主接口(pytorch 环境) CUDA_VISIBLE_DEVICES=1 setsid nohup \ ~/miniconda3/envs/pytorch/bin/python serve_recognition_api.py --port 8010 \ logs/serve_8010.log 2>&1 & # ③ 字段接口(pytorch 环境,按需) CUDA_VISIBLE_DEVICES=1 setsid nohup \ ~/miniconda3/envs/pytorch/bin/python serve_card_match.py --port 8000 \ logs/serve_8000.log 2>&1 & ``` `setsid nohup … log 2>&1 &` 让进程脱离 SSH 常驻。加载:8010 约 25-30s(图库 91116 + DINOv2 + YOLO + 预热),8000 约 10-20s,8100 约 15-20s。**启动预热完后第一个真实请求就 ~0.5s**。 ## 自检 ```bash ps aux | grep -E 'serve_lang_judge|serve_recognition_api|serve_card_match' | grep -v grep ss -ltnp 2>/dev/null | grep -E ':8000|:8010|:8100' # 8000/8010 是 0.0.0.0,8100 是 127.0.0.1 curl -s http://127.0.0.1:8100/health; curl -s http://127.0.0.1:8010/health; curl -s http://127.0.0.1:8000/health # 冒烟识别(图库里已知卡,应命中同 card_id、match_rate≈96) curl -X POST http://127.0.0.1:8010/recognize -H 'Content-Type: application/json' \ -d '{"task_id":"smoke-1","image_url":"https://illustration-1309648802.cos.ap-guangzhou.myqcloud.com/ai_image/04101495/synthesis_result_1781513293_8eb1dce1-e93b-4c78-90f1-16c9d7251498.jpg","top_k":3}' ``` 通过标志:`code:200`、`card_id == "04101495"`、`match_rate ≈ 96`、`processing_time_ms` ~0.5-1s。 ## 停止 / 重启 ```bash pkill -9 -f serve_recognition_api.py # 本机直接执行可用 pkill pkill -9 -f serve_card_match.py pkill -9 -f serve_lang_judge.py ``` > 通过 `ssh host sh -c` 远程执行时**勿用 `pkill -f`(会杀掉自己)**,改按 PID: > `for p in $(pgrep -f 'serve_card_match.py --port 8000'); do [ "$p" != "$$" ] && kill -9 "$p"; done` sidecar 一般不用频繁重启,除非改了它的代码。 ## 常见问题 | 现象 | 原因 & 处理 | |---|---| | `Address already in use` | 端口被占;`ss -ltnp \| grep 8010` 看是谁,kill 后重启 | | 日志 `[warn] OCR sidecar 不可达` | sidecar 挂了/没起。接口仍可用(降级),`curl 127.0.0.1:8100/health` 排查后重启 sidecar | | 首次请求特别慢(>3s) | 预热没跑完;看日志有无 `[启动] 预热完成`,没有就重启 | | `No module named flask` | 对应环境装:`~/miniconda3/envs//bin/pip install flask` | | `Field 'mlp_ratio' expected int, got float` | DINOv2 `config.json` 里 `mlp_ratio` 是 `4.0`,新版 huggingface_hub 严格校验,改成 `4` 即可(备份 `config.json.bak.20260717`) | | `Ultralytics requirement ['onnx'] not found` | ultralytics 会自动装,等 30-60s 后重启接口生效 | | `DualCascadeMatcher` import 失败 | 确认 `scripts/cascade_match.py` 与 `modules/feature_extractor_dual.py` 在框架根下 | | top1 rate 特别低(<30) | 图像质量差/无卡牌,属正常低置信,不是服务故障 | | `no CUDA-capable device` | 忘了 `CUDA_VISIBLE_DEVICES=` | | 显存不够 OOM | 5090 是共享的,`nvidia-smi` 看后换另一张卡的 UUID | | 缺 `gallery_upper/lower` 特征库 | 见 `宝可梦/上线/README_上线清单.md` 落位表补齐 | ## 相关文档 - 上线交付包与资产清单:`宝可梦/上线/README_上线清单.md` - 算法链路总览:`宝可梦整体流程框架详解.md` - 裁卡/摆正细节:`卡牌区域检测裁切与旋转正立流程.md` - 语种判定规则详解:`语种判断逻辑说明.md`