# スタジオプロダクション

> REST で、スタジオプロダクションを読み書きします。アトミックな操作を適用し、静止画とクリップを生成して、完了したジョブを結果に反映し、書き出しを計画して、映画を共有またはコピーします。

Source: https://nodaro.ai/ja/docs/developers/api/studio-productions

**スタジオプロダクション API** は、Nodaro Studio で作る映画を読み書きします。プロダクションは、設定に順番に並んだショットのリストを持つ、Nodaro のワークフローです。各ショットには、構図を決めた静止画、任意のアニメーションクリップ、そしてそれらを作ったプラン、ルック、キャスト、ボイスがあります。スクリプト、AI アシスタント、スタジオのエディターは、これらのルートを通じて、同時に同じプロダクションを操作します。

スタジオプロダクションは、Nodaro Cloud でのみ動作します。この機能を提供していないデプロイメントでは、すべてのルートが `404` を返すため、一覧のルートで 1 回だけ機能を検出してください。セルフホスティング環境では、[**シーン**（Scene）](https://nodaro.ai/docs/nodes/video/scene)、[**動画生成**（Generate Video）](https://nodaro.ai/docs/nodes/video/generate-video)、[**動画を結合**（Combine Videos）](https://nodaro.ai/docs/nodes/video/combine-videos)などのノードを使って、映画をワークフローとして構築します。ルートはベアラートークンを必要とします。OAuth アプリのトークンには、読み取りに `workflows:read`、書き込みに `workflows:write` が必要です。[認証](https://nodaro.ai/docs/developers/api/authentication)を参照してください。

プロダクションにアクセスできない場合、どのルートも `403` ではなく `404` を返します。そのため、ID を使ってプロダクションの存在を探ることはできません。

### スタジオでの呼び方と、ドキュメントのキー
スタジオとドキュメントでは、同じものを違う名前で呼びます。コードではドキュメントのキーを使い、人と話すときはスタジオでの呼び方を使います。

| スタジオでの呼び方 | ドキュメントでの呼び方 |
| --- | --- |
| 映画 | プロダクション |
| シーン（たとえば「シーン 3」） | `shotId` で指定する、`shots[]` のエントリー |
| シーンのフレームと、そのテイク | ショットの `still` の結果 |
| シーンのモーションと、そのテイク | ショットの `clip` の結果 |
| モーションの中のショット | ショットの `beats[]` |

## エンドポイント
| メソッド | パス | 説明 |
| --- | --- | --- |
| `GET` | `/v1/studio/productions/skill` | 作成ガイド、カタログ、プランの JSON Schema、操作の語彙です。無料です。 |
| `GET` | `/v1/studio/productions/capabilities` | このデプロイメントが対応している、任意の操作です。 |
| `POST` | `/v1/studio/productions/validate` | プランを検証します。無料で、何も保存しません。 |
| `GET` | `/v1/studio/productions` | 自分のプロダクションを、新しい順に一覧表示します。 |
| `POST` | `/v1/studio/productions` | プロダクションを作成します。プランから作成することもできます。 |
| `GET` | `/v1/studio/productions/:id` | プロダクションを読み取ります。書き込みは行いません。 |
| `POST` | `/v1/studio/productions/:id/ops` | 操作のバッチを適用するか、プレビューします。 |
| `POST` | `/v1/studio/productions/:id/reconcile` | 完了したジョブを結果に反映します。 |
| `POST` | `/v1/studio/productions/:id/import` | プランのショットをプロダクションに追加します。 |
| `POST` | `/v1/studio/productions/:id/describe` | ブリーフからプランを作成します。 |
| `POST` | `/v1/studio/productions/:id/generate` | ショットの静止画またはクリップを生成するか、見積もります。 |
| `POST` | `/v1/studio/productions/:id/frame` | クリップからフレームを取り出します。 |
| `POST` | `/v1/studio/productions/:id/voice` | ショットにセリフを読み上げて重ねます。 |
| `POST` | `/v1/studio/productions/:id/revoice` | ショットのクリップに含まれる声を差し替えます。 |
| `POST` | `/v1/studio/productions/:id/music` | サウンドトラックを生成します。 |
| `GET` | `/v1/studio/productions/:id/export-plan` | 映画を組み立てる手順を、料金付きで示します。 |
| `POST` | `/v1/studio/productions/:id/share` | リンク共有を有効または無効にします。 |
| `POST` | `/v1/studio/productions/:id/unshare` | リンク共有を無効にします。 |
| `POST` | `/v1/studio/productions/:id/clone` | プロダクションをコピーします。 |

削除のルートはありません。アーカイブは操作の 1 つで、元に戻せます。

## プロダクションを読み取る
すべてのレスポンスは、`{ "data": { "production": { … } } }` の形でラップされます。SDK はこれを展開するので、プロダクションを返すメソッドは、プロダクションそのものを返します。プロダクションの最上位には、次が含まれます。

| フィールド | 意味 |
| --- | --- |
| `id`、`name`、`version`、`updatedAt` | 識別情報と、すべての書き込みで確認されるカウンターです。 |
| `thumbnailUrl`、`shared`、`archived` | サムネイル、リンク共有が有効かどうか、アーカイブ済みかどうかです。 |
| `film`、`cast`、`folders`、`storyboard`、`music`、`musicPlan`、`cuts` | フィルムルック、ロール、タイムラインのフォルダー、ブリーフ、サウンドトラック、書き出したカットです。 |
| `trash` | `{ count, items? }`：削除されて、復元できるものです。 |
| `pending` | `{ stills, clips, music, draft }`：現在生成中のものです。 |
| `shots[]` | タイムライン順のショットで、それぞれに `still`、`clip`、`startFrame`、`endFrame`、`plan`、`beats`、`look`、`voice` などが含まれます。 |

`GET /v1/studio/productions/:id` は、`detail=summary`（デフォルト。件数と有効な URL）または `detail=full`（各ショットの履歴にあるすべての結果と、それを生んだコンテキスト）を受け付けます。`shot_id` を指定すると、1 つのショットだけを返します。生成のあとにもう一度読み取る、軽い方法です。`GET /v1/studio/productions` は、`limit`、`cursor`、`includeArchived` を受け付けます。

結果は積み重なっていきます。生成は置き換えを行いません。ショットの静止画は、それまでに得たすべての候補であり、クリップも同じです。静止画を削除しても、クリップには影響しません。結果は、その結果キーで指定します。結果キーは、ジョブ ID がある場合はそのジョブ ID、それ以外は URL であり、位置で指定することはありません。

**curl**

```bash
curl "https://app.nodaro.ai/v1/studio/productions/5d8f2b4a-7c1e-4a9d-8b3f-2e6c9a1d4f7b?detail=summary" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts

const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})

const production = await client.studio.productions.get(id, { detail: 'full' })
const shot = await client.studio.productions.get(id, { shotId: 'shot-2' })
```

SDK は、エンベロープ（`version`、`rebased`、`receipts`、`warnings`、見積もりの `credits`、実行の `jobIds`）に型を付け、プロダクションのドキュメント自体は、オープンな JSON のままにします。

## 操作で編集する
プロダクションへの変更はすべて、操作です。操作とは、ショットの名前変更、移動、キャストのロールの割り当て、候補の採用など、名前の付いた編集のことです。`POST /v1/studio/productions/:id/ops` は、操作のバッチを検証し、適用します。操作の語彙は、このページには載せず、サーバーから提供されます。`GET …/skill` の `operating` の部分を読んでください。これは、接続しているデプロイメントに、常に一致しています。

**curl**

```bash
# batch.json: { "ops": [ ...operations from GET /v1/studio/productions/skill... ], "baseVersion": 7 }
curl -X POST https://app.nodaro.ai/v1/studio/productions/5d8f2b4a-7c1e-4a9d-8b3f-2e6c9a1d4f7b/ops \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d @batch.json
```

**TypeScript SDK**

```ts
const { production, version, rebased, receipts } =
await client.studio.productions.ops(id, { ops, baseVersion: 7 })
```

レスポンスは `{ production, version, rebased, receipts, warnings }` です。すべてのバッチに、次の 6 つの規則が当てはまります。

1. **すべてか無か**。不正な操作が 1 つでもあると、バッチ全体が拒否され、何も書き込まれません。エラーは、0 から数える `opIndex` でその操作を示します。バッチに入れられる操作は、最大 100 個です。
2. **デフォルトはリベース**。`baseVersion` は情報として送るだけです。古いバージョンをもとに作ったバッチも、最新のドキュメントに適用され、レスポンスには `rebased: true` と示されます。代わりに `409 workflow_conflict` を受け取るには、`strict: true` を送ります。
3. **安定したキーで指定する**。ショットはその ID、フォルダーやカットはその ID、キャストの行はそのロールのスラッグ、結果はその結果キーで指定します。位置では指定しません。スタジオとスクリプトが、同じプロダクションを同時に編集する可能性があるためです。
4. **ID は自分で作る**。ショット、フォルダー、カット、コピーを作成する操作は、自分が渡した ID を使います。これにより、手元のコピーとサーバーは、すべての ID で一致します。
5. **削除はゴミ箱に入る**。削除は、削除したものをプロダクションのゴミ箱に移動するだけで、操作によって復元できます。何かを完全に消すのは、明示的な完全削除だけです。
6. **レスポンスをそのまま採用する**。手元のコピーは、マージするのではなく `production` に置き換え、次の `baseVersion` には `version` を送ります。

共有は操作ではありません。共有を変えようとするバッチは拒否されます。`receipts` には、操作ごとに過去形の 1 行、`{ op, summary, ids?, impact? }` が入ります。アシスタントが何をしたかを知りたい人には、これを見せてください。`warnings` には、失敗ではないものの伝える価値があること、たとえば何も変えなかった操作などが挙げられます。

### バッチを適用する前にプレビューする
`dryRun: true` は、バッチが何を行うかを尋ねます。サーバーは、実際の書き込みと同じコンテキスト、同じ拒否ルールで処理を実行しますが、保存する前に停止します。応答は `{ dryRun: true, version, receipts, warnings }` で、`production` は含まれません。各レシートには `class`（`S` は安全、`D` は削除、`P` は作品にアクセスできる人の変更、`$` はクレジットの消費）が加わり、削除を元に戻せる場合は `restorable: true` も加わります。

プレビューに対応する前のデプロイメントは、`dryRun` を無視してバッチを適用します。そのため、まず空のバッチでこのフラグを確認してください。空のバッチは、どのデプロイメントでも何も変更しません。

```json
{ "ops": [], "dryRun": true }
```

`dryRun: true` を含む応答だけが「はい」です。それ以外はすべて「いいえ」であり、ここではプレビューできないとユーザーに伝えて、バッチを送信しないでください。実際のプレビューの応答でも、このマーカーを確認してください。更新中のデプロイメントは、次のリクエストを別のサーバーで処理することがあるためです。マーカーの代わりに `production` を含む応答は、バッチが適用されたことを意味します。その場合は、その結果を採用し、もう一度送信しないでください。SDK は、この 2 つの確認をどちらも代わりに行い、`StudioPreviewUnavailable` または `StudioPreviewAppliedError` をスローします。

```ts
const preview = await client.studio.productions.ops(id, { ops, baseVersion, dryRun: true })
for (const r of preview.receipts) console.log(r.class, r.summary, r.restorable ?? false)
```

## 静止画とクリップを生成する
生成は、実行してからポーリングする方式です。`kind: "still"` または `kind: "clip"` と `shotId` を指定した `POST /v1/studio/productions/:id/generate` は、ジョブを送信し、プロダクション上に保留として記録し、`{ jobIds, lane?, deduped?, production? }` をすぐに返します。

- **ショットから組み立てる**。サーバーは、ショットのプラン、ルック、キャスト、演出からリクエストを組み立てます。そのため、スクリプトからの呼び出しも、スタジオでのクリックも、同じ画像を作ります。`overrides` を使うと、ショット自体を変えずに、その 1 回の実行だけを変更できます。
- **先に見積もる**。`dryRun: true` は、実行の料金を出すだけで、何も書き込みません。応答は `{ dryRun: true, provider, count, credits, lane? }` です。`credits: null` は、そのモデルの料金が不明であることを意味し、無料という意味ではありません。
- **安全に再試行する**。`A-Za-z0-9_.:-` からなる 8〜128 文字を自分で作った `clientRequestId` を使うと、再試行が安全になります。同じ ID を送ると、最初の呼び出しのジョブが `deduped: true` とともに返り、新たな料金はかかりません。有料の呼び出しは、これなしで再試行しないでください。
- **結果を反映する**。`POST /v1/studio/productions/:id/reconcile` は、完了したジョブを結果に変え、失敗したジョブを片付けます。`{ landed, pending, failed, warnings, production, version }` を返します。`GET` は書き込みを行わないため、何かを待っているときは reconcile してください。
- **クリップのレーンは自動で選ばれる**。クリップの場合、モデルのルート（`generate-video` または `text-to-video`）はショットの入力から決まり、`lane` として返ってきます。`mode` は、どちらの入力の組み合わせを演出の元にするかだけを指定します。`"start"` または `"references"` です。

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/studio/productions/5d8f2b4a-7c1e-4a9d-8b3f-2e6c9a1d4f7b/generate \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "still", "shotId": "shot-2", "count": 2, "clientRequestId": "still-shot-2-take-1" }'

curl -X POST https://app.nodaro.ai/v1/studio/productions/5d8f2b4a-7c1e-4a9d-8b3f-2e6c9a1d4f7b/reconcile \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts

const quote = await client.studio.productions.generateStill(id, 'shot-2', { count: 2, dryRun: true })
if (isStudioGenerateEstimate(quote)) console.log(quote.credits)

const run = await client.studio.productions.generateStill(id, 'shot-2', {
count: 2,
clientRequestId: crypto.randomUUID(),
})
// ...once the jobs finish:
const { landed, pending } = await client.studio.productions.reconcile(id)
```

ほかのメディアのルートは、1 つのショットに対して動作します。

| ルート | ボディ | 説明 |
| --- | --- | --- |
| `frame` | `{ shotId, mode?, timestamp?, target?, clientRequestId? }` | クリップからフレームを取り出します。数秒待ってから、`{ production, url? }` を返します。 |
| `voice` | `{ shotId, text, voiceId?, voiceType?, ttsProvider?, delivery?, clientRequestId? }` | ショットにセリフを読み上げて重ねます。処理を待ってから、`{ production }` を返します。 |
| `revoice` | `{ shotId, plan, clientRequestId? }` | ショットのクリップに含まれる声を差し替えます。`{ jobId, production }` を返します。reconcile で反映されます。 |
| `music` | `{ prompt, duration?, instrumental?, vocalGender?, model?, clientRequestId? }` | サウンドトラックを生成します。`{ jobId, production }` を返します。reconcile で反映されます。 |

`clientRequestId` は、`frame` と `voice` を含む、このページのすべての有料ルートで受け付けられます。

### リンクされたクリップと撮り直し
デプロイメントによっては、確認済みの 2 つのフレームの間を動くクリップに対応しています。まず `GET /v1/studio/productions/capabilities` を確認してください。`operations.generateLinkedClips` と `operations.retakeLinkedClips` が、それぞれ利用できるかどうかを示します。

- **リンクされたクリップを見積もる**。`kind: "clip"`、`shotId`、`dryRun: true` を指定した `generate` は、両方のフレームを確認し、`inputHash` を含む見積もりを返します。これを、実際のリクエストで `expectedInputHash` として送り返してください。その間に設定やフレームが変わっていた場合、このルートは何も送信する前に `409 sequence_quote_changed` を返します。
- **クリップを撮り直す**。既存のリンクされたクリップの新しいテイクを、元の設定とフレームのまま見積もるには、`retakeResultKey` を追加します。次に、見積もりの `inputHash` と新しい `clientRequestId` を付けて送信します。`mode`、`overrides`、`count` は送らないでください。元のリクエストやフレームが保持されていないテイクは、撮り直せません。

## ブリーフからプロダクションを作る
### ガイドを読む
`GET /v1/studio/productions/skill` は、`{ skill, catalog, schema, operating, generatedFrom }` を返します。プランの書き方、すべてのピッカー、モデル、使える値、プランの JSON Schema、操作の語彙です。

### プランを検証する
`{ plan }` を指定した `POST /v1/studio/productions/validate` は、`{ valid, errors, warnings, summary? }` を返します。`valid` が `true` になるまで、各エラーの `path` を修正してください。キャストの名前は、自分のライブラリと照合されます。

### プロダクションを作成する
`{ name?, plan? }` を指定した `POST /v1/studio/productions` は、プロダクションを作成し、プランを反映します。`{ plan, mode: "append" }` を指定した `POST …/:id/import` は、既存のプロダクションにプランのショットを追加します。どちらもメディアは生成しません。

### または、監督にプランを書かせる
`{ brief, llmModel, mode?, label?, clientRequestId? }` を指定した `POST …/:id/describe` は、監督（Director）の実行を開始し、`{ jobId, production }` を返します。ジョブが完了したら、reconcile して、下書きされたショットを反映してください。

```ts
const { production } = await client.studio.productions.create({ name: 'The Lighthouse' })
const { jobId } = await client.studio.productions.describe(production.id, {
brief: 'A keeper, a storm, and a light that will not start.',
llmModel: 'gpt-5-mini',
mode: 'replace',
})
// poll jobId with client.jobs.getStatus, then:
await client.studio.productions.reconcile(production.id)
```

## 書き出しを計画する
`GET /v1/studio/productions/:id/export-plan` は、映画を組み立てる手順を、順番どおりに返します。ショットごとの音声の結合、つなぎ合わせ、そして `upscale` による任意の 4K 仕上げです。何も実行しません。

```json
{
"canExport": true,
"steps": [
{
"id": "voice-shot-2",
"node": "merge-video-audio",
"label": "Voice over shot 2",
"credits": 20,
"params": { "videoUrl": "https://cdn.nodaro.ai/studio/shot-2.mp4", "audioUrl": "https://cdn.nodaro.ai/studio/vo.mp3" }
},
{
"id": "combine",
"node": "combine-videos",
"label": "Join 3 shots",
"credits": 4,
"params": {
"videoUrls": [{ "fromStep": "voice-shot-2" }, "https://cdn.nodaro.ai/studio/shot-3.mp4"],
"transition": "cut",
"audioMode": "keep"
}
}
],
"resultStepId": "combine",
"estimate": 24,
"unpriced": []
}
```

手順は、通常のノードのルート、たとえば[**動画とオーディオを結合**（Merge Video & Audio）](https://nodaro.ai/docs/nodes/video/merge-video-audio)や[動画を結合](https://nodaro.ai/docs/nodes/video/combine-videos)を使って、順番に実行します。`{ "fromStep": "…" }` という値は、前の手順の出力です。その手順が生成した URL に置き換えてください。そして、完成したファイルを、カットを追加する操作で、プロダクションにカットとして記録します。

組み立てるものがない場合、つまりクリップが 2 本未満の場合、`canExport` は `false` になります。いずれかの手順に料金がない場合、`estimate` は `null` になり、`unpriced` にそれらのモデルが示されます。合計の一部だけを示すと、実際より低い金額に見えてしまうためです。

## プロダクションを共有・コピーする
- **共有する**。`{ shared: true }` を指定した `POST …/:id/share` は、読み取り専用の共有リンクを有効にします。`{ shared: false }` または `POST …/:id/unshare` は無効にします。共有を変更できるのは、オーナーまたはワークスペースの管理者だけです（それ以外は `403 forbidden`）。`operations.revisionedSharing` が利用できる場合は、`expectedVersion` を追加すると、変更を、自分が確認したバージョンに結び付けられます。その後に別の編集があった場合は、`409 workflow_conflict` が返されます。
- **編集できるコピーを許可する**。`operations.editableSharedCopies` が利用できる場合、オーナーは `shared: true` と `expectedVersion` に加えて、`allowEditableCopy: true` も送れます。すると、ログイン済みでリンクを開いた人は、プラン、プロンプト、キャストの説明、保持されている入力、テイクの履歴をコピーできるようになります。ゴミ箱と非公開のレビューメモはコピーされず、共有を解除すると、新しいコピーはできなくなります。
- **コピーする**。`{ name? }` を指定した `POST …/:id/clone` は、自分から見た状態のまま、プロダクションをコピーします。コピーは非公開で、一覧に表示される状態で始まり、自分のストレージを使います。

## タイムラインを編集アプリに書き出す
`POST /v1/freecut-export` は、シーンのクリップからなるタイムラインを、編集アプリのプロジェクトファイルにします。外部の編集アプリで、カットの仕上げができるようにするためです。FreeCut JSON（`freecut-v1`）または Final Cut Pro XML（`fcpxml-v1.10`）ファイルを自分のストレージに書き込み、その URL を返します。クレジットはかからず、1 分あたり 10 リクエストまで使えます。このページのほかの部分と同じく、Nodaro Cloud が必要です。ほかのエディションは `404` を返します。

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/freecut-export \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"format": "json",
"name": "The Lighthouse, cut 1",
"timeline": {
"musicAssetUrl": "https://cdn.nodaro.ai/studio/score.mp3",
"scenes": [
{
"sceneEntityId": "scene-1",
"compositeUrl": "https://cdn.nodaro.ai/studio/scene-1.mp4",
"shots": [{ "shot_id": "s1", "duration_seconds": 4 }]
},
{
"sceneEntityId": "scene-2",
"compositeUrl": "https://cdn.nodaro.ai/studio/scene-2.mp4",
"shots": [{
"shot_id": "s2",
"duration_seconds": 6,
"cut_decision": { "in_offset_sec": 0, "out_offset_sec": 0.5, "transition_to_next": "dissolve" }
}]
}
]
}
}'
```

**TypeScript SDK**

```ts
const file = await client.request('POST', '/v1/freecut-export', {
body: { format: 'json', name: 'The Lighthouse, cut 1', timeline },
})
```

```json
{
"url": "https://cdn.nodaro.ai/exports/7b2d4f6a/freecut-3c5e7a9b-1d2f-4a6c-8e3b-5f7a9c1e2d4b.json",
"format": "json",
"assetId": "1e3a5c7b-9d2f-4b4a-8c6e-5f7d9b1a3c2e"
}
```

`assetId` は、そのファイルの、自分のライブラリ内のエントリーです。そのエントリーを作成できなかった場合は `null` になりますが、どちらの場合も `url` は有効です。

| フィールド | 意味 |
| --- | --- |
| `format` | 必須です。FreeCut JSON なら `json`、Final Cut Pro XML なら `fcpxml` です。 |
| `timeline` | 必須です。タイムラインです。下記を参照してください。 |
| `name` | 自分の記録用のラベルで、最大 200 文字です。 |
| `timeline.scenes` | 必須です。再生順に、1 つ以上のシーンを指定します。各シーンが、動画トラック上の 1 つのクリップになります。 |
| `timeline.musicAssetUrl` | 音楽トラックです。デフォルトの空文字列にすると、含まれません。 |
| `timeline.narrationAssetUrl` | ナレーショントラックで、専用のオーディオレーンに配置されます。 |
| `timeline.fadeOutDurationSec` | 音楽の末尾のフェードで、デフォルトは 0.8 秒です。FreeCut JSON のみです。 |
| `scene.sceneEntityId`、`scene.compositeUrl` | 必須です。シーンの ID と、その結合済みクリップです。 |
| `scene.shots` | 必須です。`{ shot_id, duration_seconds, cut_decision? }` からなる、1 つ以上のショットです。各ショットの長さを足すと、シーンの長さになります。 |
| `cut_decision.in_offset_sec`、`cut_decision.out_offset_sec` | カットの決定には必須です。開始側のトリム（シーンの最初のショットから読み取ります）と、終了側のトリム（シーンの最後のショットから読み取ります）です。 |
| `cut_decision.transition_to_next` | カットの決定には必須です。`hard_cut`、`dissolve`、`match_cut`、`overlap` のいずれかで、次のシーンへのトランジションです。 |
| `cut_decision.transition_duration_sec` | トランジションのデフォルトの長さを上書きします。`hard_cut` と `match_cut` は 0、`dissolve` は 0.5 秒、`overlap` は 1 秒です。 |

`dissolve` と `overlap` は、トランジションの長さの分だけ、2 つのクリップを重ねます。`hard_cut` と `match_cut` は、クリップをそのままつなぎます。どのショットにも `cut_decision` がない場合、書き出しは単純な連結になります。シーンごとに 1 つのクリップを、順に、すべてハードカットでつなぎ、音楽はタイムライン全体に流れます。シーンの中のトリムは適用されません。各シーンのクリップは、すでに結合されているためです。

## 共有とリミックスのレコード
**ショットレコード**は、自分が組み立てたショットの状態を保存します。ピッカーの選択、プロンプト、対象のモデル、`@` でメンションしたアセット、結果です。レコードは推測できない短い ID で保存され、この ID が `/s/:id` の共有リンクとワンクリックでのリミックスを支えています。これらのルートは、プロダクションとは別のものです。

| メソッド | パス | 認証 | 説明 |
| --- | --- | --- | --- |
| `POST` | `/v1/shots` | ベアラートークン | レコードを作成します。`{ id }` を返します。 |
| `GET` | `/v1/shots/:id` | なし | レコードを読み取ります。非公開のレコードは、オーナー以外の全員に `404` を返します。IP アドレスごとに制限されます。 |
| `PATCH` | `/v1/shots/:id` | オーナー | `visibility` を含む、任意のフィールドを更新します。 |
| `DELETE` | `/v1/shots/:id` | オーナー | レコードを削除します。 |

ボディには、`mode`（`single`、`multi-shot`、`frame-to-motion`、`storyboard` のいずれか）と、`selectionState`（各ピッカーの選択値で、`{ pickerNodeType: valueId }` または `{ pickerNodeType: { field: valueId } }` の形式）が入ります。ほかに、`freeText`、`negativePrompt`、`assembledPrompt`、`perModelPrompts`、`models`、`entityRefs`、`resultUrls` を任意で指定できます。`visibility` のデフォルトは `private` です。レコードを共有可能にするには、`public` に設定します。

`resultUrls` に指定できるのは、ふつうの公開 `http` または `https` の URL だけです。署名付き URL は拒否されるため、トークンが共有レコードに漏れることはありません。レコードには `schemaVersion` が記録されます。レコードが、もう存在しないカタログのエントリーを指している場合、リミックスを失敗させる代わりに、メモを付けて読み飛ばします。

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/shots \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"mode": "single",
"selectionState": { "mood": "melancholy", "lighting": { "timeOfDay": "blue-hour" } },
"freeText": "a woman at a bus stop",
"models": ["nano-banana-pro"],
"visibility": "public"
}'
```

**TypeScript SDK**

```ts
const { id: shotId } = await client.shots.create({
mode: 'single',
selectionState: { mood: 'melancholy', lighting: { timeOfDay: 'blue-hour' } },
freeText: 'a woman at a bus stop',
})
await client.shots.update(shotId, { visibility: 'public' })
const { shot } = await client.shots.get(shotId)
```

**CLI**

```bash
nodaro shots create --file shot.json --visibility public
nodaro shots get <id> --json
nodaro shots delete <id>
```

## MCP から使う
AI アシスタントは、`get_studio_production_skill`、`validate_studio_plan`、`create_studio_production`、`edit_studio_production`、`generate_studio_still`、`generate_studio_clip`、`plan_studio_export` などのツールを通じて、同じプロダクションを使います。書き込み権限でプロダクションを読み取ると、完了したジョブも反映されます。[MCP のスタジオプロダクション](https://nodaro.ai/docs/mcp/studio-productions)を参照してください。

## エラー
| ステータス | コード | 意味 |
| --- | --- | --- |
| `400` | `validation_error` | ボディまたはプランが正しくありません。`path` にフィールド名が示されます。書き出しのルートでは、`issues` に問題の一覧が入ります。 |
| `400` | `op_invalid`、`op_target_missing` | `…/ops` で、いずれかの操作が正しくありませんでした。`opIndex` にその操作が示されます。何も書き込まれていません。修正して、バッチをもう一度送信してください。 |
| `402` | `insufficient_credits` | アカウントが、この生成の料金をまかなえません。 |
| `403` | `forbidden` | 共有を変更できるのは、オーナーまたはワークスペースの管理者だけです。 |
| `404` | `not_found` | この呼び出し元に対応するプロダクションがないか、このデプロイメントがプロダクションを提供していません。 |
| `404` | `op_target_missing` | 生成またはメディアのルートで、指定したショット、結果、ロールが、プロダクションの中に見つかりません。 |
| `409` | `workflow_conflict` | `strict: true` または `expectedVersion` を送ったところ、プロダクションが変わっていました。もう一度読み取って、再度適用してください。 |
| `409` | `production_busy` | 書き込みの最中に、プロダクションが変わり続けました。もう一度読み取って、再試行してください。 |
| `409` | `sequence_quote_changed` | 見積もり以降に、リンクされたクリップの設定またはフレームが変わりました。もう一度見積もってください。 |
| `413` | `storage_exceeded` | アカウントが、ストレージの上限を超えています。 |
| `429` | `rate_limit_exceeded` | 1 分間に 10 回を超えてタイムラインを書き出しました。`Retry-After` にある時間だけ待ってください。 |

SDK では、これらは型付きのエラーとして届きます。`StudioOpError`（`opIndex` を含む）、書き込みの 2 つの `409` コードに対する `WorkflowConflictError`、`InsufficientCreditsError`、`StorageExceededError`、`NotFoundError` です。

## Frequently asked questions

### スタジオプロダクションとは何ですか？

Nodaro Studio で作る映画です。設定に、順番に並んだショットのリストを持つワークフローで、スタジオでは、これをシーンと呼びます。各ショットには、構図を決めた静止画、任意のアニメーションクリップ、そしてそれらを作ったプラン、ルック、キャスト、ボイスがあります。

### 生成したあとに reconcile を呼び出す必要があるのは、なぜですか？

生成はすぐに応答を返し、処理はバックグラウンドで続きます。POST /v1/studio/productions/:id/reconcile は、完了したジョブを各ショットの結果に反映します。ふつうの GET は書き込みを行わないため、reconcile するまで、完了したジョブは保留のままです。

### 有料の生成を安全に再試行するには、どうすればよいですか？

自分で作った 8〜128 文字の clientRequestId を送ります。同じ ID をもう一度送ると、最初の呼び出しで開始したジョブが deduped の印付きで返り、新たな料金はかかりません。

### 操作のバッチが何をするかを、事前に確認できますか？

はい、dryRun を true にして送ります。最初は空のバッチに dryRun を付けて送り、サーバーがプレビューに対応しているかを確認してください。対応していないサーバーは、そのバッチをそのまま適用してしまいます。

### スタジオのタイムラインを Final Cut Pro や、ほかの編集アプリに書き出せますか？

はい。POST /v1/freecut-export は、シーンのクリップからなるタイムラインを、あなたのストレージ内の FreeCut JSON または Final Cut Pro XML ファイルにします。クレジットはかかりません。
