> ## Documentation Index
> Fetch the complete documentation index at: https://docs.viggle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 快速入门：视频重混

> 将角色图片和动作视频合成为成品视频。

\*\*视频重混（Video Remix）\*\*只需一次请求，就能将角色图片和动作视频合成为成品视频，适合快速体验 Viggle。

设置环境变量 `VIGGLE_API_KEY`，并准备下方示例所需的角色图片和动作视频。

## 准备工作

* 在 [Viggle 控制台](https://portal.viggle.ai/keys)创建 API 密钥，并导出为 `VIGGLE_API_KEY`。

<Note>
  示例中的 `https://assets.viggle.ai/samples/...` 是占位地址。请替换为自己的 `image`/`image_url` 和 `motion_video`/`motion_video_url`，或使用 Viggle 正式发布的示例素材。
</Note>

## 完整示例

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  set -euo pipefail

  response=$(curl -s -X POST "https://apis.viggle.ai/v1/renders" \
    -H "Authorization: Bearer $VIGGLE_API_KEY" \
    -F "image_url=https://assets.viggle.ai/samples/character.png" \
    -F "motion_video_url=https://assets.viggle.ai/samples/motion.mp4")
  render_id=$(echo "$response" | jq -r '.id')

  for _ in $(seq 1 120); do
    render=$(curl -s "https://apis.viggle.ai/v1/videos/$render_id" \
      -H "Authorization: Bearer $VIGGLE_API_KEY")
    status=$(echo "$render" | jq -r '.status')

    case "$status" in
      ready)
        echo "$render" | jq -r '.video_url'
        exit 0
        ;;
      failed|cancelled)
        echo "Render $status: $(echo "$render" | jq -c '.error')" >&2
        exit 1
        ;;
    esac
    sleep 5
  done

  echo "Timed out waiting for render $render_id" >&2
  exit 1
  ```

  ```javascript Node theme={null}
  const baseUrl = "https://apis.viggle.ai/v1";
  const headers = { Authorization: `Bearer ${process.env.VIGGLE_API_KEY}` };
  const TERMINAL = new Set(["ready", "failed", "cancelled"]);

  async function createRender() {
    const form = new FormData();
    form.append("image_url", "https://assets.viggle.ai/samples/character.png");
    form.append("motion_video_url", "https://assets.viggle.ai/samples/motion.mp4");

    const response = await fetch(`${baseUrl}/renders`, {
      method: "POST",
      headers,
      body: form,
    });
    if (!response.ok) {
      const body = await response.json().catch(() => ({}));
      throw new Error(`HTTP ${response.status}: ${JSON.stringify(body.error ?? body)}`);
    }
    return response.json();
  }

  async function waitForRender(id) {
    for (let attempt = 0; attempt < 120; attempt++) {
      const response = await fetch(`${baseUrl}/videos/${id}`, { headers });
      if (!response.ok) {
        const body = await response.json().catch(() => ({}));
        throw new Error(`HTTP ${response.status}: ${JSON.stringify(body.error ?? body)}`);
      }
      const render = await response.json();
      if (TERMINAL.has(render.status)) return render;
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`Timed out waiting for render ${id}`);
  }

  const created = await createRender();
  const render = await waitForRender(created.id);

  if (render.status !== "ready") {
    throw new Error(`Render ${render.status}: ${JSON.stringify(render.error)}`);
  }
  console.log(render.video_url);
  ```

  ```python Python theme={null}
  import os
  import time
  import requests

  base_url = "https://apis.viggle.ai/v1"
  headers = {"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"}
  TERMINAL_STATES = {"ready", "failed", "cancelled"}

  response = requests.post(
      f"{base_url}/renders",
      headers=headers,
      files={
          "image_url": (None, "https://assets.viggle.ai/samples/character.png"),
          "motion_video_url": (None, "https://assets.viggle.ai/samples/motion.mp4"),
      },
  )
  response.raise_for_status()
  render = response.json()

  for _ in range(120):
      if render["status"] in TERMINAL_STATES:
          break
      time.sleep(5)
      response = requests.get(f"{base_url}/videos/{render['id']}", headers=headers)
      response.raise_for_status()
      render = response.json()
  else:
      raise TimeoutError(f"Timed out waiting for render {render['id']}")

  if render["status"] != "ready":
      error = render.get("error") or {}
      raise RuntimeError(f"{render['status']}: {error.get('code')} {error.get('message')}")

  print(render["video_url"])
  ```
</CodeGroup>

## 输入

* 角色：`image`（文件）、`image_url`，或[角色快速入门](/zh/v1/guides/quickstart-character)中创建的可复用 `character_id`。
* 动作：`motion_video`（文件）、`motion_video_url`，或[动作快速入门](/zh/v1/guides/quickstart-motion)中创建的可复用 `motion_id`。
* `background_mode`：`original`（默认）、`solid` 或 `transparent`。
* `bg_color`：`R,G,B` 格式的颜色，仅在 `solid` 模式下使用。

## 响应

```json theme={null}
{
  "id": "render_123abc",
  "status": "queued",
  "progress": 0,
  "created_at": "2026-07-17T10:00:00+00:00",
  "links": {
    "self": "/v1/renders/render_123abc",
    "events": "/v1/renders/render_123abc/events",
    "download": "/v1/renders/render_123abc/download"
  }
}
```

处理过程中，`stage` 可能为 `analyzing`、`rendering` 或 `finishing`。任务完成后，`status` 为 `ready`，响应中包含 `video_url`。该签名链接有效期为 1 小时，每次读取都会重新签名；请及时下载，或再次调用[获取视频](/zh/v1/api-reference/videos/get)以获取新链接。透明模式下还会返回 `alpha_url`。除轮询外，也可通过[监听渲染](/zh/v1/api-reference/renders/events)的服务器发送事件（SSE）订阅同一生命周期。

视频重混的费用见[计费与保留期限](/zh/v1/pricing#video-remix)。

<Card title="异步任务与失败处理" icon="clock" href="/zh/v1/production/async-jobs">
  了解终态、轮询与结果保留期限。
</Card>
