# 캐릭터

> REST API로 캐릭터를 만들고 수정, 보관, 복원하며, 포트레이트 후보와 표정, 앵글, 모션 클립을 생성하고 기준 포트레이트를 승인합니다.

Source: https://nodaro.ai/ko/docs/developers/api/characters

**캐릭터 API**로 캐릭터 스튜디오에서 하는 모든 작업을 스크립트로 처리할 수 있습니다. 캐릭터를 만들고, 포트레이트 후보를 생성하고, 그중 하나를 기준 포트레이트로 승인한 다음 표정, 앵글, 포즈, 조명 변형, 모션 클립을 추가합니다. 그러면 이미지 노드와 동영상 노드가 이 캐릭터를 재사용하므로, 같은 인물이 모든 샷에서 같은 모습으로 나옵니다.

이 라우트는 모든 에디션에서 작동합니다. 인증에는 Bearer 토큰을 사용합니다. 개인 API 토큰(`ndr_…`), OAuth 앱 토큰(`ndr_app_…`), 또는 Community Edition에서는 세션 토큰을 보내세요. 모든 라우트는 호출한 사용자 범위로 한정되므로, 내 캐릭터만 보고 변경할 수 있습니다. [인증](https://nodaro.ai/docs/developers/api/authentication)을 참조하세요.

## 엔드포인트
| 메서드 | 경로 | 설명 |
| --- | --- | --- |
| `GET` | `/v1/characters` | 내 캐릭터 목록을 한 페이지씩 가져옵니다. |
| `GET` | `/v1/characters/:id` | 캐릭터 하나를 진행 중인 작업과 함께 가져옵니다. `?candidates=all`을 붙이면 `previousCandidates`에 현재 포트레이트 자신의 후보도 `isPortrait: true` 표시와 함께 나열됩니다. |
| `POST` | `/v1/characters` | 캐릭터를 만듭니다. 본문에 `id`가 있으면 해당 캐릭터를 수정합니다. |
| `POST` | `/v1/characters/:id/duplicate` | 캐릭터를 이름에 `(copy)` 접미사가 붙은 새 캐릭터로 복사합니다. |
| `DELETE` | `/v1/characters/:id` | 캐릭터를 보관합니다. 보관한 캐릭터는 복원할 수 있습니다. |
| `POST` | `/v1/characters/:id/restore` | 보관한 캐릭터를 복원합니다. |
| `GET` | `/v1/characters/:id/usage` | 캐릭터를 사용하는 워크플로의 수와 목록을 가져옵니다. |
| `POST` | `/v1/generate-character` | 포트레이트 후보를 1~10개 생성합니다. |
| `POST` | `/v1/generate-character-asset` | 표정, 포즈, 앵글, 조명 변형 중 하나를 생성합니다. |
| `POST` | `/v1/generate-character-motion` | 캐릭터에 움직임을 입혀 모션 클립을 만듭니다. |
| `POST` | `/v1/characters/:id/approve-portrait` | 후보를 포트레이트로 승인하고 캐릭터의 설명을 작성합니다. |
| `POST` | `/v1/characters/:id/llm-caption` | 현재 포트레이트로 설명을 다시 작성합니다. |

캐릭터 전용 모델 학습은 Nodaro Cloud 기능이며 별도의 라우트가 있습니다. [캐릭터 학습](https://nodaro.ai/docs/developers/api/character-training)을 참조하세요.

## 캐릭터에 담기는 정보
캐릭터는 저장된 하나의 정체성입니다. 아래 필드는 `GET /v1/characters/:id`에서 camelCase로 반환됩니다.

| 필드 | 내용 |
| --- | --- |
| `id`, `name` | 식별자와 표시 이름입니다. 이름은 대소문자를 구분하지 않고 계정마다 고유합니다. |
| `description`, `gender`, `style`, `baseOutfit` | 캐릭터를 생성하는 모든 이미지에 반영되는 정체성 메모입니다. |
| `seedPrompt` | 포트레이트의 방향을 잡는 짧은 프롬프트로, 최대 4,000자입니다. |
| `sourceImageUrl` | 기준 포트레이트입니다. 후보를 승인하면 설정됩니다. |
| `canonicalDescription` | 포트레이트를 승인할 때 Nodaro가 작성하는 80~120단어 정도의 시각적 설명입니다. 캐릭터를 참조하는 프롬프트에 이 설명이 들어갑니다. |
| `expressions`, `poses`, `angles`, `bodyAngles`, `lightingVariations`, `motions` | 에셋 버킷입니다. 각 항목은 `{ name, url }`입니다. |
| `referencePhotos` | 최대 20장의 실제 사진이며, 사진마다 프레이밍이 태그로 붙습니다. |
| `realLifeRefsByVariant`, `referenceVideosByVariant` | 변형 하나에 쓰는 추가 레퍼런스 사진이나 클립입니다. 예: `smile` 표정. |
| `person`, `wardrobe` | 캐릭터 스튜디오의 피커 페이지에서 설정한, 구조화된 외모와 의상 선택입니다. |
| `voice`, `personality` | 캐릭터의 보이스와 성격입니다. |
| `identityLock` | 생성되는 에셋이 얼굴을 얼마나 엄격하게 유지할지입니다. `off`, `soft`, `strict` 중 하나이며 기본값은 `off`입니다. |
| `deletedAt` | 캐릭터를 보관하면 설정됩니다. |

### 에셋 버킷
각 버킷에는 기준 포트레이트의 변형이 담깁니다. 변형 이름은 자유롭게 정할 수 있으며, 아래는 미리 정의된 이름입니다.

| 버킷 | 보여 주는 것 | 미리 정의된 변형 |
| --- | --- | --- |
| `expressions` | 머리와 어깨, 다른 감정 | neutral, smile, angry, surprised, sad, talking, laughing, disgusted, fearful, smirk, crying |
| `angles` | 다른 카메라 앵글에서 본 머리와 어깨 | front, 3/4 left, left profile, right profile, 3/4 right |
| `bodyAngles` | 다른 앵글에서 본 전신, 팔은 편하게 내린 자세 | front, 3/4 left, left profile, right profile, 3/4 right, back |
| `poses` | 다른 자세의 전신 | standing, walking, sitting, running, crouching, pointing, fighting stance, jumping, turning |
| `lightingVariations` | 같은 포즈, 다른 조명 | daylight, night, dramatic |
| `motions` | 캐릭터가 움직이는 동영상 클립 | 임의의 라벨. 예: walking, head turn |

## 캐릭터 목록 가져오기
`GET /v1/characters`는 내 캐릭터를 최신순으로 한 페이지 반환합니다. `nextCursor`가 `null`이 될 때까지 다음 페이지를 계속 요청하세요. 계정의 캐릭터 수가 페이지 크기보다 많으면, 응답 하나가 “모든 캐릭터”가 되는 일은 없습니다.

| 쿼리 매개변수 | 설명 |
| --- | --- |
| `limit` | 페이지당 행 수입니다. 기본값은 100이고 최대값은 500입니다. |
| `cursor` | 이전 페이지의 `nextCursor`입니다. |
| `projectId` | 한 프로젝트의 캐릭터만 가져옵니다. |
| `archived` | `true`이면 보관한 캐릭터를 대신 가져옵니다. |
| `q` | 검색: 이름이나 설명에 `q`의 단어가 모두 들어 있는 캐릭터만 가져옵니다. [이름이나 설명으로 검색](#search)을 참고하세요. |

**curl**

```bash
CURSOR=""
while :; do
PAGE=$(curl -s "https://app.nodaro.ai/v1/characters?limit=100${CURSOR:+&cursor=$CURSOR}" \
    -H "Authorization: Bearer $NODARO_API_KEY")
echo "$PAGE" | jq -r '.characters[] | "\(.id) \(.name)"'
CURSOR=$(echo "$PAGE" | jq -r '.nextCursor // empty')
[ -z "$CURSOR" ] && break
done
```

**TypeScript SDK**

```ts

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

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

**CLI**

```bash
nodaro characters list --limit 100 --json
nodaro characters list --archived
```

```json
{
"characters": [
{
"id": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
"name": "Kira",
"description": "young protagonist with auburn hair",
"sourceImageUrl": "https://cdn.nodaro.ai/characters/kira-portrait.png",
"expressions": [{ "name": "smile", "url": "https://cdn.nodaro.ai/characters/kira-smile.png" }]
}
],
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA5LTIwVDEwOjAwOjAxWiJ9"
}
```

커서는 불투명한 값입니다. 서버가 준 `nextCursor`만 그대로 다시 보내고, 릴리스가 바뀐 뒤에도 쓰려고 저장해 두지 마세요. 형식이 잘못된 커서는 조용히 첫 페이지로 돌아가지 않고 `400 validation_error`를 반환합니다. 페이지를 넘기는 동안 만든 캐릭터는 포함되지 않습니다. 이 캐릭터를 보려면 커서 없이 처음부터 다시 요청하세요.

### 이름이나 설명으로 검색
`GET /v1/characters?q=red%20car`는 **이름이나 설명**에 `q`의 단어가 모두 들어 있는 캐릭터만 나열하며, 대소문자는 구분하지 않습니다. 따라서 `red car`는 “Car, red”를 찾습니다. `GET /v1/locations`, `GET /v1/objects`, `GET /v1/creatures`, `GET /v1/faces`도 같은 `q`를 받고 같은 두 필드에서 찾습니다.

- **단어.** `q`는 공백으로 나뉘며, 처음 5개 단어만 검색합니다. 각 단어는 이름에 있어도 되고 설명에 있어도 되며, 순서도 상관없지만, 모든 단어가 들어 있어야 합니다.
- **단어의 모든 문자는 그 문자 그대로 비교합니다.** `%`, `_`, `*`는 와일드카드가 아니며, 문장 부호도 유지됩니다. `Smith,`는 쉼표까지 있어야 찾습니다. 일반적인 URL 인코딩을 사용하세요. `?q=100%25`는 `100%`를 검색합니다.
- **빈 값은 검색하지 않습니다.** 앞뒤 공백은 무시되며, 빈 `q`는 전체를 나열합니다. 200자를 넘으면 `400 validation_error`로 응답합니다.
- **`q`는 다른 모든 필터와 함께 쓸 수 있으며** 페이지 나누기와도 함께 쓸 수 있습니다. `projectId`, 그리고 보관함을 검색하려면 `archived=true`를 함께 보내세요. 각 `cursor`와 함께 같은 `q`를 계속 보내세요. 결과는 목록의 순서인 최신순을 유지합니다.
- **생성된 캡션은 검색하지 않습니다.** 내가 입력한 이름과 설명만 검색합니다.

```bash
curl -s -H "Authorization: Bearer $NODARO_API_KEY" "https://app.nodaro.ai/v1/characters?q=maya&limit=20"
```

SDK에서는 `q`를 전달하세요: `client.characters.list({ q: 'maya pilot' })`. CLI에서는 `nodaro characters list --search "maya pilot"`을 사용하세요. MCP `list_characters` 도구는 같은 검색을 `search`로 받습니다.

## 캐릭터 만들기 또는 수정
`POST /v1/characters`는 본문에 `id`가 없으면 캐릭터를 만들고, 있으면 해당 캐릭터를 수정합니다. 만들 때는 `nodeId`와 `name`이 필요합니다. `nodeId`는 캐릭터를 캔버스 노드와 연결합니다. 코드에서 만들 때는 `"scripted"` 같은 아무 라벨이나 사용하세요.

수정할 때는 보낸 필드만 기록됩니다. 생략한 필드는 기존 값을 유지하므로, 저장해도 실행 중인 작업이 채우고 있는 에셋 버킷을 덮어쓰지 않습니다. 에셋 버킷은 교체하려는 경우에만 보내세요.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/characters \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"nodeId": "scripted",
"name": "Kira",
"description": "young protagonist with auburn hair",
"style": "realistic",
"seedPrompt": "kira portrait, warm natural lighting"
}'
```

**TypeScript SDK**

```ts
const { id } = await client.characters.create({
nodeId: 'scripted',
name: 'Kira',
description: 'young protagonist with auburn hair',
style: 'realistic',
seedPrompt: 'kira portrait, warm natural lighting',
})

await client.characters.update(id, { gender: 'female', identityLock: 'soft' })
```

**CLI**

```bash
nodaro characters create --name "Kira" \
  --description "young protagonist with auburn hair" \
  --style realistic --seed-prompt "kira portrait, warm natural lighting"

nodaro characters update <id> --gender female
```

```json
{ "id": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94", "name": "Kira" }
```

<TypeTable
type={{
id: { type: 'string (uuid)', description: '수정할 캐릭터입니다. 캐릭터를 만들 때는 생략하세요.' },
nodeId: { type: 'string', description: '만들 때 필수입니다. 캐릭터가 속한 캔버스 노드이며, 노드가 없으면 아무 라벨이나 사용합니다.' },
name: { type: 'string', description: '만들 때 필수입니다. 1~200자입니다. 다른 활성 캐릭터가 이미 쓰는 이름이면 409 name_taken을 반환합니다.' },
description: { type: 'string', description: '정체성 메모로, 최대 2,000자입니다.' },
gender: { type: 'string', description: '최대 50자입니다.' },
style: { type: 'string', description: '시각 스타일입니다. 예: realistic, anime, 3d-pixar, illustration.' },
baseOutfit: { type: 'string', description: '기본 의상으로, 최대 1,000자입니다.' },
seedPrompt: { type: 'string', description: '포트레이트의 방향을 잡는 짧은 프롬프트로, 최대 4,000자입니다. 더 긴 값은 400을 반환합니다.' },
identityLock: { type: "'off' | 'soft' | 'strict'", description: '생성되는 에셋이 얼굴을 얼마나 엄격하게 유지할지입니다.', default: 'off' },
referencePhotos: { type: 'array', description: '최대 20장의 { url, kind } 사진입니다. kind는 frontFace, sideLeft, sideRight, threeQuarterLeft, threeQuarterRight, frontBody, other 중 하나입니다. other를 제외한 각 kind는 한 번씩만 쓸 수 있습니다.' },
realLifeRefsByVariant: { type: 'object', description: '변형별 레퍼런스 사진 URL입니다. 예: { "smile": [url] }. 키는 최대 20개, 키마다 URL은 최대 5개입니다. 키는 앞뒤 공백이 제거되고 소문자로 바뀝니다.' },
referenceVideosByVariant: { type: 'object', description: '라벨별 레퍼런스 클립 URL이며, 제한은 같습니다. 읽어 온 값을 동영상 생성의 referenceVideoUrls에 넣으세요.' },
person: { type: 'object', description: "구조화된 외모 선택입니다(머리, 눈, 체형, 나이 등). 캐릭터 자체의 포트레이트와 에셋 프롬프트에 추가됩니다." },
wardrobe: { type: 'object', description: '구조화된 의상 선택입니다(상의, 하의, 신발, 색상 팔레트, 시대 등). 같은 프롬프트에 추가됩니다.' },
voice: { type: 'object | null', description: '보이스입니다. 형식은 { voiceId, voiceName, traits, voiceType?, ttsProvider? }입니다. null을 보내면 보이스가 지워집니다.' },
personality: { type: 'object | null', description: '형식은 { mood, speechStyle, movementStyle, behavioralNotes }입니다.' },
canonicalDescription: { type: 'string', description: '작성된 설명을 바꿉니다. 최대 4,000자입니다.' },
projectId: { type: 'string (uuid)', description: '캐릭터를 넣을 프로젝트입니다.' },
}}
/>

`person`과 `wardrobe`는 캐릭터 자체의 포트레이트와 에셋 생성에만 반영됩니다. 이 필드는 [**인물**](https://nodaro.ai/docs/nodes/creative-controls/person) 피커와 같은 카탈로그를 사용합니다. [피커 카탈로그](https://nodaro.ai/docs/developers/picker-catalogs)를 참조하세요.

워크플로가 실행되면, 캐릭터 뒤에 연결된 [**텍스트 음성 변환**](https://nodaro.ai/docs/nodes/audio/text-to-speech) 노드에 캐릭터의 `voice`가 보이스, 보이스 유형, 권장 모델을 채웁니다. **텍스트 음성 변환** 노드 자체에 설정한 값이 우선합니다.

## 포트레이트 후보 생성
`POST /v1/generate-character`는 후보마다 작업을 하나씩 시작하고 작업 id를 바로 반환합니다. 각 작업이 `completed`가 될 때까지 [작업 API](https://nodaro.ai/docs/developers/api/jobs)로 폴링하세요.

`attachToCharacterId`를 보내면, 완료되는 후보마다 캐릭터의 포트레이트에 기록되므로 마지막으로 완료된 후보가 남습니다. 후보가 하나라면 이것으로 충분합니다. 후보가 여러 개라면 마음에 드는 후보를 승인하세요. 승인하면 포트레이트가 바뀝니다. 후보를 하나 승인할 때까지 어떤 후보도 포트레이트에 기록하지 않으려면 `attachPortrait: false`를 추가하세요.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/generate-character \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"name": "Kira",
"seedPrompt": "kira portrait, warm natural lighting",
"count": 4,
"attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94"
}'
```

**TypeScript SDK**

```ts
const { jobIds } = await client.characters.generate({
name: 'Kira',
seedPrompt: 'kira portrait, warm natural lighting',
count: 4,
attachToCharacterId: id,
})
```

**CLI**

```bash
nodaro characters generate <id> --count 4 \
  --seed-prompt "kira portrait, warm natural lighting" --watch
```

```json
{
"jobId": "a1c4e8f2-5b3d-4e6a-8f7c-2d9b1e0a3c5f",
"jobIds": [
"a1c4e8f2-5b3d-4e6a-8f7c-2d9b1e0a3c5f",
"b7d2f9a1-6c4e-4a8b-9d3f-3e0c2f1b4d6a",
"c3e5a7b9-8d1f-4c2e-a6b8-4f1d3a2c5e7b",
"d9f1b3c5-2e4a-4d6f-b8c1-5a2e4b3d6f8c"
]
}
```

<TypeTable
type={{
name: { type: 'string', description: '캐릭터 이름으로, 1~200자입니다.', required: true },
seedPrompt: { type: 'string', description: '포트레이트 프롬프트입니다. seedPrompt, description, referencePhotos 중 하나를 보내거나, 설명이 있는 캐릭터를 연결 대상으로 지정하세요.' },
description: { type: 'string', description: '정체성 메모로, 최대 2,000자입니다.' },
referencePhotos: { type: 'array', description: '포트레이트의 바탕이 되는 최대 20장의 { url, kind } 사진입니다.' },
gender: { type: 'string', description: '최대 50자입니다.' },
style: { type: 'string', description: '시각 스타일입니다.' },
baseOutfit: { type: 'string', description: '최대 1,000자입니다.' },
count: { type: 'integer', description: '생성할 후보 수로, 1~10입니다.', default: '1' },
provider: { type: 'string', description: '이미지 모델 id입니다. 생략하면 기본 모델을 사용합니다.' },
quality: { type: "'basic' | 'medium' | 'high'", description: '품질 등급이 있는 모델에서 쓰는 품질 등급입니다. 가격이 달라집니다.' },
resolution: { type: 'string', description: '지원하는 모델에서 1K, 2K, 4K, 0.5 MP, 1 MP, 2 MP, 4 MP 중 하나를 지정합니다. 가격이 달라집니다.' },
aspectRatio: { type: "'1:1' | '3:4' | '16:9' | '9:16'", description: '포트레이트의 프레임입니다.', default: '3:4' },
sourceImageUrl: { type: 'string', description: '시작할 이미지입니다.' },
attachToCharacterId: { type: 'string (uuid)', description: '결과를 포트레이트로 받을 캐릭터입니다.' },
}}
/>

`quality`와 `resolution`은 [**이미지 생성**](https://nodaro.ai/docs/nodes/image/generate-image)과 똑같이 작업 가격을 정합니다. 4K나 높은 품질로 실행하면 같은 모델의 기본 등급보다 비쌉니다. 모델이 지원하지 않는 값은 거부되지 않고 지원하는 가장 가까운 값으로 바뀌며, 크레딧은 바뀐 값을 따릅니다. 이 라우트는 `adjustments` 목록을 반환하지 않습니다. 실제로 실행된 값은 `GET /v1/jobs/:id`에서 작업의 `input_data`를 읽어 확인하세요.

## 표정, 앵글, 포즈, 조명 변형 생성
`POST /v1/generate-character-asset`은 기준 포트레이트의 변형 하나를 생성하고 `{ jobId }`를 반환합니다. 결과를 캐릭터에 추가하려면 `attachToCharacterId`, `attachToColumn`, `attachName` 세 연결 필드를 모두 보내세요. 작업이 완료되면 워커가 해당 버킷에 `{ name: attachName, url }`을 추가합니다.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/generate-character-asset \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"name": "Kira",
"assetType": "expressions",
"variant": "smile",
"attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
"attachToColumn": "expressions",
"attachName": "smile"
}'
```

**TypeScript SDK**

```ts
await client.characters.generateAsset({
name: 'Kira',
assetType: 'bodyAngles',
variant: 'front',
attachToCharacterId: id,
attachToColumn: 'body_angles',
attachName: 'front',
})
```

**CLI**

```bash
nodaro characters generate-asset <id> --asset-type expressions --variant smile --watch
```

| 필드 | 설명 |
| --- | --- |
| `name` | 필수입니다. 캐릭터 이름입니다. |
| `assetType` | 필수입니다. `expressions`, `poses`, `lighting`, `headAngles`, `angles`(`headAngles`와 같음), `bodyAngles`, `custom` 중 하나입니다. |
| `variant` | 필수입니다. 생성할 변형으로, 1~100자입니다. 예: `smile`, `3/4 left`. |
| `description` | 이 변형을 한 문장으로 설명한 것으로, 최대 1,000자입니다. 캐릭터에 연결하면서 생략하면, Nodaro가 캐릭터의 기준 설명을 바탕으로 초안을 작성합니다. |
| `sourceImageUrl` | 변형할 이미지로, 보통 승인된 포트레이트입니다. |
| `provider`, `quality`, `resolution` | 이미지 모델과 출력 등급이며, 가격은 **이미지 생성**과 같은 방식으로 매겨집니다. |
| `aspectRatio` | `1:1`, `3:4`, `16:9`, `9:16` 중 하나입니다. 기본값은 유형에 따라 다릅니다. 표정은 `1:1`, 포즈와 전신 앵글은 `9:16`, 나머지는 `3:4`입니다. |
| `attachToCharacterId`, `attachToColumn`, `attachName` | 결과가 들어갈 위치입니다. `attachToColumn`은 `expressions`, `poses`, `angles`, `body_angles`, `lighting_variations` 중 하나입니다. `custom` 변형은 열을 지정해야 합니다. |

변형을 캐릭터에 연결하면, `realLifeRefsByVariant`에서 해당 변형의 키 아래 저장된 실제 사진이 요청과 함께 자동으로 전송됩니다.

## 캐릭터 애니메이션 만들기
`POST /v1/generate-character-motion`은 캐릭터의 정지 이미지를 동영상 클립으로 바꾸고 `{ jobId }`를 반환합니다. `attachToCharacterId`와 `attachName`을 보내면, 완료된 클립이 `motions` 버킷에 추가됩니다.

캐릭터에 연결하면, Nodaro는 다음 순서로 시작 프레임을 고릅니다.

1. 내가 보낸 `sourceImageUrl`. 항상 이 값이 우선합니다.
2. `bodyAngles`의 `front` 항목. 전신 프레임은 머리와 어깨만 나온 포트레이트보다 훨씬 자연스럽게 움직입니다.
3. `bodyAngles`의 다른 항목. 가장 최근 항목부터 고릅니다.
4. 기준 포트레이트.

가장 좋은 클립을 얻으려면 첫 모션을 만들기 전에 `front` 전신 앵글을 생성하세요.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/generate-character-motion \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"name": "Kira",
"motionPrompt": "slow head turn left, soft smile",
"provider": "kling",
"attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
"attachName": "head turn"
}'
```

**TypeScript SDK**

```ts
await client.characters.generateMotion({
name: 'Kira',
motionPrompt: 'slow head turn left, soft smile',
provider: 'kling',
attachToCharacterId: id,
attachName: 'head turn',
})
```

**CLI**

```bash
nodaro characters generate-motion <id> \
  --motion-prompt "slow head turn left, soft smile" --attach-name "head turn" --watch
```

| 필드 | 설명 |
| --- | --- |
| `name` | 필수입니다. 캐릭터 이름입니다. |
| `motionPrompt` | 필수입니다. 무엇이 어떻게 움직이는지를 1~2,000자로 씁니다. |
| `provider` | 동영상 모델입니다. `kling`(기본값), `kling-turbo`, `kling-3.0`, `wan-i2v`, `wan-2.7-i2v` 중 하나입니다. |
| `sourceImageUrl` | 시작 프레임입니다. 캐릭터에 연결하지 않을 때는 필수입니다. |
| `description`, `motionDescription` | 시각적 설명(최대 1,000자)과 움직임 설명(최대 500자)입니다. 캐릭터에 연결하면서 생략하면 Nodaro가 둘 다 초안을 작성합니다. |
| `aspectRatio` | `1:1`, `3:4`, `16:9`, `9:16` 중 하나입니다. 기본값은 전신 세로 클립인 `9:16`입니다. |
| `attachToCharacterId`, `attachName` | 캐릭터와 `motions`에 들어갈 클립 이름입니다. |

다음 모델로 캐릭터 애니메이션을 만들 수 있습니다. 가격은 모델로 이미지에서 동영상을 만드는 가격입니다.

| Model | Maker | Modes | Credits | Details |
| --- | --- | --- | --- | --- |
| [Kling 2.6](https://nodaro.ai/docs/models/video/kling-2-6) | Kuaishou | Image to video, Text to video | 152부터 | Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio. |
| [Kling 2.5 Turbo Pro](https://nodaro.ai/docs/models/video/kling-2-5-turbo-pro) | Kuaishou | Image to video, Text to video | 121부터 | Faster Kling — good quality at lower cost. Supports end frame. |
| [Kling 3.0](https://nodaro.ai/docs/models/video/kling-3-0) | Kuaishou | Image to video, Text to video | 297부터 | Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P. |
| [Wan 2.6 I2V](https://nodaro.ai/docs/models/video/wan-2-6-i2v) | Alibaba | Image to video | 193부터 | Wan 2.6 image-to-video — 5/10/15s at 720p/1080p. |
| [Wan 2.7 I2V](https://nodaro.ai/docs/models/video/wan-2-7-i2v) | Alibaba | Image to video | 207 | Wan 2.7 image-to-video — 2–15s at 720p/1080p, supports start+end frame. |

## 포트레이트 승인
`POST /v1/characters/:id/approve-portrait`는 완료된 후보를 기준 포트레이트로 설정합니다. 같은 호출에서 Nodaro가 포트레이트를 보고 `canonicalDescription`을 작성합니다. 이후의 프롬프트는 이 텍스트로 캐릭터를 묘사합니다. 이 설명이 없으면 장면마다 캐릭터가 훨씬 많이 달라집니다.

후보는 내 소유의 `completed` 작업이어야 합니다. 설명을 작성하지 못해도 포트레이트는 설정되며, `canonicalDescription`은 `null`이 됩니다. 이때는 `POST /v1/characters/:id/llm-caption`을 호출해 다시 시도하세요. 두 라우트 모두 반복해서 호출해도 안전합니다. 승인은 무료입니다. llm-caption은 호출 1회에 8크레딧이 들며, 실패한 호출(`502`)은 환불됩니다.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/characters/3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94/approve-portrait \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "candidateJobId": "b7d2f9a1-6c4e-4a8b-9d3f-3e0c2f1b4d6a" }'
```

**TypeScript SDK**

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

if (canonicalDescription === null) {
await client.characters.recaption(id)
}
```

**CLI**

```bash
nodaro characters approve-portrait <id> --job <jobId>
nodaro characters recaption <id>
```

```json
{
"portraitUrl": "https://cdn.nodaro.ai/characters/kira-portrait-2.png",
"canonicalDescription": "Kira is a woman in her mid-twenties with shoulder-length auburn hair, green eyes and light freckles..."
}
```

`llm-caption`은 `{ canonicalDescription }`을 반환합니다. 캐릭터에 아직 포트레이트가 없으면 `400 no_portrait`를, 설명을 작성하지 못하면 `502`를 반환합니다.

## 캐릭터 보관, 복원, 복사
- **보관.** `DELETE /v1/characters/:id`는 캐릭터를 보관하고 `{ success: true, archived: true }`를 반환합니다. 캐릭터는 기본 목록에서 빠지지만 `GET /v1/characters/:id`는 여전히 캐릭터를 반환하므로, 캐릭터를 사용하는 워크플로는 계속 작동합니다. Nodaro Cloud에서는 보관하면 진행 중인 학습도 취소되고, 학습 크레딧이 환불되며, 학습된 모델이 제거됩니다.
- **복원.** `POST /v1/characters/:id/restore`는 `{ id, name }`을 반환합니다. 같은 이름의 활성 캐릭터가 이미 있으면 Nodaro가 `(restored)` 접미사를 붙이고 새 이름을 반환합니다.
- **영구 삭제.** 캐릭터를 영구 삭제하는 API 라우트는 없습니다. 에디터 라이브러리의 보관함 화면을 사용하세요.
- **복사.** `POST /v1/characters/:id/duplicate`는 `(copy)` 접미사가 붙은 새 캐릭터의 `{ id, name }`을 반환합니다. 복사본은 다시 생성하기 전까지 원본의 에셋 URL을 공유합니다.
- **사용처.** `GET /v1/characters/:id/usage`는 `{ workflowCount, workflows: [{ id, name }] }`을 반환합니다. 캐릭터를 보관하기 전에 확인하세요.

| 동작 | curl | TypeScript SDK | CLI |
| --- | --- | --- | --- |
| 보관 | `DELETE /v1/characters/:id` | `client.characters.delete(id)` | `nodaro characters delete <id>` |
| 복원 | `POST /v1/characters/:id/restore` | `client.characters.restore(id)` | `nodaro characters restore <id>` |
| 복사 | `POST /v1/characters/:id/duplicate` | `client.characters.duplicate(id)` | `nodaro characters duplicate <id>` |
| 사용처 | `GET /v1/characters/:id/usage` | `client.characters.usage(id)` | `nodaro characters usage <id>` |

## 전체 흐름: 만들기, 생성, 승인, 변형 추가
### 캐릭터 만들기
`nodeId`, `name`, `seedPrompt`를 넣어 `POST /v1/characters`를 호출합니다. 반환된 `id`를 저장해 두세요.

### 포트레이트 후보 생성
`count: 4`와 `attachToCharacterId`를 넣어 `POST /v1/generate-character`를 호출합니다. 각 작업 id가 `completed` 또는 `failed`가 될 때까지 폴링하세요.

### 마음에 드는 후보 승인
그 후보의 작업 id를 넣어 `POST /v1/characters/:id/approve-portrait`를 호출합니다. 포트레이트와 기준 설명이 설정됩니다.

### 변형과 모션 추가
`front` 전신 앵글을 생성한 다음, `POST /v1/generate-character-asset`으로 표정과 포즈를, `POST /v1/generate-character-motion`으로 클립을 생성하세요.

## 다른 생성에서 캐릭터 사용
에셋이 생기면 그 URL을 [**이미지 생성**](https://nodaro.ai/docs/nodes/image/generate-image)이나 [**동영상 생성**](https://nodaro.ai/docs/nodes/video/generate-video)에 레퍼런스 이미지로 전달하세요. 예를 들어 `expressions`의 `smile` 항목을 읽고, 그 URL을 프롬프트와 함께 레퍼런스로 보냅니다. 명시적인 URL은 워크플로의 연결 방식에 좌우되지 않으므로 코드에서 가장 간단한 방법입니다. 요청 필드는 [단일 노드 실행](https://nodaro.ai/docs/developers/api/nodes)을 참조하세요.

워크플로에서는 [**캐릭터 에셋**](https://nodaro.ai/docs/nodes/assets/character) 노드를 이미지 노드나 동영상 노드에 연결하거나, 프롬프트에서 `@`로 캐릭터를 언급하세요. 예: `@kira:1:smile`. 언급 문법은 [레퍼런스 역할](https://nodaro.ai/docs/guides/reference-roles)에서, 전체 방법은 [일관된 캐릭터](https://nodaro.ai/docs/guides/consistent-characters)에서 설명합니다.

## MCP에서 사용
AI 어시스턴트는 다음 도구로 같은 라우트를 사용합니다. 보관과 복원은 의도적으로 MCP에서 제공하지 않습니다.

| 도구 | 설명 |
| --- | --- |
| `list_characters`, `get_character` | 캐릭터를 찾고 에셋 URL을 읽습니다. |
| `create_character`, `update_character` | 캐릭터를 만들거나 정체성 필드를 변경합니다. |
| `generate_character` | 포트레이트(`kind: "main"`) 또는 변형(`kind: "asset"`)을 생성합니다. |
| `generate_character_motion` | 캐릭터 애니메이션 클립을 만듭니다. |
| `approve_portrait`, `recaption_character` | 포트레이트를 승인하거나 설명을 다시 작성합니다. |

[MCP 도구 레퍼런스](https://nodaro.ai/docs/mcp/tools)를 참조하세요.

## 크레딧
Nodaro Cloud에서 캐릭터 생성에는 해당 노드와 같은 크레딧 가격이 적용됩니다.

| 라우트 | 가격 |
| --- | --- |
| `POST /v1/generate-character` | 이미지 모델 가격에 `count`를 곱한 값이며, 첫 작업이 시작되기 전에 모든 후보분이 예약됩니다. |
| `POST /v1/generate-character-asset` | 변형 1개당 이미지 모델 가격입니다. |
| `POST /v1/generate-character-motion` | 클립 1개당 동영상 모델로 이미지에서 동영상을 만드는 가격입니다. |
| `approve-portrait` | 무료입니다. |
| `llm-caption` | 호출 1회당 8크레딧입니다. |

각 모델의 정확한 가격은 해당 모델 페이지에 있습니다. [크레딧](https://nodaro.ai/docs/concepts/credits)을 참조하세요.

## 오류
오류는 표준 오류 응답 형식인 `{ "error": { "code", "message" } }`를 사용합니다. [오류](https://nodaro.ai/docs/developers/api/errors)를 참조하세요.

| 상태 | 코드 | 의미 |
| --- | --- | --- |
| `400` | `validation_error` | 필드가 없거나 잘못되었거나, 커서 형식이 잘못되었거나, 생성 요청에 `seedPrompt`, `description`, `referencePhotos`가 하나도 없습니다. |
| `400` | `no_portrait` | 캐릭터에 포트레이트가 생기기 전에 `llm-caption`을 호출했습니다. |
| `401` | `unauthorized` | 토큰이 없거나, 잘못되었거나, 취소되었습니다. |
| `402` | `insufficient_credits` | Nodaro Cloud 전용입니다. 계정 잔액으로 예약 금액을 충당할 수 없습니다. |
| `404` | `not_found` | 해당 id의 캐릭터나 작업이 내 소유로 존재하지 않습니다. |
| `409` | `name_taken` | 다른 활성 캐릭터가 이미 그 이름을 쓰고 있습니다. |
| `502` | — | 기준 설명을 작성하지 못했습니다. 포트레이트는 바뀌지 않습니다. 다시 시도하세요. |

## 자주 묻는 질문

### API로 모든 생성에서 같은 캐릭터를 유지하려면 어떻게 하나요?

캐릭터를 만들고 포트레이트를 생성한 뒤 승인하세요. 그러면 Nodaro가 캐릭터의 기준 설명을 작성합니다. 캐릭터의 에셋 URL을 이미지 생성이나 동영상 생성에 레퍼런스 이미지로 전달하거나, 캐릭터를 워크플로에 연결하세요.

### 한 번의 요청으로 포트레이트 후보를 몇 개까지 생성할 수 있나요?

POST /v1/generate-character는 1~10의 count를 받습니다. 모든 후보의 크레딧은 첫 작업이 시작되기 전에 예약되며, 일괄 처리 도중 하나가 실패하면 예약 전체가 되돌려집니다.

### DELETE /v1/characters/:id를 호출하면 캐릭터가 완전히 삭제되나요?

아니요. 캐릭터가 보관되며, POST /v1/characters/:id/restore로 되돌릴 수 있습니다. 캐릭터를 영구 삭제할 수 있는 곳은 에디터의 보관함 화면뿐입니다.

### 포트레이트를 승인한 뒤 canonicalDescription이 null인 이유는 무엇인가요?

포트레이트는 설정되었지만 설명을 작성하지 못했기 때문입니다. POST /v1/characters/:id/llm-caption을 호출해 다시 시도하세요. 승인과 승인 시 작성되는 설명은 무료입니다. llm-caption은 호출 1회에 8크레딧이 들며, 실패한 호출은 환불됩니다.

### 캐릭터 애니메이션을 만들 수 있는 모델은 무엇인가요?

Kling 2.6(기본값, 공급자 id는 kling), Kling 2.5 Turbo Pro, Kling 3.0, Wan 2.6 I2V, Wan 2.7 I2V입니다. 클립 1개의 가격은 해당 모델로 이미지에서 동영상을 만드는 가격과 같습니다.
