캐릭터
client.characters로 캐릭터를 만들고, 검색하고, 학습시키고, 포트레이트 후보를 생성해 하나를 승인하고, 표정, 포즈, 모션 클립을 추가하세요.
client.characters는 캐릭터 스튜디오가 하는 모든 일을 스크립트로 처리합니다. 캐릭터를 만들고 편집하고, 포트레이트 후보를 생성하고, 그중 하나를 캐릭터의 얼굴로 승인하고, 표정, 포즈, 조명, 앵글, 모션 클립을 추가합니다. 캐릭터는 포트레이트, 에셋 컬렉션, 레퍼런스 사진, 캐릭터를 설명하는 캡션을 보관하므로, 이후의 모든 이미지와 동영상이 같은 인물을 보여 줄 수 있습니다. 이 메서드들은 캐릭터 REST API를 호출합니다. 편집기 화면은 캐릭터 스튜디오를 참조하세요.
메서드
| 메서드 | 하는 일 |
|---|---|
list(params?) | 내 캐릭터를 한 페이지씩 나열 |
get(id) | 캐릭터 하나를 진행 중인 작업과 함께 읽기 |
create(input), update(id, input), upsert(input) | 캐릭터 만들기 또는 변경 |
delete(id) | 캐릭터 보관 |
restore(id) | 보관된 캐릭터 되돌리기 |
duplicate(id, input?) | 캐릭터 복사 |
usage(id) | 캐릭터를 사용하는 워크플로 수 세기 |
generate(input) | 포트레이트 후보 생성 |
generateAsset(input) | 표정, 포즈, 조명 또는 앵글 변형 생성 |
generateMotion(input) | 포트레이트를 모션 클립으로 애니메이션 |
approvePortrait(id, candidateJobId) | 후보를 캐릭터의 포트레이트로 지정 |
recaption(id) | 캐릭터 설명 다시 작성 |
train(id, input?) | Nodaro Cloud에서 캐릭터의 고충실도 모델 학습 |
trainingStatus(id) | 캐릭터 학습의 진행 상태 읽기 |
deleteLora(id) | 캐릭터의 학습된 모델 제거 |
처음부터 끝까지 캐릭터 만들기
일반적인 순서는 캐릭터를 만들고, 포트레이트 후보를 생성하고, 하나를 승인한 다음, 그 위에 변형을 추가하는 것입니다.
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",
})작업이 끝나면 get()으로 캐릭터를 다시 읽으세요. 이제 캐릭터에는 포트레이트, 미소 표정, 모션 클립이 있습니다. 캐릭터 에셋 노드로 생성에 연결하거나, 프롬프트에서 @로 언급하세요. 일관된 캐릭터를 참조하세요.
client.characters
list(params?)
내 캐릭터를 최신순으로 나열합니다. 기본적으로 활성 캐릭터만 반환합니다.
list(params?: { projectId?: string; archived?: boolean; q?: string; limit?: number; cursor?: string }): Promise<{
characters: Character[]
nextCursor: string | null
}>속성
타입
호출 한 번은 최대 limit개의 캐릭터를 반환하므로, 모든 사용자에게 호출 한 번이 전체 목록이 되지는 않습니다. nextCursor가 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)nextCursor는 받은 그대로 다시 전달하고, 저장하거나 해석하지 마세요. 형식이 잘못된 커서는 첫 페이지부터 다시 시작하지 않고 validation_error로 실패합니다.
단어로 검색. q는 이름이나 설명에 그 모든 단어가 들어 있는 캐릭터만 남기며, 순서와 대소문자는 구분하지 않습니다. 단어는 공백으로 나뉘고 처음 5개만 쓰이므로, "red car"는 “Car, red”를 찾습니다. 단어의 모든 문자는 그 문자 자체와 일치합니다. %, _, *는 와일드카드가 아닙니다. 빈 q는 검색하지 않으며, q는 최대 200자입니다. 다른 필터, 페이지 나누기와 함께 쓸 수 있으므로 각 cursor와 함께 같은 q를 보내세요.
const { characters } = await client.characters.list({ q: "maya pilot", limit: 20 })get(id)
캐릭터 하나를 읽습니다. 다시 불러온 뒤 스튜디오가 사용하는 추가 목록 세 개가 함께 들어 있습니다. pendingJobs(아직 생성 중인 변형), portraitCandidates(현재 포트레이트 실행의 후보와 그 진행 상황), previousCandidates(이전 후보)입니다.
get(id: string, options?: { candidates?: "alternates" | "all" }): Promise<CharacterDetail>속성
타입
const character = await client.characters.get(characterId)
console.log(character.sourceImageUrl, character.expressions, character.motions)보관된 캐릭터도 ID로는 여전히 반환되므로, 그 캐릭터를 가리키는 워크플로 노드는 계속 불러와집니다. Character에는 sourceImageUrl의 포트레이트, 에셋 컬렉션 여섯 개(expressions, poses, motions, angles, bodyAngles, lightingVariations, 각각 { name, url }의 목록), boards, voice, personality, canonicalDescription, identityLock이 있습니다.
upsert(input), create(input), update(id, input)
upsert()는 input.id가 없으면 캐릭터를 만들고, id가 설정되어 있으면 캐릭터를 업데이트합니다. create()와 update()는 id를 대신 설정해 주는 단축 메서드입니다. 업데이트는 보낸 필드만 기록하며, name을 포함한 나머지 필드는 그대로 유지됩니다.
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 }>속성
타입
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" })이미 사용 중인 이름이면 409 name_taken으로 실패합니다.
delete(id)
캐릭터를 보관합니다. 캐릭터는 list()에서 사라지지만 get(id)로는 여전히 불러올 수 있습니다. 되돌리려면 restore()를 사용하세요.
delete(id: string): Promise<{ success: true; archived: true }>속성
타입
await client.characters.delete(characterId)restore(id)
보관된 캐릭터를 되돌립니다. 그 이름을 지금 다른 활성 캐릭터가 사용하고 있으면, 서버가 이름에 (restored)를 붙이고 사용한 이름을 반환합니다.
restore(id: string): Promise<{ id: string; name: string }>속성
타입
const { name } = await client.characters.restore(characterId)duplicate(id, input?)
캐릭터를 이름이 (copy)로 끝나는 새 캐릭터로 복사합니다. 복사본은 새 에셋을 생성하기 전까지 원본의 에셋 URL을 공유합니다.
duplicate(id: string, input?: { nodeId?: string; projectId?: string }): Promise<{ id: string; name: string }>속성
타입
const { id: copyId, name } = await client.characters.duplicate(characterId)usage(id)
캐릭터를 사용하는 워크플로가 몇 개이고 어떤 것인지 반환합니다. 편집기는 보관하기 전에 이 정보를 보여 줍니다.
usage(id: string): Promise<{ workflowCount: number; workflows: Array<{ id: string; name: string }> }>속성
타입
const { workflowCount } = await client.characters.usage(characterId)generate(input)
포트레이트 후보를 생성합니다(POST /v1/generate-character). count가 1보다 크면 어느 작업이든 시작되기 전에 모든 작업이 예약되므로, 도중에 실패하면 일괄 처리 전체가 되돌려집니다.
generate(input: GenerateCharacterInput): Promise<{ jobId: string; jobIds: string[] }>속성
타입
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
})attachToCharacterId를 지정하면 완료되는 후보마다 포트레이트에 기록되므로, 마지막으로 완료된 후보가 남습니다. 후보가 하나이면 작업이 완료될 때 결과가 포트레이트가 됩니다. 후보가 여러 개이면 approvePortrait()로 하나를 고르세요. 후보를 하나 승인할 때까지 어떤 후보도 포트레이트에 기록하지 않으려면 attachPortrait: false를 전달하세요. quality와 resolution의 가격은 이미지 생성과 정확히 같으므로, 4K나 고품질 실행은 더 많은 크레딧을 예약합니다. 모델이 지원하지 않는 값은 거부되지 않고 무시됩니다.
generateAsset(input)
캐릭터의 포트레이트에서 변형 하나를 생성합니다. 표정, 포즈, 조명 설정 또는 앵글입니다. attachToCharacterId, attachToColumn, attachName을 지정하면, 작업이 완료될 때 결과가 캐릭터의 그 컬렉션에 추가됩니다.
generateAsset(input: GenerateAssetInput): Promise<{ jobId: string }>속성
타입
await client.characters.generateAsset({
name: "Kira",
assetType: "expressions",
variant: "smile",
attachToCharacterId: characterId,
attachToColumn: "expressions",
attachName: "smile",
})generateMotion(input)
동영상 생성의 이미지에서 동영상 모드로 캐릭터의 포트레이트를 모션 클립으로 애니메이션합니다. attachToCharacterId를 지정하면 클립이 캐릭터의 motions에 추가됩니다. sourceImageUrl이 없으면 캐릭터의 포트레이트를 사용합니다.
generateMotion(input: GenerateMotionInput): Promise<{ jobId: string }>속성
타입
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)
generate()로 만든 완료된 후보를 캐릭터의 포트레이트로 지정합니다. 그런 다음 비전 모델이 캐릭터 설명을 작성하며, 메서드는 두 가지를 모두 반환합니다.
approvePortrait(id: string, candidateJobId: string): Promise<{ portraitUrl: string; canonicalDescription: string | null }>속성
타입
const { portraitUrl, canonicalDescription } = await client.characters.approvePortrait(characterId, jobIds[0])설명을 작성하지 못하면 canonicalDescription은 null입니다. 포트레이트는 그래도 설정되므로, recaption()을 호출해 다시 시도하세요.
recaption(id)
캐릭터의 현재 포트레이트로 캐릭터 설명을 다시 작성합니다.
recaption(id: string): Promise<{ canonicalDescription: string }>속성
타입
const { canonicalDescription } = await client.characters.recaption(characterId)캐릭터에 포트레이트가 없으면 400 no_portrait로, 비전 모델이 실패하면 502로 실패합니다.
캐릭터 학습
Nodaro Cloud 전용입니다. 다른 에디션에는 이 경로가 없으며, 각 호출은 NotFoundError를 발생시킵니다. 학습은 캐릭터 자신의 이미지로 캐릭터의 고충실도 모델을 만듭니다. 그런 다음 프롬프트가 @로 그 캐릭터 하나를 언급하는 이미지 생성 노드는 학습된 모델로 실행됩니다. 편집기 화면은 캐릭터 학습을 참조하세요.
학습은 시작할 때 1,650크레딧을 예약하고, 약 15분이 걸리며, 실패하거나 취소되면 크레딧이 환불됩니다. OAuth 앱 토큰을 쓰면 train()과 deleteLora()에는 assets:write 스코프가 필요하며, 없으면 ForbiddenError를 발생시킵니다. trainingStatus()에는 스코프가 필요 없습니다. 브라우저 세션이나 개인 API 토큰에는 스코프가 필요 없습니다.
train(id, input?)
학습을 시작하고 즉시 { jobId, trainingId, triggerWord }를 반환합니다.
train(id: string, input?: { selectedImageUrls?: string[] }): Promise<TrainCharacterResult>속성
타입
기본적으로 사용할 수 있는 모든 이미지로 학습합니다. 포트레이트, 레퍼런스 사진, 표정, 포즈, 머리와 전신 앵글, 조명 변형입니다. 중복은 제거되며, 최대 20장이 쓰입니다. 서로 다른 이미지가 4장 이상 남아야 합니다. 이 메서드는 1분에 3번 호출할 수 있습니다. 이미 학습된 캐릭터를 다시 학습하면 다시 전체 가격이 듭니다.
ForbiddenError, InsufficientCreditsError, RateLimitedError, JobBlockedError 같은 타입이 있는 오류 외에, 거부는 다음 코드 중 하나를 가진 NodaroError입니다.
code | 상태 | 의미 |
|---|---|---|
insufficient_training_images | 400 | 서로 다른 이미지가 4장보다 적습니다. message에 몇 장인지 나옵니다. |
validation_error | 400 | ID나 selectedImageUrls의 형식이 잘못되었습니다. |
already_training_or_not_found | 409 | 이 캐릭터의 학습이 진행 중이거나, 내 캐릭터가 아니거나, 보관된 캐릭터입니다. |
claim_failed | 500 | 학습을 확보하지 못했으며, 아무것도 시작되지 않았습니다. 다시 시도하세요. |
training_dispatch_failed | 502 | 학습을 시작하지 못했으며, 예약된 크레딧은 환불됩니다. 이후 상태는 untrained입니다. |
public_url_not_configured, price_not_configured | 503 | 배포 환경이 학습을 위해 설정되어 있지 않습니다. |
dedup_race_winner_unresolvable | 503 | 이 요청의 중복 요청이 진행 중이었고 그 결과를 읽을 수 없었습니다. 이 호출은 아무것도 예약하지 않았습니다. 다시 시도하기 전에 trainingStatus()를 읽으세요. |
import { NodaroError } from "@nodaro/sdk"
try {
const { jobId, triggerWord } = await client.characters.train(characterId)
} catch (err) {
if (err instanceof NodaroError && err.code === "insufficient_training_images") {
// err.message: "Need at least 4 reference photos to train (have 2). …"
} else throw err
}trainingStatus(id)
캐릭터 학습의 진행 상태를 읽습니다.
trainingStatus(id: string): Promise<CharacterTrainingStatus>| 필드 | 설명 |
|---|---|
status | untrained, queued, training, succeeded, failed, cancelled 중 하나입니다. untrained는 학습한 적이 없거나, 모델이 제거되었거나, 학습을 시작하지 못했다는 뜻입니다. |
trainingId | 가장 최근의 학습입니다. |
error | 학습이 실패한 이유입니다. |
trainedAt, version | 마지막으로 성공한 학습입니다. 새 학습 중이거나, 학습이 실패 또는 취소되었거나, 시작하지 못했을 때도 값이 유지됩니다. |
triggerWord | Nodaro가 프롬프트에 대신 추가하는 단어입니다. |
imageCount | 가장 최근의 학습에 쓰인 이미지 수입니다. |
생성은 status가 succeeded일 때만 학습된 모델을 씁니다. 내 캐릭터가 아니거나 보관된 캐릭터이면 이 메서드는 NotFoundError를 발생시킵니다.
대기 도우미는 없습니다. 상태가 queued나 training인 동안 약 10초마다 폴링하세요. train()이 반환한 jobId인 학습 작업도 completed, failed, cancelled로 끝나므로, 대신 client.jobs.getStatus(jobId)를 따라가도 됩니다.
let training = await client.characters.trainingStatus(characterId)
while (training.status === "queued" || training.status === "training") {
await new Promise((resolve) => setTimeout(resolve, 10_000))
training = await client.characters.trainingStatus(characterId)
}
if (training.status !== "succeeded") console.warn(training.status, training.error)deleteLora(id)
캐릭터의 학습된 모델을 제거합니다. 진행 중인 학습을 취소하고 예약된 크레딧을 환불하며, 모델을 삭제하고 기록을 지웁니다. 그러면 trainingStatus()는 untrained를 읽고, 생성은 레퍼런스 이미지로 돌아갑니다. 되돌릴 수 없습니다. 새 모델은 새로운 유료 학습입니다.
deleteLora(id: string): Promise<void>assets:write가 없는 OAuth 앱 토큰에는 ForbiddenError를, 내 캐릭터가 아니거나 보관된 캐릭터에는 NotFoundError를 발생시킵니다.
자주 묻는 질문
Nodaro SDK로 캐릭터를 만들려면 어떻게 하나요?
nodeId와 name을 담아 client.characters.create를 호출하고, client.characters.generate로 포트레이트 후보를 만든 다음, client.characters.approvePortrait로 하나를 고르세요. 그러면 그 캐릭터를 모든 이미지 생성과 동영상 생성에 사용할 수 있습니다.
캐릭터 포트레이트를 생성하면 크레딧이 드나요?
네. 포트레이트 하나는 선택한 모델로 이미지 생성을 실행하는 것과 같은 가격입니다. quality와 resolution에 따라 가격이 달라지며, 예를 들어 4K나 고품질 실행은 더 비쌉니다.
캐릭터를 삭제하면 어떻게 되나요?
delete는 캐릭터를 보관합니다. 캐릭터는 list()에서 사라지지만 ID로는 여전히 불러올 수 있으므로, 그 캐릭터를 사용하는 워크플로는 계속 작동합니다. restore로 되돌릴 수 있습니다.
SDK로 캐릭터를 학습시키려면 어떻게 하나요?
Nodaro Cloud에서 client.characters.train(id)를 호출한 다음, 상태가 더 이상 queued나 training이 아닐 때까지 client.characters.trainingStatus(id)를 폴링하세요. 학습 한 번에 1,650크레딧이 들고, 약 15분이 걸리며, 실패하면 크레딧이 환불됩니다.
identityLock은 무슨 일을 하나요?
Nodaro가 캐릭터의 표정, 포즈 등 변형을 생성할 때 얼굴을 얼마나 강하게 유지할지 정합니다. 값은 off, soft, strict이며, 기본값은 off입니다.
관련 문서
캐릭터 스튜디오
캐릭터 일관성
캐릭터 에셋
캐릭터
캐릭터 학습
최종 업데이트