> ## 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 高斯泼溅场景，生成新视图，再重建场景。

场景模型可以用文字描述或 360° 全景图构建可探索的 3D 场景，即 `.spz` 格式的高斯泼溅。本指南走完整个流程：

1. **创建**：用 `POST /v1/scenes/pano` 根据描述文字创建场景。你会得到 `.spz`、生成的全景图，以及一个由带位姿图像组成的 `initial` 批次。
2. **生成**：用 `POST /v1/scenes/generate` 从你选定的相机生成新图像，它们成为一个 `generated` 批次。
3. **重建**：用 `POST /v1/scenes/reconstruct` 基于这两个批次重建场景，得到更新后的 `.spz`。

每一步都是异步任务：立即返回以 `scn_` 开头的任务 ID，之后轮询 `GET /v1/scenes/{task_id}`，直到状态为 `ready`。

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

## 准备工作

* 在 [Viggle 控制台](https://portal.viggle.ai/keys)创建 API 密钥，并导出为 `VIGGLE_API_KEY`。
* 一段文字描述，最多 1000 个字符；或一张 360° 全景图，可以是公开 URL，也可以是图片文件。
* 积分：从文字描述创建场景 **6 积分（\$0.06）**，其他场景任务 **1 积分（\$0.01）**。下方示例依次运行文字创建、生成视图、重建各一次，共 **8 积分（\$0.08）**。任务未以 `ready` 结束会全额退回。

## 核心概念

* **会话**：每个请求都带一个由你自定的 `sessionId`（1–128 个字母、数字、`_` 或 `-`）。一个会话包含一个场景及其图像批次，且**同一时间只运行一个任务**——同一会话中，请等上一个任务结束后再提交下一个。
* **批次**：会话中一组带位姿的图像。`pano` 生成 `initial` 批次，每次 `generate` 生成一个 `generated` 批次。`generate` 和 `reconstruct` 在 `referenceBatchIds` 中接收的就是它们的 `batchId`。
* **相机**：位姿是场景坐标系中的 4 × 4 相机到世界矩阵（c2w）。批次中的每张图像都附带其 `c2w`，因此选择新相机最简单的方法是移动一个现有相机。`generate` 也可以用 `targetPosition`（位置加上以度为单位的偏航角和俯仰角）代替矩阵来指定相机。

## 构建、扩展并重建

脚本创建一个场景，保存 `scene.spz` 和 `panorama.png`，再从第一个视图向侧面平移 0.3 个单位的相机生成新视图，然后重建并保存 `scene-v2.spz`。

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

  base="https://apis.viggle.ai/v1/scenes"
  auth="Authorization: Bearer $VIGGLE_API_KEY"
  session="quickstart-$(date +%s)"

  submit() { # 操作名、JSON 请求体 -> 任务 ID
    curl -sf -X POST "$base/$1" -H "$auth" -H "Content-Type: application/json" -d "$2" | jq -r '.id'
  }

  wait_ready() { # 任务 ID -> 就绪任务的 JSON
    for _ in $(seq 1 360); do
      task=$(curl -sf "$base/$1" -H "$auth")
      case "$(echo "$task" | jq -r '.status')" in
        ready) echo "$task"; return 0 ;;
        failed|cancelled) echo "Task $1 ended: $(echo "$task" | jq -c '.error')" >&2; return 1 ;;
      esac
      sleep 5
    done
    echo "Timed out waiting for $1" >&2
    return 1
  }

  # 1. 根据描述文字创建场景。
  task_id=$(submit pano "$(jq -n --arg s "$session" '{
    sessionId: $s,
    prompt: "A sunlit Scandinavian living room with a grey sofa and large windows",
    quality: "low"
  }')")
  pano=$(wait_ready "$task_id")
  curl -s -o scene.spz "$(echo "$pano" | jq -r '.result.spz.url')"
  curl -s -o panorama.png "$(echo "$pano" | jq -r '.result.panorama.url')"
  initial=$(echo "$pano" | jq -r '.result.outputBatch.batchId')
  echo "saved scene.spz and panorama.png; initial batch $initial"

  # 2. 将第一个相机沿其 x 轴平移 0.3 个单位，从该位置生成视图。
  target=$(echo "$pano" | jq -c '.result.outputBatch.views[0].c2w | .[0][3] += 0.3')
  task_id=$(submit generate "$(jq -n --arg s "$session" --arg b "$initial" --argjson c "$target" '{
    sessionId: $s, referenceBatchIds: [$b], targetC2w: $c
  }')")
  generated=$(wait_ready "$task_id" | jq -r '.result.outputBatch.batchId')
  echo "generated batch $generated"

  # 3. 基于两个批次重建。
  task_id=$(submit reconstruct "$(jq -n --arg s "$session" --arg a "$initial" --arg b "$generated" '{
    sessionId: $s, referenceBatchIds: [$a, $b]
  }')")
  curl -s -o scene-v2.spz "$(wait_ready "$task_id" | jq -r '.result.spz.url')"
  echo "saved scene-v2.spz"
  ```

  ```javascript Node theme={null}
  import { writeFile } from "node:fs/promises";

  const baseUrl = "https://apis.viggle.ai/v1/scenes";
  const headers = {
    Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
    "Content-Type": "application/json",
  };
  const sessionId = `quickstart-${Date.now()}`;

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

  async function waitReady(task) {
    for (let i = 0; i < 360 && (task.status === "queued" || task.status === "processing"); i++) {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      task = await api(`/${task.id}`);
    }
    if (task.status !== "ready") {
      throw new Error(`Task ${task.id} ended ${task.status}: ${JSON.stringify(task.error)}`);
    }
    return task.result;
  }

  async function download(url, path) {
    const response = await fetch(url);
    await writeFile(path, Buffer.from(await response.arrayBuffer()));
    console.log(`saved ${path}`);
  }

  // 1. 根据描述文字创建场景。
  const pano = await waitReady(await api("/pano", {
    sessionId,
    prompt: "A sunlit Scandinavian living room with a grey sofa and large windows",
    quality: "low",
  }));
  await download(pano.spz.url, "scene.spz");
  await download(pano.panorama.url, "panorama.png");
  const initial = pano.outputBatch.batchId;

  // 2. 将第一个相机沿其 x 轴平移 0.3 个单位，从该位置生成视图。
  const targetC2w = structuredClone(pano.outputBatch.views[0].c2w);
  targetC2w[0][3] += 0.3;
  const generated = await waitReady(await api("/generate", {
    sessionId,
    referenceBatchIds: [initial],
    targetC2w,
  }));
  console.log(`generated ${generated.newViewCount} views in batch ${generated.outputBatch.batchId}`);

  // 3. 基于两个批次重建。
  const rebuilt = await waitReady(await api("/reconstruct", {
    sessionId,
    referenceBatchIds: [initial, generated.outputBatch.batchId],
  }));
  await download(rebuilt.spz.url, "scene-v2.spz");
  ```

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

  base_url = "https://apis.viggle.ai/v1/scenes"
  headers = {"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"}
  session_id = f"quickstart-{int(time.time())}"


  def submit(operation, body):
      response = requests.post(f"{base_url}/{operation}", headers=headers, json=body)
      response.raise_for_status()
      return response.json()


  def wait_ready(task):
      for _ in range(360):
          if task["status"] not in ("queued", "processing"):
              break
          time.sleep(5)
          response = requests.get(f"{base_url}/{task['id']}", headers=headers)
          response.raise_for_status()
          task = response.json()
      if task["status"] != "ready":
          raise RuntimeError(f"Task {task['id']} ended {task['status']}: {task.get('error')}")
      return task["result"]


  def download(url, path):
      response = requests.get(url)
      response.raise_for_status()
      with open(path, "wb") as f:
          f.write(response.content)
      print(f"saved {path}")


  # 1. 根据描述文字创建场景。
  pano = wait_ready(submit("pano", {
      "sessionId": session_id,
      "prompt": "A sunlit Scandinavian living room with a grey sofa and large windows",
      "quality": "low",
  }))
  download(pano["spz"]["url"], "scene.spz")
  download(pano["panorama"]["url"], "panorama.png")
  initial = pano["outputBatch"]["batchId"]

  # 2. 将第一个相机沿其 x 轴平移 0.3 个单位，从该位置生成视图。
  target_c2w = copy.deepcopy(pano["outputBatch"]["views"][0]["c2w"])
  target_c2w[0][3] += 0.3
  generated = wait_ready(submit("generate", {
      "sessionId": session_id,
      "referenceBatchIds": [initial],
      "targetC2w": target_c2w,
  }))
  print(f"generated {generated['newViewCount']} views in batch {generated['outputBatch']['batchId']}")

  # 3. 基于两个批次重建。
  rebuilt = wait_ready(submit("reconstruct", {
      "sessionId": session_id,
      "referenceBatchIds": [initial, generated["outputBatch"]["batchId"]],
  }))
  download(rebuilt["spz"]["url"], "scene-v2.spz")
  ```
</CodeGroup>

用任何支持 SPZ 高斯泼溅的查看器打开 `scene.spz` 和 `scene-v2.spz`。

## 响应

每次提交都会立即返回 `queued` 状态的任务（有空闲 GPU 时为 `processing`）。`credits` 为预扣的积分，`queuePosition` 为前面排队的任务数：

```json theme={null}
{
  "id": "scn_0199f0c2-6a1e-7c3d-9b2a-4f5e6d7c8b9a.quickstart-1791856740",
  "sessionId": "quickstart-1791856740",
  "operation": "pano",
  "status": "queued",
  "credits": 6,
  "queuePosition": 0,
  "createdAt": 1791856740000,
  "completedAt": null,
  "result": null,
  "error": null
}
```

轮询 `GET /v1/scenes/{task_id}`，直到 `status` 为 `ready`、`failed` 或 `cancelled`。就绪后 `result` 包含哪些内容取决于操作：

| 操作 | `spz` | `panorama` | `outputBatch` |
| - | :-: | :-: | - |
| `pano` | ✓ | ✓ | `initial` 批次 |
| `generate` | — | — | `generated` 批次 |
| `reconstruct` | ✓ | — | — |

每个 `outputBatch.views[]` 条目包含图像 `url`、相机 `c2w` 和 `intrinsic`。签名链接有效期为 **3 小时**，过期后再查询一次任务即可拿到新链接；任务结束后约 **72 小时**内可以查询。全部字段见[查询场景任务](/zh/v1/api-reference/scenes/get)。

## 使用自己的全景图

在第 1 步中把 `prompt` 换成 `panoramaUri`：

```json theme={null}
{ "sessionId": "courtyard-01", "panoramaUri": "https://example.com/courtyard-360.jpg" }
```

也可以直接上传图片本身（JPEG、PNG 或 WebP，最大 50 MiB）：把第 1 步改为 `multipart/form-data` 请求，并带上名为 `panorama` 的文件：

```bash theme={null}
curl -X POST https://apis.viggle.ai/v1/scenes/pano \
  -H "Authorization: Bearer $VIGGLE_API_KEY" \
  -F sessionId=courtyard-01 \
  -F panorama=@courtyard-360.jpg
```

表单规则见[创建场景](/zh/v1/api-reference/scenes/pano)中的“上传全景图文件”一节。

如需把全景图按指定位姿和比例放入你自己的世界，让所有相机都以你的坐标返回，请使用 [`POST /v1/scenes/custom-pano`](/zh/v1/api-reference/scenes/custom-pano)，并传入 `panoramaC2w` 和 `metersPerUnit`。

## 用位置和角度放置相机

在第 2 步中，除了修改 `c2w` 矩阵，也可以给 `generate` 传 `targetPosition`：一个位置，加上以度为单位的偏航角和俯仰角。偏航角为正时向左转，俯仰角为正时向上看；两者都为 `0` 时相机朝 −Z 方向看。

```json theme={null}
{
  "sessionId": "courtyard-01",
  "referenceBatchIds": ["3f0a6c1e-8d2b-4b7a-9e5c-1a2b3c4d5e6f"],
  "targetPosition": { "position": [0.3, 0, 0], "yaw": 30, "pitch": 0 }
}
```

`targetC2w` 和 `targetPosition` 只能发送其一。详见[生成场景视图](/zh/v1/api-reference/scenes/generate)中的“目标位置”一节。

## 将结果保存到自己的存储

在任意请求中加上 `spzOutputUrl`，或带 `spz`、`preview`、`panorama` 的 `outputUris`，Viggle 就会用 `PUT` 把这些文件上传到你的地址。S3、R2 或 GCS 的预签名上传链接均可。上传到你地址的 `.spz` 不会由 Viggle 保留，结果中的 `spz.url` 即为你的地址。详见[创建场景](/zh/v1/api-reference/scenes/pano)中的“上传到自己的存储”一节。

## 任务失败时

* **提交时返回 `402`**：余额不足以支付该任务（文字创建场景 6 积分，其他 1 积分）。充值后重新提交。
* **提交时返回 `409`**：该会话已有任务在运行。`error.details.running_task_id` 给出该任务，等它结束后再提交。
* **任务 `failed` 或 `cancelled`**：积分全额退回，重新提交会再次预扣。
* **任务以 `INVALID_REQUEST` 失败**：模型拒绝了输入，例如使用了其他会话的批次。修正后再重试。
* **任务以 `PROCESSING_FAILED` 失败，或状态为 `cancelled`**：重新提交同样的请求。
* **轮询时返回 `404`**：任务 ID 错误、任务属于其他账号，或已超过 72 小时的保留期限。

<CardGroup cols={2}>
  <Card title="创建场景" icon="cube" href="/zh/v1/api-reference/scenes/pano">
    描述文字、全景图、参考图和质量。
  </Card>

  <Card title="生成场景视图" icon="camera" href="/zh/v1/api-reference/scenes/generate">
    相机、投影方式、视场角和 seed。
  </Card>

  <Card title="重建场景" icon="rotate" href="/zh/v1/api-reference/scenes/reconstruct">
    用你的批次重建 `.spz`。
  </Card>

  <Card title="查询场景任务" icon="magnifying-glass" href="/zh/v1/api-reference/scenes/get">
    任务对象、结果字段和错误码。
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.