# 建模 SDK

用**世界模型**把一段文字、一张照片、一张 360° 全景、同一空间的几张照片或一段视频，变成可以走进去的 3D 世界（高斯溅射 + 碰撞网格 + 全景），再把世界里的位置绑到公司的业务对象上——设备、工位、传感器——这是**数字孪生的地基**。上游是 World Labs 的 Marble 世界模型（World API）；所有调用都经过 AIDC：额度、并发、复用、留痕、账单都在平台这一侧，浏览器里拿不到上游密钥。

```js
import { modeling } from "/developer/sdk/v1/aidc.js";
```

| 做什么 | SDK | CLI |
| --- | --- | --- |
| 看价钱（不联网） | `modeling.estimate(model, input)` | `aidc modeling estimate` |
| 生成一个世界 | `modeling.generate(spec)` → `modeling.wait(id)` | `aidc modeling generate` |
| 看 / 找世界 | `modeling.get(id)` · `modeling.list()` | `aidc modeling get` · `list` |
| 在网页里走进去 | `modeling.viewer(el, world)` | — |
| 绑业务对象（孪生） | `modeling.setAnchors(id, anchors)` | `aidc modeling anchors` |
| 导出 / 下载到本机 | `modeling.exportAsset(id, …)` | `aidc modeling export` · `download` |
| 额度 | `modeling.budget()` | `aidc modeling budget` |

公开展示：[世界模型 · 空间建模](https://www.ai-dc.ai/nexus/apps/world-model)（只读展示，不接受生成请求）。

## 1. 先看价钱

World Labs 按 credits 计费，**1,250 credits = 1 美元**。一次生成 = 全景生成（按输入类型；给现成全景则免）+ 世界生成（按模型）：

| 模型 | 文字 | 单张图片 | 360° 全景 | 多张图片 / 视频 | 适合 |
| --- | --- | --- | --- | --- | --- |
| `marble-1.0-draft`（缺省） | 230（$0.18） | 230（$0.18） | 150（$0.12） | 250（$0.20） | 试提示词、看构图 |
| `marble-1.1` | 1,580（$1.26） | 1,580（$1.26） | 1,500（$1.20） | 1,600（$1.28） | 正式质量，价格固定 |
| `marble-1.1-plus` | 1,580–3,080 | 1,580–3,080 | 1,500–3,000 | 1,600–3,100（≤ $2.48） | 最大的世界，按规模加收 |

另外：HQ 网格导出每个世界 3,500 credits（$2.80，同一世界只收一次）；PLY 高斯溅射导出、上传输入、查询进度都不收钱。

```js
modeling.estimate("marble-1.1", "video");
// { maxCredits: 1600, maxUsd: 1.28, lines: [{ name: "全景生成（视频）", credits: 100 }, { name: "世界生成", credits: 1500 }] }
```

```bash
aidc modeling estimate -m marble-1.1-plus --input multi-image    # 本机算，不用登录
```

## 2. 费用控制：闸在平台这一侧

World Labs **不按单次成本拦截请求**——余额不够照样执行、月底按超额补扣，自动充值也不设上限。所以 AIDC 在调上游**之前**就把钱的事判完，按这个顺序：

1. **估价**：按最坏情况（Plus 的浮动部分按上限）算这一次最多花多少。
2. **策略**：模型是否开放、是否超过单次上限；调用方可以再给一个更严的 `maxCredits`。
3. **复用**：同一公司里同样的请求（模型 + 输入 + seed）已有结果 → 直接返回那个世界，**不花钱**（`regenerate: true` 才重新生成）。
4. **幂等**：必须带 `Idempotency-Key`（SDK / CLI 自动生成）；网络重试用同一个 Key，拿到的是同一个世界。
5. **dry-run 到此为止**：返回估价、额度、将发给上游的载荷，不调上游、不落库。
6. **每小时次数**（每公司）→ **同时在跑的上限**（全平台、每公司）→ **月度额度**（全平台总闸 + 公司额度 + 应用每日额度）：在同一把事务锁里判完并**按最坏情况预留**，并发请求不会一起挤过去。
7. **调上游**：明确失败 → 释放预留；**结果不明**（超时 / 断网，上游可能已受理）→ 预留不释放，按已花费计。
8. **结账**：完成时按上游结算的 `cost.total_credits` 结账（拿不到就按预留，宁多不少），同时在用量台账记一行（与模型通道同一本账，按公司计费）。

缺省策略（平台 env `AIDC_MODELING_POLICY` 可以部分覆盖；写坏了就**停止一切花费**）：

| 项 | 缺省 |
| --- | --- |
| 开放的模型 | `marble-1.0-draft`、`marble-1.1`（Plus 浮动计价，缺省不开） |
| 单次上限 | 1,600 credits（挡住 Plus 与 HQ 网格导出） |
| 全平台每月 | 6,250 credits（$5） |
| 公司每月 | AIDC 6,250；**其他公司 0 = 只能看，要开通找 AIDC 平台** |
| HQ 网格导出 | 关闭 |
| 同时在跑 | 全平台 3 个、每公司 2 个；每公司每小时 10 次 |

应用里生成要在清单写每日额度 `limits.worldCreditsPerDay`（不写 = 0 = 只看）；只读访客（分享链接 / viewer）不能生成。**官方公开应用（namespace `public`）永远不能生成、上传、导出或改锚点**——公开展示只看不花钱，这条写死在平台里，不靠清单。

```js
const plan = await modeling.generate({ input: { kind: "text", text: "一个整洁的汽车零部件装配工位" } }, { dryRun: true });
plan.plan.estimate.maxCredits;   // 230
plan.plan.decision;              // { ok: true } 或 { ok: false, message: "…本月的世界模型额度不够…" }
await modeling.budget();         // { month, company: { cap, settled, reserved, remaining } }
```

## 3. 生成一个世界

```js
const { world, reused } = await modeling.generate({
  input: { kind: "text", text: "一个整洁的汽车零部件装配工位：右侧白色六轴机械臂，前方红色检测台，黄色安全围栏" },
  model: "marble-1.0-draft",       // 先试稿；满意了再 marble-1.1
  displayName: "装配工位（试稿）",
  maxCredits: 300,                  // 这一次的硬上限
});
const done = await modeling.wait(world.id, { onProgress: (w) => console.log(w.progress?.description) });   // 约 5 分钟
```

```bash
aidc modeling generate --text "一个整洁的装配工位…" --dry-run            # 先看估价与额度
aidc modeling generate --text "一个整洁的装配工位…" -m marble-1.1        # 等到完成，打印资产与坐标系
aidc modeling generate --image cell.jpg --text "保留机械臂与检测台的位置"   # 本地文件自动直传 World Labs
aidc modeling generate --image a.jpg --image b.jpg --image c.jpg --reconstruct   # 同一空间多图重建
aidc modeling generate --video walk.mp4 --no-wait                       # 立即返回，稍后 aidc modeling wait <id>
```

| 输入 `input.kind` | 写法 | 建议（World Labs 官方与 AIDC 09-14 实测） |
| --- | --- | --- |
| `text` | `{ kind: "text", text }` | ≤ 2,000 字，描述一个地点而不是一个物件 |
| `image` | `{ kind: "image", image: { mediaId } \| { url }, text? }` | 长边 ≥ 1024、≤ 20 MB；要有地面 / 墙面 / 纵深；人物、特写、强反光效果差 |
| `pano` | `{ kind: "pano", image, text? }` | 2:1 等距柱状 360° 全景（约 2560 宽）；空间布局最准，不收全景生成费 |
| `multi-image` | `{ kind: "multi-image", images: [{ mediaId \| url, azimuth? }], reconstruct }` | `reconstruct: true`：2–8 张**同一空间、同一宽高比**、相邻有重叠的照片；不开时最多 4 张按方位角拼 |
| `video` | `{ kind: "video", video, text? }` | ≤ 30 秒 / 100 MB（mp4 / mov / webm）；稳、慢、一镜到底，覆盖 180°–360°；锁焦距与曝光；画面里别有人走动 |

`text` 是可选的文字引导（图片类输入不给就由上游自动描述）；`recaption: false` 让上游原样使用你的文字。`seed` 固定随机性。

**生成的是「看起来合理」的空间，不是测绘。** 照片没拍到的背面、遮挡处由模型补全；侧向移动时细杆、线缆、近处物件会拉伸或模糊；标牌文字不可信。AIDC 09-14 的四组实测（文字、单张照片、视频、四图重建）结论一致：适合工位导览、空间入口、培训与方案沟通；**不能**用于尺寸验收、碰撞安全、机器人离线编程——那需要标定、实测尺寸或 CAD。

## 4. 世界里有什么

```js
world.assets.splats["500k"];   // 高斯溅射 SPZ：100k / 150k / 500k / full_res（约 1.4 / 2.5 / 8 / 30 MB）
world.assets.collider;          // 碰撞网格 GLB（低模，用于走动、拾取、物理）
world.assets.pano;              // 生成用的全景图
world.assets.thumbnail;         // 缩略图
world.caption;                  // 上游对世界的描述
world.frame;                    // AIDC 空间坐标系（下一节）
world.cost;                     // { estimatedCredits, settledCredits, usd, lines }
```

资产放在 World Labs 的 CDN（`cdn.marble.worldlabs.ai`）上，地址本身就是访问凭证（不可猜的长路径）——只交给有权看这个世界的人。平台只存地址、坐标系与锚点，不存文件。

## 5. AIDC 空间坐标系

Marble 的原始坐标是 OpenCV 约定（Y 向下、任意单位）。平台按每个世界的尺度信息换成统一的**空间坐标系**：**米、Y 向上、地面 y = 0**；生成视点在 `(0, groundPlaneOffset, 0)`，初始视线朝 **-Z**。锚点、测距、以后接进来的设备位姿都在这个坐标系里。

```js
const frame = modeling.frameOf(world);          // { metricScaleFactor, groundPlaneOffset, metric }
modeling.toMetric([0.2, 0.6, 1.1], frame);      // 原始坐标（SPZ / 碰撞网格顶点）→ 米
modeling.fromMetric([1.2, 0.8, -3.4], frame);   // 米 → 原始坐标
modeling.distance(a, b);                        // 米
```

换算：`米 = [s·x, g − s·y, −s·z]`（s = metricScaleFactor，g = groundPlaneOffset）。碰撞网格与 SPZ 同一套原始坐标，用同一个换算。`frame.metric = false` 的老世界没有尺度信息（按 1 / 0 处理）；即使有，生成的尺度也是估计值，没有标定过。

## 6. 在网页里走进去：查看器

查看器用 World Labs 维护的高斯溅射渲染器 Spark（2.2.0）+ three.js（0.180.0），钉死版本、从 jsDelivr 加载。页面先放 import map（在任何 module 脚本之前），清单 `sdk` 登记 `modeling`、`cdn` 登记 `cdn.jsdelivr.net`——平台据此给应用的 CSP 放行 World Labs 资产与 WebAssembly 编译：

```html
<script type="importmap">
  { "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/",
      "@sparkjsdev/spark": "https://cdn.jsdelivr.net/npm/@sparkjsdev/spark@2.2.0/dist/spark.module.js" } }
</script>
<div id="stage" style="height: 70vh"></div>
<script type="module">
  import { modeling } from "/developer/sdk/v1/aidc.js";
  const [world] = await modeling.list({ status: "succeeded" });
  const view = await modeling.viewer(document.querySelector("#stage"), world, {
    quality: "500k",                     // 手机上用 "100k"
    collider: true,                      // 加载碰撞网格，支持 pick()
    onAnchor: (anchor) => console.log(anchor.object),
  });
  view.view("top");                      // home / side / back / top
  stage.addEventListener("dblclick", async (e) => console.log(await view.pick(e.clientX, e.clientY)));   // → [x, y, z]（米）
</script>
```

`modeling.importMap()` 返回同一份 import map。左键拖动旋转、滚轮缩放、右键平移；`focus(anchor)` 飞到锚点；`setQuality()` 换精度；`snapshot()` 截图；`dispose()` 释放 WebGL 资源。需要 WebGL2。

## 7. 锚点：数字孪生的地基

世界只是空间；**锚点**把空间里的位置绑到业务对象上。每个锚点：位置（米）、名称、类型（`equipment` / `sensor` / `zone` / `camera` / `note`）、静态属性（型号、编号…），以及可选的**语义层对象**（`object: { type, pk }`，类型必须在本公司语义层里）。

```js
await modeling.setAnchors(world.id, [
  { id: "robot-1", label: "焊接机械臂 R-01", kind: "equipment", position: [1.2, 0.8, -3.4],
    object: { type: "production.equipment", pk: "R-01" }, meta: { 型号: "ER20" }, verified: true },
  { id: "fence-a", label: "安全围栏 A 区", kind: "zone", position: [0, 0, -2] },
], { ifRev: world.anchorsRev });
```

整组替换（幂等），`ifRev` 做乐观并发（别人改过就 409）。`verified` 标明位置与身份是否经人工核验——生成的世界有模型补全，未核验的锚点在查看器里是空心点，只能当示意。

把锚点接到实时数据，就是一个最小的数字孪生：

```js
import { modeling, data } from "/developer/sdk/v1/aidc.js";
const view = await modeling.viewer(stage, world, {
  onAnchor: async (anchor) => {
    if (!anchor.object) return;
    const { object } = await data.table(anchor.object.type).get(anchor.object.pk);   // 语义层里的设备：状态、报警、工单…
    panel.textContent = `${anchor.label}：${object.props.status}`;
  },
});
data.watch({ types: ["production.equipment"] }, { onChange: () => refreshPanels() });   // 数据一变就更新
```

## 8. 导出与下载

```bash
aidc modeling export <id> --ply --resolution full_res   # 高斯溅射 PLY（免费、立即完成）
aidc modeling export <id> --mesh --dry-run              # HQ 网格 GLB：3,500 credits，缺省策略关闭
aidc modeling download <id> --out ./cell-01             # SPZ + 碰撞网格 + 缩略图 + world.json（坐标系与锚点）
```

下载下来的 SPZ / PLY 可以进 Unity、Unreal、Blender、Houdini（各引擎的高斯溅射插件），按 world.json 里的坐标系摆正。

## 9. 输入媒体与数据边界

```js
const { mediaId } = await modeling.upload(file);   // 先向平台换签名地址，字节直传 World Labs，不经过 AIDC
await modeling.generate({ input: { kind: "image", image: { mediaId }, text: "保留设备位置" } });
```

- 上传的照片 / 视频**发给第三方模型服务 World Labs（美国）处理**。只上传有权提供的现场内容；避免人脸、证件、屏幕上的敏感信息；客户现场的内容要客户同意。
- `mediaId` 登记在上传它的命名空间，生成时只认本命名空间的（别的公司引用不到）。浏览器直传若被存储桶跨域拦下，改用 `aidc modeling upload` 或给公开的 https 地址。
- 平台存：提示文字、媒体引用（id / 地址，不存字节）、资产地址、坐标系、锚点、成本；删除世界 = 平台打墓碑，`purge` 同时删除 World Labs 上的原件（只对经平台生成的世界）。
- **官方公开命名空间（public）只放不含客户内容的世界**（文字生成、或已获授权的内容）。

## 10. 清单与 API

```json
{
  "sdk": ["modeling", "ui"],
  "cdn": ["cdn.jsdelivr.net"],
  "limits": { "dailyTokens": 1000000, "requestsPerMinute": 30, "realtimeSessionsPerDay": 0, "worldCreditsPerDay": 1600 }
}
```

| 方法 | 路径 | 做什么 |
| --- | --- | --- |
| GET | `/api/v1/developer/modeling` | 目录：模型、价目、策略、上游是否已配置（公开） |
| GET | `/api/v1/developer/modeling/{ns}/budget` | 本月额度 |
| GET / POST | `/api/v1/developer/modeling/{ns}/worlds` | 列表 / 生成（`Idempotency-Key` 必填，支持 dry-run） |
| GET / DELETE | `/api/v1/developer/modeling/{ns}/worlds/{id}` | 详情（进行中会同步进度）/ 删除（`?purge=1`） |
| PUT | `/api/v1/developer/modeling/{ns}/worlds/{id}/anchors` | 锚点（整组替换，`ifRev`） |
| POST | `/api/v1/developer/modeling/{ns}/worlds/{id}/exports` | 导出 PLY / HQ 网格 |
| POST | `/api/v1/developer/modeling/{ns}/media` | 上传输入：换签名地址 |
| POST | `/api/v1/developer/modeling/{ns}/imports` | 登记已有的 Marble 世界（AIDC 平台账号） |

`{ns}` = `public` 或 `cell-…`。`public` 的世界谁都能读（只列已完成的、不带发起人）。错误码：`quota_exhausted`（额度 / 单次上限，details.scope 说明是哪一层）、`rate_limited`、`idempotency_key_required`、`idempotency_conflict`、`media_not_found`、`world_not_found`、`modeling_unconfigured`（平台还没配置上游）、`model_upstream_failed`。

## 11. 接下来：从空间到孪生

世界（空间底座）+ 锚点（空间 ↔ 对象）+ 语义层（对象的状态）+ 数据流（实时）+ 工作流（联动）= 数字孪生。建模 SDK 先把前两块做成稳定的契约：统一的空间坐标系、按公司隔离的世界与锚点、可控的成本。下一步：同一工位的环拍采集规范与实测尺寸标定、设备位姿接入、世界版本对比、World Labs 新模型（Atlas 的多视图重建与相机控制）开放后接入。
