# 오브젝트 및 크리처

> Nodaro SDK로 TypeScript에서 오브젝트와 크리처를 만들고, 대표 이미지와 베리언트를 생성해 애니메이션화하며, 크리처가 말하게 하는 방법을 안내합니다.

Source: https://nodaro.ai/ko/docs/developers/sdk/objects-and-creatures

<strong>`client.objects`</strong>는 오브젝트/소품 스튜디오가 소품, 제품, 탈것에 대해 하는 모든 작업을 스크립트로 처리하고, <strong>`client.creatures`</strong>는 동물과 크리처에 대해 같은 일을 합니다. 두 리소스 모두 항목을 만들고 수정하며, 대표 이미지 후보를 생성해 그중 하나를 승인하고, 베리언트와 모션 클립을 추가하며, 이후 프롬프트에서 항목을 일관되게 유지하는 설명을 작성합니다. 이 메서드들은 [오브젝트](https://nodaro.ai/docs/developers/api/objects) REST API와 [크리처](https://nodaro.ai/docs/developers/api/creatures) REST API를 호출합니다. 에디터에서 보는 화면은 [오브젝트](https://nodaro.ai/docs/guides/objects)와 [크리처](https://nodaro.ai/docs/guides/creatures)를 참고하세요.

## 메서드
두 리소스의 메서드는 같습니다.

| 메서드 | 기능 |
| --- | --- |
| [`objects.list(params?)`](#objectslistparams), [`creatures.list(params?)`](#creatureslistparams) | 내 항목 목록을 반환합니다 |
| [`objects.listArchived(params?)`](#objectslistarchivedparams) | 보관된 내 항목 목록을 반환합니다 |
| [`objects.get(id)`](#objectsgetid) | 항목 하나를 진행 중인 작업과 함께 읽어 옵니다 |
| [`objects.create(input)`](#objectscreateinput), [`creatures.create(input)`](#creaturescreateinput) | 항목을 만듭니다 |
| [`objects.update(id, input)`](#objectsupdateid-input), [`creatures.update(id, input)`](#creaturesupdateid-input) | 항목을 변경합니다 |
| [`objects.delete(id)` 및 `restore(id)`](#objectsdeleteid-and-restoreid) | 항목을 보관하거나 다시 복원합니다 |
| [`objects.permanentDelete(id)`](#objectspermanentdeleteid) | 보관된 항목과 그 파일을 완전히 삭제합니다 |
| [`objects.generate(input)`](#objectsgenerateinput), [`creatures.generate(input)`](#creaturesgenerateinput) | 대표 이미지 후보를 생성합니다 |
| [`objects.generateAsset(input)`](#objectsgenerateassetinput), [`creatures.generateAsset(input)`](#creaturesgenerateassetinput) | 베리언트를 생성합니다 |
| [`objects.generateMotion(input)`](#objectsgeneratemotioninput), [`creatures.generateMotion(input)`](#creaturesgeneratemotioninput) | 대표 이미지를 클립으로 애니메이션화합니다 |
| [`objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)`](#objectsapprovemainimageid-candidatejobid-expectedupdatedat) | 후보를 대표 이미지로 만듭니다 |
| [`objects.recaption(id)`](#objectsrecaptionid) | 설명을 다시 작성합니다 |

## client.objects
오브젝트에는 `sourceImageUrl`의 대표 이미지, 컬렉션 4개(`angles`, `materials`, `variations`, `motionClips`이며 각각 `{ name, url }` 목록), `boards`, `referencePhotos`, `canonicalDescription`, `styleLock`이 있습니다. `category`는 `furniture`, `vehicle`, `weapon`, `food`, `clothing`, `electronics`, `nature`, `tool`, `animal`, `other` 중 하나입니다.

`Object`는 JavaScript 전역 객체와 이름이 같습니다. 둘 다 필요하면 다른 이름으로 가져오세요. `import type { Object as NodaroObject } from "@nodaro/sdk"`

### objects.list(params?)
내 오브젝트 목록을 반환합니다. 기본적으로 활성 오브젝트만 반환합니다. 페이지 나누기는 선택 사항입니다. `limit` 없이 호출하면 전체 목록을 받고, 최대 500인 `limit`을 지정하면 한 페이지와 `nextCursor`를 받습니다.

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

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true이면 대신 보관된 오브젝트를 나열합니다." },
projectId: { type: 'string', description: "이 프로젝트의 오브젝트만 반환합니다." },
q: { type: 'string', description: "검색어입니다. 이름이나 설명에 모든 단어가 들어 있는 오브젝트만 반환하며, 순서와 대소문자는 구분하지 않습니다. characters.list()와 같은 검색입니다." },
limit: { type: 'number', description: "페이지 크기로, 최대 500입니다. 생략하면 전체 목록을 받습니다." },
cursor: { type: 'string', description: "이전 페이지의 nextCursor입니다." },
}}
/>

```ts
const { objects } = await client.objects.list()
const { objects: cars } = await client.objects.list({ q: "red car" })
const page = await client.objects.list({ limit: 100 })
```

### objects.listArchived(params?)
보관된 내 오브젝트 목록을 반환하며, `list({ archived: true })`의 단축 메서드입니다. `creatures.listArchived()`도 같은 방식으로 작동합니다.

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

<TypeTable
type={{
projectId: { type: 'string', description: "이 프로젝트의 오브젝트만 반환합니다." },
limit: { type: 'number', description: "페이지 크기로, 최대 500입니다." },
cursor: { type: 'string', description: "이전 페이지의 nextCursor입니다." },
}}
/>

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

### objects.get(id)
오브젝트 하나를 아직 생성 중인 베리언트인 `pendingJobs`와 함께 읽어 옵니다. 보관된 오브젝트는 반환되지 **않으며**, `NotFoundError`를 발생시킵니다. `creatures.get()`도 같은 방식으로 작동합니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "오브젝트 ID입니다." },
}}
/>

```ts
const object = await client.objects.get(objectId)
console.log(object.sourceImageUrl, object.materials)
```

### objects.create(input)
오브젝트를 만듭니다. `name`과 `nodeId`는 필수입니다. 캔버스 노드가 없는 스크립트는 `nodeId`에 `"mcp-managed"`를 전달할 수 있습니다.

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

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "오브젝트가 속한 캔버스 노드, 또는 mcp-managed입니다." },
name: { type: 'string', required: true, description: "이름입니다." },
description: { type: 'string', description: "자유 형식의 설명입니다." },
category: { type: 'ObjectCategory', description: "furniture, vehicle, weapon, food, clothing, electronics, nature, tool, animal, other 중 하나입니다." },
style: { type: 'string', description: "realistic 같은 시각 스타일입니다." },
projectId: { type: 'string', description: "오브젝트를 넣을 프로젝트입니다." },
workflowId: { type: 'string', description: "오브젝트가 만들어진 워크플로입니다." },
sourceImageUrl: { type: 'string', description: "대표 이미지 URL입니다." },
imageProvider: { type: 'string | null', description: "대표 이미지의 이미지 모델입니다." },
referencePhotos: { type: 'ObjectReferencePhoto[]', description: "레퍼런스 사진으로, 각각 { url, kind } 형식입니다. kind는 front, side, detail, context, moodBoard, other 중 하나입니다." },
canonicalDescription: { type: 'string', description: "프롬프트에 쓰이는 설명입니다." },
styleLock: { type: 'boolean', description: "베리언트를 승인된 스타일로 유지합니다." },
}}
/>

```ts
const { id: objectId } = await client.objects.create({
nodeId: "mcp-managed",
name: "Antique Lantern",
description: "Weathered brass lantern with hand-engraved filigree",
category: "tool",
style: "realistic",
})
```

### objects.update(id, input)
오브젝트를 변경합니다. 보낸 필드만 기록됩니다. 작업하는 동안 생성 작업이 베리언트 컬렉션에 항목을 추가하므로, 베리언트 컬렉션은 이 호출에 포함되지 않습니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "오브젝트 ID입니다." },
name: { type: 'string', description: "이름입니다." },
description: { type: 'string', description: "설명입니다." },
category: { type: 'ObjectCategory', description: "카테고리입니다." },
style: { type: 'string', description: "시각 스타일입니다." },
sourceImageUrl: { type: 'string', description: "대표 이미지 URL입니다." },
imageProvider: { type: 'string | null', description: "대표 이미지의 이미지 모델입니다." },
referencePhotos: { type: 'ObjectReferencePhoto[]', description: "레퍼런스 사진입니다." },
canonicalDescription: { type: 'string', description: "프롬프트에 쓰이는 설명입니다." },
styleLock: { type: 'boolean', description: "베리언트를 승인된 스타일로 유지합니다." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "레퍼런스 보드입니다." },
selectedAssetByVariant: { type: 'Record<string, string>', description: "베리언트마다 선택한 테이크입니다." },
expectedUpdatedAt: { type: 'string', description: "읽어 온 updatedAt 값입니다. 그 뒤로 오브젝트가 바뀌었다면 업데이트는 409 concurrent_modification으로 실패합니다." },
}}
/>

```ts
await client.objects.update(objectId, {
canonicalDescription: "A weathered brass lantern with engraved filigree and a glass chimney",
expectedUpdatedAt: object.updatedAt,
})
```

`409 concurrent_modification`은 일반 `NodaroError`로 전달됩니다. 오브젝트를 다시 읽고 병합한 뒤 다시 시도하세요.

### objects.delete(id) 및 restore(id)
`delete()`는 오브젝트를 보관합니다. 보관된 오브젝트에 다시 호출해도 아무것도 바뀌지 않습니다. `restore()`는 오브젝트를 다시 복원합니다. 이때 이름이 대소문자 구분 없이 활성 오브젝트의 이름과 같으면, 서버가 `(restored)`를 붙이고 실제로 사용한 이름을 반환합니다.

```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: "오브젝트 ID입니다." },
}}
/>

```ts
await client.objects.delete(objectId)
const { name } = await client.objects.restore(objectId)
```

### objects.permanentDelete(id)
보관된 오브젝트와 오브젝트가 참조하는 저장된 파일을 모두 완전히 삭제합니다. 보관된 오브젝트에서만 작동하며, 활성 오브젝트는 `400 not_archived`로 실패합니다. 먼저 `delete()`로 보관하세요. `creatures.permanentDelete()`도 같은 방식으로 작동합니다.

```ts
permanentDelete(id: string): Promise<{ success: true; permanent: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "보관된 오브젝트의 ID입니다." },
}}
/>

```ts
await client.objects.delete(objectId)
await client.objects.permanentDelete(objectId)
```

Nodaro의 MCP 도구는 이 작업을 제공하지 않으므로, AI 어시스턴트가 내 오브젝트를 완전히 삭제할 수 없습니다.

### objects.generate(input)
대표 이미지 후보를 생성합니다(`POST /v1/generate-object`). `count`가 1보다 크면 어떤 작업도 시작하기 전에 모든 작업을 먼저 예약하므로, 도중에 실패하면 일괄 처리 전체가 되돌려집니다.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "오브젝트의 이름입니다." },
description: { type: 'string', description: "오브젝트가 어떻게 보이는지입니다." },
userPrompt: { type: 'string', description: "추가 지시 사항입니다." },
category: { type: 'ObjectCategory', description: "카테고리입니다." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "시각 스타일입니다." },
sourceImageUrl: { type: 'string', description: "이미지의 바탕이 될 사진입니다." },
provider: { type: 'string', description: "이미지 모델 ID입니다." },
count: { type: 'number', description: "생성할 후보 수입니다." },
seedPromptHint: { type: 'string', description: "프롬프트에 추가할 피커 선택값으로, 예를 들면 재질 피커의 antique brass입니다." },
attachToObjectId: { type: 'string', description: "단일 결과를 기록할 오브젝트입니다." },
attachName: { type: 'string', description: "첨부한 결과의 이름입니다." },
expectedUpdatedAt: { type: 'string', description: "읽어 온 updatedAt 값으로, 더 새로운 변경 사항을 덮어쓰지 않도록 합니다." },
}}
/>

```ts
const { jobIds } = await client.objects.generate({ name: "Antique Lantern", count: 4 })
for (const jobId of jobIds) {
// poll each candidate with client.jobs.getStatus(jobId)
}
```

`jobIds`는 항상 있으며, 후보마다 ID가 하나씩 담깁니다. `jobId`는 후보 하나에 대한 예전 별칭이므로 `jobIds`를 사용하세요. `attachToObjectId`를 지정하고 후보가 하나이면, 작업이 완료될 때 결과가 대표 이미지가 됩니다. 그렇지 않으면 `approveMainImage()`로 하나를 고르세요.

### objects.generateAsset(input)
베리언트 하나를 생성합니다(`POST /v1/generate-object-asset`). `attachToObjectId`, `attachToColumn`, `attachName`을 지정하면, 작업이 완료될 때 결과가 해당 컬렉션에 추가됩니다.

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

<TypeTable
type={{
assetType: { type: '"angles" | "materials" | "variations" | "motion" | "custom"', required: true, description: "베리언트의 종류입니다." },
variant: { type: 'string', required: true, description: "gold나 three-quarter 같은 베리언트입니다." },
name: { type: 'string', required: true, description: "오브젝트의 이름입니다." },
description: { type: 'string', description: "베리언트용 프롬프트입니다. 생략하고 결과를 첨부하면, 언어 모델이 오브젝트의 설명과 베리언트 이름으로 프롬프트를 작성합니다." },
userPrompt: { type: 'string', description: "추가 지시 사항입니다." },
sourceImageUrl: { type: 'string', description: "시작할 이미지로, 보통 대표 이미지입니다." },
provider: { type: 'string', description: "이미지 모델 ID입니다." },
seedPromptHint: { type: 'string', description: "프롬프트에 추가할 피커 선택값입니다." },
attachToObjectId: { type: 'string', description: "결과를 추가할 오브젝트입니다." },
attachToColumn: { type: 'string', description: "컬렉션입니다. angles, materials, variations, motion_clips, sheets, detail_closeups 중 하나입니다. custom에는 필수입니다." },
attachName: { type: 'string', description: "새 항목의 이름입니다." },
}}
/>

```ts
const { jobId } = await client.objects.generateAsset({
name: "Antique Lantern",
assetType: "materials",
variant: "gold",
attachToObjectId: objectId,
attachToColumn: "materials",
attachName: "gold",
})
```

`angles`, `materials`, `variations`, `motion`에서는 에셋 타입에 따라 컬렉션이 정해집니다. `custom` 베리언트에는 `attachToColumn`이 필요합니다.

### objects.generateMotion(input)
[동영상 생성](https://nodaro.ai/docs/nodes/video/generate-video)의 이미지로 동영상 만들기 모드로 오브젝트의 이미지를 클립으로 애니메이션화합니다(`POST /v1/generate-object-motion`). 클립은 항상 `motionClips`에 들어갑니다. 기본값은 제품 샷에 맞춰져 있어, 모델은 `kling-turbo`, 화면 비율은 `1:1`입니다.

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

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "오브젝트가 어떻게 움직이는지, 또는 카메라가 오브젝트 주위를 어떻게 움직이는지입니다." },
sourceImageUrl: { type: 'string', required: true, description: "애니메이션화할 이미지입니다. 대체값이 없으므로 대표 이미지를 전달하세요." },
name: { type: 'string', required: true, description: "오브젝트의 이름입니다." },
provider: { type: 'string', default: '"kling-turbo"', description: "동영상 모델 ID입니다." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16" | "4:3"', default: '"1:1"', description: "클립의 화면 비율입니다. 오브젝트에는 카탈로그 샷용으로 4:3이 추가됩니다." },
duration: { type: 'number', description: "초 단위 클립 길이입니다." },
refineFromVideoUrl: { type: 'string', description: "이미지에서 시작하는 대신, 동영상으로 동영상 만들기 모드에서 새 프롬프트로 다시 작업할 기존 클립입니다." },
category: { type: 'string', description: "카테고리입니다." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "시각 스타일입니다." },
canonicalDescription: { type: 'string', description: "오브젝트의 설명입니다." },
seedPromptHint: { type: 'string', description: "프롬프트에 추가할 피커 선택값입니다." },
attachToObjectId: { type: 'string', description: "클립을 추가할 오브젝트입니다." },
attachName: { type: 'string', description: "새 클립의 이름입니다." },
}}
/>

```ts
const { jobId } = await client.objects.generateMotion({
name: "Antique Lantern",
motionPrompt: "Slow 360-degree rotation, soft golden rim light",
sourceImageUrl: object.sourceImageUrl!,
attachToObjectId: objectId,
attachName: "rotate-360",
})
```

### objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)
`generate()`로 만든 완료된 후보를 오브젝트의 대표 이미지로 만듭니다. 그다음 비전 모델이 오브젝트의 설명을 작성하며, 메서드는 둘 다 반환합니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "오브젝트 ID입니다." },
candidateJobId: { type: 'string', required: true, description: "완료된 후보의 작업 ID입니다." },
expectedUpdatedAt: { type: 'string', description: "읽어 온 updatedAt 값입니다. 그 뒤로 오브젝트가 바뀌었다면 호출은 409 concurrent_modification으로 실패합니다." },
}}
/>

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

설명을 작성하지 못하면 `canonicalDescription`은 `null`입니다. 그래도 대표 이미지는 설정되므로, `recaption()`을 호출해 다시 시도하세요.

### objects.recaption(id)
현재 대표 이미지를 바탕으로 오브젝트의 설명을 다시 작성합니다. 이 호출은 반복해도 안전하며 동시성 토큰을 받지 않습니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "오브젝트 ID입니다." },
}}
/>

```ts
const { canonicalDescription } = await client.objects.recaption(objectId)
```

오브젝트에 대표 이미지가 없으면 `400 main_image_required`로 실패하고, 비전 모델이 실패하면 502로 실패합니다.

## client.creatures
크리처는 동물이나 판타지 존재입니다. 오브젝트처럼 작동하지만, 네 가지가 다릅니다.

- <strong>`species`</strong>는 `dragon`이나 `wolf` 같은 자유 텍스트 종류이며, 대표 이미지 프롬프트의 피사체가 됩니다. `category`도 자유 텍스트입니다.
- <strong>`poses`</strong>가 `materials`를 대체하므로, 컬렉션은 `angles`, `poses`, `variations`, `motionClips`입니다. 베리언트 타입은 `angles`, `poses`, `variations`, `custom`입니다.
- <strong>`boards`</strong>는 이름이 붙은 크리처 보드를 최대 24개 담습니다. 크리처 보드는 [이미지 생성](https://nodaro.ai/docs/nodes/image/generate-image)의 크리처 보드 프리셋으로 만든 촘촘한 레퍼런스 시트입니다. 이 목록은 내가 관리합니다. `create()`와 `update()`는 목록 전체를 대체합니다.
- <strong>`voice`</strong>는 크리처를 말하는 크리처로 만듭니다. 캐릭터 보이스와 형태가 같으며, `{ voiceId, voiceName, traits, voiceType?, previewUrl?, ttsProvider? }`입니다. 보이스를 제거하려면 `voice: null`을 전달하세요.

`creatures.listArchived()`, `get()`, `delete()`, `restore()`, `permanentDelete()`, `approveMainImage()`, `recaption()`은 같은 인수를 받으며, 위의 오브젝트 메서드처럼 동작합니다.

### creatures.list(params?)
```ts
list(params?: { archived?: boolean; projectId?: string; q?: string; limit?: number; cursor?: string }): Promise<{
creatures: Creature[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true이면 대신 보관된 크리처를 나열합니다." },
projectId: { type: 'string', description: "이 프로젝트의 크리처만 반환합니다." },
q: { type: 'string', description: "검색어입니다. 이름이나 설명에 모든 단어가 들어 있는 크리처만 반환하며, 순서와 대소문자는 구분하지 않습니다." },
limit: { type: 'number', description: "페이지 크기로, 최대 500입니다. 생략하면 전체 목록을 받습니다." },
cursor: { type: 'string', description: "이전 페이지의 nextCursor입니다." },
}}
/>

```ts
const { creatures } = await client.creatures.list({ q: "dragon" })
```

`creatures.list()`는 `limit`과 `cursor`를 쓰는 선택적 페이지 나누기를 포함해 `objects.list()`와 같은 매개변수를 받습니다. `q`의 검색 규칙은 [캐릭터](https://nodaro.ai/docs/developers/sdk/characters#listparams)에 있습니다.

### creatures.create(input)
```ts
create(input: CreateCreatureInput): Promise<{ id: string }>
```

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "크리처가 속한 캔버스 노드, 또는 mcp-managed입니다." },
name: { type: 'string', required: true, description: "이름입니다." },
species: { type: 'string', description: "dragon이나 wolf 같은 크리처의 종류입니다." },
description: { type: 'string', description: "자유 형식의 설명입니다." },
category: { type: 'string', description: "자유 텍스트 카테고리입니다." },
style: { type: 'string', description: "시각 스타일입니다." },
projectId: { type: 'string', description: "크리처를 넣을 프로젝트입니다." },
sourceImageUrl: { type: 'string', description: "대표 이미지 URL입니다." },
referencePhotos: { type: 'CreatureReferencePhoto[]', description: "레퍼런스 사진으로, 각각 { url, kind } 형식입니다. kind는 front, side, detail, context, moodBoard, other 중 하나입니다." },
canonicalDescription: { type: 'string', description: "프롬프트에 쓰이는 설명입니다." },
styleLock: { type: 'boolean', description: "베리언트를 승인된 스타일로 유지합니다." },
voice: { type: 'CreatureVoice | null', description: "크리처의 보이스입니다." },
}}
/>

```ts
const { id: creatureId } = await client.creatures.create({
nodeId: "mcp-managed",
name: "Biscuit",
species: "ginger cat",
style: "realistic",
})
```

### creatures.update(id, input)
`nodeId`를 제외한 `create()`의 필드에 더해 `boards`, `selectedAssetByVariant`, `expectedUpdatedAt`을 받습니다. 보낸 필드만 기록됩니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "크리처 ID입니다." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "크리처 보드로, 최대 24개이며 목록 전체가 대체됩니다." },
voice: { type: 'CreatureVoice | null', description: "보이스입니다. null이면 보이스가 제거됩니다." },
expectedUpdatedAt: { type: 'string', description: "읽어 온 updatedAt 값으로, 더 새로운 변경 사항을 덮어쓰지 않도록 합니다." },
}}
/>

```ts
await client.creatures.update(creatureId, {
voice: { voiceId: chosenVoiceId, voiceName: "Aria", traits: "smug, unhurried" },
})
```

### creatures.generate(input)
대표 이미지 후보를 생성합니다. `objects.generate()`의 필드에 더해 `species`를 받고, `{ jobIds }`를 반환합니다.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "크리처의 이름입니다." },
species: { type: 'string', description: "크리처의 종류입니다." },
description: { type: 'string', description: "크리처가 어떻게 보이는지입니다." },
count: { type: 'number', description: "생성할 후보 수입니다." },
provider: { type: 'string', description: "이미지 모델 ID입니다." },
attachToCreatureId: { type: 'string', description: "단일 결과를 기록할 크리처입니다." },
}}
/>

```ts
const { jobIds } = await client.creatures.generate({ name: "Biscuit", species: "ginger cat", count: 4 })
```

### creatures.generateAsset(input)
베리언트 하나를 생성합니다. `objects.generateAsset()`처럼 작동하며, `attachToCreatureId`를 씁니다.

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

<TypeTable
type={{
assetType: { type: '"angles" | "poses" | "variations" | "custom"', required: true, description: "베리언트의 종류입니다." },
variant: { type: 'string', required: true, description: "walking이나 sitting 같은 베리언트입니다." },
name: { type: 'string', required: true, description: "크리처의 이름입니다." },
attachToCreatureId: { type: 'string', description: "결과를 추가할 크리처입니다." },
attachToColumn: { type: 'string', description: "angles, poses, variations, motion_clips, sheets, detail_closeups 중 하나입니다. custom에는 필수입니다." },
attachName: { type: 'string', description: "새 항목의 이름입니다." },
}}
/>

```ts
await client.creatures.generateAsset({
name: "Biscuit",
assetType: "poses",
variant: "sitting",
attachToCreatureId: creatureId,
attachToColumn: "poses",
attachName: "sitting",
})
```

### creatures.generateMotion(input)
크리처의 이미지를 클립으로 애니메이션화합니다. `objects.generateMotion()`처럼 작동하며, 기본값도 `kling-turbo`와 `1:1`로 같고, 클립을 `motionClips`에 추가합니다.

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

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "크리처가 하는 행동입니다." },
sourceImageUrl: { type: 'string', required: true, description: "애니메이션화할 이미지입니다." },
name: { type: 'string', required: true, description: "크리처의 이름입니다." },
provider: { type: 'string', default: '"kling-turbo"', description: "동영상 모델 ID입니다." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16" | "4:3"', default: '"1:1"', description: "클립의 화면 비율입니다." },
attachToCreatureId: { type: 'string', description: "클립을 추가할 크리처입니다." },
attachName: { type: 'string', description: "새 클립의 이름입니다." },
}}
/>

```ts
await client.creatures.generateMotion({
name: "Biscuit",
motionPrompt: "The cat stretches, then yawns",
sourceImageUrl: creature.sourceImageUrl!,
attachToCreatureId: creatureId,
})
```

## 말하는 크리처 만들기
음성에는 크리처 전용 메서드가 필요 없습니다. 크리처의 보이스로 대사를 렌더링한 다음, 크리처의 이미지에 립싱크하세요.

```ts
const creature = await client.creatures.get(creatureId)

// 1. Speak the line in the creature's voice
const speech = await client.nodes.runAndWait("text-to-speech", {
text: "I knocked the vase off the shelf. I regret nothing.",
voice: creature.voice!.voiceId,
provider: creature.voice!.ttsProvider,
voiceType: creature.voice!.voiceType,
})

// 2. Lip-sync the audio onto the creature's main image
const clip = await client.nodes.runAndWait("lip-sync", {
imageUrl: creature.sourceImageUrl!,
audioUrl: speech.audioUrl,
provider: "kling-avatar",
})
console.log(clip.videoUrl)
```

[립싱크](https://nodaro.ai/docs/nodes/video/lip-sync) 노드는 기존 동영상을 더빙할 수도 있습니다. `videoUrl`과 동영상을 받는 모델을 전달하세요. `volcengine-lipsync`는 더빙에서 가장 가격이 낮은 옵션이며, 여러 화자를 처리하는 유일한 옵션입니다.

```ts
const dub = await client.nodes.runAndWait("lip-sync", {
videoUrl: "https://example.com/scene.mp4",
audioUrl: "https://example.com/new-vocal.mp3",
provider: "volcengine-lipsync",
mode: "basic",          // for complex scenes
openScenedet: true,     // several speakers: scene and speaker detection
audioDurationSec: 42,   // sets the per-second price; without it, you pay for 5 minutes
})
```

크리처를 레퍼런스로 샷에 넣으려면 `@nodaro/shared`의 `toConnectedReference({ kind: "creature", id, name, url, description })`로 레퍼런스를 만드세요. 그러면 이미지 생성 노드가 크리처의 해부학적 구조, 무늬, 색을 유지하는 문장을 추가합니다. [레퍼런스](https://nodaro.ai/docs/developers/sdk/nodes#references)를 참고하세요.

## 자주 묻는 질문

### Nodaro에서 오브젝트와 크리처는 어떻게 다른가요?

오브젝트는 소품, 제품, 탈것으로, 앵글, 재질, 변형 베리언트를 가집니다. 크리처는 동물이나 판타지 존재로, 앵글, 포즈, 변형 베리언트와 함께 종, 레퍼런스 보드, 선택 사항인 보이스를 가집니다.

### 오브젝트의 대표 이미지 후보를 생성하려면 어떻게 하나요?

name과 count를 넣어 client.objects.generate를 호출하세요. 항상 후보마다 ID가 하나씩 담긴 jobIds를 반환합니다. 후보가 완료되면 client.objects.approveMainImage로 하나를 승인하세요.

### SDK로 오브젝트를 영구 삭제할 수 있나요?

네, 두 단계로 할 수 있습니다. delete()로 보관한 다음 permanentDelete()를 호출하면 오브젝트와 오브젝트가 참조하는 모든 파일이 삭제됩니다. 활성 오브젝트는 400 not_archived로 거부됩니다.

### 크리처가 말하게 하려면 어떻게 하나요?

텍스트 음성 변환 노드와 크리처의 보이스로 음성을 렌더링한 다음, 크리처의 대표 이미지와 그 오디오로 립싱크 노드를 실행하세요.
