# Produções do Studio

> Em TypeScript, crie produções do Studio com um plano, edite-as com operações, gere imagens e clipes, revise quadros planejados, compartilhe-as ou copie-as.

Source: https://nodaro.ai/pt-BR/docs/developers/sdk/studio

Uma **produção do Studio** é um workflow do Nodaro cujas configurações guardam as tomadas de um filme. Cada tomada tem uma imagem fixa enquadrada, um clipe animado opcional e o plano, os visuais, os vínculos de elenco e a voz que os geraram. **`client.studio`** lê e grava produções, então um script, um assistente de IA e o app Studio trabalham em uma mesma produção. **`client.shots`** armazena os registros compartilhados de tomadas por trás dos links de compartilhamento. Os métodos chamam a [API REST de produções do Studio](https://nodaro.ai/docs/developers/api/studio-productions). Veja [Produções do Studio pelo MCP](https://nodaro.ai/docs/mcp/studio-productions) para o guia de criação.

As produções do Studio são executadas no Nodaro Cloud. Onde as rotas não são servidas, todos os métodos lançam `NotFoundError`. Para verificar uma vez, chame `client.studio.productions.list()`: uma implantação com produções responde com uma página vazia, e uma sem elas lança `NotFoundError`.

## Dois conjuntos de métodos
`client.studio` tem duas camadas. As duas trabalham nas mesmas produções.

| Camada | Use para | Retorna |
| --- | --- | --- |
| `client.studio.productions.*` | O documento da produção: criar a partir de um plano, editar com operações, gerar imagens fixas e clipes, adicionar voz e música, compartilhar e copiar | O próprio payload |
| `client.studio.*` | Quadros planejados: recursos disponíveis, geração e revisão de quadros-chave, pacotes, salvamentos do editor e compartilhamento por link com verificação de revisão | O envelope `{ data }` da API |

**Os envelopes são tipados; o documento da produção não é.** Uma produção, uma tomada e uma operação são JSON aberto, `Record<string, unknown>`. Tudo o que você usa para decidir o fluxo é tipado: `version`, `rebased`, `receipts`, `warnings`, os `credits` de uma cotação e os `jobIds` de uma execução. O vocabulário de operações vem do servidor: leia-o em `skill()`.

## Métodos de client.studio.productions
| Método | O que faz |
| --- | --- |
| [`skill()`](#productionsskill) | Lê o guia de criação, o catálogo, o esquema do plano e o guia de operação |
| [`validatePlan(plan)`](#productionsvalidateplanplan) | Verifica um plano, de graça |
| [`list(opts?)`](#productionslistopts) | Lista as suas produções |
| [`get(productionId, opts?)`](#productionsgetproductionid-opts) | Lê uma produção |
| [`exportPlan(productionId, opts?)`](#productionsexportplanproductionid-opts) | Planeja os passos de exportação e calcula o preço deles |
| [`create(input?)`](#productionscreateinput) | Cria uma produção, opcionalmente a partir de um plano |
| [`ops(productionId, input)`](#productionsopsproductionid-input) | Aplica, ou pré-visualiza, um lote de operações |
| [`reconcile(productionId)`](#productionsreconcileproductionid) | Incorpora as gerações concluídas |
| [`importPlan(productionId, plan, opts?)`](#productionsimportplanproductionid-plan-opts) | Adiciona as cenas de um plano a uma produção |
| [`describe(productionId, input)`](#productionsdescribeproductionid-input) | Transforma um briefing em cenas |
| [`generate()`, `generateStill()`, `generateClip()`](#generate-stills-and-clips) | Enquadra ou anima uma tomada |
| [`frame(productionId, input)`](#productionsframeproductionid-input) | Extrai uma imagem fixa do clipe de uma tomada |
| [`voice(productionId, input)`](#productionsvoiceproductionid-input) | Gera a fala de uma tomada |
| [`revoice(productionId, input)`](#productionsrevoiceproductionid-input) | Troca as vozes do clipe de uma tomada |
| [`music(productionId, input)`](#productionsmusicproductionid-input) | Cria a trilha sonora do filme |
| [`share()`, `unshare()`, `clone()`](#share-and-copy) | Abre ou fecha o link de compartilhamento, ou copia a produção |

## client.studio.productions
### productions.skill()
Retorna o guia de criação, o catálogo completo, o JSON Schema do plano e o guia de operação, gerados a partir da versão que roda no servidor. É gratuito.

```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` lista as operações que `ops()` aceita. Leia-o em tempo de execução em vez de fixar o vocabulário no código.

### productions.validatePlan(plan)
Verifica um plano antes que ele vire uma produção. É gratuito, não armazena nada e resolve os nomes do elenco com base na sua biblioteca. Corrija os `errors` e valide de novo até `valid` ser `true`; depois, chame `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: "O plano de produção, no formato de skill().schema." },
}}
/>

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

`summary.bound` conta as entradas do elenco que corresponderam a um personagem da sua biblioteca.

### productions.list(opts?)
Lista as suas produções, das mais recentes para as mais antigas.

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

<TypeTable
type={{
limit: { type: 'number', description: "O tamanho da página." },
cursor: { type: 'string', description: "O nextCursor da página anterior." },
includeArchived: { type: 'boolean', default: 'false', description: "Inclui as produções arquivadas que o painel oculta." },
}}
/>

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

### productions.get(productionId, opts?)
Lê uma produção. É uma leitura pura e nunca incorpora um job concluído, então chame `reconcile()` antes quando estiver esperando por um.

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

<TypeTable
type={{
productionId: { type: 'string', required: true, description: "O ID da produção." },
detail: { type: '"summary" | "full"', default: '"summary"', description: "summary retorna contagens e as URLs ativas. full adiciona todos os resultados com o contexto que os gerou." },
shotId: { type: 'string', description: "Lê só uma tomada: a leitura barata depois de uma geração." },
}}
/>

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

### productions.exportPlan(productionId, opts?)
Retorna os passos ordenados que montam o filme, com os preços deles. Não executa nada e não gasta nada: execute os passos você mesmo com os métodos comuns de nós.

```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: "O ID da produção." },
upscale: { type: 'boolean', default: 'false', description: "Planeja também um acabamento em 4K. Custa mais, então é sempre uma escolha sua." },
}}
/>

```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` é `false` quando a produção tem menos de dois clipes. `estimate` é `null` quando algum passo não tem preço, porque uma soma parcial subestimaria o custo, e `unpriced` indica os modelos desses passos. O `node` de cada passo é um tipo de nó, como `merge-video-audio`, `combine-videos` ou `video-upscale`.

### productions.create(input?)
Cria uma produção e, opcionalmente, incorpora um plano na mesma chamada.

```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: "O nome da produção." },
plan: { type: 'Record<string, unknown>', description: "Um plano validado a incorporar." },
}}
/>

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

### productions.ops(productionId, input)
Aplica um lote de **operações** a uma produção. Toda alteração é uma operação, endereçada por uma chave estável, como o ID de uma tomada, o slug de um papel ou o ID do job de um resultado, nunca pela posição.

```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: "O ID da produção." },
ops: { type: 'unknown[]', required: true, description: "As operações, aplicadas em ordem. No máximo 100 por requisição. O vocabulário está em skill().operating." },
baseVersion: { type: 'number', description: "A versão sobre a qual você montou o lote. Por padrão, é só informativa: o lote é aplicado à versão mais recente, e a resposta indica rebased." },
strict: { type: 'boolean', description: "Recusa o rebase. Uma baseVersion mais antiga então falha com 409 workflow_conflict." },
clientRequestId: { type: 'string', description: "O seu token para este lote, de 8 a 128 caracteres, para que uma nova tentativa não seja aplicada duas vezes." },
dryRun: { type: 'true', description: "Pré-visualiza o que o lote faria, sem gravar. Escreva o true literal na própria chamada." },
}}
/>

```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)
```

- **Atômico.** Uma operação inválida recusa o lote inteiro com um [`StudioOpError`](https://nodaro.ai/docs/developers/sdk/errors#studio-batches) cujo `opIndex` a identifica, e nada é gravado.
- **Rebase automático.** Duas pessoas podem editar uma produção ao mesmo tempo. Um lote montado sobre uma versão mais antiga continua sendo aplicado à mais recente, e `rebased` é `true`.
- **Recibos.** `receipts` tem uma linha no pretérito por operação, como “Deleted take 2 of Shot 1 (in the bin)”. Quando o efeito de uma operação vai além do que ela nomeia, o `impact` dela lista os `keyframeIds` e os `shotIds` a atualizar.
- **Adote a resposta.** Substitua a sua cópia por `production` e leve `version` adiante. Não mescle a resposta com a sua cópia antiga.

**Pré-visualize um lote.** Com `dryRun: true`, a resposta diz o que o lote **faria**, para que uma pessoa possa aprovar antes as edições de um assistente. Ela tem `dryRun`, `version`, `receipts` e `warnings`, e nenhuma `production`. Cada recibo adiciona `class`: `S` seguro, `D` exclui, `P` muda quem pode acessar o trabalho, `$` gasta créditos. Ele também adiciona `restorable`, que só está presente quando a operação colocou algo na lixeira. Leia-o como `restorable ?? false`.

Escreva `dryRun: true` como um literal no próprio objeto da chamada. Passado por uma variável, ele se amplia para `boolean`, e a chamada é tipada como uma aplicação, embora continue sendo uma prévia.

Uma prévia envia **duas** requisições: primeiro um lote vazio que comprova que a implantação consegue pré-visualizar, depois o seu lote. Caso contrário, uma implantação que não consegue pré-visualizar aplicaria o seu lote sem avisar. Dois erros podem ocorrer:

```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)
Incorpora todas as gerações que terminaram desde a última vez que você verificou e informa o que ainda está em execução. É a única chamada que transforma jobs concluídos em resultados sem o app aberto, e ela só grava quando algo foi incorporado.

```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: "O ID da produção." },
}}
/>

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

`landed` lista os jobs cuja mídia já está na produção, `pending` os jobs ainda em execução e `failed` os jobs que falharam ou foram cancelados.

### productions.importPlan(productionId, plan, opts?)
Adiciona as cenas de um plano a uma produção existente.

```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: "O ID da produção." },
plan: { type: 'Record<string, unknown>', required: true, description: "O plano a adicionar." },
mode: { type: '"append"', default: '"append"', description: "Adiciona as cenas depois das existentes." },
}}
/>

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

### productions.describe(productionId, input)
Transforma um briefing em cenas com o Diretor. Inicia um job e retorna imediatamente; as cenas são incorporadas por `reconcile()`. A produção volta com a execução registrada como um rascunho pendente.

```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: "O ID da produção." },
brief: { type: 'string', required: true, description: "Do que trata o filme." },
llmModel: { type: 'string', required: true, description: "O modelo de linguagem que esboça as cenas." },
mode: { type: '"append" | "replace"', description: "append adiciona as cenas esboçadas. replace reescreve o filme." },
label: { type: 'string', description: "Um nome para a execução." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

### Gerar imagens fixas e clipes
`generateStill()` enquadra uma tomada, `generateClip()` a anima e `generate()` faz uma coisa ou outra conforme `kind`. Uma execução envia os jobs, registra um marcador pendente na produção e retorna: nada fica esperando por minutos. A requisição é montada no servidor a partir do próprio plano, dos visuais e das referências da tomada, então um script e um clique no app produzem a mesma mídia.

```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: "O ID da produção." },
shotId: { type: 'string', required: true, description: "A tomada a enquadrar ou animar." },
kind: { type: '"still" | "clip"', description: "Só em generate(): o que fazer." },
count: { type: 'number', description: "Imagens fixas: quantos candidatos. Os resultados se somam; uma nova execução nunca substitui as anteriores." },
mode: { type: '"start" | "start-end" | "references"', description: "Clipes: quais entradas enviar. start envia só o quadro inicial, start-end os dois quadros. Se for omitido, segue as entradas salvas da tomada." },
dryRun: { type: 'boolean', description: "Retorna uma cotação de preço e não envia nada." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
overrides: { type: 'Record<string, unknown>', description: "Alterações só para esta execução, como o modelo, o prompt, a proporção ou os IDs de direção. A tomada em si não muda." },
}}
/>

```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(),
})
```

- **Faça a cotação antes.** `dryRun: true` calcula o preço da execução e não grava nada. Restrinja o tipo da resposta com `isStudioGenerateEstimate()`.
- **Repita com segurança.** Com o mesmo `clientRequestId`, uma nova tentativa responde com os jobs que a primeira chamada iniciou, marcados com `deduped: true`, e não envia nem cobra nada. Nunca repita uma chamada paga sem ele. Todas as chamadas pagas desta página o aceitam, incluindo `frame()` e `voice()`.
- **A via é escolhida para você.** Para um clipe, a rota de vídeo é escolhida a partir das entradas da tomada e retornada como `lane`: `generate-video` ou `text-to-video`.

### productions.frame(productionId, input)
Extrai uma imagem fixa do clipe ativo de uma tomada e a coloca onde `target` indica. Espera pelo job, que leva segundos, e retorna a produção alterada e a `url` da imagem.

```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: "O ID da produção." },
shotId: { type: 'string', required: true, description: "A tomada cujo clipe usar." },
mode: { type: '"first" | "last" | "timestamp"', description: "Qual quadro extrair." },
timestamp: { type: 'number', description: "O tempo em segundos, com o mode timestamp." },
target: { type: '"new-shot" | "start-frame" | "end-frame" | "still"', default: '"new-shot"', description: "Para onde vai o quadro: uma nova tomada depois desta, o quadro inicial ou final desta tomada, ou outra imagem fixa desta tomada." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

### productions.voice(productionId, input)
Gera a fala de uma tomada e a registra na tomada. Espera pelo job.

```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: "O ID da produção." },
shotId: { type: 'string', required: true, description: "A tomada." },
text: { type: 'string', required: true, description: "A fala a dizer." },
voiceId: { type: 'string', description: "A voz." },
voiceType: { type: '"premade" | "custom" | "library"', description: "O tipo de voz." },
ttsProvider: { type: 'string', description: "O modelo de fala." },
delivery: { type: 'Record<string, number>', description: "Configurações de interpretação, dentro dos limites da rota de fala." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

### productions.revoice(productionId, input)
Troca as vozes do clipe ativo de uma tomada. Leva minutos, então retorna um `jobId`, e o novo clipe é incorporado pelo marcador dele.

```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: "O ID da produção." },
shotId: { type: 'string', required: true, description: "A tomada." },
plan: { type: 'Record<string, unknown>', required: true, description: "O plano de troca de vozes, na ordem dos falantes, como a rota de troca de vozes o recebe." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

```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)
Cria a trilha sonora do filme. A faixa pronta é incorporada pelo marcador pendente dela.

```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: "O ID da produção." },
prompt: { type: 'string', required: true, description: "A música que você quer." },
duration: { type: 'number', description: "A duração em segundos." },
instrumental: { type: 'boolean', description: "Música sem vocais." },
vocalGender: { type: 'string', description: "A voz de quem canta, para música com vocais." },
model: { type: 'string', description: "O modelo de música." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

### Compartilhar e copiar
`share()` abre a visualização por link e `unshare()` a fecha de novo. Compartilhar é uma chamada própria, nunca uma operação, então quem pode ver o trabalho nunca muda como efeito colateral de uma edição. `clone()` copia uma produção que é sua ou que você pode ver para o seu próprio projeto do Studio.

```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: "O ID da produção." },
name: { type: 'string', description: "Só em clone: o nome da cópia." },
}}
/>

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

Uma cópia começa privada e não arquivada: o compartilhamento e o arquivamento nunca são copiados. Ela é copiada pela **sua** visão da origem, então a lixeira de outra pessoa não vem junto.

## client.studio: quadros planejados
Estes métodos cobrem os quadros-chave planejados e a revisão deles. Verifique `capabilities()` antes de oferecer um controle, e chame `reconcile()` uma vez ao reabrir uma produção, porque a resposta de um envio pode ter se perdido. Nada aqui inicia uma geração ou aceita um candidato, a menos que você chame o método que faz isso.

| Método | O que faz |
| --- | --- |
| [`capabilities()`](#studiocapabilities) | Lê as versões do plano e as operações que esta implantação aceita |
| [`skill()`, `list()`, `validatePlan()`, `create()`](#studioskill-list-validateplan-and-create) | As mesmas leituras e a mesma criação da camada de produções, no envelope |
| [`get(id, options?)`](#studiogetid-options) | Lê uma produção com os recursos dela |
| [`edit(id, input)`](#studioeditid-input) | Aplica operações com condições de revisão |
| [`saveEditorState(id, input)`](#studiosaveeditorstateid-input) | Salva campos comuns do editor com base na revisão carregada |
| [`generateKeyframe(id, input)`](#studiogeneratekeyframeid-input) | Gera um quadro planejado, sem aceitá-lo |
| [`generateShot(id, input)`](#studiogenerateshotid-input) | Faz a cotação ou envia uma imagem fixa ou um clipe |
| [`acceptKeyframe(id, review, concurrency?)`](#studioacceptkeyframeid-review-concurrency) | Aceita um candidato revisado |
| [`reconcile(id)`](#studioreconcileid) | Registra jobs concluídos, sem aceitar nada |
| [`setShared(id, input)`](#studiosetsharedid-input) | Compartilha ou deixa de compartilhar, vinculado à revisão que você conferiu |
| [`clone(id, input?)`](#studiocloneid-input) | Copia uma produção salva |
| [`importBundle(input)`](#studioimportbundleinput) | Importa uma produção portátil |
| [`appendBundle(id, input)`](#studioappendbundleid-input) | Anexa um pacote a uma produção |

### studio.capabilities()
Retorna as versões do plano e quais operações de quadros planejados esta implantação aceita.

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

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

`operations` tem uma flag por operação, como `readKeyframes`, `editKeyframes`, `generateKeyframes`, `acceptKeyframes`, `rejectKeyframes`, `editSequencePlans`, `generateLinkedClips`, `retakeLinkedClips`, `saveEditorState`, `revisionedSharing`, `editableSharedCopies`, `cloneLinkedProductions`, `importPlannedBundles`, `importLinkedBundles`, `appendPlannedBundles` e `appendLinkedBundles`. `automaticAcceptance` e `unattendedGeneration` são sempre `false`.

### studio.skill(), list(), validatePlan() e create()
As mesmas chamadas que `productions.skill()`, `list()`, `validatePlan()` e `create()`, retornadas no envelope `{ data }`. Em `list()`, as linhas ficam em `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(): paginação e linhas arquivadas." },
plan: { type: 'Record<string, unknown>', description: "validatePlan() e create(): o plano." },
name: { type: 'string', description: "create(): o nome da produção." },
}}
/>

```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?)
Lê uma produção com os recursos dela. Nunca incorpora jobs.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID da produção." },
detail: { type: '"summary" | "full"', description: "full adiciona todos os resultados com o contexto deles." },
shotId: { type: 'string', description: "Lê só uma tomada." },
}}
/>

```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)
Aplica operações com condições de revisão (`POST .../:id/ops`), no envelope. Use uma `baseVersion` estrita para `remove_shot`, `restore_trashed` e `purge_trashed`, e desvincule um segmento de sequência vinculado antes de remover a cena dele.

```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: "O ID da produção." },
ops: { type: 'Array<{ op: string; ... }>', required: true, description: "As operações." },
baseVersion: { type: 'number', description: "A versão que você carregou." },
strict: { type: 'boolean', description: "Recusa o rebase sobre uma versão mais recente." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

Algumas operações de quadros planejados enviadas por `edit()`:

- **`reject_keyframe_result`** registra **Needs revision** sem gerar nada. Exige `operations.rejectKeyframes`.
- **`update_sequence_plan`** edita os segmentos ordenados de uma sequência, cada um `{ shotId, startKeyframeId, endKeyframeId }`, e mantém os IDs das cenas. Exige `operations.editSequencePlans`.
- **`detach_sequence_segment`** torna um segmento independente, com `mode` definido como `clear` ou `keep-accepted`. Exige `operations.editSequencePlans`.
- **`purge_trashed`** esvazia as entradas da lixeira que você mostra, e `clear_trash` esvazia todas as lixeiras, incluindo as dos quadros planejados.

### studio.saveEditorState(id, input)
Salva campos comuns do editor com base na revisão que você carregou. Verifique `operations.saveEditorState` antes. O salvamento é sempre estrito: um conflito falha com um 409, então mantenha o rascunho local e recarregue antes de resolver o conflito.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID da produção." },
expectedVersion: { type: 'number', required: true, description: "A versão que você carregou." },
graph: { type: 'object', required: true, description: "O estado do editor a salvar." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
}}
/>

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

O salvamento não pode alterar planos de quadros, aceitação, vínculos de extremidade, histórico de jobs, entradas protegidas da lixeira nem compartilhamento. Use as ações próprias de cada um.

### studio.generateKeyframe(id, input)
Gera um quadro planejado sem aceitá-lo. Não há simulação (dry run) para quadros.

```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: "O ID da produção." },
keyframeId: { type: 'string', required: true, description: "O quadro planejado." },
expectedRevision: { type: 'number', required: true, description: "A revisão do quadro que você leu." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa. Mantenha o mesmo valor quando repetir a chamada." },
overrides: { type: 'Record<string, unknown>', description: "Alterações só para esta execução." },
}}
/>

```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()
}
```

Gerar não aceita um candidato e não cria um retrato de personagem. Uma referência de elenco que tem só uma descrição não precisa de retrato.

### studio.generateShot(id, input)
Faz a cotação ou envia uma imagem fixa ou um clipe para uma tomada.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID da produção." },
kind: { type: '"still" | "clip"', required: true, description: "O que fazer." },
shotId: { type: 'string', required: true, description: "A tomada." },
count: { type: 'number', description: "Quantos candidatos." },
mode: { type: '"start" | "start-end" | "references"', description: "Clipes: quais entradas enviar." },
dryRun: { type: 'boolean', description: "Retorna uma cotação e não envia nada." },
expectedInputHash: { type: 'string', description: "Clipes vinculados: o inputHash da cotação que você conferiu." },
retakeResultKey: { type: 'string', description: "Clipes vinculados: o take a refazer, com a requisição original dele." },
clientRequestId: { type: 'string', description: "O seu token de nova tentativa." },
overrides: { type: 'Record<string, unknown>', description: "Alterações só para esta execução." },
}}
/>

```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(),
})
}
```

**Clipes vinculados.** A cotação de um clipe entre quadros planejados inclui `inputHash`, os `endpointPins` aceitos, as configurações normalizadas de duração, resolução, proporção e som, e o `creditIdentifier` usado no preço. Passe o `inputHash` conferido como `expectedInputHash`. Se as configurações ou os quadros aceitos mudaram desde a cotação, a chamada falha com `409 sequence_quote_changed` antes de enviar qualquer coisa; peça uma nova cotação. Os créditos de uma cotação são uma estimativa: a geração reserva o preço atual.

**Refações.** Quando `operations.retakeLinkedClips` é `true`, passe `retakeResultKey` com `kind: "clip"` e `shotId`, e faça a cotação com `dryRun: true`. Envie com o `expectedInputHash` conferido e um novo `clientRequestId`, e deixe de fora `mode`, `overrides` e `count`. Uma refação reutiliza a requisição original e as imagens de quadro guardadas, mesmo depois que o plano ou a aceitação mudaram. Os takes anteriores continuam no histórico. Um take sem uma requisição original verificável ou sem imagens guardadas é recusado, e uma refação não reproduz os mesmos bytes de vídeo.

### studio.acceptKeyframe(id, review, concurrency?)
Aceita um candidato revisado para um quadro planejado. É um passo separado e explícito: a geração nunca o chama.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID da produção." },
keyframeId: { type: 'string', required: true, description: "O quadro planejado." },
expectedRevision: { type: 'number', required: true, description: "A revisão do quadro que você conferiu." },
resultKey: { type: 'string', required: true, description: "O candidato a aceitar." },
expectedAcceptedResultKey: { type: 'string | null', required: true, description: "O candidato aceito antes, ou null." },
requirementChecks: { type: 'Array<{ requirementId: string; outcome: "pass" | "waived" }>', required: true, description: "O resultado de cada verificação de requisito." },
waivedReason: { type: 'string', description: "Obrigatório quando uma verificação é dispensada." },
concurrency: { type: '{ baseVersion?, strict?, clientRequestId? }', description: "Condições de revisão para a gravação." },
}}
/>

```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
})
```

Um conflito lança o erro de sempre. O SDK nunca escolhe outro resultado nem tenta de novo com uma revisão mais recente por conta própria.

### studio.reconcile(id)
Registra jobs concluídos, sem aceitar nenhum candidato e sem iniciar uma geração. Também verifica jobs de cenas na lixeira: um clipe concluído fica no grafo armazenado dessa cena, e você o recupera com `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: "O ID da produção." },
}}
/>

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

### studio.setShared(id, input)
Compartilha uma produção ou deixa de compartilhá-la, vinculado à revisão que você conferiu. Verifique `operations.revisionedSharing` e passe `expectedVersion`: uma edição simultânea então falha com `409 workflow_conflict`, e o SDK não tenta de novo. Só quem pode alterar a visibilidade pode usar este método.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID da produção." },
shared: { type: 'boolean', required: true, description: "true abre o link de compartilhamento, false o fecha." },
allowEditableCopy: { type: 'boolean', description: "Permite que quem vê o link e entrou na conta copie a produção editável. Exige operations.editableSharedCopies." },
expectedVersion: { type: 'number', description: "A versão que você conferiu." },
}}
/>

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

Com `allowEditableCopy`, um proprietário ou administrador do espaço de trabalho permite que quem vê o link copie o plano salvo, os prompts, as descrições do elenco, as entradas de referência guardadas e o histórico de takes. A lixeira e as notas de revisão privadas nunca vêm junto. As cópias começam privadas, sem nenhum quadro aceito. Desativar a cópia, ou deixar de compartilhar, bloqueia novas cópias; as cópias já feitas continuam independentes.

### studio.clone(id, input?)
Copia uma produção salva. Verifique `operations.cloneLinkedProductions` antes de copiar uma produção com quadros vinculados, e passe a `expectedVersion` dela que você carregou: uma origem alterada falha com um 409.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "A produção a copiar." },
name: { type: 'string', description: "O nome da cópia." },
projectId: { type: 'string', description: "Um projeto seu no qual colocar a cópia." },
expectedVersion: { type: 'number', description: "A versão da origem que você carregou." },
}}
/>

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

A cópia começa privada. Ela mantém as entradas dos quadros, que contam no seu armazenamento, recebe novos IDs de quadros, cenas e sequências e não traz jobs em execução nem quadros aceitos. Revise e aceite os quadros dela antes de gerar mídia que dependa deles. Copiar não envia nenhuma geração.

### studio.importBundle(input)
Importa uma produção portátil como uma nova produção privada, com novos IDs de cenas, quadros e sequências (`POST .../import-bundle`). Verifique `operations.importPlannedBundles` para receitas e planos sem mídia, e `importLinkedBundles` para pacotes com mídia de quadros guardada.

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

<TypeTable
type={{
bundle: { type: 'Record<string, unknown>', required: true, description: "A produção portátil." },
projectId: { type: 'string', description: "Um projeto seu para o qual importar." },
}}
/>

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

Um pacote vinculado indica a produção de origem dele; o servidor verifica se ela é sua e confere cada imagem guardada antes de copiar qualquer coisa. Falta de acesso ou procedência forjada recusam a importação antes de a nova produção ser criada. Nenhum dos dois tipos de importação traz aceitação ou jobs em execução, e nenhum deles gera mídia.

### studio.appendBundle(id, input)
Anexa um pacote completo a uma produção editável, com novos IDs e uma verificação exata de revisão (`POST .../:id/import-bundle`). Um token OAuth precisa de `workflows:write`, e você precisa de acesso de edição à produção. Verifique `appendPlannedBundles` ou `appendLinkedBundles` antes.

```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: "A produção à qual anexar." },
bundle: { type: 'Record<string, unknown>', required: true, description: "O pacote a anexar." },
expectedVersion: { type: 'number', required: true, description: "A versão da produção que você carregou." },
afterShotId: { type: 'string', description: "Insere depois desta tomada. Omita-o para anexar no fim." },
applyFilm: { type: 'boolean', description: "Adota o visual de filme do pacote. A música, os cortes e o briefing do filme ficam como estão." },
}}
/>

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

As cenas, os quadros, os jobs, o compartilhamento e as outras configurações existentes continuam como estão; os papéis de elenco importados são mesclados. Os quadros importados precisam ser aceitos de novo. Um `afterShotId` desconhecido ou uma `expectedVersion` desatualizada fazem a chamada falhar.

## client.shots
Registros de tomadas por trás dos links de compartilhamento `/s/:id`, para o Share and Remix. Uma tomada guarda o estado de um construtor: as escolhas dos seletores, os prompts, os modelos de destino, as referências de menções `@` e as URLs dos resultados. Esse estado fica sob um ID de 12 caracteres impossível de adivinhar, que também é a chave de compartilhamento. As tomadas são **privadas** por padrão; compartilhar é uma mudança de visibilidade que você faz.

```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 e delete: o ID da tomada." },
mode: { type: '"single" | "multi-shot" | "frame-to-motion" | "storyboard"', description: "O modo do construtor." },
selectionState: { type: 'Record<string, unknown>', description: "As escolhas dos seletores." },
freeText: { type: 'string', description: "Texto livre do prompt." },
negativePrompt: { type: 'string', description: "O que evitar." },
assembledPrompt: { type: 'string', description: "O prompt final." },
perModelPrompts: { type: 'Record<string, string>', description: "Um prompt por modelo." },
models: { type: 'string[]', description: "Os modelos de destino." },
entityRefs: { type: 'Array<{ entitySlug, variantSlug?, role?, kind? }>', description: "Os personagens, locais, objetos e criaturas mencionados." },
resultUrls: { type: 'string[]', description: "URLs de resultados. Precisam ser URLs públicas simples, http ou https: URLs assinadas são recusadas, para que um token nunca vaze em um registro de compartilhamento." },
visibility: { type: '"private" | "public"', default: '"private"', description: "Quem pode ler a tomada." },
}}
/>

```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)
```

Uma tomada pública pode ser lida por qualquer pessoa que tenha o ID dela. Uma tomada privada só pode ser lida pelo proprietário, e as outras pessoas recebem `NotFoundError`. Só o proprietário pode atualizar ou excluir uma tomada.

## Frequently asked questions

### O que é uma produção do Studio?

Uma produção é um workflow cujas configurações guardam as tomadas de um filme: cada tomada tem uma imagem fixa enquadrada, um clipe animado opcional e o plano, os visuais, o elenco e a voz que os geraram. O app Studio e o SDK leem e gravam a mesma produção.

### Por que o client.studio tem dois conjuntos de métodos?

client.studio.productions trabalha no documento da produção: operações, imagens fixas, clipes, voz e música, e retorna o payload. client.studio cobre os quadros planejados e a revisão deles, e retorna o envelope data da API. Os dois chegam às mesmas produções.

### Como repito uma geração sem pagar duas vezes?

Passe um clientRequestId que você criou e reutilize-o quando repetir a chamada. O servidor responde com os jobs que a primeira chamada iniciou, marca a resposta como deduped e não cobra nada de novo.

### Como confiro antes o preço de uma imagem fixa ou de um clipe?

Chame generateStill ou generateClip com dryRun definido como true. A resposta é uma cotação com os créditos, e nada é enviado. Restrinja o tipo da resposta com isStudioGenerateEstimate.
