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

> TypeScript から、プランでスタジオプロダクションを作成し、操作で編集し、静止画とクリップを生成し、計画フレームをレビューし、共有またはコピーします。

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

**スタジオプロダクション**は、設定に映画のショットを保持する Nodaro のワークフローです。各ショットには、構図を決めた静止画、任意のアニメーションクリップ、そしてそれらを作ったプラン、ルック、キャストの割り当て、ボイスがあります。**`client.studio`** はプロダクションを読み書きするので、スクリプト、AI アシスタント、スタジオアプリが、1 つのプロダクションを一緒に操作できます。**`client.shots`** は、共有リンクの背後にある、共有されたショットのレコードを保存します。これらのメソッドは、[スタジオプロダクション API](https://nodaro.ai/docs/developers/api/studio-productions) を呼び出します。作成ガイドについては、[MCP で使うスタジオプロダクション](https://nodaro.ai/docs/mcp/studio-productions)を参照してください。

スタジオプロダクションは、Nodaro Cloud 上で動作します。ルートが提供されていない環境では、すべてのメソッドが `NotFoundError` をスローします。利用できるかどうかを一度確認するには、`client.studio.productions.list()` を呼び出してください。プロダクション機能がある環境は空のページを返し、ない環境は `NotFoundError` をスローします。

## 2 種類のメソッド
`client.studio` には 2 つの層があります。どちらも、同じプロダクションを操作します。

| 層 | 用途 | 返すもの |
| --- | --- | --- |
| `client.studio.productions.*` | プロダクションのドキュメントです。プランからの作成、操作による編集、静止画とクリップの生成、ボイスと音楽の追加、共有とコピーです。 | ペイロードそのもの |
| `client.studio.*` | 計画フレームです。対応状況、キーフレームの生成とレビュー、バンドル、エディターの保存、リビジョンを確認したうえでのリンク共有です。 | API の `{ data }` エンベロープ |

**エンベロープには型が付きますが、プロダクションのドキュメントには付きません。**プロダクション、ショット、操作は、オープンな JSON である `Record<string, unknown>` です。分岐に使うものにはすべて型が付きます。`version`、`rebased`、`receipts`、`warnings`、見積もりの `credits`、実行の `jobIds` です。操作の語彙はサーバーから得られます。`skill()` から読み取ってください。

## client.studio.productions のメソッド
| メソッド | 内容 |
| --- | --- |
| [`skill()`](#productionsskill) | 作成ガイド、カタログ、プランのスキーマ、操作ガイドを読み取ります |
| [`validatePlan(plan)`](#productionsvalidateplanplan) | プランを無料でチェックします |
| [`list(opts?)`](#productionslistopts) | 自分のプロダクションを一覧表示します |
| [`get(productionId, opts?)`](#productionsgetproductionid-opts) | プロダクションを読み取ります |
| [`exportPlan(productionId, opts?)`](#productionsexportplanproductionid-opts) | 書き出しの手順を計画し、料金を出します |
| [`create(input?)`](#productionscreateinput) | プロダクションを作成します。プランから作成することもできます |
| [`ops(productionId, input)`](#productionsopsproductionid-input) | 操作のバッチを適用するか、プレビューします |
| [`reconcile(productionId)`](#productionsreconcileproductionid) | 完了した生成を結果に反映します |
| [`importPlan(productionId, plan, opts?)`](#productionsimportplanproductionid-plan-opts) | プランのシーンをプロダクションに追加します |
| [`describe(productionId, input)`](#productionsdescribeproductionid-input) | ブリーフをシーンに変換します |
| [`generate()`、`generateStill()`、`generateClip()`](#generate-stills-and-clips) | ショットのフレームを決めるか、アニメーションにします |
| [`frame(productionId, input)`](#productionsframeproductionid-input) | ショットのクリップから静止画を取り出します |
| [`voice(productionId, input)`](#productionsvoiceproductionid-input) | ショットのセリフを読み上げます |
| [`revoice(productionId, input)`](#productionsrevoiceproductionid-input) | ショットのクリップに含まれる声を差し替えます |
| [`music(productionId, input)`](#productionsmusicproductionid-input) | 映画にサウンドトラックを付けます |
| [`share()`、`unshare()`、`clone()`](#share-and-copy) | 共有リンクを開く、閉じる、またはプロダクションをコピーします |

## client.studio.productions
### productions.skill()
作成ガイド、カタログ全体、プランの JSON Schema、操作ガイドを返します。サーバーが実行しているバージョンから生成されます。無料です。

```ts
skill(): Promise<{ skill: string; catalog: string; schema: Record<string, unknown>; operating: string; generatedFrom: object }>
```

```ts
const { skill, schema, operating } = await client.studio.productions.skill()
```

`operating` には、`ops()` が受け付ける操作が一覧になっています。語彙をハードコードせず、実行時に読み取ってください。

### productions.validatePlan(plan)
プランがプロダクションになる前に、そのプランをチェックします。無料で、何も保存せず、キャストの名前を自分のライブラリと照合します。`valid` が `true` になるまで `errors` を直してチェックを繰り返し、そのあと `create({ plan })` を呼び出してください。

```ts
validatePlan(plan: Record<string, unknown>): Promise<{
valid: boolean
errors: Array<{ path: string; message: string; hint?: string }>
warnings: Array<{ path: string; message: string; hint?: string }>
summary?: { name?: string; scenes: number; shots: number; cast: number; bound: number }
}>
```

<TypeTable
type={{
plan: { type: 'Record<string, unknown>', required: true, description: "プロダクションのプランで、skill().schema の形式です。" },
}}
/>

```ts
const check = await client.studio.productions.validatePlan(plan)
if (!check.valid) console.log(check.errors)
```

`summary.bound` は、自分のライブラリのキャラクターと一致した、キャストのエントリーの数です。

### productions.list(opts?)
自分のプロダクションを、新しい順に一覧表示します。

```ts
list(opts?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: StudioProduction[]; nextCursor?: string }>
```

<TypeTable
type={{
limit: { type: 'number', description: "ページのサイズです。" },
cursor: { type: 'string', description: "前のページの nextCursor です。" },
includeArchived: { type: 'boolean', default: 'false', description: "ダッシュボードが隠しているアーカイブ済みのプロダクションも含めます。" },
}}
/>

```ts
const { data: productions } = await client.studio.productions.list({ limit: 20 })
```

### productions.get(productionId, opts?)
プロダクションを読み取ります。純粋な読み取りであり、完了したジョブを結果に反映することはありません。ジョブの完了を待っている場合は、先に `reconcile()` を呼び出してください。

```ts
get(productionId: string, opts?: { detail?: "summary" | "full"; shotId?: string }): Promise<StudioProduction>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
detail: { type: '"summary" | "full"', default: '"summary"', description: "summary は件数と有効な URL を返します。full は、それぞれの結果を、それを作ったコンテキストとともにすべて加えます。" },
shotId: { type: 'string', description: "1 つのショットだけを読み取ります。生成のあとの、軽い読み取りです。" },
}}
/>

```ts
const production = await client.studio.productions.get(productionId, { detail: "full" })
```

### productions.exportPlan(productionId, opts?)
映画を組み立てる手順を、順番どおりに、料金とともに返します。何も実行せず、何も消費しません。手順は、通常のノードのメソッドで自分で実行してください。

```ts
exportPlan(productionId: string, opts?: { upscale?: boolean }): Promise<{
canExport: boolean
steps: Array<{ id: string; label: string; node: string; creditModel: string; credits: number | null; params: Record<string, unknown> }>
resultStepId: string | null
estimate: number | null
unpriced: string[]
}>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
upscale: { type: 'boolean', default: 'false', description: "4K 仕上げも計画します。追加の料金がかかるため、これは常に任意です。" },
}}
/>

```ts
const plan = await client.studio.productions.exportPlan(productionId)
console.log(plan.canExport, plan.estimate)
for (const step of plan.steps) console.log(step.id, step.label, step.node, step.credits)
```

`canExport` は、プロダクションのクリップが 2 本未満の場合に `false` になります。`estimate` は、いずれかの手順に料金がない場合に `null` になります。合計の一部だけを示すと、実際より低い金額に見えてしまうためです。`unpriced` には、それらの手順のモデルが示されます。各手順の `node` はノードタイプで、`merge-video-audio`、`combine-videos`、`video-upscale` などです。

### productions.create(input?)
プロダクションを作成します。同じ呼び出しで、任意でプランを反映することもできます。

```ts
create(input?: { name?: string; plan?: Record<string, unknown> }): Promise<{
production: StudioProduction
warnings?: Array<{ path: string; message: string; hint?: string }>
summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>
```

<TypeTable
type={{
name: { type: 'string', description: "プロダクションの名前です。" },
plan: { type: 'Record<string, unknown>', description: "反映する、検証済みのプランです。" },
}}
/>

```ts
const { production, summary } = await client.studio.productions.create({ name: "Rome chase", plan })
```

### productions.ops(productionId, input)
プロダクションに、**操作**のバッチを適用します。すべての変更は操作であり、ショットの ID、ロールのスラッグ、結果のジョブ ID など、安定したキーで指定します。位置で指定することはありません。

```ts
ops(productionId: string, input: StudioOpsRequest): Promise<StudioOpsResponse>
ops(productionId: string, input: StudioOpsRequest & { dryRun: true }): Promise<StudioOpsDryRunResponse>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
ops: { type: 'unknown[]', required: true, description: "操作で、順番に適用されます。1 リクエストにつき最大 100 個です。語彙は skill().operating にあります。" },
baseVersion: { type: 'number', description: "バッチを組み立てた際のバージョンです。デフォルトでは情報として使われるだけです。バッチは最新のバージョンに適用され、応答で rebased が示されます。" },
strict: { type: 'boolean', description: "リベースを拒否します。古い baseVersion を指定すると、409 workflow_conflict で失敗します。" },
clientRequestId: { type: 'string', description: "このバッチ用のトークンで、8〜128 文字です。再試行が二重に適用されなくなります。" },
dryRun: { type: 'true', description: "書き込みを行わずに、バッチが何を行うかをプレビューします。呼び出しの中に、リテラルの true を直接書いてください。" },
}}
/>

```ts
const result = await client.studio.productions.ops(productionId, {
ops: [/* operations from the operating guide */],
baseVersion: version,
clientRequestId: crypto.randomUUID(),
})
version = result.version // carry it forward as the next baseVersion
for (const r of result.receipts) console.log(r.summary)
```

- **アトミックです。**不正な操作が 1 つでもあると、バッチ全体が [`StudioOpError`](https://nodaro.ai/docs/developers/sdk/errors#studio-batches) で拒否されます。その `opIndex` が、その操作を示します。何も書き込まれません。
- **リベースされます。**2 人が同時に 1 つのプロダクションを編集できます。古いバージョンをもとに組み立てたバッチも、最新のバージョンに適用され、`rebased` は `true` になります。
- **レシートです。**`receipts` には、操作ごとに過去形の 1 行が入ります。たとえば「Deleted take 2 of Shot 1 (in the bin)」のようなものです。操作の影響が、その名前の対象を超えて及ぶ場合、その `impact` に、更新すべき `keyframeIds` と `shotIds` が示されます。
- **応答をそのまま採用します。**手元のコピーを `production` に置き換え、`version` を次に持ち越してください。古いコピーにマージしないでください。

**バッチをプレビューする。**`dryRun: true` を指定すると、応答は、そのバッチが**何を行うか**を示すだけになります。そのため、アシスタントの編集を、先に人が承認できます。応答には `dryRun`、`version`、`receipts`、`warnings` が含まれ、`production` は含まれません。各レシートには `class` が加わります。`S` は安全、`D` は削除、`P` は作品にアクセスできる人の変更、`$` はクレジットの消費です。操作がゴミ箱に何かを入れた場合にのみ、`restorable` も加わります。`restorable ?? false` として読み取ってください。

`dryRun: true` は、呼び出し自体のオブジェクトの中に、リテラルとして書いてください。変数を経由して渡すと、型が `boolean` に広がってしまい、実際にはプレビューのままなのに、呼び出しの型は適用として扱われてしまいます。

プレビューは、リクエストを **2 回**送信します。まず、デプロイメントがプレビューに対応していることを確認する空のバッチを送り、次に自分のバッチを送ります。そうしないと、プレビューに対応していないデプロイメントは、警告なしにバッチを適用してしまいます。この結果、2 種類のエラーが起こり得ます。

```ts

try {
const preview = await client.studio.productions.ops(productionId, { ops, baseVersion, dryRun: true })
for (const r of preview.receipts) console.log(r.class, r.summary, r.restorable ?? false)
} catch (err) {
if (err instanceof StudioPreviewUnavailable) {
// Nothing was sent. Say that no preview is available; do not apply the batch instead.
} else if (err instanceof StudioPreviewAppliedError) {
// The batch was applied. Adopt err.applied.production and err.applied.version.
// Do not send it again. When err.applied is undefined, read the production first.
} else {
throw err
}
}
```

### productions.reconcile(productionId)
前回確認して以降に完了したすべての生成を結果に反映し、まだ実行中のものを報告します。アプリを開かなくても、完了したジョブを結果に変える唯一の呼び出しであり、何かが反映されたときにしか書き込みを行いません。

```ts
reconcile(productionId: string): Promise<{
landed: string[]
pending: string[]
failed: string[]
warnings: string[]
production: StudioProduction
version: number
}>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
}}
/>

```ts
const { landed, pending } = await client.studio.productions.reconcile(productionId)
```

`landed` には、そのメディアがプロダクションに反映されたジョブが一覧になります。`pending` には、まだ実行中のジョブが、`failed` には、失敗またはキャンセルされたジョブが一覧になります。

### productions.importPlan(productionId, plan, opts?)
既存のプロダクションに、プランのシーンを追加します。

```ts
importPlan(productionId: string, plan: Record<string, unknown>, opts?: { mode?: "append" }): Promise<{
production: StudioProduction
warnings?: Array<{ path: string; message: string; hint?: string }>
summary?: { shotsAdded: number; castEnrolled: number; castBound: number }
}>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
plan: { type: 'Record<string, unknown>', required: true, description: "追加するプランです。" },
mode: { type: '"append"', default: '"append"', description: "既存のシーンの後に、シーンを追加します。" },
}}
/>

```ts
await client.studio.productions.importPlan(productionId, extraScenesPlan)
```

### productions.describe(productionId, input)
監督（Director）を使って、ブリーフをシーンに変換します。ジョブを開始し、すぐに応答を返します。シーンは `reconcile()` を通じて反映されます。プロダクションは、その実行が保留中の下書きとして記録された状態で返ってきます。

```ts
describe(productionId: string, input: {
brief: string
llmModel: string
mode?: "append" | "replace"
label?: string
clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
brief: { type: 'string', required: true, description: "映画の内容です。" },
llmModel: { type: 'string', required: true, description: "シーンを下書きする言語モデルです。" },
mode: { type: '"append" | "replace"', description: "append は、下書きしたシーンを追加します。replace は、映画を書き直します。" },
label: { type: 'string', description: "実行の名前です。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
const { jobId } = await client.studio.productions.describe(productionId, {
brief: "A courier races across Rome in the rain to deliver a violin.",
llmModel,
})
```

### 静止画とクリップを生成する
`generateStill()` はショットのフレームを決め、`generateClip()` はそれをアニメーションにし、`generate()` は `kind` に応じてどちらかを行います。実行は、ジョブを送信し、プロダクションに保留中のマーカーを記録して、応答を返します。何分も待つことはありません。リクエストは、ショット自身のプラン、ルック、リファレンスからサーバー側で組み立てられるため、スクリプトからの呼び出しも、アプリでのクリックも、同じメディアを作ります。

```ts
generate(productionId: string, input: StudioGenerateRequest): Promise<StudioGenerateResult>
generateStill(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>
generateClip(productionId: string, shotId: string, opts?: StudioGenerateOptions): Promise<StudioGenerateResult>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
shotId: { type: 'string', required: true, description: "フレームを決めるか、アニメーションにするショットです。" },
kind: { type: '"still" | "clip"', description: "generate() のみ。何を作るかです。" },
count: { type: 'number', description: "静止画の場合、候補の数です。結果は積み重なります。新しい実行が、以前の結果を置き換えることはありません。" },
mode: { type: '"start" | "start-end" | "references"', description: "クリップの場合、どの入力を送るかです。start は開始フレームだけを送り、start-end は両方のフレームを送ります。省略すると、ショットに保存されている入力に従います。" },
dryRun: { type: 'boolean', description: "料金の見積もりを返し、何も送信しません。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
overrides: { type: 'Record<string, unknown>', description: "この実行だけに適用する変更です。モデル、プロンプト、アスペクト比、演出の ID などです。ショット自体は変更されません。" },
}}
/>

```ts

const quote = await client.studio.productions.generateStill(productionId, "shot-2", { count: 2, dryRun: true })
if (isStudioGenerateEstimate(quote)) console.log(quote.credits) // null means unpriced, not free

const run = await client.studio.productions.generateStill(productionId, "shot-2", {
count: 2,
clientRequestId: crypto.randomUUID(),
})
```

- **先に見積もります。**`dryRun: true` は実行の料金を見積もるだけで、何も書き込みません。応答は `isStudioGenerateEstimate()` で絞り込みます。
- **安全に再試行します。**同じ `clientRequestId` を使うと、再試行は、最初の呼び出しで開始したジョブを、`deduped: true` の印とともに返し、何も送信せず、新たな料金もかかりません。トークンなしで、有料の呼び出しを再試行しないでください。このページのすべての有料の呼び出しがこれを受け付けます。`frame()` と `voice()` も含みます。
- **レーンは自動で選ばれます。**クリップの場合、動画のルートはショットの入力から選ばれ、`lane` として返されます。`generate-video` または `text-to-video` です。

### productions.frame(productionId, input)
ショットの有効なクリップから静止画を取り出し、`target` が示す場所に置きます。数秒で終わるジョブの完了を待ってから、変更後のプロダクションと、画像の `url` を返します。

```ts
frame(productionId: string, input: {
shotId: string
mode?: "first" | "last" | "timestamp"
timestamp?: number
target?: "new-shot" | "start-frame" | "end-frame" | "still"
clientRequestId?: string
}): Promise<StudioMediaResponse>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
shotId: { type: 'string', required: true, description: "使用するクリップを持つショットです。" },
mode: { type: '"first" | "last" | "timestamp"', description: "取り出すフレームです。" },
timestamp: { type: 'number', description: "時刻（秒）です。mode が timestamp の場合に使います。" },
target: { type: '"new-shot" | "start-frame" | "end-frame" | "still"', default: '"new-shot"', description: "フレームの行き先です。このショットの後の新しいショット、このショットの開始または終了フレーム、あるいはこのショットのもう 1 枚の静止画です。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
const { url } = await client.studio.productions.frame(productionId, { shotId: "shot-2", mode: "last" })
```

### productions.voice(productionId, input)
ショットのセリフを読み上げ、そのショットに記録します。ジョブの完了を待ちます。

```ts
voice(productionId: string, input: {
shotId: string
text: string
voiceId?: string
voiceType?: "premade" | "custom" | "library"
ttsProvider?: string
delivery?: Record<string, number>
clientRequestId?: string
}): Promise<StudioMediaResponse>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
shotId: { type: 'string', required: true, description: "ショットです。" },
text: { type: 'string', required: true, description: "読み上げるセリフです。" },
voiceId: { type: 'string', description: "ボイスです。" },
voiceType: { type: '"premade" | "custom" | "library"', description: "ボイスの種類です。" },
ttsProvider: { type: 'string', description: "音声合成モデルです。" },
delivery: { type: 'Record<string, number>', description: "話し方の設定です。音声のルートが許す範囲内です。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
await client.studio.productions.voice(productionId, { shotId: "shot-3", text: "We're out of time." })
```

### productions.revoice(productionId, input)
ショットの有効なクリップに含まれる声を差し替えます。数分かかるため、`jobId` を返し、新しいクリップはそのマーカーを通じて反映されます。

```ts
revoice(productionId: string, input: { shotId: string; plan: Record<string, unknown>; clientRequestId?: string }): Promise<{ production: StudioProduction; jobId: string }>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
shotId: { type: 'string', required: true, description: "ショットです。" },
plan: { type: 'Record<string, unknown>', required: true, description: "声の差し替えプランで、話者の順に並んでいます。ボイス差し替えのルートが受け付ける形式です。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
const { jobId } = await client.studio.productions.revoice(productionId, {
shotId: "shot-3",
plan: recastPlan, // the speaker-ordered plan the voice recast route takes
})
```

### productions.music(productionId, input)
映画にサウンドトラックを付けます。完成したトラックは、その保留中のマーカーを通じて反映されます。

```ts
music(productionId: string, input: {
prompt: string
duration?: number
instrumental?: boolean
vocalGender?: string
model?: string
clientRequestId?: string
}): Promise<{ production: StudioProduction; jobId: string }>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
prompt: { type: 'string', required: true, description: "求める音楽です。" },
duration: { type: 'number', description: "長さ（秒）です。" },
instrumental: { type: 'boolean', description: "ボーカルなしの音楽です。" },
vocalGender: { type: 'string', description: "ボーカル入りの音楽での、歌い手の声です。" },
model: { type: 'string', description: "音楽のモデルです。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
const { jobId } = await client.studio.productions.music(productionId, {
prompt: "Tense strings building to a chase",
instrumental: true,
})
```

### 共有とコピー
`share()` はリンクによる共有ビューを開き、`unshare()` は再び閉じます。共有は独立した呼び出しであり、操作の一部になることはありません。そのため、作品を見られる人が、編集の副作用として変わることはありません。`clone()` は、自分が所有している、または閲覧できるプロダクションを、自分のスタジオのプロジェクトにコピーします。

```ts
share(productionId: string): Promise<StudioProduction>
unshare(productionId: string): Promise<StudioProduction>
clone(productionId: string, input?: { name?: string }): Promise<StudioProduction>
```

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "プロダクションの ID です。" },
name: { type: 'string', description: "clone のみ。コピーの名前です。" },
}}
/>

```ts
await client.studio.productions.share(productionId)
const copy = await client.studio.productions.clone(productionId, { name: "Rome chase, take 2" })
```

コピーは、非公開で、アーカイブされていない状態で始まります。共有とアーカイブの状態は引き継がれません。コピーは**自分**から見えるソースの状態をもとに作られるため、ほかの人のゴミ箱の中身が一緒に来ることはありません。

## client.studio：計画フレーム
これらのメソッドは、計画フレームとそのレビューを扱います。コントロールを提示する前に `capabilities()` を確認し、プロダクションを開き直すときは、一度 `reconcile()` を呼び出してください。送信への応答が失われている場合があるためです。ここにあるメソッドは、そのメソッドを呼び出さない限り、生成を開始したり候補を採用したりすることはありません。

| メソッド | 内容 |
| --- | --- |
| [`capabilities()`](#studiocapabilities) | プランのバージョンと、このデプロイメントが対応している操作を読み取ります |
| [`skill()`、`list()`、`validatePlan()`、`create()`](#studioskill-list-validateplan-and-create) | productions 層と同じ読み取りと作成を、エンベロープの形で行います |
| [`get(id, options?)`](#studiogetid-options) | プロダクションを、その対応状況とともに読み取ります |
| [`edit(id, input)`](#studioeditid-input) | リビジョンの条件付きで、操作を適用します |
| [`saveEditorState(id, input)`](#studiosaveeditorstateid-input) | 読み込んだリビジョンに対して、通常のエディターのフィールドを保存します |
| [`generateKeyframe(id, input)`](#studiogeneratekeyframeid-input) | 計画フレームを、採用せずに生成します |
| [`generateShot(id, input)`](#studiogenerateshotid-input) | 静止画またはクリップを見積もるか、送信します |
| [`acceptKeyframe(id, review, concurrency?)`](#studioacceptkeyframeid-review-concurrency) | レビュー済みの候補を採用します |
| [`reconcile(id)`](#studioreconcileid) | 何も採用せずに、完了したジョブを記録します |
| [`setShared(id, input)`](#studiosetsharedid-input) | レビュー済みのリビジョンに結び付けて、共有または共有解除します |
| [`clone(id, input?)`](#studiocloneid-input) | 保存済みのプロダクションをコピーします |
| [`importBundle(input)`](#studioimportbundleinput) | 移植可能なプロダクションをインポートします |
| [`appendBundle(id, input)`](#studioappendbundleid-input) | プロダクションにバンドルを追加します |

### studio.capabilities()
プランのバージョンと、このデプロイメントが対応している計画フレームの操作を返します。

```ts
capabilities(): Promise<{ data: StudioProductionCapabilities }>
```

```ts
const { data: caps } = await client.studio.capabilities()
if (caps.operations.generateKeyframes) showGenerateFrameButton()
```

`operations` には、操作ごとに 1 つのフラグがあります。`readKeyframes`、`editKeyframes`、`generateKeyframes`、`acceptKeyframes`、`rejectKeyframes`、`editSequencePlans`、`generateLinkedClips`、`retakeLinkedClips`、`saveEditorState`、`revisionedSharing`、`editableSharedCopies`、`cloneLinkedProductions`、`importPlannedBundles`、`importLinkedBundles`、`appendPlannedBundles`、`appendLinkedBundles` です。`automaticAcceptance` と `unattendedGeneration` は、常に `false` です。

### studio.skill()、list()、validatePlan()、create()
`productions.skill()`、`list()`、`validatePlan()`、`create()` と同じ呼び出しで、`{ data }` エンベロープで返されます。`list()` では、行は `response.data.data` に入ります。

```ts
skill(): Promise<{ data: Record<string, unknown> }>
list(options?: { limit?: number; cursor?: string; includeArchived?: boolean }): Promise<{ data: {
data: Array<{ id: string; name: string; version: number; updatedAt: string; thumbnailUrl: string | null; shared: boolean; archived: boolean; shotCount: number }>
nextCursor?: string
} }>
validatePlan(plan: Record<string, unknown>): Promise<{ data: { valid: boolean; errors: object[]; warnings: object[]; summary?: object } }>
create(input: { name?: string; plan?: Record<string, unknown> }): Promise<{ data: StudioProductionReply }>
```

<TypeTable
type={{
options: { type: '{ limit?, cursor?, includeArchived? }', description: "list()。ページングとアーカイブされた行です。" },
plan: { type: 'Record<string, unknown>', description: "validatePlan() と create()。プランです。" },
name: { type: 'string', description: "create()。プロダクションの名前です。" },
}}
/>

```ts
const { data: page } = await client.studio.list({ limit: 20 })
for (const row of page.data) console.log(row.name, row.shotCount)
```

### studio.get(id, options?)
プロダクションを、その対応状況とともに読み取ります。ジョブを結果に反映することはありません。

```ts
get(id: string, options?: { detail?: "summary" | "full"; shotId?: string }): Promise<{ data: StudioProductionReply }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
detail: { type: '"summary" | "full"', description: "full は、それぞれの結果を、そのコンテキストとともにすべて加えます。" },
shotId: { type: 'string', description: "1 つのショットだけを読み取ります。" },
}}
/>

```ts
const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0] // { id, label, revision, previewUrl, acceptedUrl, pending, ... }
```

### studio.edit(id, input)
リビジョンの条件付きで操作を適用し（`POST .../:id/ops`）、エンベロープの形で返します。`remove_shot`、`restore_trashed`、`purge_trashed` には、厳密な `baseVersion` を使ってください。また、シーンを削除する前に、結び付けられたシーケンスのセグメントを切り離してください。

```ts
edit(id: string, input: { ops: Array<{ op: string; [field: string]: unknown }>; baseVersion?: number; strict?: boolean; clientRequestId?: string }): Promise<{ data: StudioProductionReply & { version: number; rebased: boolean; receipts: object[] } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
ops: { type: 'Array<{ op: string; ... }>', required: true, description: "操作です。" },
baseVersion: { type: 'number', description: "読み込んだバージョンです。" },
strict: { type: 'boolean', description: "新しいバージョンへのリベースを拒否します。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
await client.studio.edit(productionId, {
ops: [{ op: "reject_keyframe_result", keyframeId, expectedRevision, resultKey, expectedAcceptedResultKey, reason: "Face drifted" }],
baseVersion: version,
strict: true,
})
```

`edit()` で送る、計画フレームの操作の例です。

- **`reject_keyframe_result`** は、生成を行わずに **Needs revision** を記録します。`operations.rejectKeyframes` が必要です。
- **`update_sequence_plan`** は、シーケンスの順番に並んだセグメント（それぞれ `{ shotId, startKeyframeId, endKeyframeId }`）を編集し、シーンの ID はそのまま保ちます。`operations.editSequencePlans` が必要です。
- **`detach_sequence_segment`** は、`mode` を `clear` または `keep-accepted` に設定して、1 つのセグメントを独立させます。`operations.editSequencePlans` が必要です。
- **`purge_trashed`** は、表示しているゴミ箱のエントリーを空にし、`clear_trash` はすべてのゴミ箱を、計画フレームも含めて空にします。

### studio.saveEditorState(id, input)
読み込んだリビジョンに対して、通常のエディターのフィールドを保存します。先に `operations.saveEditorState` を確認してください。保存は常に厳密です。競合すると 409 で失敗するため、ローカルの下書きを保持したまま、解決する前に読み込み直してください。

```ts
saveEditorState(id: string, input: { expectedVersion: number; graph: object; clientRequestId?: string })
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
expectedVersion: { type: 'number', required: true, description: "読み込んだバージョンです。" },
graph: { type: 'object', required: true, description: "保存するエディターの状態です。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
}}
/>

```ts
await client.studio.saveEditorState(productionId, { expectedVersion: version, graph })
```

この保存では、フレームのプラン、採用状態、エンドポイントの結び付け、ジョブの履歴、保護されたゴミ箱のエントリー、共有設定は変更できません。それらには、それぞれ専用のアクションを使ってください。

### studio.generateKeyframe(id, input)
計画フレームを、採用せずに生成します。フレームの生成には、見積もりのためのドライランはありません。

```ts
generateKeyframe(id: string, input: { keyframeId: string; expectedRevision: number; clientRequestId?: string; overrides?: Record<string, unknown> }): Promise<{ data: { jobIds: string[]; deduped?: true; lane?: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
keyframeId: { type: 'string', required: true, description: "計画フレームです。" },
expectedRevision: { type: 'number', required: true, description: "読み取った、そのフレームのリビジョンです。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。再試行するときは、同じ値を使ってください。" },
overrides: { type: 'Record<string, unknown>', description: "この実行だけに適用する変更です。" },
}}
/>

```ts
const { data: caps } = await client.studio.capabilities()
const { data: { production } } = await client.studio.get(productionId, { detail: "full" })
const frame = production.keyframes?.[0]

if (frame && caps.operations.generateKeyframes) {
const { data: generation } = await client.studio.generateKeyframe(productionId, {
keyframeId: frame.id,
expectedRevision: frame.revision,
clientRequestId: crypto.randomUUID(),
})
// follow generation.jobIds with client.jobs, then call reconcile()
}
```

生成は、候補を採用するものではなく、キャラクターのポートレートを作成するものでもありません。説明しかないキャストのリファレンスには、ポートレートは必要ありません。

### studio.generateShot(id, input)
ショットの静止画またはクリップを、見積もるか送信します。

```ts
generateShot(id: string, input: StudioShotGenerationInput): Promise<{ data: StudioGenerationReply }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
kind: { type: '"still" | "clip"', required: true, description: "何を作るかです。" },
shotId: { type: 'string', required: true, description: "ショットです。" },
count: { type: 'number', description: "候補の数です。" },
mode: { type: '"start" | "start-end" | "references"', description: "クリップの場合、どの入力を送るかです。" },
dryRun: { type: 'boolean', description: "見積もりを返し、何も送信しません。" },
expectedInputHash: { type: 'string', description: "リンクされたクリップの場合、確認した見積もりの inputHash です。" },
retakeResultKey: { type: 'string', description: "リンクされたクリップの場合、元のリクエストのまま、もう一度行うテイクです。" },
clientRequestId: { type: 'string', description: "再試行用のトークンです。" },
overrides: { type: 'Record<string, unknown>', description: "この実行だけに適用する変更です。" },
}}
/>

```ts
const { data: quote } = await client.studio.generateShot(productionId, { kind: "clip", shotId, dryRun: true })
if ("inputHash" in quote) {
await client.studio.generateShot(productionId, {
kind: "clip",
shotId,
expectedInputHash: quote.inputHash,
clientRequestId: crypto.randomUUID(),
})
}
```

**リンクされたクリップ。**計画フレームの間のクリップの見積もりには、`inputHash`、採用済みの `endpointPins`、正規化された長さ、解像度、アスペクト比、サウンドの設定、そして料金に使われた `creditIdentifier` が含まれます。確認した `inputHash` を、`expectedInputHash` として渡してください。見積もり以降に、設定または採用済みのフレームが変わっていた場合、呼び出しは、何かが送信される前に `409 sequence_quote_changed` で失敗します。新しい見積もりを求めてください。見積もりに含まれるクレジットは概算であり、生成では現在の料金が確保されます。

**撮り直し。**`operations.retakeLinkedClips` が `true` の場合、`kind: "clip"` と `shotId` とともに `retakeResultKey` を渡し、`dryRun: true` で見積もります。確認した `expectedInputHash` と新しい `clientRequestId` を付けて送信し、`mode`、`overrides`、`count` は省略してください。撮り直しは、プランや採用状態が変わった後でも、元のリクエストと保持されているフレーム画像を再利用します。以前のテイクは履歴に残ります。検証できる元のリクエストや保持された画像がないテイクは拒否され、撮り直しは同じ動画のバイトデータを再現するものではありません。

### studio.acceptKeyframe(id, review, concurrency?)
計画フレームの、レビュー済みの候補を採用します。これは独立した、明示的なステップです。生成がこれを呼び出すことはありません。

```ts
acceptKeyframe(id: string, review: StudioKeyframeAcceptanceInput, concurrency?: { baseVersion?: number; strict?: boolean; clientRequestId?: string })
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
keyframeId: { type: 'string', required: true, description: "計画フレームです。" },
expectedRevision: { type: 'number', required: true, description: "レビューした、そのフレームのリビジョンです。" },
resultKey: { type: 'string', required: true, description: "採用する候補です。" },
expectedAcceptedResultKey: { type: 'string | null', required: true, description: "以前に採用された候補、または null です。" },
requirementChecks: { type: 'Array<{ requirementId: string; outcome: "pass" | "waived" }>', required: true, description: "各要件チェックの結果です。" },
waivedReason: { type: 'string', description: "チェックを免除する場合に必須です。" },
concurrency: { type: '{ baseVersion?, strict?, clientRequestId? }', description: "書き込みのリビジョン条件です。" },
}}
/>

```ts
await client.studio.acceptKeyframe(productionId, {
keyframeId: frame.id,
expectedRevision: frame.revision,
resultKey,
expectedAcceptedResultKey: frame.acceptedResultKey,
requirementChecks, // one { requirementId, outcome } per requirement of the frame
})
```

競合が起きると、通常のエラーがスローされます。SDK が、自分の判断でほかの結果を選んだり、新しいリビジョンに対して再試行したりすることはありません。

### studio.reconcile(id)
完了したジョブを、候補を採用せず、生成も開始せずに記録します。ゴミ箱にあるシーンのジョブも確認します。完了したクリップは、そのシーンの保存されたグラフに残っており、`restore_trashed` で復元できます。

```ts
reconcile(id: string): Promise<{ data: StudioProductionReply & { landed: string[]; pending: string[]; failed: string[]; version: number } }>
```

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

```ts
const { data } = await client.studio.reconcile(productionId)
console.log(data.landed, data.pending)
```

### studio.setShared(id, input)
プロダクションを、レビューしたリビジョンに結び付けて、共有するか共有を解除します。`operations.revisionedSharing` を確認し、`expectedVersion` を渡してください。同時に編集があった場合は `409 workflow_conflict` で失敗し、SDK が再試行することはありません。公開範囲を変更できる呼び出し元だけが使えます。

```ts
setShared(id: string, input: { shared: boolean; allowEditableCopy?: boolean; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "プロダクションの ID です。" },
shared: { type: 'boolean', required: true, description: "true で共有リンクを開き、false で閉じます。" },
allowEditableCopy: { type: 'boolean', description: "リンクをログイン中に開いた人が、編集できるプロダクションをコピーできるようにします。operations.editableSharedCopies が必要です。" },
expectedVersion: { type: 'number', description: "レビューしたバージョンです。" },
}}
/>

```ts
await client.studio.setShared(productionId, { shared: true, allowEditableCopy: true, expectedVersion: version })
```

`allowEditableCopy` を使うと、オーナーまたはワークスペースの管理者は、リンクの閲覧者が、保存済みのプラン、プロンプト、キャストの説明、保持されているリファレンスの入力、テイクの履歴をコピーできるようにできます。ゴミ箱と非公開のレビューメモは、決して含まれません。コピーは非公開の状態で始まり、フレームは 1 つも採用されていません。コピーの許可をオフにするか共有を解除すると、新しいコピーはできなくなります。すでに作られたコピーは、独立したまま残ります。

### studio.clone(id, input?)
保存済みのプロダクションをコピーします。リンクされたフレームを持つプロダクションをコピーする前に `operations.cloneLinkedProductions` を確認し、読み込んだ `expectedVersion` を渡してください。ソースが変わっていた場合は、409 で失敗します。

```ts
clone(id: string, input?: { name?: string; projectId?: string; expectedVersion?: number }): Promise<{ data: StudioProductionReply }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "コピーするプロダクションです。" },
name: { type: 'string', description: "コピーの名前です。" },
projectId: { type: 'string', description: "コピーを置く、自分のプロジェクトです。" },
expectedVersion: { type: 'number', description: "読み込んだ、ソースのバージョンです。" },
}}
/>

```ts
const { data: { production: copy } } = await client.studio.clone(productionId, {
name: "Rome chase copy",
expectedVersion: version,
})
```

コピーは非公開の状態で始まります。フレームの入力は保持され、自分のストレージの使用量に数えられます。フレーム、シーン、シーケンスの ID は新しくなり、実行中のジョブや採用済みのフレームは引き継がれません。それらのフレームに依存するメディアを生成する前に、フレームをレビューして採用してください。コピー自体は、生成を送信しません。

### studio.importBundle(input)
移植可能なプロダクションを、新しい非公開のプロダクションとして、新しいシーン、フレーム、シーケンスの ID でインポートします（`POST .../import-bundle`）。メディアを含まないレシピやプランには `operations.importPlannedBundles` を、保持されたフレームのメディアを含むバンドルには `importLinkedBundles` を確認してください。

```ts
importBundle(input: { bundle: Record<string, unknown>; projectId?: string }): Promise<{ data: StudioProductionReply }>
```

<TypeTable
type={{
bundle: { type: 'Record<string, unknown>', required: true, description: "移植可能なプロダクションです。" },
projectId: { type: 'string', description: "インポート先の、自分のプロジェクトです。" },
}}
/>

```ts
const { data: { production } } = await client.studio.importBundle({ bundle })
```

リンクされたバンドルは、そのソースのプロダクションを名指しします。サーバーは、何かをコピーする前に、それを所有しているかを確認し、保持されているすべての画像を検証します。アクセス権がない場合や、来歴が偽装されている場合は、新しいプロダクションが作られる前に、インポートが拒否されます。どちらの種類のインポートも、採用状態や実行中のジョブを引き継がず、メディアを生成することもありません。

### studio.appendBundle(id, input)
完全なバンドルを、編集可能なプロダクションに、新しい ID と正確なリビジョンチェックとともに追加します（`POST .../:id/import-bundle`）。OAuth トークンには `workflows:write` が必要で、プロダクションへの編集権限も必要です。先に `appendPlannedBundles` または `appendLinkedBundles` を確認してください。

```ts
appendBundle(id: string, input: { bundle: Record<string, unknown>; expectedVersion: number; afterShotId?: string; applyFilm?: boolean }): Promise<{
data: { production: StudioProductionRecord; importedShotIds: string[]; importedKeyframeIds: string[] }
}>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "追加先のプロダクションです。" },
bundle: { type: 'Record<string, unknown>', required: true, description: "追加するバンドルです。" },
expectedVersion: { type: 'number', required: true, description: "読み込んだ、プロダクションのバージョンです。" },
afterShotId: { type: 'string', description: "このショットの後に挿入します。省略すると、末尾に追加されます。" },
applyFilm: { type: 'boolean', description: "バンドルのフィルムルックを採用します。音楽、カット、フィルムブリーフはそのままです。" },
}}
/>

```ts
const { data } = await client.studio.appendBundle(productionId, { bundle, expectedVersion: version })
console.log(data.importedShotIds)
```

既存のシーン、フレーム、ジョブ、共有、その他の設定はそのまま残ります。インポートされたキャストのロールは統合されます。インポートされたフレームは、もう一度採用する必要があります。不明な `afterShotId` や、古い `expectedVersion` を指定すると失敗します。

## client.shots
共有とリミックスのための、`/s/:id` の共有リンクの背後にあるショットのレコードです。ショットは、ビルダーの状態を保存します。ピッカーの選択、プロンプト、対象のモデル、`@` メンションのリファレンス、結果の URL です。これらは、推測できない 12 文字の ID で保存され、この ID は共有キーでもあります。ショットは、デフォルトでは**非公開**です。共有は、自分で行う公開範囲の変更です。

```ts
create(input?: CreateShotInput): Promise<{ id: string }>
get(id: string): Promise<{ shot: Shot }>
update(id: string, input: UpdateShotInput): Promise<{ shot: Shot }>
delete(id: string): Promise<void>
```

<TypeTable
type={{
id: { type: 'string', description: "get、update、delete。ショットの ID です。" },
mode: { type: '"single" | "multi-shot" | "frame-to-motion" | "storyboard"', description: "ビルダーのモードです。" },
selectionState: { type: 'Record<string, unknown>', description: "ピッカーの選択内容です。" },
freeText: { type: 'string', description: "自由記述のプロンプトです。" },
negativePrompt: { type: 'string', description: "避けたい内容です。" },
assembledPrompt: { type: 'string', description: "最終的なプロンプトです。" },
perModelPrompts: { type: 'Record<string, string>', description: "モデルごとのプロンプトです。" },
models: { type: 'string[]', description: "対象のモデルです。" },
entityRefs: { type: 'Array<{ entitySlug, variantSlug?, role?, kind? }>', description: "メンションされたキャラクター、ロケーション、オブジェクト、クリーチャーです。" },
resultUrls: { type: 'string[]', description: "結果の URL です。ふつうの公開 http または https の URL である必要があります。署名付き URL は拒否されるため、トークンが共有レコードに漏れることはありません。" },
visibility: { type: '"private" | "public"', default: '"private"', description: "そのショットを読み取れる人です。" },
}}
/>

```ts
const { id } = await client.shots.create({ mode: "single", freeText: "A lighthouse in a storm", models: ["nano-banana-2"] })
await client.shots.update(id, { visibility: "public" }) // anyone with the id can now read it
const { shot } = await client.shots.get(id)
```

公開されたショットは、その ID を持つ誰でも読み取れます。非公開のショットは、オーナーだけが読み取れ、それ以外の人には `NotFoundError` が返されます。ショットを更新または削除できるのは、オーナーだけです。

## Frequently asked questions

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

プロダクションは、設定に映画のショットを保持するワークフローです。各ショットには、構図を決めた静止画、任意のアニメーションクリップ、そしてそれらを作ったプラン、ルック、キャスト、ボイスがあります。スタジオアプリと SDK は、同じプロダクションを読み書きします。

### なぜ client.studio には、2 種類のメソッドがあるのですか？

client.studio.productions は、プロダクションのドキュメントを操作します。操作、静止画、クリップ、ボイス、音楽を扱い、ペイロードそのものを返します。client.studio は、計画フレームとそのレビューを扱い、API のデータエンベロープを返します。どちらも、同じプロダクションを扱います。

### 生成を、2 回分の料金を払わずに再試行するには、どうすればよいですか？

自分で作った clientRequestId を渡し、再試行のときも同じものを再利用します。サーバーは、最初の呼び出しで開始したジョブを返し、応答に deduped の印を付け、新たな料金は請求しません。

### 静止画やクリップの料金を、先に確認するには、どうすればよいですか？

dryRun を true にして generateStill または generateClip を呼び出します。応答はクレジットを含む見積もりで、何も送信されません。isStudioGenerateEstimate で絞り込めます。
