# 素材引用生视频实战

> 端到端教程：创建素材组 → 注册素材 → 等待 Active → asset:// 引用生成 → 轮询下载成片

本页把 [素材库](/asset-management/asset-library) 与 [视频生成](/api-capabilities/seedance2/video-generation) 两组接口串成一条完整链路，实现**角色一致性**视频生成。

<Info>
本页聚焦代码链路。素材库逐接口说明见 [素材库 API](/asset-management/asset-library)；真实人像的认证前置步骤见 [真人认证接口](/asset-management/real-person)；生成参数全集见 [视频生成 API](/api-capabilities/seedance2/video-generation)。
</Info>

## 链路总览

<Steps>
  <Step title="创建素材组">`Action=CreateAssetGroup`（虚拟形象）或走真人认证获得素材组</Step>
  <Step title="注册素材">`Action=CreateAsset`，提交公网媒体 URL（图片 / 视频 / 音频，用 `AssetType` 指定，默认图片）</Step>
  <Step title="等待素材 Active">`Action=GetAsset` 每 3 秒轮询，直到 `Status: "Active"`</Step>
  <Step title="发起生成">`POST /api/v3/contents/generations/tasks`，`image_url` 填 `asset://素材ID`</Step>
  <Step title="轮询任务">每 15–30 秒查询，直到 `succeeded`</Step>
  <Step title="下载成片">从 `content.video_url` 下载（2 小时内有效，不带 Authorization 头）</Step>
</Steps>

<Warning>
提示词中用「图片1」「图片2」按 `content` 顺序指代素材，不要出现 `asset-` 开头的素材 ID 字面量。
</Warning>

## 鉴权方式

本页的素材管理 Action 支持两种认证方式：

- **API Key**：`Authorization: Bearer sk-...`，下方完整示例默认使用这种方式。
- **火山官方 AK/SK**：使用 API Key 所绑定的 AK/SK，按火山官方 `HMAC-SHA256` 方式签名；请求根路径为 `/`，Service 为 `ark`，Region 推荐 `cn-beijing`。

已有火山客户端只需将 Endpoint 和 AK/SK 替换为 LoopRouter 配置，Action、Version、JSON 请求体和响应解析不变。下面示例使用火山官方 Go SDK 对素材注册请求签名：

```go
package main

import (
  "bytes"
  "io"
  "net/http"
  "os"

  "github.com/volcengine/volc-sdk-golang/base"
)

func main() {
  body := []byte(`{"GroupId":"group-xxx","Name":"角色A-全身","URL":"https://example.com/character-a.png"}`)
  req, err := http.NewRequest(
    http.MethodPost,
    "https://api.looprouter.com/?Action=CreateAsset&Version=2024-01-01",
    bytes.NewReader(body),
  )
  if err != nil {
    panic(err)
  }
  req.Header.Set("Content-Type", "application/json")

  credentials := base.Credentials{
    AccessKeyID:     os.Getenv("LOOPROUTER_AK"),
    SecretAccessKey: os.Getenv("LOOPROUTER_SK"),
    Region:          "cn-beijing",
    Service:         "ark",
  }
  req = credentials.Sign(req)

  resp, err := http.DefaultClient.Do(req)
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()
  io.Copy(os.Stdout, resp.Body)
}
```

<Note>
AK/SK 仅替代素材管理与真人认证 Action 的 Bearer 认证。`POST /api/v3/contents/generations/tasks` 等视频生成接口仍使用 API Key；本地文件 `multipart/form-data` 上传也继续使用 API Key。
</Note>

## 完整代码

<CodeGroup>

```python Python
import time
import requests

BASE = "https://api.looprouter.com"
HEADERS = {"Authorization": "Bearer sk-你的APIKey", "Accept-Encoding": "identity"}


def asset_action(name, body):
    resp = requests.post(
        f"{BASE}/api/v3/assets?Action={name}&Version=2024-01-01",
        headers=HEADERS, json=body,
    )
    resp.raise_for_status()
    return resp.json()["Result"]


def create_asset(group_id, name, url):
    result = asset_action("CreateAsset", {"GroupId": group_id, "Name": name, "URL": url})
    asset_id = result["Id"]
    deadline = time.time() + 90
    while time.time() < deadline:
        result = asset_action("GetAsset", {"Id": asset_id})
        if result["Status"] == "Active":
            return asset_id
        if result["Status"] == "Failed":
            raise RuntimeError(f"素材处理失败: {result.get('Error', {}).get('Message')}")
        time.sleep(3)
    raise TimeoutError("素材处理超时，请稍后用 GetAsset 复查")


def generate_video(content):
    resp = requests.post(
        f"{BASE}/api/v3/contents/generations/tasks",
        headers=HEADERS,
        json={
            "model": "doubao-seedance-2-0-260128",
            "content": content,
            "resolution": "720p",
            "ratio": "adaptive",
            "duration": 5,
        },
    )
    resp.raise_for_status()
    task_id = resp.json()["id"]
    while True:
        task = requests.get(
            f"{BASE}/api/v3/contents/generations/tasks/{task_id}", headers=HEADERS
        ).json()
        if task["status"] in ("succeeded", "failed", "cancelled", "expired"):
            return task
        time.sleep(20)


if __name__ == "__main__":
    group = asset_action("CreateAssetGroup", {"Name": "短剧角色A", "GroupType": "AIGC"})
    asset_id = create_asset(group["Id"], "角色A-全身", "https://example.com/character-a.png")

    task = generate_video([
        {"type": "text", "text": "图片1中的角色撑着伞走在雨中的巷子里，镜头跟随"},
        {"type": "image_url", "image_url": {"url": f"asset://{asset_id}"}, "role": "reference_image"},
    ])
    if task["status"] != "succeeded":
        raise SystemExit(f"生成失败: {task.get('error')}")

    video_url = task["content"]["video_url"]
    with requests.get(video_url, stream=True) as r:  # 不带 Authorization 头
        with open("output.mp4", "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 20):
                f.write(chunk)
    print("已保存 output.mp4")
```

```bash cURL 分步
KEY="sk-你的APIKey"
BASE="https://api.looprouter.com"

# 1. 创建素材组
curl -X POST "$BASE/api/v3/assets?Action=CreateAssetGroup&Version=2024-01-01" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"Name": "短剧角色A", "GroupType": "AIGC"}'
# => Result.Id = group-20260709185838-dcnvd

# 2. 注册素材
curl -X POST "$BASE/api/v3/assets?Action=CreateAsset&Version=2024-01-01" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"GroupId": "group-20260709185838-dcnvd", "Name": "角色A-全身", "URL": "https://example.com/character-a.png"}'
# => Result.Id = asset-20260709190143-ntwhb

# 3. 轮询素材状态到 Active
curl -X POST "$BASE/api/v3/assets?Action=GetAsset&Version=2024-01-01" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"Id": "asset-20260709190143-ntwhb"}'

# 4. 引用素材生成视频
curl -X POST "$BASE/api/v3/contents/generations/tasks" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "model": "doubao-seedance-2-0-260128",
    "content": [
      {"type": "text", "text": "图片1中的角色撑着伞走在雨中的巷子里，镜头跟随"},
      {"type": "image_url", "image_url": {"url": "asset://asset-20260709190143-ntwhb"}, "role": "reference_image"}
    ],
    "resolution": "720p",
    "duration": 5
  }'
# => {"id": "task_xxxx"}

# 5. 轮询任务
curl "$BASE/api/v3/contents/generations/tasks/task_xxxx" -H "Authorization: Bearer $KEY"

# 6. 下载成片（不带 Authorization 头）
curl -o output.mp4 "<content.video_url>"
```

```javascript Node.js
const BASE = "https://api.looprouter.com";
const KEY = process.env.LOOPROUTER_API_KEY;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function assetAction(name, body) {
  const res = await fetch(`${BASE}/api/v3/assets?Action=${name}&Version=2024-01-01`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${name} failed: ${await res.text()}`);
  return (await res.json()).Result;
}

async function main() {
  const group = await assetAction("CreateAssetGroup", { Name: "短剧角色A", GroupType: "AIGC" });
  const { Id: assetId } = await assetAction("CreateAsset", {
    GroupId: group.Id,
    Name: "角色A-全身",
    URL: "https://example.com/character-a.png",
  });

  for (;;) {
    const asset = await assetAction("GetAsset", { Id: assetId });
    if (asset.Status === "Active") break;
    if (asset.Status === "Failed") throw new Error(`素材处理失败: ${asset.Error?.Message}`);
    await sleep(3000);
  }

  const createRes = await fetch(`${BASE}/api/v3/contents/generations/tasks`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
    body: JSON.stringify({
      model: "doubao-seedance-2-0-260128",
      content: [
        { type: "text", text: "图片1中的角色撑着伞走在雨中的巷子里，镜头跟随" },
        { type: "image_url", image_url: { url: `asset://${assetId}` }, role: "reference_image" },
      ],
      resolution: "720p",
      duration: 5,
    }),
  });
  const { id } = await createRes.json();

  for (;;) {
    await sleep(20000);
    const task = await (
      await fetch(`${BASE}/api/v3/contents/generations/tasks/${id}`, {
        headers: { Authorization: `Bearer ${KEY}` },
      })
    ).json();
    if (task.status === "succeeded") return console.log("视频地址:", task.content.video_url);
    if (["failed", "cancelled", "expired"].includes(task.status))
      throw new Error(JSON.stringify(task.error));
  }
}

main();
```

</CodeGroup>

## 多素材进阶用法

- 一次请求最多引用 **9 张参考图**，`asset://`、公网 URL、Base64 可以混用；但真实人脸只能走 `asset://`（且需真人认证）。
- 可再叠加 **0–3 段参考视频** 与 **0–3 段参考音频**（音频不能单独出现，须至少配一张图或一段视频）；携带参考视频的请求自动享受视频输入计费折扣。
- 同一角色建议同时引用「全身照 + 面部特写」两张素材，提示词写「图片1为角色全身，图片2为角色面部」。
- 同一请求中的多个 `asset://` 素材必须来自同一账号。

## 常见报错速查

| 现象 | 原因 | 处理 |
| --- | --- | --- |
| 400 `asset not found or not active` | 素材还在 `Processing` 或 ID 写错 | 先 `GetAsset` 确认 `Active` |
| 400 素材引用相关 | 同一请求混用了不同账号的素材 | 同一请求只引用同一账号的素材，必要时重建素材组 |
| 403 内容审核 | 真实人脸未走认证直传 | 完成 [真人认证](/asset-management/real-person) 后用 `asset://` 引用 |
| 素材 `Failed` | 素材格式/尺寸/内容不合规，或与 `AssetType` 不匹配 | 按 `Error.Message` 修正后重新 `CreateAsset` |
| 视频 404 下载失败 | `video_url` 已过期（约 2 小时） | 重新查询任务获取新链接，尽快转存 |
| Python JSON 解析异常 | 网关 gzip 协商问题 | 请求头加 `Accept-Encoding: identity` |

## 相关页面

<CardGroup cols={3}>
  <Card title="能力总览" icon="circle-info" href="/api-capabilities/seedance2/overview">
    模型规格与定价
  </Card>
  <Card title="视频生成 API" icon="clapperboard" href="/api-capabilities/seedance2/video-generation">
    全部生成参数
  </Card>
  <Card title="素材库 API" icon="images" href="/asset-management/asset-library">
    素材管理接口
  </Card>
</CardGroup>
