# ロケーション

> TypeScript からロケーションを作成し、エスタブリッシングショットを生成して承認し、時間帯、天候、季節、アングル、モーションのバリエーションを追加します。

Source: https://nodaro.ai/ja/docs/developers/sdk/locations

**`client.locations`** は、ロケーションスタジオが行うことすべてをスクリプトから操作します。ロケーションの作成と編集、エスタブリッシングショットの候補の生成、その 1 枚をメイン画像として承認、時間帯、天候、季節、アングル、ライティングのバリエーションと雰囲気のクリップの追加です。ロケーションは、メイン画像、バリエーションのコレクション、リファレンス写真、そして説明文を保持するため、以降のどのショットにも同じ場所を映せます。これらのメソッドは、[ロケーションの REST API](https://nodaro.ai/docs/developers/api/locations) を呼び出します。エディターでの見え方については、[ロケーション](https://nodaro.ai/docs/guides/locations)を参照してください。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`list(params?)`](#listparams) | ロケーションを一覧表示します |
| [`listArchived(params?)`](#listarchivedparams) | アーカイブ済みのロケーションを一覧表示します |
| [`get(id)`](#getid) | 1 つのロケーションを、進行中のジョブとともに読み取ります |
| [`create(input)`](#createinput) | ロケーションを作成します |
| [`update(id, input)`](#updateid-input) | ロケーションを変更します |
| [`delete(id)` と `restore(id)`](#deleteid-and-restoreid) | ロケーションをアーカイブするか、元に戻します |
| [`generate(input)`](#generateinput) | エスタブリッシングショットの候補を生成します |
| [`generateAsset(input)`](#generateassetinput) | 時間帯、天候、季節、アングル、ライティングのバリエーションを生成します |
| [`generateSurroundContinuation(input)`](#generatesurroundcontinuationinput) | 360 度のリングの次のビューを生成します |
| [`generateMotion(input)`](#generatemotioninput) | メイン画像をアニメーション化し、雰囲気のクリップにします |
| [`removeAsset(id, data)`](#removeassetid-data) | バリエーションのコレクションから 1 件を削除します |
| [`approveMainImage(id, candidateJobId)`](#approvemainimageid-candidatejobid) | 候補をメイン画像にします |
| [`recaption(id)`](#recaptionid) | ロケーションの説明文を書き直します |

## client.locations
### list(params?)
ロケーションを一覧表示します。デフォルトでは、有効なロケーションだけが返されます。ページングは省略できます。`limit` を指定しない場合はリスト全体が返り、カーソルは付きません。`limit`（最大 500）を指定すると、1 ページ分の結果と `nextCursor` が返ります。

```ts
list(params?: { archived?: boolean; limit?: number; cursor?: string }): Promise<{
locations: Location[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true にすると、代わりにアーカイブ済みのロケーションを一覧表示します。" },
limit: { type: 'number', description: "ページのサイズで、最大 500 です。省略するとリスト全体を取得します。" },
cursor: { type: 'string', description: "前のページの nextCursor です。" },
}}
/>

```ts
const { locations } = await client.locations.list()

const page = await client.locations.list({ limit: 100 })
const next = await client.locations.list({ limit: 100, cursor: page.nextCursor ?? undefined })
```

`nextCursor` が `null` になるまで、ページの取得を続けてください。

### listArchived(params?)
アーカイブ済みのロケーションを一覧表示します。`list({ archived: true })` のショートカットです。

```ts
listArchived(params?: { limit?: number; cursor?: string }): Promise<{ locations: Location[]; nextCursor?: string | null }>
```

<TypeTable
type={{
limit: { type: 'number', description: "ページのサイズで、最大 500 です。" },
cursor: { type: 'string', description: "前のページの nextCursor です。" },
}}
/>

```ts
const { locations: archived } = await client.locations.listArchived()
```

### get(id)
1 つのロケーションを、`pendingJobs`（まだ生成中のバリエーション）と `previousCandidates`（それまでのメイン画像の候補が、新しい順に最大 5 件）とともに読み取ります。それらの候補は、`approveMainImage()` で昇格させます。

```ts
get(id: string): Promise<LocationDetail>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
}}
/>

```ts
const location = await client.locations.get(locationId)
console.log(location.sourceImageUrl, location.weather, location.atmosphereMotions)
```

アーカイブ済みのロケーションも、ID を指定すれば読み取れるので、それを参照するワークフローのノードは、そのまま読み込めます。`Location` は、メイン画像を `sourceImageUrl` に持ち、6 つのコレクション（`timeOfDay`、`weather`、`seasons`、`angles`、`lighting`、`atmosphereMotions`。それぞれ `{ name, url }` のリストです）、`boards`、`referencePhotos`、`canonicalDescription`、`styleLock`、`updatedAt` を持ちます。

### create(input)
ロケーションを作成します。`name` と `nodeId` は必須です。キャンバスのノードを持たないスクリプトは、`nodeId` に `"mcp-managed"` を渡せます。

```ts
create(input: CreateLocationInput): Promise<{ id: string }>
```

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "ロケーションが属するキャンバスのノードです。mcp-managed も指定できます。" },
name: { type: 'string', required: true, description: "名前です。" },
description: { type: 'string', description: "自由形式の説明文です。" },
category: { type: 'string', description: "カテゴリーです。urban や nature などです。" },
style: { type: 'string', description: "ビジュアルスタイルです。realistic や anime などです。" },
projectId: { type: 'string', description: "格納先のプロジェクトです。" },
workflowId: { type: 'string', description: "元になったワークフローです。" },
sourceImageUrl: { type: 'string', description: "メイン画像の URL です。" },
imageProvider: { type: 'string | null', description: "メイン画像を作成した画像モデルです。" },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "ムードボードの写真で、最大 20 枚、それぞれ { url, kind } の形式です。kind は wide、interior、exterior、detail、moodBoard、other のいずれかです。" },
canonicalDescription: { type: 'string', description: "プロンプトで使う説明文です。" },
styleLock: { type: 'boolean', description: "バリエーションを、ロケーションの承認済みのスタイルに保ちます。" },
}}
/>

```ts
const { id: locationId } = await client.locations.create({
nodeId: "mcp-managed",
name: "Rainy Tokyo Alley",
description: "Neon-soaked alley with vending machines",
category: "urban",
style: "realistic",
})
```

### update(id, input)
ロケーションを変更します。書き込まれるのは、送信したフィールドだけです。バリエーションのコレクションは、この呼び出しの対象ではありません。作業中も生成ジョブがそこに追加していくためです。コレクションには `generateAsset()` と `removeAsset()` を使ってください。

```ts
update(id: string, input: UpdateLocationInput): Promise<{ id: string; updatedAt: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
name: { type: 'string', description: "名前です。" },
description: { type: 'string', description: "自由形式の説明文です。" },
category: { type: 'string', description: "カテゴリーです。" },
style: { type: 'string', description: "ビジュアルスタイルです。" },
sourceImageUrl: { type: 'string', description: "メイン画像の URL です。" },
imageProvider: { type: 'string | null', description: "メイン画像の画像モデルです。" },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "ムードボードの写真で、最大 20 枚です。" },
canonicalDescription: { type: 'string', description: "プロンプトで使う説明文です。" },
styleLock: { type: 'boolean', description: "バリエーションを、承認済みのスタイルに保ちます。" },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "ロケーションのリファレンスボードです。" },
selectedAssetByVariant: { type: 'Record<string, string>', description: "各バリエーションで選ばれている 1 件です。" },
piiConsentAt: { type: 'string', description: "リファレンス写真の権利を持っていることをユーザーが確認した日時を記録する、ISO 8601 形式の時刻です。referencePhotos を初めて添付するときに設定します。" },
expectedUpdatedAt: { type: 'string', description: "読み取った updatedAt の値です。その後ロケーションが変更されていた場合、更新は 409 concurrent_modification で失敗します。" },
}}
/>

```ts
await client.locations.update(locationId, {
canonicalDescription: "A narrow, rain-soaked alley lit by neon signs",
styleLock: false,
piiConsentAt: new Date().toISOString(),
expectedUpdatedAt: location.updatedAt,
})
```

`409 concurrent_modification` は、その `code` を持つ、通常の `NodaroError` として届きます。ロケーションをもう一度読み取り、変更をマージしてから再試行してください。

### delete(id) と restore(id)
`delete()` はロケーションをアーカイブし、`restore()` は元に戻します。完全な削除は、Nodaro のアプリ上でしか行えません。元に戻した名前が、大文字と小文字を区別せずに有効なロケーションの名前と一致する場合、サーバーは `(restored)` を追加し、実際に使った名前を返します。

```ts
delete(id: string): Promise<{ success: true; archived: true }>
restore(id: string): Promise<{ id: string; name: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
}}
/>

```ts
await client.locations.delete(locationId)
const { name } = await client.locations.restore(locationId)
```

### generate(input)
エスタブリッシングショットの候補を生成します（`POST /v1/generate-location`）。`count` が 1 より大きい場合、実行が始まる前に、すべてのジョブの分が確保されます。そのため、途中で失敗すると、そのバッチ全体が取り消されます。

```ts
generate(input: GenerateLocationInput): Promise<{ jobIds: string[]; jobId?: string }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "ロケーションの名前です。" },
description: { type: 'string', description: "その場所がどのように見えるかです。" },
userPrompt: { type: 'string', description: "追加の指示です。" },
category: { type: '"indoor" | "outdoor" | "urban" | "nature" | "fantasy" | "sci-fi" | "historical" | "futuristic" | "other"', description: "カテゴリーです。" },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "ビジュアルスタイルです。" },
sourceImageUrl: { type: 'string', description: "ショットのもとになる写真です。" },
provider: { type: 'string', description: "画像モデルの ID です。" },
count: { type: 'number', description: "生成する候補の数です。" },
quality: { type: 'string', description: "品質設定を持つモデルでの medium、high、basic のいずれかです。料金が変わります。" },
resolution: { type: 'string', description: "対応するモデルでの 1K、2K、4K、0.5 MP、1 MP、2 MP、4 MP のいずれかです。料金が変わります。" },
attachToLocationId: { type: 'string', description: "単一の結果の書き込み先となるロケーションです。" },
}}
/>

```ts
// One candidate, written to the location when it completes
const { jobIds: [jobId] } = await client.locations.generate({
name: "Rainy Tokyo Alley",
description: "Neon-soaked alley with vending machines",
attachToLocationId: locationId,
})

// Four candidates to choose from
const { jobIds } = await client.locations.generate({ name: "Rainy Tokyo Alley", count: 4 })
```

`attachToLocationId` を指定し、`count` が 1 の場合、ジョブが完了すると、その結果がメイン画像になります。それ以外の場合は、`approveMainImage()` で候補を選んでください。`quality` と `resolution` の料金は、[**画像生成**（Generate Image）](https://nodaro.ai/docs/nodes/image/generate-image)と同じです。モデルが対応していない値は無視されます。

### generateAsset(input)
ロケーションのバリエーションを 1 つ生成します（`POST /v1/generate-location-asset`）。`attachToLocationId`、`attachToColumn`、`attachName` を指定すると、ジョブが完了したときに、その結果が該当するコレクションに追加されます。

```ts
generateAsset(input: GenerateLocationAssetInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
assetType: { type: '"timeOfDay" | "weather" | "seasons" | "angles" | "lighting" | "custom"', required: true, description: "バリエーションの種類です。" },
variant: { type: 'string', required: true, description: "バリエーションです。ゴールデンアワー、嵐、冬、空撮、ネオンなどです。" },
name: { type: 'string', required: true, description: "ロケーションの名前です。" },
description: { type: 'string', description: "ロケーションの説明です。" },
userPrompt: { type: 'string', description: "追加の指示です。" },
sourceImageUrl: { type: 'string', description: "元にする画像です。通常はメイン画像です。" },
provider: { type: 'string', description: "画像モデルの ID です。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "画像のアスペクト比です。" },
quality: { type: 'string', description: "generate() と同じです。料金が変わります。" },
resolution: { type: 'string', description: "generate() と同じです。料金が変わります。" },
attachToLocationId: { type: 'string', description: "結果を追加するロケーションです。" },
attachToColumn: { type: 'string', description: "コレクションです。time_of_day、weather、seasons、angles、lighting、atmosphere_motions、sheets、detail_closeups のいずれかです。" },
attachName: { type: 'string', description: "新しいエントリーの名前です。" },
}}
/>

```ts
const { jobId } = await client.locations.generateAsset({
name: "Rainy Tokyo Alley",
assetType: "weather",
variant: "storm",
attachToLocationId: locationId,
attachToColumn: "weather",
attachName: "storm",
})
```

### generateSurroundContinuation(input)
前のビューの続きとして、360 度のリングの次のビューを生成します（`POST /v1/generate-surround-continuation`）。サーバーは前のビューの半分をそのまま保ち、残りの半分を描き、色を合わせます。そのため、隣接するビューは境目なくつながります。このメソッドは Nodaro Cloud で動作します。ほかのエディションでは、処理を始める前に `403 edition_required` を返し、SDK はこれを `ForbiddenError` としてスローします。

```ts
generateSurroundContinuation(input: GenerateSurroundContinuationInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
referenceImageUrl: { type: 'string', required: true, description: "リングの前のビューです。" },
direction: { type: '"right" | "up" | "down"', required: true, description: "リングが続く方向です。" },
degrees: { type: 'number', description: "ビューが回転する角度です。45 などです。" },
carriedFraction: { type: 'number', default: '0.5', description: "前のビューのうち、変更せずに引き継ぐ割合です。" },
userPrompt: { type: 'string', description: "追加の指示です。" },
provider: { type: 'string', description: "画像モデルの ID です。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "画像のアスペクト比です。" },
attachToLocationId: { type: 'string', description: "ビューを追加するロケーションです。" },
attachToColumn: { type: 'string', description: "コレクションです。ロケーションスタジオでは angles を使います。" },
attachName: { type: 'string', description: "新しいエントリーの名前です。Surround 45° などです。" },
}}
/>

```ts
const { jobId } = await client.locations.generateSurroundContinuation({
referenceImageUrl: previousRingView,
direction: "right",
degrees: 45,
provider: "nano-banana-pro",
aspectRatio: "16:9",
attachToLocationId: locationId,
attachToColumn: "angles",
attachName: "Surround 45°",
})
```

### generateMotion(input)
ロケーションの画像をアニメーション化し、雰囲気のクリップにします（`POST /v1/generate-location-motion`）。[**動画生成**（Generate Video）](https://nodaro.ai/docs/nodes/video/generate-video)の画像から動画へのモードを使います。ロケーションが持つモーションのコレクションは 1 つだけなので、クリップは必ず `atmosphereMotions` に入り、`attachToColumn` はありません。

```ts
generateMotion(input: GenerateLocationMotionInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "シーンの中で何が動くかです。" },
sourceImageUrl: { type: 'string', required: true, description: "アニメーション化する画像です。通常はメイン画像です。" },
name: { type: 'string', required: true, description: "ロケーションの名前です。" },
provider: { type: 'string', description: "動画モデルの ID です。kling などです。" },
category: { type: 'string', description: "カテゴリーです。" },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "ビジュアルスタイルです。" },
canonicalDescription: { type: 'string', description: "ロケーションの説明です。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "クリップのアスペクト比です。" },
attachToLocationId: { type: 'string', description: "クリップを追加するロケーションです。" },
attachName: { type: 'string', description: "新しいクリップの名前です。" },
}}
/>

```ts
const { jobId } = await client.locations.generateMotion({
name: "Rainy Tokyo Alley",
motionPrompt: "Slow dolly-in, neon signs flicker, light rain falling",
sourceImageUrl: location.sourceImageUrl!,
provider: "kling",
attachToLocationId: locationId,
attachName: "neon dolly-in",
})
```

### removeAsset(id, data)
バリエーションのコレクションから 1 件を削除します（`POST /v1/locations/:id/remove-asset`）。その `url` を持つエントリーは、まとめて 1 回の操作ですべて削除されます。たとえば、360 度のビューを再生成する前などに使います。

```ts
removeAsset(id: string, data: { column: LocationAttachColumn; url: string }): Promise<{ removed: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
column: { type: 'LocationAttachColumn', required: true, description: "コレクションです。angles や weather などです。" },
url: { type: 'string', required: true, description: "削除する対象の URL です。" },
}}
/>

```ts
await client.locations.removeAsset(locationId, { column: "angles", url: oldViewUrl })
```

URL がそのコレクションにない場合、またはロケーションが自分のものではない場合は、`NotFoundError` をスローします。

### approveMainImage(id, candidateJobId)
`generate()` で完了した候補を、ロケーションのメイン画像にします。続いてビジョンモデルがロケーションの説明を書き、このメソッドはその両方を返します。

```ts
approveMainImage(id: string, candidateJobId: string): Promise<{ sourceImageUrl: string; canonicalDescription: string | null }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
candidateJobId: { type: 'string', required: true, description: "完了した候補のジョブ ID です。" },
}}
/>

```ts
const { sourceImageUrl, canonicalDescription } = await client.locations.approveMainImage(locationId, jobIds[0])
```

説明を書けなかった場合、`canonicalDescription` は `null` になります。その場合もメイン画像は設定されるので、`recaption()` を呼び出してもう一度試してください。

### recaption(id)
現在のメイン画像から、ロケーションの説明を書き直します。

```ts
recaption(id: string): Promise<{ canonicalDescription: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "ロケーション ID です。" },
}}
/>

```ts
const { canonicalDescription } = await client.locations.recaption(locationId)
```

ロケーションにメイン画像がない場合は `400 no_source_image` で失敗し、ビジョンモデルが失敗した場合は 502 で失敗します。

## Frequently asked questions

### Nodaro の SDK でロケーションを作成するには、どうすればよいですか？

nodeId と name を指定して client.locations.create を呼び出し、client.locations.generate でエスタブリッシングショットを生成し、client.locations.approveMainImage で 1 枚を承認します。

### ロケーションには、どのようなバリエーションを生成できますか？

generateAsset で、時間帯、天候、季節、アングル、ライティングのバリエーションに加えて、カスタムバリエーションを生成できます。generateMotion では、雰囲気のクリップを生成できます。Nodaro Cloud では、generateSurroundContinuation で 360 度のビューを追加できます。

### ほかの人が変更したロケーションを、上書きしないようにするには、どうすればよいですか？

読み取った updatedAt の値を expectedUpdatedAt として client.locations.update に渡します。その間にロケーションが変更されていた場合、呼び出しは 409 concurrent_modification で失敗します。もう一度読み取ってマージし、再試行してください。

### SDK で、ロケーションを完全に削除できますか？

できません。delete はロケーションをアーカイブし、restore は元に戻します。完全な削除は、Nodaro のアプリ上でしかできません。
