# Produções do Studio

> Leia e edite produções do Studio via REST com operações atômicas, gere imagens fixas e clipes, incorpore jobs, planeje a exportação, compartilhe e copie.

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

A **API de produções do Studio** lê e grava os filmes que você monta no Nodaro Studio. Uma produção é um workflow do Nodaro cujas configurações guardam uma lista ordenada de tomadas. Cada tomada tem uma imagem fixa enquadrada, um clipe animado opcional e o plano, os visuais, o elenco e a voz que os produziram. Um script, um assistente de IA e o editor do Studio trabalham na mesma produção por meio destas rotas, ao mesmo tempo.

As produções do Studio só rodam no Nodaro Cloud. Uma implantação que não oferece produções retorna `404` em todas as rotas, então detecte o recurso uma vez com a rota de listagem. Em uma instalação self-hosted, monte o filme como um workflow com nós como [**Cena** (Scene)](https://nodaro.ai/docs/nodes/video/scene), [**Gerar vídeo** (Generate Video)](https://nodaro.ai/docs/nodes/video/generate-video) e [**Combinar vídeos** (Combine Videos)](https://nodaro.ai/docs/nodes/video/combine-videos). As rotas recebem um bearer token; os tokens de app OAuth precisam de `workflows:read` para ler e de `workflows:write` para gravar. Veja [Autenticação](https://nodaro.ai/docs/developers/api/authentication).

Toda rota retorna `404`, nunca `403`, quando você não tem acesso a uma produção, então não é possível sondar um ID.

### Os termos do editor e as chaves do documento
O editor e o documento dão nomes diferentes às mesmas coisas. Use as chaves do documento no código e os termos do editor com as pessoas.

| No editor | No documento |
| --- | --- |
| filme | a produção |
| cena, por exemplo Cena 3 | uma entrada de `shots[]`, identificada pelo `shotId` |
| o quadro de uma cena e os takes dele | os resultados `still` da tomada |
| o movimento de uma cena e os takes dele | os resultados `clip` da tomada |
| as tomadas dentro de um movimento | os `beats[]` da tomada |

## Endpoints
| Método | Caminho | O que faz |
| --- | --- | --- |
| `GET` | `/v1/studio/productions/skill` | O guia de criação, o catálogo, o JSON Schema do plano e o vocabulário de operações. Grátis. |
| `GET` | `/v1/studio/productions/capabilities` | Quais operações opcionais esta implantação aceita. |
| `POST` | `/v1/studio/productions/validate` | Valida um plano. Grátis, e não guarda nada. |
| `GET` | `/v1/studio/productions` | Lista as suas produções, da mais nova para a mais antiga. |
| `POST` | `/v1/studio/productions` | Cria uma produção, opcionalmente a partir de um plano. |
| `GET` | `/v1/studio/productions/:id` | Lê uma produção. Nunca grava nada. |
| `POST` | `/v1/studio/productions/:id/ops` | Aplica um lote de operações, ou mostra uma prévia dele. |
| `POST` | `/v1/studio/productions/:id/reconcile` | Transforma jobs concluídos em resultados. |
| `POST` | `/v1/studio/productions/:id/import` | Adiciona as tomadas de um plano à produção. |
| `POST` | `/v1/studio/productions/:id/describe` | Escreve o plano a partir de um briefing. |
| `POST` | `/v1/studio/productions/:id/generate` | Gera uma imagem fixa ou um clipe para uma tomada, ou faz a cotação. |
| `POST` | `/v1/studio/productions/:id/frame` | Extrai um quadro de um clipe. |
| `POST` | `/v1/studio/productions/:id/voice` | Narra uma fala sobre uma tomada. |
| `POST` | `/v1/studio/productions/:id/revoice` | Troca as vozes do clipe de uma tomada. |
| `POST` | `/v1/studio/productions/:id/music` | Gera a trilha sonora. |
| `GET` | `/v1/studio/productions/:id/export-plan` | Os passos com preço que montam o filme. |
| `POST` | `/v1/studio/productions/:id/share` | Ativa ou desativa o compartilhamento por link. |
| `POST` | `/v1/studio/productions/:id/unshare` | Desativa o compartilhamento por link. |
| `POST` | `/v1/studio/productions/:id/clone` | Copia uma produção. |

Não existe rota de exclusão. Arquivar é uma operação, e ela pode ser desfeita.

## Ler uma produção
Toda resposta vem envolvida como `{ "data": { "production": { … } } }`. O SDK desembrulha esse envelope: um método que retorna uma produção resolve para a própria produção. O nível superior da produção guarda:

| Campo | O que guarda |
| --- | --- |
| `id`, `name`, `version`, `updatedAt` | A identidade e o contador que toda gravação verifica. |
| `thumbnailUrl`, `shared`, `archived` | O quadro de capa, se o compartilhamento por link está ativo e se a produção está arquivada. |
| `film`, `cast`, `folders`, `storyboard`, `music`, `musicPlan`, `cuts` | O visual do filme, os papéis, as pastas da linha do tempo, o briefing, a trilha sonora e os cortes exportados. |
| `trash` | `{ count, items? }`: o que foi removido e pode ser restaurado. |
| `pending` | `{ stills, clips, music, draft }`: o que está sendo gerado agora. |
| `shots[]` | As tomadas na ordem da linha do tempo, cada uma com `still`, `clip`, `startFrame`, `endFrame`, `plan`, `beats`, `look`, `voice` e mais. |

`GET /v1/studio/productions/:id` aceita `detail=summary` (o padrão: contagens e as URLs ativas) ou `detail=full` (todos os resultados do histórico de cada tomada, com o contexto que os produziu). `shot_id` retorna uma tomada, o jeito barato de ler de novo depois de uma geração. `GET /v1/studio/productions` aceita `limit`, `cursor` e `includeArchived`.

Os resultados se acumulam. Gerar nunca substitui: as imagens fixas de uma tomada são todas as candidatas que ela já teve, e o mesmo vale para os clipes. Excluir uma imagem fixa nunca afeta os clipes. Um resultado é identificado pela chave de resultado, que é o ID do job quando ele tem um e, caso contrário, a URL, nunca pela posição.

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

O SDK tipa os envelopes (`version`, `rebased`, `receipts`, `warnings`, os `credits` de uma cotação, os `jobIds` de uma execução) e deixa o documento da produção como JSON aberto.

## Editar com operações
Toda alteração em uma produção é uma operação: uma edição com nome, como renomear uma tomada, movê-la, vincular um papel do elenco ou manter uma candidata. `POST /v1/studio/productions/:id/ops` valida e aplica um lote delas. O vocabulário é servido pela API, não impresso aqui: leia a parte `operating` de `GET …/skill`, que sempre corresponde à implantação com que você está falando.

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

A resposta é `{ production, version, rebased, receipts, warnings }`. Seis regras valem para todo lote:

1. **Tudo ou nada.** Uma operação inválida faz o lote inteiro ser recusado, e nada é gravado. O erro indica a operação pelo `opIndex`, contado a partir de zero. Um lote tem até 100 operações.
2. **Rebase por padrão.** `baseVersion` é informativo: um lote escrito com base em uma versão mais antiga é aplicado ao documento mais novo, e a resposta diz `rebased: true`. Envie `strict: true` para receber `409 workflow_conflict` no lugar.
3. **Identifique pela chave estável.** Uma tomada pelo ID, uma pasta ou um corte pelo ID, uma linha do elenco pelo slug do papel, um resultado pela chave de resultado. Nunca pela posição, porque o editor e um script podem editar a mesma produção ao mesmo tempo.
4. **Crie os seus próprios IDs.** Uma operação que cria uma tomada, uma pasta, um corte ou uma cópia usa o ID que você fornece. Assim, a sua cópia local e o servidor concordam em todos os IDs.
5. **As exclusões vão para a lixeira.** Uma remoção move o que foi removido para a lixeira da produção, e uma operação o restaura. Só uma exclusão definitiva explícita destrói algo.
6. **Adote a resposta.** Substitua a sua cópia por `production` em vez de mesclar, e envie `version` como o próximo `baseVersion`.

O compartilhamento não é uma operação; um lote que tenta alterá-lo é recusado. `receipts` tem uma linha no passado por operação, `{ op, summary, ids?, impact? }`: mostre essas linhas a quem quiser saber o que um assistente fez. `warnings` lista coisas que vale dizer e que não são falhas, como uma operação que não mudou nada.

### Ver a prévia de um lote antes de aplicá-lo
`dryRun: true` pergunta o que um lote faria. O servidor executa a própria gravação, no mesmo contexto e com as mesmas recusas, e para antes de salvar. A resposta é `{ dryRun: true, version, receipts, warnings }`, sem `production`. Cada recibo adiciona `class` (`S`: seguro, `D`: exclui, `P`: muda quem tem acesso ao trabalho, `$`: gasta) e, quando uma exclusão pode ser desfeita, `restorable: true`.

Uma implantação anterior às prévias ignora `dryRun` e aplica o lote. Por isso, confirme a flag primeiro, com um lote vazio, que não muda nada em nenhuma implantação:

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

Só uma resposta que traz `dryRun: true` é um sim. Qualquer outra coisa é um não: diga à pessoa que não é possível ver a prévia aqui e não envie o lote. Verifique o marcador também na resposta da prévia real, porque uma implantação em atualização pode atender a próxima requisição a partir de outro servidor. Uma resposta com `production` no lugar do marcador significa que o lote foi aplicado: adote essa resposta e não envie o lote de novo. O SDK faz as duas verificações por você e lança `StudioPreviewUnavailable` ou `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)
```

## Gerar imagens fixas e clipes
A geração funciona em dois tempos: executar e depois consultar periodicamente. `POST /v1/studio/productions/:id/generate` com `kind: "still"` ou `kind: "clip"` e um `shotId` envia os jobs, registra-os como pendentes na produção e retorna na hora com `{ jobIds, lane?, deduped?, production? }`.

- **Montada a partir da tomada.** O servidor monta a requisição com o plano, os visuais, o elenco e a direção da tomada. Por isso, uma chamada de um script e um clique no editor produzem a mesma imagem. `overrides` muda uma execução sem mudar a tomada.
- **Cote primeiro.** `dryRun: true` calcula o preço da execução e não grava nada. A resposta é `{ dryRun: true, provider, count, credits, lane? }`. `credits: null` significa que o modelo não tem preço, ou seja, o custo é desconhecido, não gratuito.
- **Repita com segurança.** `clientRequestId`, de 8 a 128 caracteres de `A-Za-z0-9_.:-` criados por você, torna uma nova tentativa segura: o mesmo ID retorna os jobs da primeira chamada com `deduped: true` e não cobra nada. Nunca repita uma chamada paga sem ele.
- **Incorpore os resultados.** `POST /v1/studio/productions/:id/reconcile` transforma os jobs concluídos em resultados e limpa os que falharam. A rota retorna `{ landed, pending, failed, warnings, production, version }`. Um `GET` nunca grava nada, então chame reconcile quando estiver esperando algo.
- **A via do clipe é escolhida automaticamente.** Para um clipe, a rota do modelo (`generate-video` ou `text-to-video`) decorre das entradas da tomada e volta como `lane`. `mode` só diz a partir de qual conjunto de entradas dirigir: `"start"` ou `"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)
```

As outras rotas de mídia trabalham em uma tomada:

| Rota | Corpo | O que faz |
| --- | --- | --- |
| `frame` | `{ shotId, mode?, timestamp?, target?, clientRequestId? }` | Extrai um quadro de um clipe. Espera alguns segundos e retorna `{ production, url? }`. |
| `voice` | `{ shotId, text, voiceId?, voiceType?, ttsProvider?, delivery?, clientRequestId? }` | Narra uma fala sobre a tomada. Espera e retorna `{ production }`. |
| `revoice` | `{ shotId, plan, clientRequestId? }` | Troca as vozes do clipe da tomada. Retorna `{ jobId, production }`; o resultado é incorporado pelo reconcile. |
| `music` | `{ prompt, duration?, instrumental?, vocalGender?, model?, clientRequestId? }` | Gera a trilha sonora. Retorna `{ jobId, production }`; o resultado é incorporado pelo reconcile. |

`clientRequestId` é aceito em todas as rotas pagas aqui, inclusive `frame` e `voice`.

### Clipes vinculados e novos takes
Algumas implantações aceitam clipes que vão de um quadro revisado a outro. Verifique primeiro `GET /v1/studio/productions/capabilities`: `operations.generateLinkedClips` e `operations.retakeLinkedClips` dizem se eles estão disponíveis.

- **Cote um clipe vinculado.** `generate` com `kind: "clip"`, o `shotId` e `dryRun: true` verifica os dois quadros e retorna uma cotação com um `inputHash`. Envie esse valor de volta como `expectedInputHash` na requisição real. Quando as configurações ou os quadros mudaram nesse meio-tempo, a rota retorna `409 sequence_quote_changed` antes de enviar qualquer coisa.
- **Gere um novo take de um clipe.** Adicione `retakeResultKey` para cotar um novo take de um clipe vinculado existente, com as configurações e os quadros originais. Depois, envie a requisição com o `inputHash` da cotação e um `clientRequestId` novo. Não envie `mode`, `overrides` nem `count`. Um take cuja requisição original ou cujos quadros não foram guardados não pode ganhar um novo take.

## Do briefing à produção
### Ler o guia
`GET /v1/studio/productions/skill` retorna `{ skill, catalog, schema, operating, generatedFrom }`: como um plano é escrito, todos os seletores, modelos e valores permitidos, o JSON Schema do plano e o vocabulário de operações.

### Validar um plano
`POST /v1/studio/productions/validate` com `{ plan }` retorna `{ valid, errors, warnings, summary? }`. Corrija o `path` de cada erro até `valid` ser `true`. Os nomes do elenco são verificados na sua própria biblioteca.

### Criar a produção
`POST /v1/studio/productions` com `{ name?, plan? }` cria a produção e incorpora o plano. `POST …/:id/import` com `{ plan, mode: "append" }` adiciona as tomadas de um plano a uma produção existente. Nenhuma das duas rotas gera mídia.

### Ou deixar o diretor escrever o plano
`POST …/:id/describe` com `{ brief, llmModel, mode?, label?, clientRequestId? }` inicia uma execução do diretor e retorna `{ jobId, production }`. Quando o job terminar, chame reconcile para incorporar as tomadas rascunhadas.

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

## Planejar a exportação
`GET /v1/studio/productions/:id/export-plan` retorna os passos ordenados que montam o filme: as mesclagens de áudio por tomada, a junção e um acabamento opcional em 4K com `upscale`. A rota não executa nada.

```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 2 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": []
}
```

Execute os passos em ordem com as rotas comuns de nós, como [**Mesclar vídeo e áudio** (Merge Video & Audio)](https://nodaro.ai/docs/nodes/video/merge-video-audio) e [Combinar vídeos](https://nodaro.ai/docs/nodes/video/combine-videos). Um valor `{ "fromStep": "…" }` é a saída de um passo anterior: substitua-o pela URL que esse passo produziu. Depois, registre o arquivo final na produção como um corte, com a operação que adiciona um corte.

`canExport` é `false` quando não há nada para montar, ou seja, menos de dois clipes. `estimate` é `null` quando algum passo não tem preço, e `unpriced` indica esses modelos, porque uma soma parcial subestimaria o custo.

## Compartilhar e copiar uma produção
- **Compartilhar.** `POST …/:id/share` com `{ shared: true }` ativa o link de compartilhamento somente leitura; `{ shared: false }` ou `POST …/:id/unshare` o desativa. Só o dono ou um administrador do espaço de trabalho pode mudar o compartilhamento (caso contrário, `403 forbidden`). Quando `operations.revisionedSharing` está disponível, adicione `expectedVersion` para vincular a mudança à versão que você revisou; uma edição mais nova então retorna `409 workflow_conflict`.
- **Permitir cópias editáveis.** Com `operations.editableSharedCopies`, o dono também pode enviar `allowEditableCopy: true` com `shared: true` e `expectedVersion`. Quem abrir o link com a sessão iniciada pode então copiar o plano, os prompts, as descrições do elenco, as entradas mantidas e o histórico de takes. A lixeira e as notas de revisão privadas não são copiadas, e desativar o compartilhamento impede novas cópias.
- **Copiar.** `POST …/:id/clone` com `{ name? }` copia uma produção a partir da sua própria visão dela. A cópia começa privada e não arquivada, e usa o seu armazenamento.

## Exportar uma linha do tempo para um app de edição
`POST /v1/freecut-export` transforma uma linha do tempo de clipes de cenas em um arquivo de projeto de edição, para que você finalize a montagem em um app de edição externo. A rota grava um arquivo FreeCut JSON (`freecut-v1`) ou Final Cut Pro XML (`fcpxml-v1.10`) no seu armazenamento e retorna a URL dele. Ela não custa créditos e aceita 10 requisições por minuto. Como o resto desta página, ela exige o Nodaro Cloud; as outras edições retornam `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` é a entrada do arquivo na sua biblioteca, ou `null` quando não foi possível criar essa entrada; a `url` é válida nos dois casos.

| Campo | O que guarda |
| --- | --- |
| `format` | Obrigatório. `json` para FreeCut JSON ou `fcpxml` para Final Cut Pro XML. |
| `timeline` | Obrigatório. A linha do tempo, descrita abaixo. |
| `name` | Um rótulo para os seus registros, até 200 caracteres. |
| `timeline.scenes` | Obrigatório. Pelo menos uma cena, na ordem de reprodução. Cada cena vira um clipe na faixa de vídeo. |
| `timeline.musicAssetUrl` | A faixa de música. Uma string vazia, o padrão, deixa a música de fora. |
| `timeline.narrationAssetUrl` | Uma faixa de narração, colocada em uma camada de áudio própria. |
| `timeline.fadeOutDurationSec` | O fade no fim da música, 0,8 segundo por padrão. Só no FreeCut JSON. |
| `scene.sceneEntityId`, `scene.compositeUrl` | Obrigatórios. O ID da cena e o clipe mesclado dela. |
| `scene.shots` | Obrigatório. Pelo menos uma tomada, `{ shot_id, duration_seconds, cut_decision? }`. As durações das tomadas somam a duração da cena. |
| `cut_decision.in_offset_sec`, `cut_decision.out_offset_sec` | Obrigatórios em uma decisão de corte. O recorte no início (lido da primeira tomada de uma cena) e no fim (lido da última tomada). |
| `cut_decision.transition_to_next` | Obrigatório em uma decisão de corte. `hard_cut`, `dissolve`, `match_cut` ou `overlap`: a transição para a próxima cena. |
| `cut_decision.transition_duration_sec` | Substitui a duração padrão da transição: 0 para `hard_cut` e `match_cut`, 0,5 segundo para `dissolve`, 1 segundo para `overlap`. |

`dissolve` e `overlap` sobrepõem os dois clipes pela duração da transição; `hard_cut` e `match_cut` encostam um no outro. Quando nenhuma tomada tem `cut_decision`, a exportação é uma concatenação simples: um clipe por cena, um após o outro, com cortes secos e a música por toda a linha do tempo. Os recortes dentro de uma cena não são aplicados, porque cada clipe de cena já vem mesclado.

## Registros para compartilhar e remixar
Um **registro de tomada** salva o estado de uma tomada que você montou: as escolhas dos seletores, os prompts, os modelos de destino, as entidades mencionadas com `@` e os resultados. Ele fica atrás de um ID curto e impossível de adivinhar, que alimenta os links de compartilhamento `/s/:id` e os remixes com um clique. Estas rotas são separadas das produções.

| Método | Caminho | Autenticação | O que faz |
| --- | --- | --- | --- |
| `POST` | `/v1/shots` | Bearer token | Cria um registro. Retorna `{ id }`. |
| `GET` | `/v1/shots/:id` | Nenhuma | Lê um registro. Um registro privado retorna `404` para todos, menos para o dono. Limitado por endereço IP. |
| `PATCH` | `/v1/shots/:id` | Dono | Atualiza quaisquer campos, inclusive `visibility`. |
| `DELETE` | `/v1/shots/:id` | Dono | Exclui o registro. |

O corpo traz `mode` (`single`, `multi-shot`, `frame-to-motion` ou `storyboard`), `selectionState` (o valor escolhido em cada seletor, como `{ pickerNodeType: valueId }` ou `{ pickerNodeType: { field: valueId } }`) e, opcionalmente, `freeText`, `negativePrompt`, `assembledPrompt`, `perModelPrompts`, `models`, `entityRefs` e `resultUrls`. `visibility` é `private` por padrão; defina como `public` para tornar o registro compartilhável.

`resultUrls` aceita só URLs públicas simples, `http` ou `https`. URLs assinadas são recusadas, para que um token nunca vaze em um registro compartilhado. Os registros trazem um `schemaVersion`. Quando um registro indica uma entrada de catálogo que não existe mais, pule essa entrada com um aviso em vez de fazer o remix falhar.

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

## Usar pelo MCP
Os assistentes de IA usam as mesmas produções por meio de ferramentas como `get_studio_production_skill`, `validate_studio_plan`, `create_studio_production`, `edit_studio_production`, `generate_studio_still`, `generate_studio_clip` e `plan_studio_export`. Ler uma produção com acesso de escrita também incorpora os jobs concluídos. Veja [Produções do Studio pelo MCP](https://nodaro.ai/docs/mcp/studio-productions).

## Erros
| Status | Código | Significado |
| --- | --- | --- |
| `400` | `validation_error` | O corpo ou o plano está errado. `path` indica o campo; na rota de exportação, `issues` lista os problemas. |
| `400` | `op_invalid`, `op_target_missing` | Em `…/ops`: uma operação estava errada, e `opIndex` indica qual. Nada foi gravado. Corrija a operação e envie o lote de novo. |
| `402` | `insufficient_credits` | A conta não tem créditos para cobrir a geração. |
| `403` | `forbidden` | Só o dono ou um administrador do espaço de trabalho pode mudar o compartilhamento. |
| `404` | `not_found` | Não existe essa produção para quem faz a chamada, ou a implantação não oferece produções. |
| `404` | `op_target_missing` | Em uma rota de geração ou de mídia: a tomada, o resultado ou o papel indicado não está na produção. |
| `409` | `workflow_conflict` | `strict: true` ou `expectedVersion` foi enviado, e a produção mudou. Leia a produção de novo e reaplique. |
| `409` | `production_busy` | A produção continuou mudando durante a gravação. Leia a produção de novo e tente outra vez. |
| `409` | `sequence_quote_changed` | As configurações ou os quadros de um clipe vinculado mudaram desde a cotação. Faça a cotação de novo. |
| `413` | `storage_exceeded` | A conta passou do limite de armazenamento. |
| `429` | `rate_limit_exceeded` | Mais de 10 exportações de linha do tempo em um minuto. Espere o tempo indicado em `Retry-After`. |

No SDK, esses erros chegam tipados: `StudioOpError` (com `opIndex`), `WorkflowConflictError` para os dois códigos `409` de uma gravação, `InsufficientCreditsError`, `StorageExceededError` e `NotFoundError`.

## Frequently asked questions

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

Um filme que você monta no Nodaro Studio. É um workflow cujas configurações guardam uma lista ordenada de tomadas, que o editor chama de cenas. Cada tomada tem uma imagem fixa enquadrada, um clipe animado opcional e o plano, os visuais, o elenco e a voz que os produziram.

### Por que preciso chamar reconcile depois de uma geração?

A geração retorna na hora e é executada em segundo plano. POST /v1/studio/productions/:id/reconcile transforma os jobs concluídos em resultados nas tomadas deles. Um GET simples nunca grava nada, então um job concluído fica pendente até você chamar reconcile.

### Como repito uma geração paga com segurança?

Envie um clientRequestId, de 8 a 128 caracteres, criado por você. Enviar o mesmo ID de novo retorna os jobs que a primeira chamada iniciou, marcados como deduped, e não cobra nada.

### Posso ver antes o que um lote de operações vai fazer?

Sim, com dryRun igual a true. Primeiro, envie um lote vazio com dryRun para confirmar que o servidor aceita prévias, porque um servidor sem esse recurso aplicaria o lote.

### Posso exportar uma linha do tempo do Studio para o Final Cut Pro ou outro app de edição?

Sim. POST /v1/freecut-export transforma uma linha do tempo de clipes de cenas em um arquivo FreeCut JSON ou Final Cut Pro XML no seu armazenamento. Não custa créditos.
