# 素材库

视频生成等多模态接口可以用你上传到**素材库**的图片、视频、音频作为参考。素材只需上传一次，之后在任意请求里用 `asset://<ASSET_ID>` 引用即可，**国内版和海外版格式完全一致**。

相比每次请求都传一个 1 小时过期的预签名 URL，用素材库的好处：

- 永久引用，草稿保存再久也不会失效
- 同一张图可以跨多次生成复用，不用重复上传
- 素材可以复用到不同模型、不同任务

## 三步完成素材引用

### 1. 创建 AssetGroup（首次）

素材必须挂在一个 AssetGroup 下。一个用户可以有多个 Group。

```bash
curl -X POST https://api.xrtoken.ai/v1/asset-groups \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "Name": "我的素材库" }'
```

返回：

```json
{ "GroupId": "asset-group-20260423-abc12", "Name": "我的素材库" }
```

拿到 `GroupId` 之后复用，不用每次都建新的。

一个 API key 服务多个最终用户时，body 里加 `external_user_id`，见 [第三方 SaaS](#external-user-id)。

### 2. 上传文件拿到 URL

两种方式：

**方式 A — 走我们的 `/v1/files` 上传**（推荐，OpenAI 兼容）：

```bash
curl -X POST https://api.xrtoken.ai/v1/files \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -F 'file=@/path/to/reference.png' \
  -F 'purpose=assistants'
```

返回：

```json
{
  "id": "file-xxxxxxxx",
  "object": "file",
  "url": "https://xrtoken-storage-*.tos-*.bytepluses.com/uploads/<user>/<uuid>/reference.png?X-Tos-...",
  "bytes": 123456,
  "filename": "reference.png",
  "purpose": "assistants"
}
```

返回的 `url` 是 24h 签名 URL，下一步注册 Asset 用它当 `URL` 字段。如果 24h 内没注册完，可以重新 `GET /v1/files/{id}` 取一份新签名。

**方式 B — 用你自己公网可访问的 URL**（必须支持 GET 直接下载图/视频字节，不能是 HTML 页面）。

### 3. 注册为 Asset

```bash
curl -X POST https://api.xrtoken.ai/v1/assets \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "GroupId":    "asset-group-20260423-abc12",
    "URL":        "<上一步的 URL>",
    "AssetType":  "Image",
    "Name":       "女主角参考图",
    "Moderation": { "Strategy": "Skip" }
  }'
```

`AssetType` 可取 `Image` / `Video` / `Audio`。`Moderation.Strategy=Skip` 跳过上游内容审查（仅海外版生效，国内忽略）。

返回：

```json
{ "AssetId": "asset-20260423183015-xk4m2", ... }
```

### 4. 在视频生成里引用

把 `image_url.url` / `video_url.url` / `audio_url.url` 换成 `asset://<AssetId>` 即可，其它字段不变：

```json
{
  "model": "doubao-seedance-2-0-260128",
  "content": [
    { "type": "text", "text": "把女主角放到这个场景里走过来" },
    {
      "type": "image_url",
      "role": "reference_image",
      "image_url": { "url": "asset://asset-20260423183015-xk4m2" }
    },
    {
      "type": "image_url",
      "role": "reference_image",
      "image_url": { "url": "asset://asset-20260419234137-txt6j" }
    }
  ],
  "duration": 5,
  "ratio": "16:9",
  "resolution": "720p"
}
```

## 可以引用 asset:// 的字段

只能放在 URL 类字段里：

| 字段 | 用途 |
|---|---|
| `image_url.url` | 参考图（`reference_image`）、首帧（`first_frame`）、尾帧（`last_frame`）|
| `video_url.url` | 参考视频（`reference_video`，Seedance 2.0） |
| `audio_url.url` | 参考音频（`reference_audio`，Seedance 2.0） |

`content[].text` 里写 `asset://` 没用，不会被解析。

## 混用规则

同一个请求里 `asset://` 和普通 `https://` URL **可以混用**，比如一张参考图用素材库的、另一张临时图用签名 URL 的：

```json
"content": [
  { "type": "text", "text": "..." },
  { "type": "image_url", "role": "reference_image",
    "image_url": { "url": "asset://asset-20260423183015-xk4m2" } },
  { "type": "image_url", "role": "reference_image",
    "image_url": { "url": "https://your-cdn.com/temp/xxx.png" } }
]
```

## 限制

- 单个请求里最多 **10 个 asset 引用**（上游 Ark 限制）
- Asset 有 **90 天 TTL**（上游 Ark 限制），过期后 `asset://` 会解析失败；长期素材在第 89 天重新注册即可
- Asset 只对**创建它的用户**可见，用别人的 `AssetId` 会返回 403
- `asset://` 前缀 **不能** 用在 `text` 字段里 — 只能在 URL 字段里

## 火山原生格式（drop-in 兼容）

如果你已经用火山 Ark SDK / Open API 对接过素材库，可以直接把 base URL 指向我们、把 AK/SK 签名换成 Bearer，请求路径、参数、响应格式和官方保持一致，不用改业务代码：

```bash
# 创建素材（官方 Action=CreateAsset）
curl -X POST "https://api.xrtoken.net/?Action=CreateAsset&Version=2024-01-01" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "GroupId": "group-2026**********-*****",
    "URL": "https://example.com/image.jpg",
    "Name": "test",
    "AssetType": "Image",
    "ProjectName": "default"
  }'

# 查询素材（官方 Action=GetAsset）
curl -X POST "https://api.xrtoken.net/?Action=GetAsset&Version=2024-01-01" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "Id": "asset-20260423183015-xk4m2" }'
```

`ProjectName` 非必填：不传时自动使用网关项目（当前 `xrtoken`）；显式传值（包括 `default`）则原样透传，尊重你指定的项目。`Name` 可选。创建、查询、列表、更新、删除和真人核验都走官方 `POST /?Action=...`，响应沿用火山 `ResponseMetadata` / `Result` 信封，并保持素材库的账号隔离（只能操作自己名下的分组和素材）。完整说明见 [ARK SDK 兼容](/docs/ark-compatibility)。

<a id="external-user-id"></a>

## 第三方 SaaS：external_user_id

一个 API key 服务自己的 N 个最终用户时，每次请求带上该用户的 `external_user_id`，素材组和真人脸就会绑到这个人，其他人看不到、用不了。

不传 = 整个 API key 共享同一个素材池（我们自己 Dashboard 走这条）。

### 字段规则

| 项 | 值 |
|---|---|
| 字段名 | `external_user_id` |
| 含义 | 你自己系统里的用户 ID，我们当不透明字符串存 |
| 长度 | 最长 128 字符，超出会被截断 |
| POST / PUT | 放 JSON body |
| GET / DELETE | 放 query：`?external_user_id=...` |
| 鉴权 | 仍用你的 API key：`Authorization: Bearer tr-xxx` |

**同一条用户链路里，创建、轮询、列表、详情、更新、删除都要传同一个值。** 创建会话时传了，轮询 **不会** 自动带上——服务端没有 `BytedToken → external_user_id` 的存储。

国内站 `https://api.xrtoken.net`，海外站 `https://api.xrtoken.ai`。下面用 `$XRT_BASE` 表示。

### 建组 / 传素材

```bash
curl -X POST "$XRT_BASE/v1/asset-groups" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "Name": "用户42的素材",
    "external_user_id": "end-user-42"
  }'
```

```bash
curl "$XRT_BASE/v1/asset-groups?external_user_id=end-user-42" \
  -H "Authorization: Bearer $XRT_API_KEY"
```

上传素材时，body 里同样带 `external_user_id`，且 `GroupId` 必须属于这个人。视频生成里用 `asset://<AssetId>` 即可。

### 真人扫码：只轮询（推荐先接）

创建会话时的 `external_user_id` **不会**写进 `BytedToken`。绑组发生在 `validate-result` 成功那一次，**轮询必须再传**。

素材库 CRUD（建组、上传）不需要下面两项。真人扫码要先联系商务开通：账号是 **trusted_creator**，且已完成 **企业认证**。公开 API 不能自助开。

```bash
curl -X POST "$XRT_BASE/v1/asset-groups/validate-session" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "external_user_id": "end-user-42"
  }'
```

响应（字段可能在顶层，也可能在 `Result` 里）：

```json
{
  "BytedToken": "eyJhbGci...",
  "H5Link": "https://h5-v2.kych5.com?...",
  "QRCodeDataURL": "data:image/png;base64,..."
}
```

把 `QRCodeDataURL` 放进 `<img src>`，或让用户打开 `H5Link`。`BytedToken` 约 120 秒有效。建议每 5 秒轮询一次，直到成功、失败或超时：

```bash
curl -X POST "$XRT_BASE/v1/asset-groups/validate-result" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bytedToken": "eyJhbGci...",
    "external_user_id": "end-user-42"
  }'
```

| 响应 | 含义 |
|---|---|
| `GroupId` + `"status": "active"` | 成功，真人组已绑到 `end-user-42` |
| 没有 `GroupId` | 还在扫，继续轮询 |
| `ResponseMetadata.Error` | 失败 |

这里不传 `external_user_id`，组会绑成 `NULL`。之后用 `end-user-42` 去列表，**看不到**这个组。

真人组列表：

```bash
curl "$XRT_BASE/v1/asset-groups?type=real_person&external_user_id=end-user-42" \
  -H "Authorization: Bearer $XRT_API_KEY"
```

第一方扫码流程见 [真人活体](/docs/realperson-liveness)。

### 真人扫码：callback 跳转

适合你有一个完成后的落地页。不需要你开 webhook。开会话时两个字段一起传：

```bash
curl -X POST "$XRT_BASE/v1/asset-groups/validate-session" \
  -H "Authorization: Bearer $XRT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://your.app/face-done",
    "external_user_id": "end-user-42"
  }'
```

`callback_url` 必须是 `http` / `https` 且带 host。你自己的 query 会保留。

用户扫完后：火山先跳到我们的 `/v1/asset-groups/face-callback`（你不用调），我们验签、拿 `GroupId`、按创建时的 `external_user_id` 绑组，再 **302** 到你的页面：

- 成功：`https://your.app/face-done?status=success&group_id=group-...&external_user_id=end-user-42`
- 失败：`https://your.app/face-done?status=failed`（没有 `group_id`）

不传 `callback_url` → 跳回我们 dashboard，适合自己用 playground。

callback 和轮询可能同时写同一条组。如果同时还在轮询，每次仍要带同一个 `external_user_id`。不传的话：轮询先到会先写成 `NULL`，等 callback 再到才会补上。

### 不要这样做

| 做法 | 结果 |
|---|---|
| 只在 `validate-session` 传，轮询不传 | 组绑到 `NULL`，用该用户 id 列表看不到 |
| 创建、轮询、列表用了三个不同的 id | 彼此看不见 |
| GET 把 id 放 body | GET / DELETE 只认 query |
| 不传 id 去列表 | 隔离失效，可能看到该 API key 下其他人的组 |
| 直接调 `/v1/asset-groups/face-callback` | 这是火山跳转用的公开地址，不是给你调的 |

## 相关 API

- [创建 AssetGroup](/docs/api/createAssetGroup) · `POST /v1/asset-groups`
- [列出 AssetGroup](/docs/api/listAssetGroups) · `GET /v1/asset-groups`
- [创建 Asset](/docs/api/createAsset) · `POST /v1/assets`
- [列出 Asset](/docs/api/listAssets) · `GET /v1/assets`
- [删除 Asset](/docs/api/deleteAsset) · `DELETE /v1/assets/{id}`
- [发起真人会话](/docs/api/createVisualValidateSession) · `POST /v1/asset-groups/validate-session`
- [轮询真人结果](/docs/api/getVisualValidateResult) · `POST /v1/asset-groups/validate-result`
- [创建视频生成任务](/docs/api/createVideoGeneration) · `POST /v1/videos/generations`
