# キャラクター

> client.characters を使って、TypeScript からキャラクターを作成し、ポートレート候補を生成して 1 つを承認し、表情、ポーズ、モーションクリップを追加します。

Source: https://nodaro.ai/ja/docs/developers/sdk/characters

**`client.characters`** は、キャラクタースタジオが行うことをすべてスクリプトで再現します。キャラクターの作成と編集、ポートレート候補の生成、候補の 1 つをキャラクターの顔として承認すること、そして表情、ポーズ、ライティング、アングル、モーションクリップの追加です。キャラクターは、ポートレート、アセットのコレクション、リファレンス写真、そして人物を説明するキャプションを保持するため、以降のすべての画像と動画で同じ人物を表示できます。これらのメソッドは、[キャラクター REST API](https://nodaro.ai/docs/developers/api/characters) を呼び出します。エディターでの操作については、[キャラクタースタジオ](https://nodaro.ai/docs/guides/character-studio)を参照してください。

## メソッド
| メソッド | 内容 |
| --- | --- |
| [`list(params?)`](#listparams) | 自分のキャラクターを、ページ単位で一覧表示します |
| [`get(id)`](#getid) | 1 人のキャラクターを、進行中のジョブとともに読み取ります |
| [`create(input)`、`update(id, input)`、`upsert(input)`](#upsertinput-createinput-and-updateid-input) | キャラクターを作成または変更します |
| [`delete(id)`](#deleteid) | キャラクターをアーカイブします |
| [`restore(id)`](#restoreid) | アーカイブされたキャラクターを元に戻します |
| [`duplicate(id, input?)`](#duplicateid-input) | キャラクターを複製します |
| [`usage(id)`](#usageid) | キャラクターを使うワークフローを数えます |
| [`generate(input)`](#generateinput) | ポートレート候補を生成します |
| [`generateAsset(input)`](#generateassetinput) | 表情、ポーズ、ライティング、アングルのバリエーションを生成します |
| [`generateMotion(input)`](#generatemotioninput) | ポートレートをアニメーション化してモーションクリップにします |
| [`approvePortrait(id, candidateJobId)`](#approveportraitid-candidatejobid) | 候補をキャラクターのポートレートにします |
| [`recaption(id)`](#recaptionid) | キャラクターの説明を書き直します |

## キャラクターを最初から最後まで作成する
通常は、キャラクターを作成し、ポートレート候補を生成し、その 1 つを承認してから、その上にバリエーションを追加していきます。

```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",
})
```

ジョブが完了したら、`get()` でキャラクターをもう一度読み取ります。ポートレート、smile の表情、モーションクリップが揃っています。生成先には[**キャラクターアセット**（Character Asset）](https://nodaro.ai/docs/nodes/assets/character)ノードを接続するか、プロンプトで `@` を使ってメンションします。[キャラクターの一貫性](https://nodaro.ai/docs/guides/consistent-characters)を参照してください。

## client.characters
### list(params?)
自分のキャラクターを、新しい順に一覧表示します。デフォルトでは、有効なキャラクターだけを返します。

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

<TypeTable
type={{
projectId: { type: 'string', description: "このプロジェクトのキャラクターだけに絞ります。" },
archived: { type: 'boolean', default: 'false', description: "true にすると、代わりにアーカイブされたキャラクターを一覧表示します。" },
limit: { type: 'number', default: '100', description: "ページのサイズで、最大 500 です。" },
cursor: { type: 'string', description: "前のページの nextCursor です。" },
}}
/>

1 回の呼び出しで返るのは最大 `limit` 件のキャラクターまでなので、1 回の呼び出しで、すべてのキャラクターが返るとは限りません。`nextCursor` が `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)
```

`nextCursor` は、そのままの形で渡してください。保存したり解析したりしないでください。不正な形式のカーソルは、最初のページからやり直すのではなく、`validation_error` で失敗します。

### get(id)
1 人のキャラクターを、スタジオが再読み込み後に使う 3 つの追加の一覧とともに読み取ります。`pendingJobs`（まだ生成中のバリエーション）、`portraitCandidates`（現在のポートレート生成の候補と、その進行状況）、`previousCandidates`（それより前の候補）です。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
}}
/>

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

アーカイブされたキャラクターも、ID を指定すれば引き続き返されるため、それを参照するワークフローのノードは読み込み続けられます。`Character` は、`sourceImageUrl` にポートレートを持ち、6 つのアセットコレクション（`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` を含む、それ以外のフィールドはそのまま残ります。

```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: "upsert のみ：更新するキャラクターです。省略すると作成します。" },
name: { type: 'string', description: "名前です。create では必須です。" },
nodeId: { type: 'string', description: "キャラクターが属するキャンバスのノードです。スクリプトからは、scripted のような任意のラベルを渡せます。" },
projectId: { type: 'string', description: "キャラクターを登録するプロジェクトです。" },
workflowId: { type: 'string', description: "キャラクターの由来となるワークフローです。" },
description: { type: 'string', description: "自由記述の説明です。" },
gender: { type: 'string', description: "性別です。" },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "見た目のスタイルです。" },
baseOutfit: { type: 'string', description: "デフォルトの衣装です。" },
seedPrompt: { type: 'string', description: "ポートレートの生成元になるプロンプトです。" },
sourceImageUrl: { type: 'string', description: "ポートレートの URL です。" },
imageProvider: { type: 'string | null', description: "ポートレートの生成に使われた画像モデルです。" },
canonicalDescription: { type: 'string', description: "プロンプトで使われる説明です。approvePortrait() が自動的に書き込みます。" },
identityLock: { type: '"off" | "soft" | "strict"', default: '"off"', description: "バリエーションを生成するときに、顔をどれだけ厳密に保持するかです。" },
referencePhotos: { type: 'ReferencePhoto[]', description: "リファレンス写真で、それぞれ { url, kind } です。kind は frontFace、sideLeft、sideRight、threeQuarterLeft、threeQuarterRight、frontBody、other のいずれかです。" },
voice: { type: '{ voiceId, voiceName, traits, voiceType?, previewUrl?, ttsProvider? } | null', description: "キャラクターのボイスです。null にすると削除されます。" },
personality: { type: '{ mood, speechStyle, movementStyle, behavioralNotes } | null', description: "スクリプトとプロンプト用の性格に関するメモです。" },
'expressions, poses, lightingVariations, angles, bodyAngles, motions': { type: 'Array<{ name: string; url: string }>', description: "アセットのコレクションです。それぞれ全体が置き換えられます。" },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "キャラクターのリファレンスボードです。" },
selectedAssetByVariant: { type: 'Record<string, string>', description: "各バリエーションで選ばれているテイクです。" },
}}
/>

```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" })
```

すでに使われている名前は、`409 name_taken` で失敗します。

### delete(id)
キャラクターをアーカイブします。`list()` には表示されなくなりますが、`get(id)` では引き続き読み込めます。元に戻すには `restore()` を使います。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
}}
/>

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

### restore(id)
アーカイブされたキャラクターを元に戻します。その名前がすでにほかの有効なキャラクターに使われている場合、サーバーは名前に `(restored)` を追加し、実際に使った名前を返します。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
}}
/>

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

### duplicate(id, input?)
キャラクターを、名前が `(copy)` で終わる新しいキャラクターとしてコピーします。コピーは、新しいものを生成するまで、元のアセットの URL を共有します。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "コピーするキャラクターです。" },
nodeId: { type: 'string', description: "コピーが属するキャンバスのノードです。" },
projectId: { type: 'string', description: "コピーのプロジェクトです。" },
}}
/>

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

### usage(id)
キャラクターを使っているワークフローの数と、その内訳を返します。エディターは、アーカイブする前にこれを表示します。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
}}
/>

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

### generate(input)
ポートレート候補を生成します（`POST /v1/generate-character`）。`count` が 1 を超える場合、どのジョブを開始するよりも前に、すべてのジョブの分が確保されるため、途中で失敗すると、バッチ全体がロールバックされます。

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "キャラクターの名前です。" },
description: { type: 'string', description: "キャラクターの説明です。" },
seedPrompt: { type: 'string', description: "生成元になるプロンプトです。" },
userPrompt: { type: 'string', description: "追加の指示です。" },
gender: { type: 'string', description: "性別です。" },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "見た目のスタイルです。" },
baseOutfit: { type: 'string', description: "衣装です。" },
sourceImageUrl: { type: 'string', description: "ポートレートのもとになる写真です。" },
referencePhotos: { type: 'ReferencePhoto[]', description: "その人物のリファレンス写真です。" },
provider: { type: 'string', description: "画像モデルの ID です。" },
count: { type: 'number', description: "生成する候補の数です。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "ポートレートのアスペクト比です。" },
quality: { type: 'string', description: "品質の設定があるモデルでの medium、high、basic のいずれかです。価格が変わります。" },
resolution: { type: 'string', description: "対応するモデルでの 1K、2K、4K、0.5 MP、1 MP、2 MP、4 MP のいずれかです。価格が変わります。" },
attachToCharacterId: { type: 'string', description: "結果の書き込み先となるキャラクターです。" },
}}
/>

```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
})
```

`attachToCharacterId` を指定し、候補が 1 つの場合、ジョブが完了すると、その結果がそのままポートレートになります。候補が複数の場合は、`approvePortrait()` で 1 つを選びます。`quality` と `resolution` は、[**画像生成**（Generate Image）](https://nodaro.ai/docs/nodes/image/generate-image)とまったく同じ基準で価格が決まるため、4K や高品質での実行では、より多くのクレジットが確保されます。モデルが対応していない値は、拒否されるのではなく無視されます。

### generateAsset(input)
キャラクターのポートレートから、表情、ポーズ、ライティング、アングルのいずれか 1 つのバリエーションを生成します。`attachToCharacterId`、`attachToColumn`、`attachName` を指定すると、ジョブが完了したときに、結果がキャラクターの該当するコレクションに追加されます。

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

<TypeTable
type={{
assetType: { type: '"expressions" | "poses" | "lighting" | "angles" | "headAngles" | "bodyAngles" | "custom"', required: true, description: "バリエーションの種類です。" },
variant: { type: 'string', required: true, description: "作成するバリエーションです。smile や three-quarter view などです。" },
name: { type: 'string', required: true, description: "キャラクターの名前です。" },
description: { type: 'string', description: "キャラクターの説明です。" },
userPrompt: { type: 'string', description: "追加の指示です。" },
sourceImageUrl: { type: 'string', description: "生成のもとになる画像です。通常はポートレートです。" },
realLifeRefs: { type: 'string[]', description: "バリエーションを示す実写の写真です。" },
provider: { type: 'string', description: "画像モデルの ID です。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "画像のアスペクト比です。" },
quality: { type: 'string', description: "generate() と同じです。価格が変わります。" },
resolution: { type: 'string', description: "generate() と同じです。価格が変わります。" },
attachToCharacterId: { type: 'string', description: "結果の追加先となるキャラクターです。" },
attachToColumn: { type: 'string', description: "追加先のコレクションです。expressions などです。" },
attachName: { type: 'string', description: "新しいエントリーの名前です。" },
}}
/>

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

### generateMotion(input)
[**動画生成**（Generate Video）](https://nodaro.ai/docs/nodes/video/generate-video)を画像から動画のモードで使い、キャラクターのポートレートをモーションクリップにアニメーション化します。`attachToCharacterId` を指定すると、クリップはキャラクターの `motions` に追加されます。`sourceImageUrl` を指定しない場合は、キャラクターのポートレートが使われます。

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

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "キャラクターの動作の内容です。" },
name: { type: 'string', required: true, description: "キャラクターの名前です。" },
sourceImageUrl: { type: 'string', description: "アニメーション化する画像です。デフォルトはポートレートです。" },
provider: { type: 'string', description: "動画モデルの ID です。kling などです。" },
description: { type: 'string', description: "キャラクターの説明です。" },
motionDescription: { type: 'string', description: "動作についての、より詳しい説明です。" },
realLifeRefs: { type: 'string[]', description: "動作の実写リファレンスです。" },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16"', description: "クリップのアスペクト比です。" },
attachToCharacterId: { type: 'string', description: "クリップの追加先となるキャラクターです。" },
attachName: { type: 'string', description: "新しいクリップの名前です。" },
}}
/>

```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)
`generate()` で完了した候補を、キャラクターのポートレートにします。その後、ビジョンモデルがキャラクターの説明を書き、このメソッドは両方を返します。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
candidateJobId: { type: 'string', required: true, description: "完了した候補のジョブ ID です。" },
}}
/>

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

説明を書けなかった場合、`canonicalDescription` は `null` になります。それでもポートレートは設定されます。もう一度試すには、`recaption()` を呼び出してください。

### recaption(id)
現在のポートレートから、キャラクターの説明を書き直します。

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "キャラクターの ID です。" },
}}
/>

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

キャラクターにポートレートがない場合は `400 no_portrait` で、ビジョンモデルが失敗した場合は 502 で失敗します。

## Frequently asked questions

### Nodaro SDK でキャラクターを作成するにはどうすればよいですか？

nodeId と name を指定して client.characters.create を呼び出し、次に client.characters.generate でポートレート候補を生成し、client.characters.approvePortrait で 1 つを選びます。これで、キャラクターをあらゆる画像や動画の生成で使えるようになります。

### キャラクターのポートレートを生成すると、クレジットがかかりますか？

かかります。ポートレートの料金は、選んだモデルでの画像生成（Generate Image）の実行と同じです。quality と resolution によって価格が変わり、たとえば 4K や高品質での実行は、より高くなります。

### キャラクターを削除すると、どうなりますか？

delete は、キャラクターをアーカイブします。list() には表示されなくなりますが、id を指定すれば引き続き読み込めるため、それを使うワークフローは動作し続けます。restore で元に戻せます。

### identityLock は何をしますか？

Nodaro がキャラクターの表情、ポーズなどのバリエーションを生成するときに、顔をどれだけ厳密に保持するかを設定します。off、soft、strict のいずれかで、デフォルトは off です。
