# API 文档 · 围合尺寸估测 → 生成 3D 模型(全自动) > Base URL:**`https://scan.eachother.work`** | 版本 `v1`(2026-09-12) > 一句话:**传几张照片进去,拿一个 URL;没做完就 GET 到进度,做完了 GET 到 GLB。** ## 你不需要告诉它尺寸 尺寸有两条路,**都不需要你操心**: 1. **默认全自动**:服务自己估测围合尺寸 → 做**置信化评测** → 做**保守决策** → 直接给你结果。 2. **可选覆盖**:你确实量准了(卷尺/商品页),才传 `target_w_mm/target_d_mm/target_h_mm`。 > **留空 = 全自动。** 置信度怎么算、不确定时怎么办、用哪种缩放策略——**都是服务的责任,不是调用方的**。 --- ## 0. 60 秒上手 ```bash # 1) 传图片,拿一个 URL(不用传任何尺寸) curl -s -X POST https://scan.eachother.work/v1/jobs \ -F "images=@front.jpg" -F "images=@left45.jpg" -F "images=@right45.jpg" \ -F "category=chair" # → {"jobId":"job-ab12cd34","state":"processing","url":"https://scan.eachother.work/v1/jobs/job-ab12cd34", ...} # 2) 反复 GET 那个 URL curl -i https://scan.eachother.work/v1/jobs/job-ab12cd34 # 没做完 → Content-Type: application/json,body 是进度(含服务正在做的决策) # 做完了 → Content-Type: model/gltf-binary,body 就是 GLB 文件本身 curl -o model.glb https://scan.eachother.work/v1/jobs/job-ab12cd34 ``` **核心就这两个调用。** --- ## 1. 全自动:服务替你做哪些决策 当你**没传尺寸**时,服务内部依次做三件事。 ### ① 估测:多图取中位数,不信单点 从你给的图片里挑**主体最完整**的 2–3 张,各估一次围合尺寸,**逐轴取中位数**(抗离群)。 单张估值会波动,中位数不会。 ### ② 置信化评测:把"这次到底可不可信"算成一个分 综合五个**互相独立**的信号: | 信号 | 影响 | |---|---| | **多图离散度** | 同一物体、不同角度估出来差得多 → 模型自己也不确定(**权重最大**) | | 有效样本数 | 只有 1 张有效 → 不构成中位数,直接扣分 | | 模型自评把握 | 逐张自述 confidence 的平均 | | 类别交叉校验 | 识别出的类别与你填的 `category` 不符 → 可能连物体都认错了 | | 与类目先验的偏离 | 落在该类目常见尺寸内 → 加分;偏离 3 倍以上 → 重罚 | 输出 `high` / `medium` / `low` 三档,**并给出可读的理由**(不是黑盒分数)。 ### ③ 保守决策:不确定时**主动退让**,而不是把风险丢给调用方 | 置信度 | 服务怎么做 | |---|---| | **high** | 直接采用估测值 | | **medium** | **向该类目常见尺寸收拢 30%**(几何加权)——在"相信它"和"相信常识"之间折中 | | **low** | **改用它识别出的类目的常见尺寸**——宁可"典型的对",也不要"自信的错" | 再加两条保守规则: - **夹紧**:任一轴超出该类目先验的 0.4–2.5 倍 → 拉回边界并记录(防止产出 3 米宽的椅子) - **均衡缩放**:既然是估测(三边都不确定),就**不死盯一个轴**——取三轴缩放比的几何平均,让"最坏的那一轴"尽量不那么坏 > **实测对比**(资料架:模型原始 858×1600×500,目标 450×350×1200) > > | 策略 | 结果 | 最大相对误差 | > |---|---|---| > | 死盯高度 | 643 × 1200 × 375 | **+43%**(全压在宽度上) | > | **均衡缩放** | 558 × 1041 × 325 | **+24%**(三轴分摊) | > > 都不完美,但均衡缩放**没有任何一轴特别离谱**——这才是保守。 **结论:你拿到的永远是一个"结果",不是一个需要你自己判断的警告列表。** 决策过程完整写在响应的 `decision` 字段里供追溯,但**不需要你解读**。 --- ## 2. 核心接口 ### `POST /v1/jobs` —— 提交任务 **输入**:若干图片(1–32 张)+ 可选参数 **输出**:`202` + 任务对象,其中 `url` 是你要反复 fetch 的地址 **① `multipart/form-data`(推荐)** ```bash curl -X POST https://scan.eachother.work/v1/jobs \ -F "images=@1.jpg" -F "images=@2.jpg" -F "images=@3.jpg" \ -F "category=chair" # 只有已经量准了才需要加(可选覆盖): # -F "target_w_mm=450" -F "target_d_mm=450" -F "target_h_mm=900" ``` **② `application/json`(图片 base64)** ```bash curl -X POST https://scan.eachother.work/v1/jobs \ -H "Content-Type: application/json" \ -d '{"images":["",""],"category":"chair"}' ``` **响应 `202`:** ```json { "jobId": "job-ab12cd34", "state": "processing", "url": "https://scan.eachother.work/v1/jobs/job-ab12cd34", "statusUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34?format=json", "glbUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34/glb", "usdzUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34/usdz", "manifestUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34/manifest.json", "images": 3, "autoEstimate": true, "pollAfterMs": 5000 } ``` ### `GET /v1/jobs/:id` —— 取结果(一个 URL 两种形态) | 任务状态 | 响应 | |---|---| | **未完成** | `200` + `Content-Type: application/json`,body 是**进度 JSON** | | **已完成** | `200` + `Content-Type: model/gltf-binary`,body 就是 **GLB 文件** | | 失败/取消 | `200` + JSON,`state` 为 `failed` / `canceled` | > ⚠️ 判断依据是 **`Content-Type`**,两种情况都是 200。 > **不想判断就用 `?format=json`(或请求头 `Accept: application/json`)**——永远返回 JSON,完成时给出 `glbUrl`。 > **程序化调用推荐这种方式。** **进度 JSON(进行中)——注意 `decision` 已经在里面了:** ```json { "jobId": "job-ab12cd34", "state": "processing", "stage": "generate", "progress": 42, "message": "生成中… 已等待 210s", "decision": { "automatic": true, "confidence": { "level": "medium", "score": 0.52, "reasons": ["多图基本一致(离散度 22%)", "尺寸落在该类目常见范围内"] }, "chosen": { "target_mm": {"w":450,"d":350,"h":1200}, "fit_mode": "balanced", "size_source": "vlm_estimate_blended_with_prior", "category_used": "shelf" }, "adjustments": ["置信度中等 → 已向「shelf」常见尺寸收拢 30%(几何加权)", "三边均来自 AI 估测 → 采用均衡缩放"], "rationale": "来源=vlm_estimate_blended_with_prior;置信度=medium(0.52)。…" }, "estimate": { "dims_mm": {"w":430,"d":330,"h":1180}, "spread": 0.22, "confidence": 0.5, "objectName": "资料架", "category": "shelf" }, "target_mm": {"w":450,"d":350,"h":1200}, "pollAfterMs": 5000 } ``` **结果 JSON(`?format=json` 且已完成):** ```json { "jobId": "job-ab12cd34", "state": "done", "progress": 100, "glbUrl": "https://scan.eachother.work/models/asset-xxx/model.glb", "glbDirectUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34/glb", "usdzUrl": "https://scan.eachother.work/models/asset-xxx/model.usdz", "manifestUrl": "https://scan.eachother.work/v1/jobs/job-ab12cd34/manifest.json", "assetId": "asset-xxx", "decision": { "automatic": true, "confidence": { "level": "medium", "score": 0.52, "reasons": ["…"] }, "chosen": { "target_mm": {}, "fit_mode": "balanced", "size_source": "…" }, "adjustments": ["…"], "rationale": "…" }, "sizes": { "target_mm": {"w":450,"d":350,"h":1200}, "actual_mm": {"w":558,"d":325,"h":1041}, "residual_mm": {"w":108,"d":-25,"h":-159}, "relError": {"w":0.24,"d":0.07,"h":0.133,"max":0.24}, "fit_mode": "balanced", "passed": true }, "anchor": { "method": "vlm_estimate_blended_with_prior", "confidence": 0.55 }, "warnings": ["…"], "pipelineMs": 396000 } ``` ### 其它子资源 | 路径 | 说明 | |---|---| | `GET /v1/jobs/:id/glb` | 恒定返回 GLB。未完成时 `409` + 进度 JSON | | `GET /v1/jobs/:id/usdz` | USDZ(iOS AR 用)。未生成时 `409` | | `GET /v1/jobs/:id/manifest.json` | **尺度清单**(真值 + 溯源,见 §5) | | `GET /docs` | 本文档 | --- ## 3. 请求参数 | 参数 | 默认 | 说明 | |---|---|---| | `images` | **必填** | 1–32 张,`jpeg`/`png`/`webp`,单张 ≤12MB。**建议 ≥5 张**,绕物体一圈、相邻两张画面重合约 60% | | `category` | `other` | `chair` `sofa` `table` `bed` `wardrobe` `cabinet` `shelf` `lamp` `appliance` `other`。**填了会更准**(先验校验用);填 `other` 时服务用识别出的类别做决策 | | `target_w_mm`
`target_d_mm`
`target_h_mm` | 空 | **可选覆盖**。三个都给 → 跳过 AI 估测,直接用你的尺寸(**最准**),缩放策略改为保比例的高度锚定。缺任一则忽略、走全自动 | | `fit_mode` | 自动 | **高级选项,一般不用管。** 留空由服务决定(全自动走 `balanced`)。可选 `uniform`(等比+高度锚定)/ `nonuniform`(三边精确吻合但变形) | | `estimate_images` | `3` | 自动估测用几张照片(各估一次取中位数)。越多越稳但越慢 | | `anchor_axis` | `h` | `fit_mode=uniform` 时以哪个轴为准:`h`/`w`/`d`/`max` | | `min_images` | `1` | 少于这个数直接判失败 | | `run_qc` | `false` | 是否对每张跑语义质检(模糊/完整度),开了每张多 2–3 秒 | --- ## 4. 状态机与耗时预期 ``` estimating → upload → generate → download → normalize → usdz → done ↘ failed / canceled ``` | 阶段 | 典型耗时 | |---|---| | `estimating` | 5–15 秒(含置信评测与保守决策) | | `upload` | 1–5 秒 | | **`generate`** | **3–8 分钟(大头,云端生成)** | | `download` / `normalize` | 1–3 秒 | | `usdz` | 30–60 秒 | **端到端通常 4–9 分钟。** 建议按 `pollAfterMs`(5 秒)轮询。 > `progress` 是**时间推算值**,不是云端真实百分比(生成服务只返回状态机,不给百分比)。 > 用途是"让你看见它在动",**别拿它做工期预测**。 --- ## 5. ⚠️ 输出产物与单位约定(用之前必须知道) | 产物 | 内容 | |---|---| | `model.glb` | **最终模型**。单位 **米**,原点 = **底面中心**(最低点 Y=0、水平居中),+Y 朝上、正面朝 +Z | | `model.usdz` | iOS AR Quick Look 用(`rel="ar"` 直接拉起,1:1 真实尺寸) | | `manifest.json` | **尺度真值清单**:目标/实测/残差、尺寸来源、置信度、可核查证据 | | `place.json` | 放置契约(Unity AR Foundation + glTFast 直接可用) | **单位铁律**:GLB 里是**米**(glTF 规范强制),manifest 里是**毫米**。 ```jsonc // manifest.json 关键字段 { "anchor": { // 尺寸从哪来 —— 本服务的核心概念 "method": "vlm_estimate_blended_with_prior", // vlm_estimate(_blended_with_prior) | category_prior | manual_input "confidence": 0.55, "size_mm": { "w":450, "d":350, "h":1200 }, "evidence": { "samples": 3, "spread": 0.22, "objectName": "资料架", "perImage": [] } }, "autoDecision": { "confidence": {}, "adjustments": [], "rationale": "…" }, "verification": { "target_mm": {}, "actual_mm": {}, "residual_mm": {}, "pass": true }, "relError": { "w":0.24, "d":0.07, "h":0.133, "max":0.24 }, "fit": { "mode": "balanced", "method": "bake" }, "warnings": [ "…" ] } ``` ### 怎么看"结果好不好"——**看 `relError.max`,别看 `passed`** - **`passed: true` 不代表尺寸准。** 它只校验「锚定轴是否命中」,而锚定轴在构造上必然命中(`nonuniform` 则三轴都精确命中),**信息量接近于零**。 - **真正有意义的指标是各轴相对误差 `relError`**,尤其是最大值 `relError.max`: - `≤ 0.15` → 很贴合 - `≤ 0.35` → 可接受(均衡缩放的验收线) - `> 0.35` → 模型原始比例与目标尺寸分歧过大,结果仅供参考(会有 `warnings`) - `uniform`/`balanced` 模式下,非锚定轴的偏差**不是缩放错误**,而是「目标尺寸」与「模型自身比例」的固有分歧——**等比缩放只可能命中一个轴,其余轴必然分摊误差**。 想三边都精确吻合 → `fit_mode=nonuniform`(代价:模型被拉伸变形)。 --- ## 6. 完整示例 ### Node.js(18+,内置 fetch / FormData)—— 全自动,不传尺寸 ```js import fs from 'node:fs'; const BASE = 'https://scan.eachother.work'; const fd = new FormData(); for (const p of ['front.jpg', 'left.jpg', 'right.jpg']) fd.append('images', new Blob([fs.readFileSync(p)], { type: 'image/jpeg' }), p); fd.append('category', 'chair'); // 尺寸不用传,服务自己决定 const job = await (await fetch(`${BASE}/v1/jobs`, { method: 'POST', body: fd })).json(); console.log('jobId =', job.jobId); for (;;) { const p = await (await fetch(job.statusUrl)).json(); // ?format=json:永远是 JSON if (p.state === 'failed' || p.state === 'canceled') throw new Error(p.error); if (p.state === 'done') { fs.writeFileSync('model.glb', Buffer.from(await (await fetch(p.glbUrl)).arrayBuffer())); console.log('服务自己做的决策:', p.decision.confidence.level, '/', p.decision.chosen.size_source); console.log('实际尺寸:', p.sizes.actual_mm, '最大相对误差:', p.sizes.relError.max); break; } console.log(`${p.stage} ${p.progress}% ${p.message}`); await new Promise(r => setTimeout(r, p.pollAfterMs ?? 5000)); } ``` ### Python(requests) ```python import time, requests BASE = "https://scan.eachother.work" paths = ["front.jpg", "left.jpg", "right.jpg"] files = [("images", (p, open(p, "rb"), "image/jpeg")) for p in paths] job = requests.post(f"{BASE}/v1/jobs", files=files, data={"category": "chair"}).json() print("jobId =", job["jobId"]) while True: p = requests.get(job["statusUrl"]).json() # 永远 JSON if p["state"] in ("failed", "canceled"): raise SystemExit(p.get("error")) if p["state"] == "done": open("model.glb", "wb").write(requests.get(p["glbUrl"]).content) print("决策:", p["decision"]["confidence"]["level"], p["decision"]["chosen"]["size_source"]) print("尺寸:", p["sizes"]["actual_mm"], "最大相对误差:", p["sizes"]["relError"]["max"]) break print(p.get("stage"), p["progress"], "%", p.get("message")) time.sleep(p.get("pollAfterMs", 5000) / 1000) ``` ### curl 一行版(完成才下载,未完成输出进度) ```bash JOB=$(curl -s -X POST https://scan.eachother.work/v1/jobs \ -F "images=@a.jpg" -F "images=@b.jpg" -F "images=@c.jpg" -F "category=chair" | jq -r .url) while :; do CT=$(curl -s -o /tmp/out -w '%{content_type}' "$JOB") if [[ "$CT" == model/gltf-binary ]]; then mv /tmp/out model.glb; echo "✅ model.glb"; break; fi jq -c '{state,stage,progress,confidence:.decision.confidence.level}' /tmp/out sleep 5 done ``` ### 指定尺寸(你已经量准了) ```bash curl -X POST https://scan.eachother.work/v1/jobs \ -F "images=@a.jpg" -F "images=@b.jpg" -F "images=@c.jpg" \ -F "category=chair" \ -F "target_w_mm=450" -F "target_d_mm=450" -F "target_h_mm=900" # → decision.automatic = false,跳过估测,直接按你的尺寸做(保比例、高度锚定) ``` --- ## 7. 错误码 | 码 | 含义 | 处理 | |---|---|---| | `400` | 没收到图片 / JSON 解析失败 / 无 multipart boundary | 检查请求体 | | `404` | `jobId` 不存在 | | | `409` | 任务未完成就访问 `/glb`、`/usdz` | 继续轮询主 URL | | `413` | 请求体过大(单次 ≤64MB,单张 ≤12MB) | 压缩图片 | | `415` | 不是可识别的图片(按**文件魔数**判定,不信 Content-Type) | 用 jpeg/png/webp | | `500` | 服务内部错误 | 重试 | | 任务级 `failed` | 见 JSON 的 `error` | 传 `target_*_mm` 重试可绕过自动估测 | --- ## 8. 两步式接口(可选,适合有交互界面的场景) 如果你要做"边拍边估测、用户确认后再生成"的界面,用这套(本服务的 Web 界面就是用它): | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/session` | 建会话 `{category, target_mm, minImages}` | | POST | `/api/session/:id/image` | 传一张 `{dataUrl, angle}` → 返回质检结论 | | POST | `/api/session/:id/estimate` | **估测 + 置信评测 + 保守决策**(返回 `dims_mm/spread/perImage/decision`) | | POST | `/api/session/:id/target` | 手动设定尺寸与拟合方式(覆盖自动决策) | | POST | `/api/session/:id/build` | 启动生成(异步,轮询下面的 GET) | | GET | `/api/session/:id` | 会话状态 `{stage, progress, message, images, result}` | | GET | `/api/sessions` | **历史记录**(含存储占用) | | GET | `/history` | 历史记录页(手机可看) | | GET | `/view?asset=` | 3D 预览页(three.js,含 1m 参照立方体、独立复算尺寸) | | GET | `/api/health?deep=1` | 深度自检(真去戳生成服务 + 视觉模型,确认链路通) | --- ## 9. 限制与诚实声明 **请把这些一并告诉对接的人:** 1. **全自动尺寸是视觉估测,不是测量,精度约 ±20~30%。** 服务已用"多图中位数 + 置信评测 + 保守退让"把风险压到最低,但**它仍然不是尺子**。要准:卷尺量好传 `target_*_mm`(此时精度取决于你量的)。 2. **上游的 `aiPredictSize` 不是独立验证。** 生成服务返回的模型尺寸就是它自己预测的尺寸,两者是同一个数,**不能互相印证**。 3. **本服务当前没有鉴权。** URL 泄露 = 任何人都能提交任务、消耗你的额度。生产使用请自行加网关 / 加 key。 4. **单次生成约 4–9 分钟**,受云端队列影响;服务**不做全局并发控制**,别一次性打几十个任务。 5. **存储成本约 36MB/次**(原图 + 中间产物 + GLB + USDZ),历史记录不自动清理。 6. **图片质量决定成败**:模糊、主体不全、背景太乱都会显著拉低生成质量。相邻照片重合 ~60%、绕物体一圈、光线均匀。 7. **输出单位是米、pivot 是底面中心**(见 §5),直接放进 Unity / AR Quick Look 就是真实大小,**不要再乘任何缩放系数**。 --- ## 10. 相关 - 服务源码与部署文档:`scan-service/README.md` - 尺度与单位约定规范:`../2026-09-12-尺度与单位约定-v1.md` - 自测:`node selftest.mjs`(尺度数学 + 决策逻辑 **68 项**)· `node e2e.mjs`(HTTP 全链路 45 项,需先起 MOCK 服务)