三合一文档,取代原
后端对接文档.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) |
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 的结果为准。
| Header | 必填 | 说明 |
|---|---|---|
Content-Type: application/json |
是 | JSON 请求 |
X-Request-Id |
否 | 后端生成的追踪 ID,算法侧原样写日志 |
Authorization: Bearer <token> |
否 | 当前未开启鉴权,不用传(仅当服务带 API_TOKEN 环境变量启动时才校验) |
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
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 | 调试/入库用,见下 |
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 属低置信。是否采纳由后端决定,算法侧默认不卡门槛、全部返回。
{
"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 }
]
}
}
只要 code != 200,响应结构就变:data 只保留 4 个字段,不再有 matches / model_version / processing_time_ms。
{
"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"
}
}
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 报警。
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}'
传一张图片,返回最匹配那一张卡的业务字段。两个入口输入完全一样,只是返回结构不同:
| 接口 | 方法 | 用途 |
|---|---|---|
http://192.168.77.249:8000/match |
GET / POST | 单对象返回,给后端服务调用 |
http://192.168.77.249:8000/match_fields |
GET / POST | 数组返回(多一个图片字段),给小程序前端调用 |
| 场景 | Header |
|---|---|
| GET 传参 | 无需特殊头 |
| JSON POST | Content-Type: application/json |
| 上传文件 | Content-Type: multipart/form-data(curl 用 -F 自动带) |
无鉴权。
| 方式 | 参数 | 说明 |
|---|---|---|
| 上传文件 | multipart 字段 image |
前端本机图片,-F "image=@xxx.jpg" |
| 图片 URL | image_url |
必须公网可下载 |
| 服务器本地路径 | image_url |
服务器上已有文件的绝对路径,一般用不到 |
可选 top_k(默认 5)。注意:top_k 只影响内部召回,两个入口都只返回 Top-1 那张卡,不返回候选列表;要候选列表请用 8010。
/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 有 |
// 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之外的字段来自算法侧图库快照,如与业务库有出入,以业务库为准。
与 8010 不同,8000 返回的是裸 error 文本,没有 error_code 枚举,靠 HTTP 状态码区分:
{ "error": "缺少图片:请上传 image 文件或传 image_url" } // 400
{ "error": "上传文件为空" } // 400
{ "error": "图片下载失败(公网 URL 访问不到): https://..." } // 500
{ "error": "未检出卡牌区域或特征提取失败" } // 500
| 状态码 | 含义 | 处理 |
|---|---|---|
| 400 | 请求参数问题 | 检查是否传了 image 或 image_url |
| 500 | 图片下载失败 / 未识别出卡牌 | 换图重试;无需额外特殊处理 |
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"}'
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 原样逻辑):
tcg jp(日文铁证,最高优先级)hanzidentifier 投票判 简中 / 繁中tcg us卡面英文元素(HP/Weakness/版权/编号)天然拉高 latin,故中文阈值放宽到 25%。
| Header | 说明 |
|---|---|
Content-Type: multipart/form-data |
必须。multipart 字段名 image,值为卡面裁剪图 |
无鉴权,无其它参数。
| 字段 | 类型 | 说明 |
|---|---|---|
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 字符 |
{ "language": "tcg us", "angle": 0, "detail": {"kana":0,"latin":86,"cjk":2}, "text": "Fading Town..." }
| 情况 | 响应 | 说明 |
|---|---|---|
| 没传图 / 字段名不对 | 400 {"error":"缺少图片:请以 multipart 字段 image 上传"} |
|
| 上传文件为空 | 400 {"error":"上传文件为空"} |
|
| OCR 内部异常 | 200 {"language":null,"angle":0,"detail":{"err":"..."},"text":""} |
★注意:不是 HTTP 错误,而是降级返回 language=null,调用方需判空 |
8010 主接口自动降级仍可用(走全库检索、angle=0),日志出现 [warn] OCR sidecar 不可达。可选环境变量 LANG_JUDGE_URL(默认 http://127.0.0.1:8100/lang_judge)、LANG_JUDGE_TIMEOUT(默认 8 秒,超时即降级)。
验证 sidecar 是否被真实调用:
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"} |
两条硬性判据:
gallery_size 必须是 91116。若是 78272,说明特征库是旧版、权重与图库混版了,立即停服(新老下半区权重向量余弦仅 0.058,检索会全废)。/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 不支持)。
cd ~/顾工交接/wzj
# ① 先起 OCR sidecar(paddleocr 环境,8010 依赖它)
CUDA_VISIBLE_DEVICES=1 setsid nohup \
~/miniconda3/envs/paddleocr/bin/python serve_lang_judge.py --port 8100 \
</dev/null >logs/serve_8100.log 2>&1 &
# ② 主接口(pytorch 环境)
CUDA_VISIBLE_DEVICES=1 setsid nohup \
~/miniconda3/envs/pytorch/bin/python serve_recognition_api.py --port 8010 \
</dev/null >logs/serve_8010.log 2>&1 &
# ③ 字段接口(pytorch 环境,按需)
CUDA_VISIBLE_DEVICES=1 setsid nohup \
~/miniconda3/envs/pytorch/bin/python serve_card_match.py --port 8000 \
</dev/null >logs/serve_8000.log 2>&1 &
setsid nohup … </dev/null >log 2>&1 & 让进程脱离 SSH 常驻。加载:8010 约 25-30s(图库 91116 + DINOv2 + YOLO + 预热),8000 约 10-20s,8100 约 15-20s。启动预热完后第一个真实请求就 ~0.5s。
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。
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/<env>/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=<UUID> |
| 显存不够 OOM | 5090 是共享的,nvidia-smi 看后换另一张卡的 UUID |
缺 gallery_upper/lower 特征库 |
见 宝可梦/上线/README_上线清单.md 落位表补齐 |
宝可梦/上线/README_上线清单.md宝可梦整体流程框架详解.md卡牌区域检测裁切与旋转正立流程.md语种判断逻辑说明.md