# Personagens

> Crie personagens, gere candidatos a retrato, aprove um deles e adicione expressões, poses e clipes de movimento em TypeScript com client.characters.

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

**`client.characters`** automatiza por script tudo o que o Estúdio de personagens faz: cria e edita personagens, gera candidatos a retrato, aprova um deles como o rosto do personagem e adiciona expressões, poses, iluminação, ângulos e clipes de movimento. Um personagem guarda o retrato, as coleções de mídias, as fotos de referência e uma legenda que o descreve, para que todas as imagens e vídeos seguintes mostrem a mesma pessoa. Os métodos chamam a [API REST de personagens](https://nodaro.ai/docs/developers/api/characters). Veja [Estúdio de personagens](https://nodaro.ai/docs/guides/character-studio) para a visão do editor.

## Métodos
| Método | O que faz |
| --- | --- |
| [`list(params?)`](#listparams) | Lista os seus personagens, uma página por vez |
| [`get(id)`](#getid) | Lê um personagem, com os jobs dele em andamento |
| [`create(input)`, `update(id, input)` e `upsert(input)`](#upsertinput-createinput-and-updateid-input) | Cria ou altera um personagem |
| [`delete(id)`](#deleteid) | Arquiva um personagem |
| [`restore(id)`](#restoreid) | Traz de volta um personagem arquivado |
| [`duplicate(id, input?)`](#duplicateid-input) | Copia um personagem |
| [`usage(id)`](#usageid) | Conta os workflows que usam um personagem |
| [`generate(input)`](#generateinput) | Gera candidatos a retrato |
| [`generateAsset(input)`](#generateassetinput) | Gera uma variação de expressão, pose, iluminação ou ângulo |
| [`generateMotion(input)`](#generatemotioninput) | Anima o retrato em um clipe de movimento |
| [`approvePortrait(id, candidateJobId)`](#approveportraitid-candidatejobid) | Torna um candidato o retrato do personagem |
| [`recaption(id)`](#recaptionid) | Escreve de novo a descrição do personagem |

## Criar um personagem do início ao fim
A ordem habitual: crie o personagem, gere candidatos a retrato, aprove um deles e depois adicione variações a partir dele.

```ts

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

async function waitForJob(jobId: string) {
for (;;) {
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed" || data.status === "failed" || data.status === "cancelled") return data
await new Promise((resolve) => setTimeout(resolve, 3_000))
}
}

// 1. Create the character
const { id: characterId } = await client.characters.create({
nodeId: "scripted",
name: "Kira",
description: "Young protagonist with auburn hair",
style: "realistic",
seedPrompt: "Kira portrait, warm natural lighting",
})

// 2. Generate 4 portrait candidates
const { jobIds } = await client.characters.generate({
name: "Kira",
seedPrompt: "Kira portrait, warm natural lighting",
count: 4,
attachToCharacterId: characterId,
})
const results = await Promise.all(jobIds.map(waitForJob))
const firstDone = jobIds[results.findIndex((r) => r.status === "completed")]

// 3. Approve one candidate as the portrait
const { portraitUrl, canonicalDescription } = await client.characters.approvePortrait(characterId, firstDone)

// 4. Add a smile
await client.characters.generateAsset({
name: "Kira",
assetType: "expressions",
variant: "smile",
attachToCharacterId: characterId,
attachToColumn: "expressions",
attachName: "smile",
})

// 5. Animate the portrait
await client.characters.generateMotion({
name: "Kira",
motionPrompt: "Slow head turn to the left, soft smile",
provider: "kling",
attachToCharacterId: characterId,
attachName: "head turn",
})
```

Leia o personagem de novo com `get()` quando os jobs terminarem. Ele terá então um retrato, uma expressão de sorriso e um clipe de movimento. Conecte-o às gerações com o nó [**Personagem** (Character Asset)](https://nodaro.ai/docs/nodes/assets/character) ou mencione-o com `@` em um prompt. Veja [Personagens consistentes](https://nodaro.ai/docs/guides/consistent-characters).

## client.characters
### list(params?)
Lista os seus personagens, dos mais recentes para os mais antigos. Por padrão, retorna apenas os personagens ativos.

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

<TypeTable
type={{
projectId: { type: 'string', description: "Apenas os personagens deste projeto." },
archived: { type: 'boolean', default: 'false', description: "true lista os personagens arquivados em vez dos ativos." },
limit: { type: 'number', default: '100', description: "O tamanho da página, no máximo 500." },
cursor: { type: 'string', description: "O nextCursor da página anterior." },
}}
/>

Uma chamada retorna no máximo `limit` personagens, então uma única chamada não traz a lista completa para todo mundo. Pagine até `nextCursor` ser `null`:

```ts

const all: Character[] = []
let cursor: string | undefined
do {
const page = await client.characters.list({ projectId, cursor })
all.push(...page.characters)
cursor = page.nextCursor ?? undefined
} while (cursor)
```

Passe `nextCursor` de volta como está, sem armazená-lo nem interpretá-lo. Um cursor malformado falha com `validation_error` em vez de recomeçar da primeira página.

### get(id)
Lê um personagem, com três listas extras que o estúdio usa depois de recarregar: `pendingJobs` (variações ainda em geração), `portraitCandidates` (os candidatos da execução de retrato atual, com o progresso deles) e `previousCandidates` (candidatos anteriores).

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

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

```ts
const character = await client.characters.get(characterId)
console.log(character.sourceImageUrl, character.expressions, character.motions)
```

Um personagem arquivado continua sendo retornado pelo ID, então os nós de workflow que apontam para ele continuam carregando. Um `Character` tem o retrato em `sourceImageUrl`, seis coleções de mídias (`expressions`, `poses`, `motions`, `angles`, `bodyAngles` e `lightingVariations`, cada uma uma lista de `{ name, url }`), `boards`, `voice`, `personality`, `canonicalDescription` e `identityLock`.

### upsert(input), create(input) e update(id, input)
`upsert()` cria um personagem quando `input.id` está ausente e o atualiza quando `id` está definido. `create()` e `update()` são atalhos que definem `id` por você. Uma atualização grava apenas os campos que você envia; os outros, incluindo `name`, continuam como estão.

```ts
upsert(input: UpsertCharacterInput): Promise<{ id: string; name?: string }>
create(input: Omit<UpsertCharacterInput, "id"> & { name: string }): Promise<{ id: string; name?: string }>
update(id: string, input: Omit<UpsertCharacterInput, "id">): Promise<{ id: string; name?: string }>
```

<TypeTable
type={{
id: { type: 'string', description: "Só no upsert: o personagem a atualizar. Omita-o para criar um." },
name: { type: 'string', description: "O nome. Obrigatório no create." },
nodeId: { type: 'string', description: "O nó do canvas a que o personagem pertence. Scripts podem passar qualquer rótulo, como scripted." },
projectId: { type: 'string', description: "O projeto em que guardar o personagem." },
workflowId: { type: 'string', description: "O workflow de onde o personagem vem." },
description: { type: 'string', description: "Uma descrição em texto livre." },
gender: { type: 'string', description: "O gênero." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "O estilo visual." },
baseOutfit: { type: 'string', description: "O figurino padrão." },
seedPrompt: { type: 'string', description: "O prompt de onde os retratos partem." },
sourceImageUrl: { type: 'string', description: "A URL do retrato." },
imageProvider: { type: 'string | null', description: "O modelo de imagem com que o retrato foi feito." },
canonicalDescription: { type: 'string', description: "A descrição usada nos prompts. approvePortrait() a escreve para você." },
identityLock: { type: '"off" | "soft" | "strict"', default: '"off"', description: "Com que intensidade o rosto é preservado quando as variações são geradas." },
referencePhotos: { type: 'ReferencePhoto[]', description: "Fotos de referência, cada uma no formato { url, kind }. kind é frontFace, sideLeft, sideRight, threeQuarterLeft, threeQuarterRight, frontBody ou other." },
voice: { type: '{ voiceId, voiceName, traits, voiceType?, previewUrl?, ttsProvider? } | null', description: "A voz do personagem. null a remove." },
personality: { type: '{ mood, speechStyle, movementStyle, behavioralNotes } | null', description: "Notas de personalidade para roteiros e prompts." },
'expressions, poses, lightingVariations, angles, bodyAngles, motions': { type: 'Array<{ name: string; url: string }>', description: "As coleções de mídias, cada uma substituída por inteiro." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "Painéis de referência do personagem." },
selectedAssetByVariant: { type: 'Record<string, string>', description: "O take escolhido de cada variação." },
}}
/>

```ts
const { id } = await client.characters.create({
nodeId: "scripted",
name: "Kira",
description: "Young protagonist with auburn hair",
style: "realistic",
identityLock: "strict",
})

await client.characters.update(id, { baseOutfit: "Green raincoat and boots" })
```

Um nome que já está em uso falha com `409 name_taken`.

### delete(id)
Arquiva um personagem. Ele some de `list()`, mas continua carregando com `get(id)`. Use `restore()` para trazê-lo de volta.

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

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

```ts
await client.characters.delete(characterId)
```

### restore(id)
Traz de volta um personagem arquivado. Quando o nome dele já é usado por outro personagem ativo, o servidor acrescenta `(restored)` ao nome e retorna o nome que usou.

```ts
restore(id: string): Promise<{ id: string; name: string }>
```

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

```ts
const { name } = await client.characters.restore(characterId)
```

### duplicate(id, input?)
Copia um personagem para um novo, cujo nome termina em `(copy)`. A cópia compartilha as URLs de mídias do original até você gerar novas.

```ts
duplicate(id: string, input?: { nodeId?: string; projectId?: string }): Promise<{ id: string; name: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "O personagem a copiar." },
nodeId: { type: 'string', description: "O nó do canvas a que a cópia pertence." },
projectId: { type: 'string', description: "O projeto da cópia." },
}}
/>

```ts
const { id: copyId, name } = await client.characters.duplicate(characterId)
```

### usage(id)
Retorna quantos workflows usam um personagem, e quais. O editor mostra essa informação antes de arquivar.

```ts
usage(id: string): Promise<{ workflowCount: number; workflows: Array<{ id: string; name: string }> }>
```

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

```ts
const { workflowCount } = await client.characters.usage(characterId)
```

### generate(input)
Gera candidatos a retrato (`POST /v1/generate-character`). 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 inteiro.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "O nome do personagem." },
description: { type: 'string', description: "Uma descrição do personagem." },
seedPrompt: { type: 'string', description: "O prompt a partir do qual gerar." },
userPrompt: { type: 'string', description: "Instruções extras." },
gender: { type: 'string', description: "O gênero." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "O estilo visual." },
baseOutfit: { type: 'string', description: "O figurino." },
sourceImageUrl: { type: 'string', description: "Uma foto em que basear o retrato." },
referencePhotos: { type: 'ReferencePhoto[]', description: "Fotos de referência da pessoa." },
provider: { type: 'string', description: "O ID do modelo de imagem." },
count: { type: 'number', description: "Quantos candidatos gerar." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "O formato do retrato." },
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." },
attachToCharacterId: { type: 'string', description: "O personagem em que gravar o resultado." },
}}
/>

```ts
const { jobIds } = await client.characters.generate({
name: "Kira",
seedPrompt: "Kira portrait, warm natural lighting",
count: 4,
attachToCharacterId: characterId,
provider: "gpt-image",
quality: "high", // priced as gpt-image:high
})
```

Com `attachToCharacterId` e um único candidato, o resultado vira o retrato quando o job termina. Com vários candidatos, escolha um com `approvePortrait()`. `quality` e `resolution` são cobrados exatamente como no nó [**Gerar imagem** (Generate Image)](https://nodaro.ai/docs/nodes/image/generate-image), então uma execução em 4K ou em alta qualidade reserva mais créditos. Um valor que o modelo não aceita é ignorado, não recusado.

### generateAsset(input)
Gera uma variação a partir do retrato do personagem: uma expressão, uma pose, uma configuração de iluminação ou um ângulo. Com `attachToCharacterId`, `attachToColumn` e `attachName`, o resultado é adicionado a essa coleção do personagem quando o job termina.

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

<TypeTable
type={{
assetType: { type: '"expressions" | "poses" | "lighting" | "angles" | "headAngles" | "bodyAngles" | "custom"', required: true, description: "O tipo de variação." },
variant: { type: 'string', required: true, description: "A variação a criar, como smile ou three-quarter view." },
name: { type: 'string', required: true, description: "O nome do personagem." },
description: { type: 'string', description: "A descrição do personagem." },
userPrompt: { type: 'string', description: "Instruções extras." },
sourceImageUrl: { type: 'string', description: "A imagem de partida. Em geral, o retrato." },
realLifeRefs: { type: 'string[]', description: "Fotos reais que mostram a variação." },
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." },
attachToCharacterId: { type: 'string', description: "O personagem ao qual adicionar o resultado." },
attachToColumn: { type: 'string', description: "A coleção à qual adicioná-lo, como expressions." },
attachName: { type: 'string', description: "O nome da nova entrada." },
}}
/>

```ts
await client.characters.generateAsset({
name: "Kira",
assetType: "expressions",
variant: "smile",
attachToCharacterId: characterId,
attachToColumn: "expressions",
attachName: "smile",
})
```

### generateMotion(input)
Anima o retrato do personagem em um clipe de movimento, com o [**Gerar vídeo** (Generate Video)](https://nodaro.ai/docs/nodes/video/generate-video) no modo imagem para vídeo. Com `attachToCharacterId`, o clipe é adicionado a `motions` do personagem. Sem `sourceImageUrl`, é usado o retrato do personagem.

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

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "O que o personagem faz." },
name: { type: 'string', required: true, description: "O nome do personagem." },
sourceImageUrl: { type: 'string', description: "A imagem a animar. O padrão é o retrato." },
provider: { type: 'string', description: "O ID do modelo de vídeo, como kling." },
description: { type: 'string', description: "A descrição do personagem." },
motionDescription: { type: 'string', description: "Uma descrição mais longa do movimento." },
realLifeRefs: { type: 'string[]', description: "Referências reais do movimento." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "O formato do clipe." },
attachToCharacterId: { type: 'string', description: "O personagem ao qual adicionar o clipe." },
attachName: { type: 'string', description: "O nome do novo clipe." },
}}
/>

```ts
await client.characters.generateMotion({
name: "Kira",
motionPrompt: "Slow head turn to the left, soft smile",
provider: "kling",
attachToCharacterId: characterId,
attachName: "head turn",
})
```

### approvePortrait(id, candidateJobId)
Torna um candidato concluído de `generate()` o retrato do personagem. Em seguida, um modelo de visão escreve a descrição do personagem, e o método retorna os dois.

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

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

```ts
const { portraitUrl, canonicalDescription } = await client.characters.approvePortrait(characterId, jobIds[0])
```

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

### recaption(id)
Escreve de novo a descrição do personagem a partir do retrato atual.

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

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

```ts
const { canonicalDescription } = await client.characters.recaption(characterId)
```

Falha com `400 no_portrait` quando o personagem não tem retrato, e com um 502 quando o modelo de visão falha.

## Frequently asked questions

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

Chame client.characters.create com um nodeId e um name, depois client.characters.generate para obter candidatos a retrato e client.characters.approvePortrait para escolher um. A partir daí, o personagem pode ser usado em qualquer geração de imagem ou vídeo.

### Gerar o retrato de um personagem custa créditos?

Sim. Um retrato custa o mesmo que uma execução do nó Gerar imagem (Generate Image) no modelo escolhido. quality e resolution mudam o preço; por exemplo, uma execução em 4K ou em alta qualidade custa mais.

### O que acontece quando eu excluo um personagem?

delete arquiva o personagem. Ele some de list(), mas continua carregando pelo ID, então os workflows que o usam continuam funcionando. restore o traz de volta.

### O que identityLock faz?

Define com que intensidade o rosto é preservado quando o Nodaro gera as expressões, poses e outras variações do personagem: off, soft ou strict. O padrão é off.
