Characters
Create characters, generate portrait candidates, approve one, and add expressions, poses and motion clips from TypeScript with client.characters.
client.characters scripts everything Character Studio does: it creates and edits characters, generates portrait candidates, approves one as the character's face, and adds expressions, poses, lighting, angles and motion clips. A character keeps its portrait, its asset collections, its reference photos and a caption that describes it, so every later image and video can show the same person. The methods call the Characters REST API. See Character Studio for the editor view.
Methods
| Method | What it does |
|---|---|
list(params?) | List your characters, a page at a time |
get(id) | Read one character, with its jobs in progress |
create(input), update(id, input) and upsert(input) | Create or change a character |
delete(id) | Archive a character |
restore(id) | Bring an archived character back |
duplicate(id, input?) | Copy a character |
usage(id) | Count the workflows that use a character |
generate(input) | Generate portrait candidates |
generateAsset(input) | Generate an expression, pose, lighting or angle variant |
generateMotion(input) | Animate the portrait into a motion clip |
approvePortrait(id, candidateJobId) | Make a candidate the character's portrait |
recaption(id) | Write the character's description again |
Create a character from start to finish
The usual order: create the character, generate portrait candidates, approve one, then add variants on top.
import { createClient, StaticTokenAuth } from "@nodaro/sdk"
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",
})Read the character again with get() when the jobs finish. It then has a portrait, a smile expression and a motion clip. Wire it into generations with the Character Asset node, or mention it with @ in a prompt. See Consistent characters.
client.characters
list(params?)
Lists your characters, newest first. By default it returns active characters only.
list(params?: { projectId?: string; archived?: boolean; limit?: number; cursor?: string }): Promise<{
characters: Character[]
nextCursor: string | null
}>Prop
Type
One call returns at most limit characters, so a single call is not the full list for everyone. Page until nextCursor is null:
import type { Character } from "@nodaro/sdk"
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)Pass nextCursor back as it is, and do not store or parse it. A malformed cursor fails with validation_error instead of starting again from the first page.
get(id)
Reads one character, with three extra lists the studio uses after a reload: pendingJobs (variants still generating), portraitCandidates (candidates of the current portrait run, with their progress) and previousCandidates (earlier candidates).
get(id: string): Promise<CharacterDetail>Prop
Type
const character = await client.characters.get(characterId)
console.log(character.sourceImageUrl, character.expressions, character.motions)An archived character is still returned by id, so workflow nodes that point at it keep loading. A Character has its portrait in sourceImageUrl, six asset collections (expressions, poses, motions, angles, bodyAngles and lightingVariations, each a list of { name, url }), boards, voice, personality, canonicalDescription and identityLock.
upsert(input), create(input) and update(id, input)
upsert() creates a character when input.id is absent and updates it when id is set. create() and update() are shortcuts that set id for you. An update writes only the fields you send; the others, name included, stay as they are.
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 }>Prop
Type
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" })A name that is already in use fails with 409 name_taken.
delete(id)
Archives a character. It disappears from list() but still loads with get(id). Use restore() to bring it back.
delete(id: string): Promise<{ success: true; archived: true }>Prop
Type
await client.characters.delete(characterId)restore(id)
Brings an archived character back. When its name is now used by another active character, the server adds (restored) to the name and returns the name it used.
restore(id: string): Promise<{ id: string; name: string }>Prop
Type
const { name } = await client.characters.restore(characterId)duplicate(id, input?)
Copies a character to a new one whose name ends in (copy). The copy shares the asset URLs of the original until you generate new ones.
duplicate(id: string, input?: { nodeId?: string; projectId?: string }): Promise<{ id: string; name: string }>Prop
Type
const { id: copyId, name } = await client.characters.duplicate(characterId)usage(id)
Returns how many workflows use a character, and which ones. The editor shows it before archiving.
usage(id: string): Promise<{ workflowCount: number; workflows: Array<{ id: string; name: string }> }>Prop
Type
const { workflowCount } = await client.characters.usage(characterId)generate(input)
Generates portrait candidates (POST /v1/generate-character). With count above 1, every job is reserved before any starts, so a failure part way through rolls the whole batch back.
generate(input: GenerateCharacterInput): Promise<{ jobId: string; jobIds: string[] }>Prop
Type
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
})With attachToCharacterId and one candidate, the result becomes the portrait when the job completes. With several candidates, pick one with approvePortrait(). quality and resolution are priced exactly as in Generate Image, so a 4K or high-quality run reserves more credits. A value the model does not support is ignored, not refused.
generateAsset(input)
Generates one variant from the character's portrait: an expression, a pose, a lighting setup or an angle. With attachToCharacterId, attachToColumn and attachName, the result is added to that collection of the character when the job completes.
generateAsset(input: GenerateAssetInput): Promise<{ jobId: string }>Prop
Type
await client.characters.generateAsset({
name: "Kira",
assetType: "expressions",
variant: "smile",
attachToCharacterId: characterId,
attachToColumn: "expressions",
attachName: "smile",
})generateMotion(input)
Animates the character's portrait into a motion clip, with Generate Video in image-to-video mode. With attachToCharacterId, the clip is added to the character's motions. Without sourceImageUrl, the character's portrait is used.
generateMotion(input: GenerateMotionInput): Promise<{ jobId: string }>Prop
Type
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)
Makes a completed candidate from generate() the character's portrait. A vision model then writes the character's description, and the method returns both.
approvePortrait(id: string, candidateJobId: string): Promise<{ portraitUrl: string; canonicalDescription: string | null }>Prop
Type
const { portraitUrl, canonicalDescription } = await client.characters.approvePortrait(characterId, jobIds[0])canonicalDescription is null when the description could not be written. The portrait is still set; call recaption() to try again.
recaption(id)
Writes the character's description again from its current portrait.
recaption(id: string): Promise<{ canonicalDescription: string }>Prop
Type
const { canonicalDescription } = await client.characters.recaption(characterId)Fails with 400 no_portrait when the character has no portrait, and with a 502 when the vision model fails.
Frequently asked questions
Related
Character Studio
Consistent characters
Character Asset
Characters
Locations
Last updated on
3D scenes
Generate an editable 3D scene from a prompt, edit it, render it to MP4, and run 3D Render Pro from TypeScript with client.scene3d and the 3D scene nodes.
Locations
Create locations, generate establishing shots, approve one, and add time-of-day, weather, season, angle and motion variants from TypeScript.