> ## 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.

# 快速入门：文本生成动作

> 通过文本提示词生成可下载的 3D 动画。

用文字描述动作，以 JSON 提交请求，再轮询返回的动作资源，直到生成的 GLB 就绪。本流程无需源视频，按请求收取固定费用；[3D 动作](/zh/v1/guides/quickstart-mocap)的 `glb` 提取流程则按源视频时长计费。

设置 `VIGGLE_API_KEY` 后即可运行下方示例，无需其他输入素材。

## 准备工作

* 在 [Viggle 控制台](https://portal.viggle.ai/keys)创建 API 密钥，并导出为 `VIGGLE_API_KEY`。
* 长度为 1–400 个字符的动作提示词。

## 创建、轮询并导出动作

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

  response=$(curl -s -X POST "https://apis.viggle.ai/v1/motions" \
    -H "Authorization: Bearer $VIGGLE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"text":"a person doing a cartwheel","duration_seconds":5}')
  motion_id=$(echo "$response" | jq -r '.id')

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

    case "$status" in
      ready)
        exported=$(curl -s "https://apis.viggle.ai/v1/motions/$motion_id/export?download_type=mixamo" \
          -H "Authorization: Bearer $VIGGLE_API_KEY")
        echo "$exported" | jq -r '.glb_url'
        exit 0
        ;;
      failed)
        echo "Motion failed: $(echo "$motion" | jq -c '.error')" >&2
        exit 1
        ;;
    esac
    sleep 5
  done

  echo "Timed out waiting for motion $motion_id" >&2
  exit 1
  ```

  ```javascript Node theme={null}
  const baseUrl = "https://apis.viggle.ai/v1";
  const headers = {
    Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
    "Content-Type": "application/json",
  };

  async function createMotion() {
    const response = await fetch(`${baseUrl}/motions`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        text: "a person doing a cartwheel",
        duration_seconds: 5,
      }),
    });
    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 waitForMotion(id) {
    for (let attempt = 0; attempt < 60; attempt++) {
      const response = await fetch(`${baseUrl}/motions/${id}`, {
        headers: { Authorization: headers.Authorization },
      });
      if (!response.ok) {
        const body = await response.json().catch(() => ({}));
        throw new Error(`HTTP ${response.status}: ${JSON.stringify(body.error ?? body)}`);
      }
      const motion = await response.json();
      if (["ready", "failed"].includes(motion.status)) return motion;
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`Timed out waiting for motion ${id}`);
  }

  const created = await createMotion();
  const motion = await waitForMotion(created.id);

  if (motion.status !== "ready") {
    throw new Error(`Motion failed: ${JSON.stringify(motion.error)}`);
  }

  const exportResponse = await fetch(
    `${baseUrl}/motions/${motion.id}/export?download_type=mixamo`,
    { headers: { Authorization: headers.Authorization } },
  );
  if (!exportResponse.ok) {
    const body = await exportResponse.json().catch(() => ({}));
    throw new Error(`HTTP ${exportResponse.status}: ${JSON.stringify(body.error ?? body)}`);
  }
  const exported = await exportResponse.json();
  console.log(exported.glb_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']}"}

  response = requests.post(
      f"{base_url}/motions",
      headers=headers,
      json={
          "text": "a person doing a cartwheel",
          "duration_seconds": 5,
      },
  )
  response.raise_for_status()
  motion = response.json()

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

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

  response = requests.get(
      f"{base_url}/motions/{motion['id']}/export",
      params={"download_type": "mixamo"},
      headers=headers,
  )
  response.raise_for_status()
  print(response.json()["glb_url"])
  ```
</CodeGroup>

创建请求会立即返回动作资源，`status` 通常为 `queued`。每 5 秒轮询 `GET /v1/motions/{motion_id}`，直到状态变为 `ready` 或 `failed`；动作资源没有 `cancelled` 状态。

就绪后，调用 `GET /v1/motions/{motion_id}/export?download_type=mixamo` 获取 Mixamo 骨架，也可改用 `download_type=metahuman`。两种版本会一起生成，不提供 `fbx` 输出。`glb_url` 是签名链接，有效期为 1 小时，每次调用都会重新签名。导出的 GLB 可用于 **Mixamo**、**MetaHuman** 以及其他兼容 **Unity** 的动画流程。

`mixamo` 是通用的 50 关节骨架，需要重定向到自己的角色；`metahuman` 与 Viggle vsplat 角色的骨架一一对应，无需重定向。完整对比见[导出 3D 动作](/zh/v1/api-reference/motions/export)，其中也说明了 `metahuman` 的 441 关节（含面部）与 86 关节（仅身体）版本。

费用见[计费与保留期限](/zh/v1/pricing#text-to-motion)：文本生成动作每次请求固定收取 \$0.10（10 积分），与 `duration_seconds` 无关。

<Card title="从文本创建动作" icon="message" href="/zh/v1/api-reference/motions/create-from-text">
  查看全部 JSON 请求字段和动作响应字段。
</Card>
