# Locais

> Crie locais, gere planos de estabelecimento, aprove um deles e adicione variações de hora do dia, clima, estação, ângulo e movimento em TypeScript.

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

**`client.locations`** automatiza por script tudo o que o Estúdio de locais faz. Ele cria e edita locais, gera candidatos a plano de estabelecimento, aprova um deles como a imagem principal e adiciona variações de hora do dia, clima, estações, ângulos e iluminação, além de clipes de atmosfera. Um local guarda a imagem principal, as coleções de variações, as fotos de referência e uma legenda, para que todos os planos seguintes mostrem o mesmo lugar. Os métodos chamam a [API REST de locais](https://nodaro.ai/docs/developers/api/locations). Veja [Locais](https://nodaro.ai/docs/guides/locations) para a visão do editor.

## Métodos
| Método | O que faz |
| --- | --- |
| [`list(params?)`](#listparams) | Lista os seus locais |
| [`listArchived(params?)`](#listarchivedparams) | Lista os seus locais arquivados |
| [`get(id)`](#getid) | Lê um local, com os jobs dele em andamento |
| [`create(input)`](#createinput) | Cria um local |
| [`update(id, input)`](#updateid-input) | Altera um local |
| [`delete(id)` e `restore(id)`](#deleteid-and-restoreid) | Arquiva um local ou o traz de volta |
| [`generate(input)`](#generateinput) | Gera candidatos a plano de estabelecimento |
| [`generateAsset(input)`](#generateassetinput) | Gera uma variação de hora do dia, clima, estação, ângulo ou iluminação |
| [`generateSurroundContinuation(input)`](#generatesurroundcontinuationinput) | Gera a próxima vista de um anel de 360 graus |
| [`generateMotion(input)`](#generatemotioninput) | Anima a imagem principal em um clipe de atmosfera |
| [`removeAsset(id, data)`](#removeassetid-data) | Remove um take de uma coleção de variações |
| [`approveMainImage(id, candidateJobId)`](#approvemainimageid-candidatejobid) | Torna um candidato a imagem principal |
| [`recaption(id)`](#recaptionid) | Escreve de novo a descrição do local |

## client.locations
### list(params?)
Lista os seus locais. Por padrão, retorna apenas os locais ativos. A paginação é opcional: sem `limit`, você recebe a lista inteira e nenhum cursor; com `limit`, no máximo 500, você recebe uma página e um `nextCursor`.

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

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true lista os locais arquivados em vez dos ativos." },
limit: { type: 'number', description: "O tamanho da página, no máximo 500. Omita-o para receber a lista inteira." },
cursor: { type: 'string', description: "O nextCursor da página anterior." },
}}
/>

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

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

Continue paginando até `nextCursor` ser `null`.

### listArchived(params?)
Lista os seus locais arquivados. É um atalho para `list({ archived: true })`.

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

<TypeTable
type={{
limit: { type: 'number', description: "O tamanho da página, no máximo 500." },
cursor: { type: 'string', description: "O nextCursor da página anterior." },
}}
/>

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

### get(id)
Lê um local, com `pendingJobs`, as variações ainda em geração, e `previousCandidates`, até 5 candidatos a imagem principal de gerações anteriores, dos mais recentes para os mais antigos. Promova um deles com `approveMainImage()`.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
}}
/>

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

Um local arquivado continua sendo retornado pelo ID, então os nós de workflow que apontam para ele continuam carregando. Um `Location` tem a imagem principal em `sourceImageUrl`, seis coleções (`timeOfDay`, `weather`, `seasons`, `angles`, `lighting` e `atmosphereMotions`, cada uma uma lista de `{ name, url }`), `boards`, `referencePhotos`, `canonicalDescription`, `styleLock` e `updatedAt`.

### create(input)
Cria um local. `name` e `nodeId` são obrigatórios. Um script sem nó no canvas pode passar `"mcp-managed"` como `nodeId`.

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

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "O nó do canvas a que o local pertence, ou mcp-managed." },
name: { type: 'string', required: true, description: "O nome." },
description: { type: 'string', description: "Uma descrição em texto livre." },
category: { type: 'string', description: "A categoria, como urban ou nature." },
style: { type: 'string', description: "O estilo visual, como realistic ou anime." },
projectId: { type: 'string', description: "O projeto em que guardá-lo." },
workflowId: { type: 'string', description: "O workflow de onde ele vem." },
sourceImageUrl: { type: 'string', description: "A URL da imagem principal." },
imageProvider: { type: 'string | null', description: "O modelo de imagem com que a imagem principal foi feita." },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "Fotos de mood board, no máximo 20, cada uma no formato { url, kind }. kind é wide, interior, exterior, detail, moodBoard ou other." },
canonicalDescription: { type: 'string', description: "A descrição usada nos prompts." },
styleLock: { type: 'boolean', description: "Mantém as variações no estilo aprovado do local." },
}}
/>

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

### update(id, input)
Altera um local. Só os campos que você envia são gravados. As coleções de variações não fazem parte desta chamada, porque os jobs de geração as alimentam enquanto você trabalha. Use `generateAsset()` e `removeAsset()` para elas.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
name: { type: 'string', description: "O nome." },
description: { type: 'string', description: "Uma descrição em texto livre." },
category: { type: 'string', description: "A categoria." },
style: { type: 'string', description: "O estilo visual." },
sourceImageUrl: { type: 'string', description: "A URL da imagem principal." },
imageProvider: { type: 'string | null', description: "O modelo de imagem da imagem principal." },
referencePhotos: { type: 'LocationReferencePhoto[]', description: "Fotos de mood board, no máximo 20." },
canonicalDescription: { type: 'string', description: "A descrição usada nos prompts." },
styleLock: { type: 'boolean', description: "Mantém as variações no estilo aprovado." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "Painéis de referência do local." },
selectedAssetByVariant: { type: 'Record<string, string>', description: "O take escolhido de cada variação." },
piiConsentAt: { type: 'string', description: "Um horário ISO 8601 que registra quando o usuário confirmou que tem os direitos sobre as fotos de referência. Defina-o quando anexar referencePhotos pela primeira vez." },
expectedUpdatedAt: { type: 'string', description: "O valor de updatedAt que você leu. Se o local tiver mudado desde então, a atualização falha com 409 concurrent_modification." },
}}
/>

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

O `409 concurrent_modification` chega como um `NodaroError` simples com esse `code`. Leia o local de novo, mescle a sua alteração e tente outra vez.

### delete(id) e restore(id)
`delete()` arquiva um local, e `restore()` o traz de volta. A exclusão permanente só está disponível no app do Nodaro. Quando o nome restaurado coincide com o nome de um local ativo, sem diferenciar maiúsculas de minúsculas, o servidor acrescenta `(restored)` e retorna o nome que usou.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
}}
/>

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

### generate(input)
Gera candidatos a plano de estabelecimento (`POST /v1/generate-location`). Com `count` acima de 1, todos os jobs são reservados antes que qualquer um comece, então uma falha no meio do caminho desfaz o lote.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "O nome do local." },
description: { type: 'string', description: "Como o lugar é." },
userPrompt: { type: 'string', description: "Instruções extras." },
category: { type: '"indoor" | "outdoor" | "urban" | "nature" | "fantasy" | "sci-fi" | "historical" | "futuristic" | "other"', description: "A categoria." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "O estilo visual." },
sourceImageUrl: { type: 'string', description: "Uma foto em que basear o plano." },
provider: { type: 'string', description: "O ID do modelo de imagem." },
count: { type: 'number', description: "Quantos candidatos gerar." },
quality: { type: 'string', description: "medium, high ou basic, nos modelos com configuração de qualidade. Muda o preço." },
resolution: { type: 'string', description: "1K, 2K, 4K, 0.5 MP, 1 MP, 2 MP ou 4 MP, nos modelos que oferecem essa opção. Muda o preço." },
attachToLocationId: { type: 'string', description: "O local em que gravar um resultado único." },
}}
/>

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

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

Com `attachToLocationId` e `count` igual a 1, o resultado vira a imagem principal quando o job termina. Caso contrário, escolha um candidato com `approveMainImage()`. `quality` e `resolution` são cobrados como no nó [**Gerar imagem** (Generate Image)](https://nodaro.ai/docs/nodes/image/generate-image). Um valor que o modelo não aceita é ignorado.

### generateAsset(input)
Gera uma variação do local (`POST /v1/generate-location-asset`). Com `attachToLocationId`, `attachToColumn` e `attachName`, o resultado é adicionado a essa coleção quando o job termina.

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

<TypeTable
type={{
assetType: { type: '"timeOfDay" | "weather" | "seasons" | "angles" | "lighting" | "custom"', required: true, description: "O tipo de variação." },
variant: { type: 'string', required: true, description: "A variação, como golden hour, storm, winter, aerial ou neon." },
name: { type: 'string', required: true, description: "O nome do local." },
description: { type: 'string', description: "A descrição do local." },
userPrompt: { type: 'string', description: "Instruções extras." },
sourceImageUrl: { type: 'string', description: "A imagem de partida. Em geral, a imagem principal." },
provider: { type: 'string', description: "O ID do modelo de imagem." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "O formato da imagem." },
quality: { type: 'string', description: "Como em generate(). Muda o preço." },
resolution: { type: 'string', description: "Como em generate(). Muda o preço." },
attachToLocationId: { type: 'string', description: "O local ao qual adicionar o resultado." },
attachToColumn: { type: 'string', description: "A coleção: time_of_day, weather, seasons, angles, lighting, atmosphere_motions, sheets ou detail_closeups." },
attachName: { type: 'string', description: "O nome da nova entrada." },
}}
/>

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

### generateSurroundContinuation(input)
Gera a próxima vista de um anel de 360 graus (`POST /v1/generate-surround-continuation`), continuando a partir da vista anterior. O servidor mantém metade da vista anterior exatamente como está, pinta a outra metade e harmoniza as cores, para que vistas vizinhas se juntem sem emenda. Este método funciona no Nodaro Cloud; as outras edições respondem `403 edition_required` antes de qualquer processamento, e o SDK lança isso como `ForbiddenError`.

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

<TypeTable
type={{
referenceImageUrl: { type: 'string', required: true, description: "A vista anterior do anel." },
direction: { type: '"right" | "up" | "down"', required: true, description: "Em que direção o anel continua." },
degrees: { type: 'number', description: "Quanto a vista gira, como 45." },
carriedFraction: { type: 'number', default: '0.5', description: "Quanto da vista anterior é mantido sem alterações." },
userPrompt: { type: 'string', description: "Instruções extras." },
provider: { type: 'string', description: "O ID do modelo de imagem." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "O formato da imagem." },
attachToLocationId: { type: 'string', description: "O local ao qual adicionar a vista." },
attachToColumn: { type: 'string', description: "A coleção. O estúdio usa angles." },
attachName: { type: 'string', description: "O nome da nova entrada, como Surround 45°." },
}}
/>

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

### generateMotion(input)
Anima a imagem do local em um clipe de atmosfera (`POST /v1/generate-location-motion`), com o [**Gerar vídeo** (Generate Video)](https://nodaro.ai/docs/nodes/video/generate-video) no modo imagem para vídeo. Um local tem uma única coleção de movimento, então o clipe sempre vai para `atmosphereMotions` e não existe `attachToColumn`.

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

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "O que se move na cena." },
sourceImageUrl: { type: 'string', required: true, description: "A imagem a animar, em geral a imagem principal." },
name: { type: 'string', required: true, description: "O nome do local." },
provider: { type: 'string', description: "O ID do modelo de vídeo, como kling." },
category: { type: 'string', description: "A categoria." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "O estilo visual." },
canonicalDescription: { type: 'string', description: "A descrição do local." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "O formato do clipe." },
attachToLocationId: { type: 'string', description: "O local ao qual adicionar o clipe." },
attachName: { type: 'string', description: "O nome do novo clipe." },
}}
/>

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

### removeAsset(id, data)
Remove um take de uma coleção de variações (`POST /v1/locations/:id/remove-asset`). Todas as entradas com essa `url` são removidas de uma vez. Use-o, por exemplo, antes de gerar de novo uma vista de 360 graus.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
column: { type: 'LocationAttachColumn', required: true, description: "A coleção, como angles ou weather." },
url: { type: 'string', required: true, description: "A URL do take a remover." },
}}
/>

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

Lança `NotFoundError` quando a URL não está nessa coleção ou o local não é seu.

### approveMainImage(id, candidateJobId)
Torna um candidato concluído de `generate()` a imagem principal do local. Em seguida, um modelo de visão escreve a descrição do local, e o método retorna as duas.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
candidateJobId: { type: 'string', required: true, description: "O ID do job do candidato concluído." },
}}
/>

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

`canonicalDescription` é `null` quando não foi possível escrever a descrição. A imagem principal é definida mesmo assim; chame `recaption()` para tentar de novo.

### recaption(id)
Escreve de novo a descrição do local a partir da imagem principal atual.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do local." },
}}
/>

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

Falha com `400 no_source_image` quando o local não tem imagem principal, e com um 502 quando o modelo de visão falha.

## Frequently asked questions

### Como criar um local com o SDK do Nodaro?

Chame client.locations.create com um nodeId e um name, gere planos de estabelecimento com client.locations.generate e aprove um deles com client.locations.approveMainImage.

### Quais variações posso gerar para um local?

Hora do dia, clima, estações, ângulos e iluminação, além de variações personalizadas, com generateAsset, e clipes de atmosfera com generateMotion. No Nodaro Cloud, generateSurroundContinuation adiciona vistas de 360 graus.

### Como evitar sobrescrever um local que outra pessoa alterou?

Passe expectedUpdatedAt, o valor de updatedAt que você leu, para client.locations.update. Se o local tiver mudado, a chamada falha com 409 concurrent_modification. Leia o local de novo, mescle e tente outra vez.

### Posso excluir um local de forma permanente com o SDK?

Não. delete arquiva o local, e restore o traz de volta. A exclusão permanente só está disponível no app do Nodaro.
