# クイックスタート — Argus

複数のパノラマまたはピンホール画像を ZIP でアップロードし、Argus タスクを送信して再構成結果を取得します。

Canonical: https://developer.realsee.ai/ja/docs/argus/quickstart
Source: https://developer.realsee.ai/ja/docs/argus/quickstart.md
Language: ja

# Argus クイックスタート

複数の **2:1 正距円筒パノラマ画像**または **1:1 ピンホール画像**から空間を再構成します。バッチごとに同じ形式の画像を ZIP にまとめます。このガイドでは海外向けゲートウェイとバックエンドの Node.js を使用します。

```text
Authenticate → GET file/token → upload input.zip → POST task/submit → GET task/info → download output.zip
```

<span id="before-you-start" class="docs-anchor-alias" aria-hidden="true"></span>

## 開始前の準備

[アプリ一覧](https://developer.realsee.ai/ja/dashboard/apps) で所有元の Team を選び、有効な Developer app を使用するか、Team オーナーにトライアル用アプリの作成を依頼します。上限は Team ごとに 10 個で、期限切れ・無効なアプリも含みます。新規アプリは標準 14 日間のトライアルです。アプリと AK の紐付けが確定すると申請は完了します。再構成タスクの状態は別に管理します。

アプリは共通の `/open` 権限範囲を使い、Argus 専用の機能申請は不要です。AK/SK、トークン、アップロード認証情報、処理の制御はバックエンドに保持します。ローカルサーバー環境に `REALSEE_APP_KEY` と `REALSEE_APP_SECRET` を設定し、SK をプロンプトやブラウザーのコードに含めないでください。

Node.js 24 を使い、海外向けストレージ用 AWS アダプターの実行時依存関係とアップローダーをインストールします：

```bash
npm install @realsee/universal-uploader@0.1.1 @aws-sdk/client-s3@3.1084.0
```

<span id="prepare-the-input-zip" class="docs-anchor-alias" aria-hidden="true"></span>

## 入力 ZIP を準備する

JPEG・PNG・WebP 画像を 1〜99 枚、アーカイブの直下に置きます。RGB・各チャンネル 8 ビットで、全画像を 2:1 パノラマまたは 1:1 ピンホール画像に統一します。両形式を混在させないでください。

```text
input.zip
├── entrance.jpg
├── living-room.jpg
└── kitchen.jpg
```

ディレクトリ、パストラバーサル、制御文字、同じ拡張子除去後の名前、Unicode 正規化や大文字小文字の同一視で衝突する名前を避け、異なるファイル名を使います。出力の `name_mapping` で各入力に対応する出力を特定します。

Argus の ZIP に `manifest.json` や `images/` サブディレクトリはありません。[Pano-to-VR](https://developer.realsee.ai/ja/docs/pano-to-vr) は別のパッケージ形式を使う API です。

<span id="implement-the-server-workflow" class="docs-anchor-alias" aria-hidden="true"></span>

## サーバー側の処理を実装する

以下をバックエンドプロジェクトの `argus.mjs` に保存します。3 つのルートは [API の説明](https://developer.realsee.ai/ja/docs/argus/reference) に定義されています。

```js
import { Uploader } from '@realsee/universal-uploader'
import { AwsAdaptor } from '@realsee/universal-uploader/adaptors/aws'

const gateway = 'https://app-gateway.realsee.ai'

async function readData(response) {
  if (!response.ok) throw new Error(`Argus HTTP error: ${response.status}`)
  const payload = await response.json()
  if (payload.code !== 0 || !payload.data || typeof payload.data !== 'object' || Array.isArray(payload.data)) {
    throw new Error('Argus request was not successful')
  }
  return payload.data
}

export async function getArgusAccessToken() {
  const appKey = process.env.REALSEE_APP_KEY
  const appSecret = process.env.REALSEE_APP_SECRET
  if (!appKey || !appSecret) throw new Error('Configure server-side app credentials')
  const response = await fetch(`${gateway}/auth/access_token`, {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ app_key: appKey, app_secret: appSecret }),
    signal: AbortSignal.timeout(30_000),
  })
  const data = await readData(response)
  if (typeof data.access_token !== 'string' || !data.access_token || !Number.isInteger(data.expire_at) || data.expire_at * 1000 <= Date.now()) {
    throw new Error('Argus returned an invalid access token')
  }
  return data.access_token
}

async function requestArgus(path, accessToken, body) {
  return readData(await fetch(`${gateway}${path}`, {
    method: body === undefined ? 'GET' : 'POST',
    headers: {
      authorization: accessToken,
      ...(body === undefined ? {} : { 'content-type': 'application/json' }),
    },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    signal: AbortSignal.timeout(30_000),
  }))
}

export async function createArgusUpload(accessToken) {
  const lease = await requestArgus('/open/v1/argus/file/token', accessToken)
  for (const field of ['tmpSecretId', 'tmpSecretKey', 'sessionToken', 'bucket', 'region', 'prefix']) {
    if (typeof lease[field] !== 'string' || !lease[field]) {
      throw new Error('Argus returned an incomplete upload lease')
    }
  }
  return lease
}

export async function uploadArgusZip(accessToken, filePath) {
  const uploader = new Uploader(AwsAdaptor, {
    getToken: () => createArgusUpload(accessToken),
  })
  const receipt = await uploader.uploadFile('input.zip', filePath)
  if (!receipt.objectPath) throw new Error('Upload did not return a full object key')
  return receipt.objectPath
}

export async function submitArgusTask(accessToken, privateCosKey, title) {
  if (!privateCosKey || !title?.trim()) throw new Error('Object key and title are required')
  // Submit once. A lost response can mean the server already created the task.
  const data = await requestArgus('/open/v1/argus/task/submit', accessToken, {
    private_cos_keys: [privateCosKey],
    title,
  })
  if (typeof data.task_code !== 'string' || !data.task_code) {
    throw new Error('Submission returned no task code; do not resubmit automatically')
  }
  return data.task_code
}

export async function pollArgusTask(accessToken, taskCode) {
  if (!taskCode) throw new Error('Task code is required')
  const query = new URLSearchParams({ task_code: taskCode })
  // Ten minutes is this example's waiting policy, not a service completion SLA.
  for (let attempt = 0; attempt < 300; attempt += 1) {
    const data = await requestArgus(`/open/v1/argus/task/info?${query}`, accessToken)
    if (data.status === 2) {
      if (typeof data.output_url !== 'string' || !data.output_url || !Number.isInteger(data.expiration_timestamp) ||
          data.expiration_timestamp * 1000 <= Date.now()) {
        throw new Error('Task succeeded without a usable result URL; query task info again')
      }
      return data
    }
    if (data.status === 3) throw new Error('Argus task failed; inspect its task info on your backend')
    if (data.status !== 0 && data.status !== 1) throw new Error('Unexpected Argus task status')
    await new Promise((resolve) => setTimeout(resolve, 2000))
  }
  throw new Error('Stopped waiting; resume queries with the existing task code')
}
```

アップローダーの `receipt.objectPath` は、許可されたプレフィックスを含む完全なオブジェクトキーです。`private_cos_keys` にそのまま渡し、ダウンロード URL に置き換えたりプレフィックスを再追加したりしないでください。送信に `type` パラメーターはありません。

<span id="upload-and-submit-once" class="docs-anchor-alias" aria-hidden="true"></span>

## 1 回だけアップロードして送信する

`input.zip` を検証後、サーバーで次の順に実行します：

```js
import { getArgusAccessToken, uploadArgusZip, submitArgusTask } from './argus.mjs'

const accessToken = await getArgusAccessToken()
const privateCosKey = await uploadArgusZip(accessToken, './input.zip')
const taskCode = await submitArgusTask(accessToken, privateCosKey, 'My reconstruction')
// Persist taskCode with your task record before starting status queries.
```

アップロード失敗時は送信しません。送信は冪等ではないため、結果が不明な場合は既存のタスクを調査し、自動で再送信しないでください。バックエンド再起動後も、保存した `task_code` で照会を再開できます。

<span id="query-and-collect-the-result" class="docs-anchor-alias" aria-hidden="true"></span>

## 状態を照会し、結果を取得する

現在有効なトークンと保存したコードで `pollArgusTask` を呼びます。状態は `0` 待機、`1` 処理中、`2` 成功、`3` 失敗です。ポーリングはバックエンドで行い、例では新しいタスクを作成せず 10 分後に待機を終了します。

成功後は返された Unix 時刻 `expiration_timestamp` より前に `output_url` を取得します。ZIP を管理されたストレージへストリーミング保存し、一時 URL の公開や記録は避けます。固定の有効期間や永続保存は保証されていません。

成果物を使う前に ZIP 内の `output.json` を読みます。`success`、`partial`、`error` はゲートウェイのタスク状態とは別です。EXR 深度、GLB 点群、カメラ姿勢、検証規則は [出力と結果の処理](https://developer.realsee.ai/ja/docs/argus/outputs) を参照してください。これらは完全な Five SDK Work や完成した VR ツアーではありません。

<span id="continue" class="docs-anchor-alias" aria-hidden="true"></span>

## 次のステップ

- [API の説明](https://developer.realsee.ai/ja/docs/argus/reference)：リクエストと応答の正確な項目。
- [出力と結果の処理](https://developer.realsee.ai/ja/docs/argus/outputs)：ファイル形式、座標、一部成功の結果。
- [Pano-to-VR](https://developer.realsee.ai/ja/docs/pano-to-vr)：別の API で閲覧可能な Realsee プロジェクトを作成。
- [Realsee Discord コミュニティ](https://discord.gg/JvxBZJkbNZ)：連携の相談窓口。SK やトークンは共有しないでください。

Team のアプリと入力 ZIP で実行し、タスク完了と出力マニフェストを確認してから再構成結果を使用してください。

