# 노드

> POST /v1/<node-type>으로 Nodaro 노드를 실행하고, 노드·모델·피커 값을 조회하며, 레퍼런스와 연출 ID로 프롬프트를 조정하는 방법입니다.

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

**노드 실행**은 워크플로를 만들지 않고 Nodaro 노드 하나를 직접 호출합니다. 노드의 설정을 본문에 담아 `POST /v1/<node-type>`을 보내면 됩니다. `generate-image`와 `generate-video`부터 `text-to-speech`까지 모든 생성 노드가 이 형식을 따르며, 조회 엔드포인트는 어떤 노드, 모델, 설정이 있는지 알려 줍니다. 대부분의 노드 실행은 비동기입니다. 응답으로 `jobId`를 받고, 결과가 준비될 때까지 이 작업을 폴링합니다.

## 노드 실행
노드 타입의 경로는 `POST /v1/` 뒤에 타입을 붙인 것이고, 본문은 노드의 설정을 담은 JSON입니다.

```http
POST /v1/generate-image
Authorization: Bearer ndr_…
Content-Type: application/json

{ "prompt": "a lighthouse in a storm, oil painting", "provider": "nano-banana-pro", "aspectRatio": "3:4" }
```

돌아오는 응답은 노드에 따라 다릅니다.

- **생성 노드**는 `{ "jobId": "…" }`와 함께 `200`으로 응답합니다. 작업은 워커에서 실행되며, 상태가 `completed`가 될 때까지 `GET /v1/jobs/:id/status`로 작업을 폴링합니다. [작업](https://nodaro.ai/docs/developers/api/jobs)을 참고하세요.
- **이미지 생성과 동영상 생성**은 선택한 모델에 맞게 서버가 보정한 설정 목록인 `adjustments`를 추가할 수 있습니다. `generate-video`와 `generate-image`는 `warnings`도 추가할 수 있습니다. [파라미터 보정](https://nodaro.ai/docs/developers/api/workflows#parameter-corrections)을 참고하세요.
- **인라인 노드**는 `combine-text`처럼 `jobId` 없이 전체 결과를 바로 반환합니다.
- **스크래퍼**도 `web-scrape`처럼 바로 응답합니다. 응답에는 기록용 `jobId`와 데이터 자체가 함께 들어 있습니다.

생성은 시작할 때 크레딧을 예약합니다. 계정 잔액으로 실행을 충당할 수 없으면 `402 insufficient_credits`로 응답합니다.

### 경로가 더 긴 노드
언어 모델을 호출하는 텍스트 노드는 대부분 같은 `POST /v1/<node-type>` 규칙을 따릅니다. `generate-script`, `image-critic`, `qa-check`, `describe-to-picker`가 그렇습니다. 몇몇 다른 노드는 더 긴 경로에 등록되어 있습니다.

| 노드 타입 | 경로 |
| --- | --- |
| `llm-chat` | `POST /v1/llm-chat/generate` |
| `after-effects` | `POST /v1/after-effects/generate` |
| `motion-graphics` | `POST /v1/motion-graphics/generate` |
| `lottie-overlay` | `POST /v1/lottie-overlay/generate` |
| `3d-title` | `POST /v1/3d-title/generate` |
| `image-to-text` | `POST /v1/image-to-text/describe` |
| `video-composer` | `POST /v1/scene-graph/generate` |

SDK의 `client.nodes.run(type, params)`와 CLI의 `nodaro nodes run`은 이런 노드 타입을 각각의 경로로 대신 보내 줍니다. [노드 실행](https://nodaro.ai/docs/developers/sdk/nodes#runtype-params-options)을 참고하세요.

언어 모델 경로는 선택 필드 두 개를 받습니다.

- **`reasoningEffort`**: 모델에 따라 `none`, `low`, `medium`, `high`, `xhigh` 또는 `max`입니다. 생략하거나 모델이 지원하지 않는 수준을 고르면 모델의 기본값이 쓰입니다. `xhigh`와 `max`는 크레딧 등급이 한 단계 올라갑니다. 이것만으로는 프리미엄 모델의 가격이 프리미엄 그대로입니다. Claude 모델에서는 어떤 추론 강도든 Anthropic 자체 API에서 실행되며, 이때도 크레딧이 한 단계 올라갑니다. 따라서 프리미엄 Claude 모델은 premium-direct 가격이 됩니다.
- **`advancedMode: true`**: Gemini와 Claude 모델에서 쓸 수 있습니다. 요청이 모델 제작사의 자체 API에서 실행되며, `temperature`, `maxTokens`, 전체 추론 범위가 적용되는 유일한 실행 경로입니다. `reasoningEffort`와 별개로 크레딧이 한 단계 올라갑니다. 프리미엄 모델은 premium-direct 가격으로 올라갑니다. 이 실행 경로가 없는 모델은 `400 advanced_mode_unsupported`로 응답합니다.

`describe-to-picker`는 같은 요청 안에서 결과를 응답하며, 서버 전송 이벤트로 답을 스트리밍할 수도 있습니다. [피커로 설명](https://nodaro.ai/docs/nodes/image/describe-to-picker#api)을 참고하세요. `generate-script`의 필드는 [대본 생성](https://nodaro.ai/docs/nodes/video/generate-script#api)에 있습니다.

## 예시: 이미지 생성
### 작업 시작
**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"prompt": "a knight on a hill at dawn, cinematic",
"provider": "nano-banana-pro",
"aspectRatio": "16:9",
"resolution": "2K"
}'
```

```json
{ "jobId": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10" }
```

**TypeScript SDK**

```ts

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

const result = await client.nodes.run('generate-image', {
prompt: 'a knight on a hill at dawn, cinematic',
provider: 'nano-banana-pro',
aspectRatio: '16:9',
resolution: '2K',
})
```

**CLI**

```bash
nodaro nodes run generate-image \
  --param prompt="a knight on a hill at dawn, cinematic" \
  --param provider=nano-banana-pro \
  --param aspectRatio=16:9 \
  --param resolution=2K
```

### 작업 폴링
작업이 `completed` 또는 `failed`가 될 때까지 2~5초마다 상태를 요청하세요.

```bash
curl -s https://app.nodaro.ai/v1/jobs/0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10/status \
  -H "Authorization: Bearer $NODARO_API_KEY" | jq -r .data.status
```

### 결과 읽기
완료된 이미지 작업은 `output_data.imageUrl`에 이미지 URL을 담고 있습니다. 동영상 작업은 `videoUrl`, 오디오 작업은 `audioUrl`을 사용하며, 많은 작업에 `thumbnailUrl`도 함께 있습니다.

```json
{
"data": {
"id": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
"status": "completed",
"progress": 100,
"output_data": { "imageUrl": "https://…/0f1a9c2e.png" },
"error_message": null
}
}
```

SDK와 CLI가 폴링을 대신 처리할 수 있습니다.

**TypeScript SDK**

```ts
const output = await client.nodes.runAndWait('generate-image', {
prompt: 'a knight on a hill at dawn, cinematic',
provider: 'nano-banana-pro',
})
console.log(output.imageUrl)

// Several candidates at once, in input order:
const results = await client.nodes.runMany('generate-image', [
{ prompt: 'a knight on a hill, sunrise' },
{ prompt: 'a knight on a hill, golden hour' },
{ prompt: 'a knight on a hill, blue hour' },
])
for (const { jobId, output } of results) console.log(jobId, output.imageUrl)
```

**CLI**

```bash
nodaro nodes run generate-image \
  --param prompt="a knight on a hill at dawn, cinematic" \
  --param provider=nano-banana-pro \
  --watch --json | jq -r '.output_data.imageUrl'
```

`runAndWait`는 기본적으로 최대 15분 동안 2,000ms마다 폴링합니다. `pollMs`와 `maxMs`로 이 값을 바꾸고, `signal`에 `AbortSignal`을 넣어 중지하고, `onProgress`로 진행 상황을 확인하세요. `instanceof`로 잡을 수 있는 타입이 지정된 오류를 던집니다.

| 오류 | 발생 시점 |
| --- | --- |
| `InsufficientCreditsError`, `StorageExceededError`, `JobBlockedError` | 작업이 시작되기 전에 실행이 거부되었습니다. |
| `JobFailedError` | 작업이 `failed` 또는 `cancelled`로 끝났습니다. `jobId`와 오류 메시지를 담고 있습니다. |
| `JobTimeoutError` | `maxMs`가 지났습니다. 작업은 취소되지 않으며 대개는 그대로 완료됩니다. 나중에 `client.jobs.get(jobId)`로 가져오세요. |
| `JobAbortedError` | 내 `signal`이 발생했습니다. |
| `JobHeldError` | 결과를 검토하는 배포 환경에서 작업이 `pending_review` 상태가 되었습니다. 작업은 취소되지 않으므로 나중에 다시 확인하세요. |

CLI에서는 배열이나 중첩 객체처럼 복잡한 본문을 `--params-file body.json`으로 파일에 담아 보내세요. 같은 키에 대해서는 플래그 값이 파일보다 우선하며, `true`, `false`, `null`과 숫자는 텍스트에서 변환됩니다.

### 이미지 생성 설정
아래 필드는 `POST /v1/generate-image`에서 가장 많이 쓰는 설정입니다. 각 설정이 에디터에서 하는 일은 [이미지 생성](https://nodaro.ai/docs/nodes/image/generate-image)을, 각 모델이 지원하는 기능은 [이미지 모델](https://nodaro.ai/docs/models/image)을 읽어 보세요.

<TypeTable
type={{
prompt: { type: 'string', description: '이미지에 대한 설명입니다. 전송 시 최대 30,000자이며, 모델마다 더 낮은 자체 한도가 있어 더 긴 프롬프트는 한도에 맞게 줄어듭니다.', required: true },
provider: { type: 'string', description: 'nano-banana-pro나 gpt-image-2-5-flare 같은 모델 ID입니다. GET /v1/nodes/generate-image의 providers에 모든 ID가 나열됩니다.' },
aspectRatio: { type: 'string', description: 'auto, 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 5:4, 4:5, 21:9 등입니다. 모델이 지원하지 않는 값은 보정됩니다.' },
resolution: { type: 'string', description: '지원하는 모델에서 1K, 2K, 4K 또는 0.5 MP, 1 MP, 2 MP, 4 MP입니다. 크레딧 가격에 영향을 줍니다.' },
quality: { type: 'string', description: '품질 등급이 있는 모델에서 basic, medium 또는 high입니다. 크레딧 가격에 영향을 줍니다.' },
negativePrompt: { type: 'string', description: '피할 요소이며, 최대 5,000자입니다.' },
seed: { type: 'integer', description: '지원하는 모델에서 실행을 반복할 수 있게 만드는 고정된 숫자입니다.' },
referenceImageUrls: { type: 'string[]', description: '레퍼런스 이미지 URL 최대 14개이며, 중요한 것부터 넣습니다.' },
connectedReferences: { type: 'object[]', description: '경로가 프롬프트에 조합해 넣는, 레이블이 붙은 레퍼런스입니다. 레퍼런스 사용을 참고하세요.' },
describedReferences: { type: 'object[]', description: '이미지가 없는, 이름이 있는 대상 최대 10개입니다.' },
referenceOrder: { type: 'string[]', description: '레퍼런스 번호를 매길 순서대로 나열한 레퍼런스 ID입니다.' },
referenceLock: { type: 'string', description: 'standard 또는 multi-person입니다. 검증된 레퍼런스 충실도 문구를 추가합니다.' },
direction: { type: 'object', description: '카메라, 조명, 룩에 대한 피커 ID입니다. ID로 지정하는 시네마틱 연출을 참고하세요.' },
subject: { type: 'object', description: '샷 속 인물, 스타일링, 소품에 대한 피커 ID입니다.' },
baseImageUrl: { type: 'string', description: '처음부터 생성하는 대신 편집할 이미지입니다.' },
maskUrl: { type: 'string', description: 'baseImageUrl과 함께 쓰는 마스크로, 흰색은 바꿀 영역이고 검은색은 유지할 영역입니다. 흰색 영역만 다시 생성됩니다.' },
strength: { type: 'number', description: '0~1입니다. 지원하는 모델에서 리파인이 baseImageUrl로부터 얼마나 멀어질 수 있는지 정합니다.' },
guidanceScale: { type: 'number', description: '지원하는 모델에서 0~20입니다.' },
presetId: { type: 'string', description: '편집기와 같은 방식으로 적용할 기본 제공 프리셋 또는 사용자 지정 프리셋입니다. 프리셋 적용을 참고하세요.' },
presetVariables: { type: 'object', description: '기본값이 없는 프리셋 자리 표시자의 값입니다. 예: { "BRAND": "Acme" }.' },
count: { type: 'integer', description: '1~4입니다. 그 수만큼 이미지를 만들며, 각 이미지가 별도의 작업입니다. 한 번의 호출로 이미지 여러 장을 참고하세요.' },
models: { type: 'string[]', description: 'provider 대신 쓰는, 서로 다른 이미지 모델 ID 2~4개입니다. 각 모델에 같은 요청을 보냅니다.' },
promptPrefix: { type: 'string', description: 'AI 프롬프트 노드에서처럼 프롬프트 앞에 추가되는 텍스트입니다.' },
promptSuffix: { type: 'string', description: '프롬프트 뒤에 추가되는 텍스트입니다. promptPrefix와 프롬프트를 합쳐 프롬프트 한도를 넘을 수 없습니다.' },
userPrompt: { type: 'string', description: 'prompt가 앱이 확장한 버전일 때, 사용자가 입력한 그대로의 텍스트입니다. 작업에 저장되며 모델에는 보내지 않습니다. 기본값은 prompt입니다.' },
sessionId: { type: 'string', description: '내 생성 세션 중 하나입니다. 작업이 그 세션에 정리됩니다. 생성 세션을 참고하세요.' },
origin: { type: 'string', description: '앱 slug입니다. 소문자, 숫자, 대시로 최대 40자입니다. 작업에 저장되며, GET /v1/jobs?origin=으로 필터링할 수 있습니다.' },
}}
/>

마스크 편집이나 리파인은 해당 모델에서 새로 생성하는 것과 같은 비용이 듭니다.

`/v1/generate-image`는 요청한 것 중 보내지 않은 것이 있을 때 `{ code, message }` 목록인 `warnings`로 응답할 수 있습니다. 현재 코드는 `moodboard_images_not_sent` 하나뿐입니다. [무드보드](#palettes-and-moodboards)의 이미지 일부가 모델의 레퍼런스 한도에 들어가지 않았다는 뜻입니다. 실행은 계속되며, 작업은 같은 경고를 `input_data.warnings`에 보관합니다.

### 프리셋 적용
`POST /v1/generate-image`는 `presetId`를 받습니다. `generate-image/cinematic-still` 같은 기본 제공 ID이거나 내 프리셋의 uuid입니다. 기본 제공 ID는 토큰이 필요 없는 `GET /v1/node-presets/factory?nodeType=generate-image`로 나열하세요. 라우트는 편집기가 **이미지 생성** 노드에 적용하는 방식으로 프리셋을 적용하므로, 작업은 편집기의 **실행**이 보낼 작업과 같습니다.

- 프리셋의 프롬프트와 그 앞뒤 텍스트(`promptPrefix`와 `promptSuffix`).
- 기본값으로 채워진 `{name || default}` 자리 표시자.
- 프롬프트에 합쳐지는 프리셋의 `style`.
- 프리셋의 네거티브 프롬프트. 이를 받는 모델에는 그대로 보내고, 그 밖의 모든 모델에는 프롬프트에 `Avoid:` 줄로 추가합니다.
- 프리셋의 설정과, 프리셋이 비워 둔 설정에 대한 편집기의 기본값: 모델 `gpt-image-2`와 비율 `16:9`. 모델과 가격은 편집기와 같습니다.

내가 보낸 필드는 적용된 노드를 편집한 것처럼 프리셋보다 우선합니다. 내 `prompt`는 프리셋의 프롬프트를 대신하며 프리셋의 앞뒤 텍스트로 감싸집니다. `provider`, `aspectRatio`, `resolution`, `quality`, `negativePrompt`, `style`과 그 밖의 설정은 프리셋의 값을 대신합니다. 레퍼런스, `sessionId`, `origin`은 유지됩니다.

```bash
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"presetId": "generate-image/logo-wordmark", "presetVariables": {"BRAND": "Acme"}}'
```

- **기본값이 없는 자리 표시자**는 `{BRAND}`처럼 `presetVariables`에 값이 있어야 합니다. 값이 없으면 라우트는 `code: "preset_variables_required"`와 `tokens: ["BRAND"]`를 담아 `400`으로 응답합니다.
- **알 수 없는 `presetId`**는 `404 preset_not_found`로 응답합니다.
- **크레딧 확인은 프리셋을 적용한 뒤의 요청으로 가격을 계산합니다.** `presetId`와 `presetVariables`는 작업에 저장되지 않습니다.
- **사진을 바탕으로 하는 프리셋**은 `minReferences`가 0보다 큽니다. 그 수만큼 이미지를 `referenceImageUrls`로 보내세요. [프리셋](https://nodaro.ai/docs/developers/api/presets)을 참고하세요.
- **프리셋은 `count`, `models`와 함께 쓸 수 있습니다.** 다음 섹션을 참고하세요.
- **프리셋은 내 팔레트나 무드보드를 쓸 수 없습니다.** 내가 보냈거나 프리셋에 저장된 `direction`이 `palette:<id>` 또는 `moodboard:<id>`를 가리키면, 라우트는 무엇이든 만들거나 청구하기 전에 그 키를 `field`에 담아 `400 validation_error`로 응답합니다.

### 한 번의 호출로 이미지 여러 장
`POST /v1/generate-image`는 이미지를 한 번에 여러 장 만들 수 있습니다.

- **`count`**(1~4)는 한 모델로 그 수만큼 이미지를 만듭니다. 각 이미지는 별도의 작업입니다.
- **`models`**(서로 다른 이미지 모델 ID 2~4개, `provider`가 받는 것과 같은 ID)는 `provider`를 대신하며, 각 모델에 같은 요청을 보냅니다. 각 모델의 설정은 단일 호출에서처럼 그 모델에 맞춰지고, 보정은 모델마다 보고됩니다.
- **둘 다 쓰면** `count`는 모델마다 적용됩니다. `models: ["gpt-image-2", "flux-2-max"]`와 `count: 2`는 이미지 4장을 만듭니다. 한 요청은 최대 8장을 만듭니다.

```bash
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f8e6c1a-try-1" \
  -d '{"prompt": "a lighthouse at dusk", "models": ["gpt-image-2", "flux-2-max"], "count": 2, "seed": 41}'
```

```text
{
"jobId": "<first job>",
"jobIds": ["<job 1>", "<job 2>", "<job 3>", "<job 4>"],
"batchId": "6b1f7e2a-0c4d-5e8f-9a1b-2c3d4e5f6a7b",
"groups": [
{ "model": "gpt-image-2", "jobIds": ["<job 1>", "<job 2>"], "credits": <credits> },
{ "model": "flux-2-max", "jobIds": ["<job 3>", "<job 4>"], "credits": <credits>,
"adjustments": [{ "field": "resolution", "from": "1K", "to": "2 MP", "reason": "..." }] }
],
"totalCredits": <credits>
}
```

각 이미지에는 그 이미지를 단일 호출로 만들 때의 비용이 청구됩니다. `count`와 `models`가 없거나 `count: 1`이면 응답은 평소의 `{ "jobId" }`입니다. `jobId`는 항상 첫 번째 작업이므로, 이 값을 읽는 코드는 계속 작동합니다.

- **크레딧: 일괄 처리 전체 아니면 아무것도.** 무엇이든 만들기 전에 잔액을 일괄 처리의 합계와 비교합니다. 부족하면 라우트는 `required`를 합계로 설정해 `402 insufficient_credits`로 응답하며, 아무것도 만들지 않습니다. 청구가 도중에 실패하면, 이미 만든 작업은 응답 전에 환불되고 제거됩니다.
- **멱등성은 이미지마다.** `Idempotency-Key: K`를 쓰면 이미지 *i*의 키는 `K:i`입니다. 같은 키로 요청을 반복하면 같은 작업 ID를 반환하며, 작업을 두 번 청구하지 않습니다. 일부만 만들어진 일괄 처리는 마저 완성됩니다. 같은 키로 동시에 보낸 두 번째 요청은 첫 번째를 기다리거나, 15초 뒤 `409 batch_in_progress`로 응답하므로 다시 시도하세요. 같은 키를 다른 본문으로 반복하면 `422 idempotency_key_reused`로 응답합니다.
- **시드.** `seed`를 쓰면 각 모델의 이미지 *i*는 `seed + i`를 받으므로, 이미지가 서로 다릅니다.
- **프리셋과 함께 쓰면** `count`는 프리셋 요청의 이미지를 N장 만듭니다. `models`와 함께 쓰면 프리셋이 모델마다 한 번씩 적용되므로, 각 모델은 그 모델에 대한 편집기의 요청을 받습니다.
- **나중에 일괄 처리 찾기.** 일괄 처리의 모든 작업에는 같은 `input_data.batchId`와 그 위치인 `input_data.batchIndex`가 담깁니다. `GET /v1/jobs?batchId=<id>`가 이를 나열합니다.
- **무엇이든 만들기 전에 거부되는 경우**(`400 validation_error`): `count`가 1~4를 벗어날 때, `models`가 2개보다 적거나 4개보다 많을 때, 모델이 중복되거나 알 수 없을 때, `models`와 `provider`를 함께 보낼 때, 이미지가 8장을 넘을 때입니다. 배포 환경이 제공하지 않는 모델은 `403 model_not_available`로 응답합니다.

## 동영상 생성
동영상을 만드는 경로는 두 개입니다. `POST /v1/generate-video`는 이미지에서 애니메이션을 만듭니다. `imageUrl`에 시작 프레임을, 선택적으로 `endFrameUrl`에 마지막 프레임을 넣거나, 지원하는 모델에서는 레퍼런스만 보낼 수도 있습니다. `POST /v1/text-to-video`는 프롬프트만으로 클립을 만들므로 `prompt`가 필수입니다. `provider`를 생략하면 플랫폼의 기본 동영상 모델이 사용됩니다.

```json
{
"imageUrl": "https://…/frame.png",
"provider": "seedance-2",
"prompt": "she turns toward the window",
"duration": 8,
"resolution": "720p",
"direction": { "cameraMotion": "dolly-in", "timeOfDay": "dawn" }
}
```

응답은 `{ "jobId": "…" }`이며, 해당되는 경우 `warnings`와 `adjustments`가 함께 옵니다. 완료된 작업에는 `output_data.videoUrl`이 있습니다.

<TypeTable
type={{
imageUrl: { type: 'string', description: '시작 프레임입니다. 모델이 레퍼런스만으로 실행할 수 있는 경우가 아니면 필수입니다.' },
endFrameUrl: { type: 'string', description: '첫 프레임과 마지막 프레임을 지정하는 동영상을 지원하는 모델에서 쓰는 마지막 프레임입니다.' },
prompt: { type: 'string', description: '클립에서 일어나는 일이며, 전송 시 최대 30,000자입니다. 동영상 모델의 자체 한도는 이보다 훨씬 낮습니다.' },
provider: { type: 'string', description: 'seedance-2, kling-3.0, veo3.1 같은 모델 ID입니다.' },
duration: { type: 'number', description: '초 단위이며, 모델에 따라 1~60입니다. Seedance 2 계열에서 -1은 자동을 뜻합니다.' },
resolution: { type: 'string', description: '예를 들어 480p, 720p, 1080p, 4k입니다. 지원하지 않는 값은 모델이 가진 가장 가까운 등급으로 맞춰집니다.' },
aspectRatio: { type: 'string', description: '모델에 따라 16:9, 9:16, 1:1, 4:3, 3:4, 4:5, 5:4, 21:9, 9:21 또는 adaptive입니다.' },
generateAudio: { type: 'boolean', description: '소리를 만들 수 있는 모델에서 동영상과 함께 소리를 요청합니다.' },
negativePrompt: { type: 'string', description: '피할 요소입니다.' },
referenceImageUrls: { type: 'string[]', description: '레퍼런스 이미지로, 전송 시 최대 30개입니다. 모델마다 받는 최대 개수가 다릅니다.' },
referenceVideoUrls: { type: 'string[]', description: '지원하는 모델에서 레퍼런스 클립 최대 10개입니다.' },
referenceAudioUrls: { type: 'string[]', description: '지원하는 모델에서 레퍼런스 오디오 최대 10개입니다.' },
connectedReferences: { type: 'object[]', description: '레이블이 붙은 이미지 레퍼런스입니다. 레퍼런스 사용을 참고하세요.' },
direction: { type: 'object', description: '카메라 모션, 프레이밍, 조명, 룩에 대한 피커 ID입니다.' },
subject: { type: 'object', description: '인물, 스타일링, 소품에 대한 피커 ID입니다.' },
seed: { type: 'integer', description: '지원하는 모델에서 실행을 반복할 수 있게 하는 고정된 숫자입니다.' },
characterReferences: { type: 'object[]', description: 'Gemini Omni와 Gemini Omni Flash에서 클립이 얼굴을 유지할 인물로, 최대 3명입니다. POST /v1/text-to-video에서도 받습니다. 아래 규칙을 참고하세요.' },
}}
/>

모델에 따라 다른 규칙이 몇 가지 있습니다.

- **Seedance 2 계열에서는 `resolution`과 `aspectRatio`가 그대로 전달됩니다.** 모델이 지원하지 않는 값은 무시되며, 거부되지 않습니다. `4k`와 `adaptive`를 지원하는 모델은 [Seedance 2](https://nodaro.ai/docs/models/video/seedance-2)뿐입니다. [Seedance 2.5](https://nodaro.ai/docs/models/video/seedance-2-5)는 한 번의 호출로 최대 30초를 만들며, 시작 프레임이 있으면 항상 그 프레임의 화면 비율로 렌더링합니다.
- **[MiniMax H3](https://nodaro.ai/docs/models/video/minimax-h3)는 `resolution`으로 `2K` 또는 `768P`를 받습니다.** 정확히 이 표기대로 쓰세요. `GET /v1/nodes/:type`의 `providerResolutionWire`에 보낼 값이 나와 있습니다.
- **Seedance 2와 MiniMax H3에서는 프레임과 레퍼런스를 함께 쓸 수 있습니다.** 시작 프레임이나 종료 프레임과 함께 레퍼런스를 보내면, 프레임은 고정된 시작점이나 끝점이 아니라 프롬프트 속 번호가 매겨진 레퍼런스가 됩니다. [Wan 3.0](https://nodaro.ai/docs/models/video/wan-3-0)은 둘을 함께 보낼 수 없으므로, 대신 프레임이 레퍼런스 목록 끝에 추가되며 호출은 그대로 성공합니다.
- **레퍼런스 동영상은 비용이 더 듭니다.** 레퍼런스 동영상에 요금을 매기는 모델에서는 레퍼런스 클립의 길이와 출력 길이를 더해 가격이 정해지므로, 원본 클립이 길수록 더 많은 크레딧이 예약됩니다. Wan 3.0은 출력 길이만 초 단위로 청구합니다.
- **`characterReferences`는 실제 인물의 얼굴을 유지합니다.** [Gemini Omni](https://nodaro.ai/docs/models/video/gemini-omni)와 [Gemini Omni Flash](https://nodaro.ai/docs/models/video/gemini-omni-flash)에서 쓸 수 있습니다. 각 항목은 `{ imageUrl, description, bodyImageUrl?, name?, voice? }`로, 인물 사진, 최대 2,000자의 인물 설명, 선택 사항인 전신 사진, 최대 100자의 선택 사항인 이름입니다. `referenceImageUrls`는 모델에 느슨한 맥락일 뿐이지만, 이 입력은 얼굴을 유지합니다. 다른 모델은 `400 character_references_unsupported`로 응답합니다. 같은 요청에 시작 프레임이나 종료 프레임이 있으면 `400 character_references_with_start_frame` 또는 `400 character_references_with_end_frame`으로 응답합니다. 요청은 모델의 입력 예산인 7단위에 맞아야 합니다. 이미지는 1, 동영상은 2, 인물은 1이며 `bodyImageUrl`이 있으면 2입니다. 예산을 넘는 요청은 `400 character_references_quota`로 응답합니다. 인물 레퍼런스는 크레딧을 추가하지 않습니다.
- **`voice`는 인물 레퍼런스에 보이스 하나를 고정합니다.** 여러 클립에 걸쳐 적용되며, 형식은 `{ preset, description?, exampleLine? }`이고 `preset`은 Gemini 보이스 ID 중 하나입니다. 한 요청에서 서로 다른 보이스를 최대 3개까지 고정할 수 있으며, 넘으면 `400 character_voices_over_limit`으로 응답합니다. 보이스 목록은 [동영상 생성](https://nodaro.ai/docs/nodes/video/generate-video)을 참고하세요.

모든 모델의 길이, 해상도, 크레딧 가격은 [동영상 모델](https://nodaro.ai/docs/models/video)을 읽거나 `GET /v1/models`를 호출해 확인하세요.

## 레퍼런스 사용
레퍼런스는 모델이 맞춰야 할 이미지이며, 동영상에서는 클립과 오디오도 포함됩니다. URL을 나열한 단순 목록으로 보내거나, 경로가 번호와 레이블을 붙여 프롬프트에 써 넣는 **구조화된 레퍼런스**로 보낼 수 있습니다. 에디터가 연결된 노드를 처리하는 방식과 똑같습니다.

### 구조화된 레퍼런스
`connectedReferences`는 `POST /v1/generate-image`, `POST /v1/generate-video`, `POST /v1/text-to-video`, `POST /v1/extend-video`에서 받습니다. 각 항목은 이미지 하나를 설명합니다.

<TypeTable
type={{
id: { type: 'string', description: '레퍼런스에 붙이는 나만의 고정 ID입니다. : 와 /를 포함할 수 있으므로 이미지 URL도 유효한 ID입니다.', required: true },
defaultName: { type: 'string', description: '레퍼런스 이름입니다. 예를 들어 Maya나 Old Town입니다.', required: true },
source: { type: 'string', description: 'manual, wired-image, wired-character, wired-face, wired-object, wired-creature 또는 wired-location입니다.', required: true },
url: { type: 'string', description: '이미지 URL입니다. 사설 주소나 http, https가 아닌 스킴은 거부됩니다.', required: true },
description: { type: 'string', description: '프롬프트가 이 레퍼런스에 쓰는 레이블입니다.' },
defaultRole: { type: 'string', description: '멘션에 역할이 없을 때 이미지에서 가져올 요소입니다. 예를 들어 background입니다.' },
identityLock: { type: '{ enabled: boolean; text?: string }', description: '기본값은 꺼짐입니다. 켜면 이 레퍼런스의 외모를 정확히 고정하는 짧은 문장을 추가합니다. text는 기본 문구를 대체하며, 그 안의 {ref}는 레퍼런스 바인딩을 나타냅니다.' },
descriptionOverride: { type: 'string', description: '최대 2,000자입니다. 이번 실행에만 적용되는, 이 레퍼런스에 대한 설명입니다. 캐릭터나 오브젝트에 저장된 설명보다 우선합니다.' },
}}
/>

동영상 경로에서는 경로가 이 항목들을 번호가 매겨진 레퍼런스로 바꿉니다.

- **멘션하지 않은 레퍼런스는 모두 첨부됩니다.** URL은 중복이 제거된 채 레퍼런스 목록에 추가되고, `@image_1 (reference): <label>` 같은 줄이 붙습니다. `wired-character` 항목은 그 대신 `Use these characters:` 지시의 일부가 됩니다.
- **목록은 번호를 매기기 전에 모델의 한도에 맞게 잘립니다.** 따라서 프롬프트의 레퍼런스 번호가 전송되지 않은 이미지를 가리키는 일은 없습니다.
- **프롬프트의 `{image:N:label}` 토큰**은 첨부된 레퍼런스를 기준으로 번호가 매겨져 `the label from @image_N`이 됩니다.
- **`{ref:<id>}`와 `{ref:<id>:label}` 토큰**은 내가 지정한 `id`로 레퍼런스를 가리킵니다. 플랫폼이 목록에 번호를 매긴 뒤 토큰을 레퍼런스 번호로 바꾸므로, 번호를 직접 계산할 필요가 없습니다. 목록의 번호는 이 순서로 매겨집니다. 단순 목록인 `referenceImageUrls`가 먼저 오고, 그다음 멘션하지 않은 캐릭터, 그다음 나머지 항목이 내가 보낸 순서대로 옵니다. 한도를 넘었거나 모델이 레퍼런스를 받지 않아서 레퍼런스가 첨부되지 않은 토큰은 레이블로, 그다음 `defaultName`으로, 그다음 빈 값으로 대체됩니다. 토큰이 원시 텍스트 그대로 모델에 전달되는 일은 없습니다.
- **레퍼런스 ID 목록인 `referenceOrder` 필드**는 레퍼런스 순서를 바꾸고 그에 맞게 번호를 다시 매깁니다. `POST /v1/generate-image`도 이 필드를 받습니다.

`connectedReferences`는 이미지만 전달합니다. `referenceVideoUrls`와 `referenceAudioUrls`는 단순 목록으로 유지됩니다. `connectedReferences`를 생략하면 경로는 이전과 같이 동작합니다. 내 `prompt`와 `referenceImageUrls`가 그대로 전송됩니다.

`POST /v1/generate-image`와 MCP 도구 `generate_image`에서 `prompt`의 `{image:N}` 또는 `{image:N:label}` 토큰은 N번째 레퍼런스 이미지를 가리킵니다.

- **연결된 레퍼런스가 없으면** N은 단순 목록인 `referenceImageUrls`를 순서대로 셉니다. 따라서 `{image:1}`은 내가 보낸 첫 번째 URL입니다. 다른 레퍼런스가 `describedReferences`뿐인 요청도 마찬가지입니다.
- **`connectedReferences` 항목이 하나 이상 있으면** N은 `connectedReferences` 항목을 순서대로 셉니다. 같은 요청의 단순 목록 `referenceImageUrls`도 연결된 레퍼런스보다 앞에 첨부되지만, 그 이미지를 가리키는 토큰은 없습니다. 프롬프트에서 사진을 지칭하려면 그 사진을 `connectedReferences` 항목으로 보내세요.
- **해당 위치에 이미지가 없는 토큰**은 레이블로 바뀝니다. `{image:2:person}`은 `person`이 됩니다. 레이블이 없는 `{image:2}`는 빠집니다.

편집기와 워크플로 실행도 같은 방식으로 토큰에 번호를 매깁니다. 텍스트 노드처럼 중간 노드를 거쳐 이미지 생성 노드에 도달한 이미지도 전송되고 번호가 매겨집니다.

### 이미지 레퍼런스를 받는 동영상 모델
| 모델 계열 | 이미지 레퍼런스 |
| --- | --- |
| [Seedance 2](https://nodaro.ai/docs/models/video/seedance-2) 계열 | 최대 9개 |
| [HappyHorse Ref2V](https://nodaro.ai/docs/models/video/happyhorse-1-1-ref2v) | 최대 9개 |
| [Gemini Omni](https://nodaro.ai/docs/models/video/gemini-omni), [Kling 3 Omni](https://nodaro.ai/docs/models/video/kling-3-omni), [Grok Imagine 이미지로 동영상 만들기](https://nodaro.ai/docs/models/video/grok-imagine-i2v) | 최대 7개 |
| [VEO 3.1 Fast](https://nodaro.ai/docs/models/video/veo-3-1-fast)와 [VEO 3.1 Lite](https://nodaro.ai/docs/models/video/veo-3-1-lite) | 최대 3개 |

그 밖의 모델에서는 `{image:N}` 토큰이 레이블로 줄어들고 아무것도 첨부되지 않습니다. [VEO 3.1 Quality](https://nodaro.ai/docs/models/video/veo-3-1-quality)는 목록에 없습니다. `veo3`로 보낸 레퍼런스는 무시되며, 실행에는 프레임이 사용됩니다.

### 레퍼런스만으로 동영상 만들기
`POST /v1/generate-video`에서는 보내는 레퍼런스 종류 중 하나 이상을 모델이 지원하면 시작 프레임이 선택 사항입니다. 예를 들어 Kling 3 Omni에 `referenceImageUrls`만 보내는 경우입니다. VEO 3.1 Fast나 Lite에서 레퍼런스만으로 실행하면 자동으로 레퍼런스 모드로 바뀌므로 `generationType`이 필요하지 않습니다. 모델이 사용할 수 없는 레퍼런스 종류는 포함되지 않습니다. 이미지만 받는 모델에 오디오 레퍼런스만 보내면 `400`으로 거부됩니다.

**종료 프레임만** 보내는 요청은 종료 프레임을 레퍼런스에 포함하는 모델에서 받아들여집니다. Seedance 2 계열, MiniMax H3, Wan 3.0이 해당됩니다. `referenceImageUrls`에 같은 이미지를 반복하지 말고 `endFrameUrl`을 한 번만 보내세요. `@nodaro/shared` 패키지가 `videoProviderFoldsLoneEndFrame(provider)`를 내보내므로, 내 인터페이스에서도 같은 규칙을 쓸 수 있습니다.

요청에 시작 프레임도, 레퍼런스 모드도, 모델이 쓸 수 있는 레퍼런스도 없으면 모델에 따라 응답이 다릅니다.

- Kling 3 Omni, HappyHorse Ref2V, Hailuo 2.3처럼 텍스트만으로 동영상을 만들 수 없는 모델은 `400 image_required`로 응답합니다. 메시지에서 레퍼런스로 대신할 수 있는지 알려 줍니다. 이런 모델이 무엇인지는 `GET /v1/models`가 기준입니다.
- 그 밖의 모든 모델은 `400 validation_error`로 응답하며, 프롬프트만으로 클립을 만들려면 `POST /v1/text-to-video`를 사용하라고 안내합니다.

### 동영상 연장
`POST /v1/extend-video`는 `provider: "seedance-2-extend"`일 때만 `connectedReferences`와 `referenceImageUrls`를 받습니다. 다른 연장 모델은 이 필드들을 `400`으로 거부합니다. 원본 클립의 마지막 프레임이 내 이미지 다음에 레퍼런스 자리 하나를 차지하므로 한도는 내 이미지 8개이며, 그래서 내 번호가 밀리지 않습니다. 원본의 마지막 2초는 `@video_1`로 전달되며 연장 요금에 이미 포함되어 있으므로, 레퍼런스 이미지는 크레딧을 추가하지 않습니다. 동영상 연장에는 `direction`과 `subject`가 없습니다. 프롬프트가 이미 룩이 정해진 클립을 이어 가기 때문입니다.

### 설명으로 지정하는 레퍼런스
`describedReferences`는 설명할 수는 있지만 아직 이미지가 없는 대상, 예를 들어 스크립트 속 배역에 이름을 붙입니다. `POST /v1/generate-image`, `/v1/generate-video`, `/v1/text-to-video`, `/v1/extend-video`는 `{ name, description }` 항목을 최대 10개 받습니다. 이름은 최대 80자, 설명은 최대 2,000자입니다.

- **아무것도 첨부되지 않습니다.** 설명으로 지정한 레퍼런스는 레퍼런스 자리를 차지하지 않으며, 다른 레퍼런스의 번호도 바뀌지 않습니다.
- **모델에는 텍스트 한 줄로 전달됩니다.** 형식은 `<Name> — <description>.`입니다. 프롬프트에 이름을 그대로 쓰세요. 예를 들어 `Natalie walks down the pier.`처럼 쓰면, 이 줄이 Natalie가 누구인지 모델에 알려 줍니다. 이 레퍼런스에는 `@` 멘션을 쓰지 마세요. 그 문법은 첨부된 이미지를 가리킵니다.
- **단독으로도 작동합니다.** `connectedReferences` 없이 `describedReferences`만 보내도 됩니다. 캐릭터가 생기기 전에 이야기를 먼저 쓰는 경우가 대표적입니다.
- **이름이나 설명이 없는 항목은 제외되며**, 반복된 이름은 한 번만 쓰입니다. 설명으로 지정한 레퍼런스에는 URL이 없으므로 모든 연장 모델이 받습니다.

### 실행별 설명과 캡션
- **`connectedReferences` 항목의 `descriptionOverride` 필드**는 이번 실행에만 해당하는 레퍼런스의 정체를 최대 2,000자로 알려 줍니다. 레퍼런스의 지시에 이미 있는 설명 자리를 저장된 설명보다 먼저 채웁니다. 지시에 설명이 없으면 한 줄을 추가하므로, 모델은 이 내용을 두 번이 아니라 한 번만 듣습니다.
- **`referenceVideoCaptions`와 `referenceAudioCaptions` 필드**는 `POST /v1/generate-video`와 `/v1/text-to-video`에서 레퍼런스 클립과 오디오를 각각 최대 500자로 설명합니다. 인덱스가 맞춰져 있어 `referenceVideoCaptions[0]`은 `referenceVideoUrls[0]`을 설명합니다. 각 캡션은 `@video_1: <caption>.` 같은 줄이 되며, 빈 항목은 정렬을 깨지 않고 클립 하나를 건너뜁니다.

### 프롬프트에서 레퍼런스 멘션
`POST /v1/generate-image`에서는 레퍼런스를 끝의 목록에 두는 대신 문장 안에 넣을 수 있습니다. `@<name-slug>:<index>`라고 쓰거나, 가져올 요소를 지정하려면 `@<name-slug>:<index>:<role>`이라고 쓰세요.

```json
{
"prompt": "a wide shot of @nessie:1 rising beside @dock:2:material",
"connectedReferences": [
{ "id": "cr-1", "defaultName": "Nessie", "source": "wired-creature", "url": "https://…/nessie.png" },
{ "id": "ob-1", "defaultName": "Dock", "source": "wired-object", "url": "https://…/dock.png" }
]
}
```

모델은 “레퍼런스 이미지 A의 크리처가 레퍼런스 이미지 B의 재질 옆에서 솟아오르는 와이드 샷”이라는 뜻의 영어 문장 `a wide shot of the creature from reference image A rising beside the material from reference image B`를 받습니다.

- **슬러그는 `defaultName`에서 만들어집니다.** 소문자로 바꾸고, 그 밖의 문자가 이어진 부분은 각각 `-` 하나로 바꿉니다. `Old Town`은 `old-town`이 됩니다. 따로 설정할 슬러그 필드는 없습니다.
- **인덱스는 멘션과 레퍼런스를 짝짓는 데만 쓰입니다.** 프롬프트에는 쓰이지 않으며, 레퍼런스 번호는 플랫폼이 직접 매깁니다.
- **역할**은 이미지(`manual`과 `wired-image`)의 경우 `object`, `person`, `face`, `clothes`, `background`, `style`, `pose`, `texture`입니다. 크리처는 `creature`, `anatomy`, `markings`, `pose`, `color`, `style`을 받습니다. 오브젝트는 `object`, `shape`, `material`, `color`, `texture`, `style`을 받습니다. 그 밖의 한 단어는 쓴 그대로 전달됩니다. 역할이 없으면 항목의 `defaultRole`이 적용됩니다.
- **`~lock`과 `~nolock` 접미사**를 `@town:1:background~lock`처럼 멘션 뒤에 붙이면, 그 멘션에서 해당 레퍼런스의 외모 고정을 켜거나 끕니다.
- **이름은 캐릭터, 장소, 이미지, 크리처, 오브젝트 순서로 해석됩니다.** 캐릭터와 이미지가 같은 이름을 쓰면 캐릭터를 뜻하며, 같은 종류 안에서는 먼저 일치하는 항목이 우선합니다.
- **슬러그가 숫자로 시작하는 이름은 멘션할 수 없습니다.** 예를 들어 `3D Render`는 `3d-render`가 됩니다. 멘션하려면 레퍼런스 이름을 바꾸세요. 모델의 한도를 넘은 레퍼런스를 멘션하면 그대로 텍스트로 남습니다.
- **레퍼런스를 멘션하면 그 레퍼런스가 이동합니다.** 끝의 목록에서 내가 입력한 위치로 옮겨지고, 그 뒤에 오는 레퍼런스들의 글자가 문장 순서에 맞게 바뀝니다. 크리처나 오브젝트의 경우, 멘션은 끝에 추가될 줄도 대체합니다.

### 레퍼런스 고정
`POST /v1/generate-image`의 `referenceLock`은 장면 앞에 Nodaro의 검증된 레퍼런스 충실도 문구를 추가합니다.

| 값 | 추가 내용 | 용도 |
| --- | --- | --- |
| `standard` | 레퍼런스에 보이는 것만 사용하고, 닮은 모습을 유지하고, 레퍼런스를 함께 구성하라는 지시 | 여러 레퍼런스로 만드는 구성 |
| `multi-person` | 위와 같은 지시에, 얼굴을 절대 바꾸거나 섞지 말라는 규칙을 더한 것 | 한 샷에 얼굴이 둘 이상인 경우 |

텍스트가 아니라 ID를 보냅니다. 문구는 플랫폼이 관리하며, 클라이언트를 업데이트하지 않아도 개선됩니다. 필드를 생략하면 고정이 추가되지 않습니다.

## ID로 지정하는 시네마틱 연출
`direction`은 카메라, 조명, 룩을 문장 대신 **피커 ID**로 설명합니다. `POST /v1/generate-image`, `POST /v1/generate-video`, `POST /v1/text-to-video`가 이 필드를 받습니다. Nodaro가 ID마다 검증된 자체 문구를 프롬프트에 써 넣으므로, 저장된 요청은 클라이언트가 쓴 텍스트에 고정되지 않고 시간이 지나며 개선된 문구를 반영합니다.

```json
{
"prompt": "a knight on a hill",
"provider": "nano-banana-pro",
"direction": {
"shotSize": "wide-shot",
"lens": "wide-24mm",
"lightingStyle": "rembrandt",
"style": "anime",
"mood": ["happy", "joyful"]
}
}
```

### 키와 ID의 출처
각 키는 크리에이티브 컨트롤 피커의 필드입니다. 유효한 ID는 에디터의 피커가 쓰는 것과 같은 카탈로그인 `GET /v1/picker-catalogs/<picker>`에서 가져오세요.

| 키 | 피커 | 이미지 | 동영상 |
| --- | --- | --- | --- |
| `shotSize`, `angle`, `coverage`, `composition`, `vantage` | [프레이밍](https://nodaro.ai/docs/nodes/creative-controls/framing) | 예 | 예 |
| `pose` | [포즈](https://nodaro.ai/docs/nodes/creative-controls/pose) | 예 | 예 |
| `compositionEffect` | [컴포지션 효과](https://nodaro.ai/docs/nodes/creative-controls/composition-effects) | 예 | 예 |
| `cameraFormat` | [카메라 / 필름](https://nodaro.ai/docs/nodes/creative-controls/camera-format) | 예 | 예 |
| `lens` | [렌즈](https://nodaro.ai/docs/nodes/creative-controls/lens) | 예 | 예 |
| `aperture`, `shutterSpeed`, `isoValue` | [노출 설정](https://nodaro.ai/docs/nodes/creative-controls/exposure-settings) | 예 | 아니요 |
| `timeOfDay`, `lightingStyle`, `lightingDirection`, `lightingRatio`, `colorTemperature` | [조명](https://nodaro.ai/docs/nodes/creative-controls/lighting) | 예 | 예 |
| `colorLook` | [색감 / 룩](https://nodaro.ai/docs/nodes/creative-controls/color-look) | 예 | 예 |
| `atmosphere` | [대기 효과](https://nodaro.ai/docs/nodes/creative-controls/atmosphere) | 예 | 예 |
| `postProcess` | [후처리 효과](https://nodaro.ai/docs/nodes/creative-controls/post-process-effects) | 예 | 아니요 |
| `style` | [스타일](https://nodaro.ai/docs/nodes/creative-controls/style) | 예 | 예 |
| `mood` | [분위기](https://nodaro.ai/docs/nodes/creative-controls/mood) | 예 | 예 |
| `aesthetic` | [감성](https://nodaro.ai/docs/nodes/creative-controls/aesthetic) | 예 | 예 |
| `photoGenre` | [사진 장르](https://nodaro.ai/docs/nodes/creative-controls/photo-genre) | 예 | 아니요 |
| `photographer` | [사진작가](https://nodaro.ai/docs/nodes/creative-controls/photographer) | 예 | 아니요 |
| `renderQuality` | [렌더링 품질](https://nodaro.ai/docs/nodes/creative-controls/render-quality) | 예 | 아니요 |
| `setting` | [배경](https://nodaro.ai/docs/nodes/creative-controls/setting) | 예 | 예 |
| `era` | [시대](https://nodaro.ai/docs/nodes/creative-controls/era) | 예 | 예 |
| `backdrop` | [배경막](https://nodaro.ai/docs/nodes/creative-controls/backdrop) | 예 | 예 |
| `cameraMotion` | [카메라 모션](https://nodaro.ai/docs/nodes/creative-controls/camera-motion) | 아니요 | 예 |
| `actionFx` | [액션 효과](https://nodaro.ai/docs/nodes/creative-controls/action-fx) | 아니요 | 예 |
| `temporalSpeed`, `temporalFreeze`, `temporalDirection`, `temporalShutter` | [시간 표현](https://nodaro.ai/docs/nodes/creative-controls/temporal) | 아니요 | 예 |
| `transition` | [전환](https://nodaro.ai/docs/nodes/creative-controls/transition) | 아니요 | 예 |
| `loopSubject` | [루프 소재](https://nodaro.ai/docs/nodes/creative-controls/loop-subject) | 아니요 | 예 |

경로에 해당하지 않는 키는 받아들여지지만 아무것도 추가하지 않습니다. 따라서 하나의 룩 ID 맵을 이미지 경로와 동영상 경로에 그대로 보낼 수 있습니다.

### 값과 한도
- **ID 하나 또는 목록.** 다중 선택 키인 `mood`, `aesthetic`, `photographer`, `atmosphere`, `postProcess`, `composition`, `lightingStyle`은 각자의 한도까지 받으며, 초과한 ID는 제외됩니다. 단일 선택 키에 목록을 보내면 첫 번째 항목이 사용됩니다.
- **두 가지 한도는 거부됩니다.** 한 키에 항목이 8개를 넘는 경우와 ID가 100자를 넘는 경우이며, `400 validation_error`로 응답합니다.
- **없는 것과 빈 것은 다릅니다.** 키가 없으면 힌트가 없다는 뜻이지, 기본값을 쓰라는 뜻이 아닙니다. 빈 문자열이나 빈 목록은 아무것도 추가하지 않습니다.
- **내 팔레트와 무드보드는 절대 건너뛰지 않습니다.** `/v1/generate-image`에서는 `colorLook`이 `"palette:<id>"`도 받고, `style`이 `"moodboard:<id>"`도 받습니다. [내 팔레트와 무드보드](#palettes-and-moodboards)를 참고하세요.
- **알 수 없는 키와 ID는 거부되지 않고 건너뜁니다.** 이전 서버에 새 클라이언트가 요청하면 오류 대신 힌트가 줄어들 뿐이므로, 새 키를 보내는 클라이언트보다 서버를 먼저 업데이트하세요.
- **사용자 지정 카탈로그 팩.** 자체 카탈로그 팩을 등록한 배포 환경에서는 `GET /v1/picker-catalogs`가 팩이 추가한 ID를 나열합니다. 이 ID는 받아들여지지만 `direction`에 문구를 추가하지 않습니다.

### 문구가 들어가는 위치
구절은 내 프롬프트 뒤의 `[style]` 섹션에 추가됩니다. 필름 줄에는 `cameraFormat`, `colorLook`, `style`, `era`가 들어가고, 장면 줄에는 나머지 룩 키가 들어갑니다.

```text
a knight on a hill

[style]:
<film line>
<scene line>
```

- 한 줄 안의 순서는 키를 보낸 순서가 아니라 플랫폼이 정한 고정 순서를 따릅니다. 두 키가 같은 구절을 만들면 한 번만 쓰입니다.
- 비어 있는 줄은 생략됩니다. 아무 키도 내용을 추가하지 않으면 섹션 자체가 없으며, 내 `prompt`가 그대로 모델에 전달됩니다.
- 동영상 경로에서는 모션 키가 다르게 동작합니다. `cross-dissolve` 같은 짧은 전문 용어를 추가하며, 모션은 샷의 일부이므로 본문의 내 문장 뒤에 남습니다. `cameraMotion`이 가장 먼저 옵니다. `[style]` 섹션에는 룩 키만 들어갑니다.

### 프롬프트가 너무 길 때
모델마다 받을 수 있는 프롬프트 길이가 다릅니다. `direction`을 모두 채우면 그것만으로도 작은 한도를 넘을 수 있습니다. 예를 들어 이미지 쪽의 Seedream은 3,000자, 동영상 쪽의 Kling은 1,000자입니다. 이런 경우 Nodaro는 고정 순서의 끝에서부터 연출 구절을 하나씩 제거해 프롬프트가 한도에 들어가게 합니다. 그 전에 다른 것이 제거되는 일은 없습니다.

- 피사체 구절은 모든 연출 구절이 제거된 뒤에만 제거됩니다.
- 내 문장, 내 레퍼런스와 이를 연결하는 구문, `@` 멘션, `Style:`과 `Avoid:` 줄은 항상 남습니다.
- 힌트가 하나도 남지 않았는데도 프롬프트가 한도에 들어가지 않을 때만 텍스트 끝이 잘리고 `...`이 붙습니다.

동영상 경로에서는 경로가 추가하는 레퍼런스 지시도 예산에 포함되며, 이 지시는 절대 제거되지 않습니다. 네거티브 프롬프트 설정이 없는 모델에서는 내 `negativePrompt`가 `Avoid:` 줄로 추가되고 이 줄의 공간이 먼저 확보되므로, 긴 네거티브 프롬프트는 내 문장이 아니라 힌트 구절을 줄입니다. 선택 사항인 `injectCharacterContext` 텍스트는 이 단계 이후에 추가되며 예산에 포함되지 않습니다.

작업에는 무슨 일이 있었는지 기록됩니다. `input_data.prompt`는 모델이 받은 내용이고, `input_data.userPrompt`는 내가 보낸 텍스트이며(`direction`만 보냈다면 빈 문자열), `input_data.direction`은 내가 보낸 그대로의 ID입니다.

### 노드에 저장된 연출
저장된 워크플로의 [이미지 생성](https://nodaro.ai/docs/nodes/image/generate-image) 노드는 API, MCP 또는 워크플로를 만드는 앱이 써 넣은 같은 `direction` 객체를 데이터에 담을 수 있습니다. 에디터는 모든 실행과 최종 프롬프트 미리보기에서 이를 반영합니다. 저장된 ID는 연결된 프레이밍, 조명, 스타일 피커에 더해집니다. 연결된 힌트가 먼저 오고, 그다음 저장된 ID가 옵니다. 프리셋과 워크플로 내보내기는 노드의 나머지 데이터와 함께 ID도 보존합니다.

## 내 팔레트와 무드보드
**팔레트**는 내가 만든 2~12개 색상의 모음이고, **무드보드**는 내가 만든 1~12개 레퍼런스 이미지와 설명의 모음입니다. 한 번 저장해 두면, 기본 제공 색감이나 스타일을 고르는 것과 같은 방식으로 어떤 `POST /v1/generate-image` 실행이든 이것으로 조정할 수 있습니다. 모든 에디션에서 작동합니다. 편집기에서 하는 일은 [팔레트와 무드보드](https://nodaro.ai/docs/guides/palettes-and-moodboards)를 읽어 보세요.

| | 팔레트 | 무드보드 |
| --- | --- | --- |
| 담는 내용 | 1~80자의 이름, `#RRGGBB` 형식의 색상 2~12개, 최대 500자의 선택 메모 | 이름, 최대 1,000자의 선택 설명, 이미지 1~12개 `{ url, caption? }` |
| 사용 방법 | `direction.colorLook: "palette:<id>"` | `direction.style: "moodboard:<id>"` |
| 실행에 들어가는 것 | 모든 색상이 일상적인 이름과 16진수 코드로 프롬프트에 들어감 | 설명이 프롬프트에 들어가고, 이미지가 스타일 레퍼런스로 들어감 |

```json
{
"prompt": "a lighthouse on a cliff at dusk",
"provider": "nano-banana-pro",
"referenceImageUrls": ["https://example.com/my-lighthouse.png"],
"direction": {
"colorLook": "palette:0f9e8d7c-6b5a-4c3d-9e2f-1a2b3c4d5e6f",
"style": "moodboard:1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lightingStyle": "rembrandt"
}
}
```

### 팔레트와 무드보드 관리
| 메서드 | 경로 | 본문 또는 쿼리 | 하는 일 |
| --- | --- | --- | --- |
| `GET` | `/v1/palettes` | `limit`(1~100, 기본값 50), `cursor` | 내 팔레트를 최신순으로 반환합니다: `{ data, nextCursor }`. |
| `POST` | `/v1/palettes` | `{ name, colors, description? }` | 팔레트를 만듭니다. `201`로 응답합니다. 색상은 대문자로 저장됩니다. |
| `GET` | `/v1/palettes/:id` | 없음 | 팔레트 하나입니다. |
| `PATCH` | `/v1/palettes/:id` | `name`, `colors`, `description` 중 아무것이나 | 보낸 필드만 씁니다. `description: null`은 설명을 지웁니다. |
| `DELETE` | `/v1/palettes/:id` | 없음 | `{ success: true }`를 반환합니다. |
| `GET` | `/v1/moodboards` | `limit`, `cursor` | 내 무드보드를 최신순으로 반환합니다: `{ data, nextCursor }`. |
| `POST` | `/v1/moodboards` | `{ name, description?, images }` | 무드보드를 만듭니다. `201`로 응답합니다. |
| `GET` | `/v1/moodboards/:id` | 없음 | 무드보드 하나입니다. |
| `PATCH` | `/v1/moodboards/:id` | `name`, `description`, `images` 중 아무것이나 | 보낸 필드만 씁니다. `images`는 이미지 전체를 교체합니다. |
| `DELETE` | `/v1/moodboards/:id` | 없음 | `{ success: true }`를 반환합니다. 연결된 이미지는 건드리지 않습니다. |

- **무드보드 이미지는 링크이며, 사본이 아닙니다.** 각 `url`은 `localhost`나 사설 네트워크가 아닌 `http` 또는 `https` 주소이며, 최대 2,048자입니다. 캡션은 최대 200자입니다. 내가 삭제한 이미지나 요금제의 미디어 보존 기간에 따라 제거된 이미지는 무드보드에서도 사라집니다.
- **`nextCursor`**는 불투명한 토큰입니다. 다음 페이지를 받으려면 `cursor`로 다시 보내세요. 마지막 페이지에서는 `null`입니다.
- **나만 쓸 수 있습니다.** 다른 사람의 팔레트나 무드보드는 존재하지 않는 것과 똑같이 `404`로 응답합니다.
- **범위:** OAuth 앱 토큰은 읽기에 `assets:read`, 쓰기에 `assets:write`가 필요합니다. 쓰기는 분당 30회로 제한됩니다.
- **같은 라우트**가 SDK(`client.palettes`, `client.moodboards`), CLI(`nodaro palettes`, `nodaro moodboards`), MCP 도구 `list_palettes`, `save_palette`, `list_moodboards`, `save_moodboard`에 있습니다. MCP `generate_image`는 `palette_id`와 `moodboard_id`를 받습니다. MCP에는 삭제 기능이 없습니다.

### 실행에 들어가는 것
- **팔레트**는 `[style]` 섹션의 필름 줄, 기본 제공 색감이 들어갈 자리에 `color palette: <name> <hex>, …`를 추가합니다. 모델은 이름이 붙은 색상을 더 잘 따르므로, 각 색상에는 가장 가까운 일상적인 이름과 정확한 16진수 코드가 붙습니다. 팔레트의 메모는 프롬프트에 추가되지 않습니다.
- **무드보드**는 기본 제공 스타일이 들어갈 자리에 설명을 한 줄로 추가합니다. 설명이 없으면 이미지만 들어갑니다.
- **다른 모든 연출 구절처럼**, 프롬프트가 모델의 한도를 넘으면 이 구절이 먼저 제거됩니다.
- **무드보드의 이미지는 레퍼런스 이미지로 모델에 보내지며**, 각 이미지에는 피사체나 구도가 아니라 색상, 빛, 질감, 분위기를 가져가라고 모델에 알려 주는 짧은 줄이 붙습니다. 이미지에 캡션이 있으면 캡션도 추가됩니다.
- **내 레퍼런스가 먼저입니다.** 내가 직접 보낸 이미지는 자리와 번호를 유지합니다. 무드보드의 이미지는 그 뒤에 남은 레퍼런스 자리만 채웁니다.
- **모델이 받는 수를 절대 넘지 않습니다.** 들어가지 않는 이미지는 무드보드의 순서대로 보내지 않으며, 응답에 `moodboard_images_not_sent` 경고가 담깁니다. 레퍼런스 이미지를 받지 않는 모델은 이미지를 하나도 받지 않으며, 설명은 그래도 들어갑니다.
- **가격.** 이미지는 레퍼런스로 계산되므로, 실행은 그만큼의 레퍼런스가 있는 다른 실행과 같은 가격이 매겨집니다. 실행 전에 확인하는 가격과 예약되는 가격은 같은 이미지, 곧 실제로 보내는 이미지를 셉니다.
- **`aspectRatio: "auto"`**는 실행이 보내는 첫 번째 이미지의 비율을 유지합니다. 내 이미지를 보내지 않으면 무드보드의 첫 번째 이미지입니다.
- **작업은 사본이 아니라 ID를 저장합니다.** 다시 실행하면 그 시점의 팔레트나 무드보드를 사용합니다.

### 오류와 한도
| 상태 | 코드 | 경우 |
| --- | --- | --- |
| `400` | `validation_error` | 뒤에 uuid가 없는 `palette:` 또는 `moodboard:`, `colorLook` 이외의 키에 쓴 팔레트, `style` 이외의 키에 쓴 무드보드, 같은 키에서 다른 값과 함께 쓴 경우, 또는 `presetId`도 함께 보낸 요청입니다. `field`가 키를 알려 줍니다. |
| `404` | `direction_not_found` | 팔레트나 무드보드가 내 것이 아니거나, 더 이상 존재하지 않습니다. `field`는 `direction.colorLook` 또는 `direction.style`입니다. |

두 응답 모두 무엇이든 만들거나 청구하기 전에 옵니다. 팔레트와 무드보드는 `generate-image`에서만 작동합니다. `POST /v1/generate-video`와 `POST /v1/text-to-video`는 이를 받으면 `400 validation_error`로 응답합니다. 저장된 워크플로에서 `direction`이 팔레트나 무드보드를 가리키는 **이미지 생성** 노드는 워크플로가 실행될 때마다 그 내용을 새로 읽습니다.

## ID로 피사체 지정
`subject`는 같은 개념을 **샷에 등장하는 대상**에 적용합니다. 인물, 인물의 스타일링, 프레임 속 소품입니다. `POST /v1/generate-image`, `POST /v1/generate-video`, `POST /v1/text-to-video`가 이 필드를 받습니다.

```json
{
"prompt": "on the seawall at dusk",
"provider": "nano-banana-pro",
"subject": {
"type": "woman",
"age": "age-30s",
"ethnicity": "east-asian",
"hairBase": "base-short-straight",
"makeup": "makeup-smoky",
"outerwear": "outerwear-trench",
"heldProp": "smartphone"
}
}
```

- **키는 [인물](https://nodaro.ai/docs/nodes/creative-controls/person) 피커와 [스타일링](https://nodaro.ai/docs/nodes/creative-controls/styling) 피커의 필드입니다.** `type`, `age`, `ethnicity`, `faceShape`, `hairColor`, `skinTone`, `makeup`, `outfit`, `outerwear`, `footwear` 등이 있으며, 소품 키 세 개가 더 있습니다. `heldProp`([손에 든 소품](https://nodaro.ai/docs/nodes/creative-controls/held-prop)), `material`([재질](https://nodaro.ai/docs/nodes/creative-controls/material)), `animal`([동물](https://nodaro.ai/docs/nodes/creative-controls/animal))입니다. `GET /v1/picker-catalogs/person`과 `/styling`이 모든 필드와 ID를 나열합니다.
- **`subject`와 `direction`은 겹치지 않습니다.** 키가 서로 다르므로 하나의 선택이 구절 두 개를 추가하는 일은 없습니다. `pose`는 `direction`에 속합니다.
- **`customAge`만 숫자입니다.** 나이를 세 단위로 정확히 지정하려면 `"age": "age-custom"`과 함께 `"customAge": 34`를 보내세요. 값은 반올림되며 0~120 사이로 유지됩니다.
- **목록에는 키마다 한도가 있습니다.** `jewelry`, `wardrobeState`, `distinctiveFeature`는 ID 3개를 받습니다. `ethnicity`, `regionalAesthetic`, `hairColor`, `eyeColor`, `lipState`, `eyeState`, `skinTexture`, `hairState`, `heldProp`, `material`은 2개를 받습니다. `animal`을 포함한 나머지 키는 1개를 받습니다. 초과한 ID는 제외됩니다.
- **거부되는 한도:** 한 키에 항목이 8개를 넘거나, ID가 100자를 넘거나, 키가 128개를 넘거나, 키 이름이 64자를 넘으면 `400 validation_error`로 응답합니다.
- **알 수 없는 키는 제거되고 알 수 없는 ID는 건너뜁니다.** 작업의 `input_data.subject`에 실제로 사용된 ID가 정확히 기록됩니다.
- **피사체 구절은 내 문장의 일부입니다.** 연출 구절보다 앞에 오며, `[style]` 섹션에는 절대 들어가지 않습니다. 인물은 한 구절, 스타일링은 또 다른 구절이 되며, 겹치는 선택은 한 번만 쓰입니다. 이미지 경로에서는 선택마다 전체 구절이 추가되고, 동영상 경로에서는 시작 프레임이 이미 피사체를 보여 주므로 짧은 용어가 추가됩니다.

## 생성 세션
이미지, 동영상, 음악 제작 도구처럼 API로 만든 창작 도구는 작업을 내 **프로젝트** 안의 **세션**에 보관할 수 있습니다. 프로젝트는 세션을 담고, 각 세션은 항목을 담습니다. 모든 기기에서 같은 세션이 보이며, 앱은 자체적으로 아무것도 저장하지 않습니다.

```bash
# 1. Open a session, optionally inside one of your projects.
SESSION=$(curl -s -X POST "https://app.nodaro.ai/v1/sessions" \
  -H "Authorization: Bearer $NODARO_API_KEY" -H "Content-Type: application/json" \
  -d '{"projectId": "<project-id>", "tool": "create-image", "title": "Beach"}' | jq -r .data.id)

# 2. Generate inside it: send sessionId beside the usual settings.
curl -s -X POST "https://app.nodaro.ai/v1/generate-image" \
  -H "Authorization: Bearer $NODARO_API_KEY" -H "Content-Type: application/json" \
  -d "{\"prompt\": \"a red car\", \"sessionId\": \"$SESSION\", \"origin\": \"create-image\"}"

# 3. Read the session's feed, newest first.
curl -s "https://app.nodaro.ai/v1/sessions/$SESSION/items" -H "Authorization: Bearer $NODARO_API_KEY"
```

| 메서드 | 경로 | 하는 일 |
| --- | --- | --- |
| `POST` | `/v1/sessions` | 세션을 만듭니다. 본문: `tool`(필수: 소문자, 숫자, 대시로 최대 40자), `projectId`(선택, 내 프로젝트 중 하나), `title`(선택, 최대 200자). |
| `GET` | `/v1/sessions` | 내 세션을 최신순으로 반환합니다. 쿼리: `projectId`, `tool`, `includeArchived=true`, `limit`(1~100, 기본값 50), `cursor`. 각 세션에는 `itemCount`와, 가장 최근에 완료된 항목의 이미지인 `previewUrls`가 최대 4개 담깁니다. |
| `GET` | `/v1/sessions/:id` | 세션 하나입니다. |
| `PATCH` | `/v1/sessions/:id` | `title`(`null`은 제목 없음)과 `archived`를 바꿉니다. 세션의 프로젝트는 만들 때 정해지며 바뀌지 않습니다. |
| `DELETE` | `/v1/sessions/:id` | 세션과 그 링크를 삭제합니다. 작업은 유지되며, 그 작업을 담은 다른 모든 세션에 그대로 남습니다. |
| `GET` | `/v1/sessions/:id/items` | 피드: 세션에서 만들었거나 가져온 모든 항목을 최신순으로 반환합니다. 각 항목에는 `jobId`, `addedAt`, `imported`, `importedFrom { sessionId, title }`, 그리고 `GET /v1/jobs/:id`가 반환하는 형식의 `job`이 있습니다. `limit`과 `cursor`로 페이지를 넘기세요. |
| `POST` | `/v1/sessions/:id/items` | `{ "jobIds": [...] }`로 작업 1~100개를 사본이 아닌 링크로 가져옵니다. ID 중 하나라도 내 작업이 아니면 아무것도 가져오지 않습니다(`404`). `{ imported, requested }`로 응답합니다. |
| `DELETE` | `/v1/sessions/:id/items/:jobId` | 이 세션에서 항목 하나를 제거합니다. 링크만 사라지며, 작업과 그 작업을 담은 다른 모든 세션은 그대로 남습니다. |

- **작업을 만드는 모든 라우트는 `sessionId`를 받습니다.** `generate-image`, `generate-video`, `text-to-video`, `text-to-speech`, `generate-music` 같은 라우트입니다. 내 세션 중 하나여야 하며, 요청과 같은 워크스페이스 쪽에 있어야 합니다. 그렇지 않으면 요청은 `404`로 응답하며, 아무것도 만들거나 청구하지 않습니다. 워크플로 실행은 절대 세션에 정리되지 않습니다.
- **작업의 `session_id`**는 작업을 만들 때 한 번 설정됩니다. `GET /v1/jobs/:id`와 `GET /v1/jobs`에 표시됩니다.
- **작업을 만드는 모든 라우트는 앱 slug인 `origin`도 받습니다.** 작업은 이를 `input_data.origin`에 저장하며, `GET /v1/jobs?origin=`으로 필터링할 수 있습니다. slug가 아닌 값은 `400 validation_error`로 응답하며, 아무것도 만들거나 청구하지 않습니다.
- **작업을 다른 프로젝트로 옮기려면**, 그 프로젝트의 세션으로 항목을 가져온 다음 이전 세션에서 제거하세요. 제거는 아무것도 삭제하지 않습니다. `DELETE /v1/jobs/:id`로 작업을 삭제하면 모든 세션에서 사라집니다.
- **범위:** OAuth 앱 토큰으로 세션을 읽으려면 `jobs:read`가 필요합니다. 만들기, 편집, 가져오기, 제거에는 `assets:write`가 필요하며, 생성에 `sessionId`를 보낼 때도 마찬가지입니다. 일부 워크플로로 제한된 개인 API 토큰은 이 라우트에 접근할 수 없습니다.

## 노드 조회
`GET /v1/nodes`는 서버가 아는 모든 노드 타입을 나열하고, `GET /v1/nodes/:type`은 하나를 반환합니다. 둘 다 공개되어 있어 토큰이 필요 없고, 5분 동안 캐시됩니다. 알 수 없는 타입에는 `404 not_found`로 응답합니다.

**curl**

```bash
curl -s https://app.nodaro.ai/v1/nodes/generate-image | jq .data
```

**TypeScript SDK**

```ts
const { data: nodes } = await client.nodes.list()
const imageNodes = nodes.filter((n) => n.category === 'ai-image')

const { data: generateImage } = await client.nodes.get('generate-image')
console.log(generateImage.providers)
```

**CLI**

```bash
nodaro nodes list --category ai-image
nodaro nodes get generate-image
```

```json
{
"data": {
"type": "generate-image",
"label": "Generate Image",
"category": "ai-image",
"description": "Generate an image from a text prompt using an AI provider.",
"outputType": "image",
"creditCost": "3-682",
"providers": ["nano-banana-pro", "gpt-image-2", "gpt-image-2-5-flare", "seedream-5-pro", "z-image"],
"capabilities": ["supports-reference-image", "supports-aspect-ratio"],
"inputSchema": {
"fields": [
{ "key": "prompt", "type": "text", "required": true },
{ "key": "provider", "type": "select", "options": ["nano-banana-pro", "gpt-image-2"] },
{ "key": "aspectRatio", "type": "select" },
{ "key": "promptPrefix", "type": "text" },
{ "key": "promptSuffix", "type": "text" }
]
}
}
}
```

| 필드 | 의미 |
| --- | --- |
| `type` | 노드 타입입니다. 대부분의 노드 타입에서는 경로이기도 합니다. `POST /v1/<type>`입니다. |
| `label`, `category`, `description` | 에디터가 노드에 붙이는 이름과 분류입니다. |
| `outputType` | `text`, `image`, `video`, `audio`, `data` 또는 `none`입니다. |
| `creditCost` | 노드의 크레딧 비용 또는 그 범위입니다. 실행에 청구되는 가격으로, 노드의 **실행** 버튼에 표시되는 수치와 같습니다. 동영상 자르기, 동영상 루프, 동영상 합치기에서는 5초 단위 한 단계의 가격이며, 실행 비용은 단계 수에 따라 정해집니다. Nodaro Cloud 전용이며, 크레딧이 없는 에디션에서는 생략됩니다. |
| `providers` | 노드가 `provider`에 받는 모델 ID입니다. |
| `capabilities` | `supports-reference-image` 같은 기능 플래그입니다. |
| `inputSchema.fields` | 노드의 설정으로, 타입, 필수 여부, 옵션이 함께 있습니다. |

프롬프트를 받는 모든 노드는 `promptPrefix`와 `promptSuffix`도 나열합니다. 프롬프트 앞뒤에 추가되는 텍스트입니다. [프롬프트 앞뒤 텍스트](https://nodaro.ai/docs/concepts/prompt-pre-post-text)를 참고하세요. 모델별 한도가 있는 노드에는 추가 필드가 있습니다.

| 필드 | 의미 |
| --- | --- |
| `maxDurationSec` | 노드가 받는 가장 긴 길이입니다. |
| `sparseProviders` | 세그먼트 길이가 몇 가지뿐인 모델입니다. 그 사이의 값은 가장 가까운 값으로 맞춰집니다. |
| `providerResolutions` | 모델별 해상도입니다. 예: `{ "minimax-h3": ["2K", "768P"] }` |
| `providerResolutionWire` | 해상도마다 보낼 정확한 값입니다. 예를 들어 MiniMax H3의 저렴한 등급에는 `768p`가 아니라 `768P`를 보냅니다. |
| `soundtrack` | 동영상 생성 Pro에서 서버가 원본 오디오 `soundtrack` 입력을 받는다는 뜻입니다. |

디스크립터는 계속 확장되므로 모르는 필드는 무시하세요. 같은 데이터로 [노드 참조](https://nodaro.ai/docs/nodes)가 만들어집니다.

## 모델 조회
`GET /v1/models`는 종류와 제작사별로 묶은 모델 카탈로그를 반환합니다. 공개되어 있고 5분 동안 캐시되며, MCP `list_models` 도구가 반환하는 것과 같은 데이터입니다.

**curl**

```bash
curl -s "https://app.nodaro.ai/v1/models?kind=video&mode=i2v" | jq '.totalModels'
```

**TypeScript SDK**

```ts
const catalog = await client.models.list({ kind: 'video', mode: 'i2v' })
for (const section of catalog.sections)
for (const family of section.families)
for (const m of family.models) console.log(m.id)
```

**CLI**

```bash
nodaro models list --kind video --mode i2v
```

응답은 `{ sections, recommendations, totalModels }`입니다. 모든 모델은 **제작사**를 밝히고 제작사의 마크를 담습니다.

| 필드 | 타입 | 의미 |
| --- | --- | --- |
| `maker` | `{ id, name }` | 모델을 만든 회사입니다. `id`는 회사마다 하나씩 바뀌지 않는 값으로, `google`, `black-forest-labs`, `bytedance`처럼 소문자와 하이픈으로 씁니다. 내 에셋은 이 값을 키로 삼고, 새 서버가 제작사를 추가할 수 있으므로 모르는 id도 처리하세요. 모델이 묶이는 `family`와 항상 같지는 않습니다. |
| `logoUrl` | string 또는 `null` | 이 서버의 미디어 CDN에 있는 제작사의 공식 마크 SVG이며, 서버가 공개하지 않으면 `null`입니다. 그럴 때는 제작사 이름이나 첫 글자 같은 대체 표시를 그리세요. 주소는 받은 그대로 쓰세요. 파일 버전이 담겨 있으므로, 마크가 바뀌면 새 주소로 옵니다. |

MCP `list_models` 도구는 `maker`는 담지만 `logoUrl`은 담지 않습니다.

각 모델에는 기능(`modes`, `features`, `aspectRatios`, `resolutions`, `durations`), Nodaro Cloud에서의 변형별 크레딧 `pricing`, 짧은 `promptTips`, 그리고 모델 계열에 출처가 있는 프롬프트 가이드가 있을 때만 `true`인 `doctrineCovered`가 있습니다. `pricing` 크레딧은 실행에 청구되는 가격으로, 에디터의 **실행** 버튼에 표시되는 수치와 같습니다.

텍스트 음성 변환 모델이나 대화 모델 같은 음성 모델은, 음성이 길이로 가격이 매겨지는 곳에서 두 번째 `pricing` 행 `<model>:per-100-chars`를 나열합니다. 이 행은 보낸 텍스트의 100자가 시작될 때마다의 가격이며, 실행당 최소 8단위입니다. 요청은 전체로 가격을 계산한 뒤 한 번만 반올림하므로, `N = max(8, ceil(characters / 100))`일 때 `N × 이 행의 크레딧`은 상한일 뿐입니다. 정확한 가격은 식별자 `<model>:per-100-chars:<N>u`이며, `POST /v1/credits/model-costs`와 `GET /v1/credits/model-cost`가 이 식별자에 답합니다. 곱한 값이 아니라 이 값으로 견적을 내세요. 이 행이 없으면 모든 `:<N>u` 식별자도 없습니다. 모델의 고정 가격 행으로 견적을 내세요. [단위별로 가격이 매겨지는 실행의 정확한 가격](https://nodaro.ai/docs/developers/api/credits#exact-prices-of-runs-priced-per-unit)을 참고하세요.

| 쿼리 | 값 | 필터 대상 |
| --- | --- | --- |
| `kind` | `image`, `video` 또는 `audio` | 한 종류의 미디어 |
| `mode` | 예: `t2i`, `i2v`, `t2v`, `tts`, `video-analysis` | 한 가지 작업 유형 |
| `family` | 제작사, 예: `Google` 또는 `Bytedance` | 한 제작사 |
| `featuredOnly` | `true` | 추천 모델 |

[모델 페이지](https://nodaro.ai/docs/models)에서도 같은 카탈로그를 볼 수 있습니다.

## 피커 값 조회
크리에이티브 컨트롤 피커에는 유효한 ID를 담은 공개 카탈로그가 있으며, 이 ID가 `direction`과 `subject`에 들어가는 값입니다.

| 메서드 | 경로 | 반환 내용 |
| --- | --- | --- |
| `GET` | `/v1/picker-catalogs` | 모든 피커입니다. `nodeType`, `label`, `kind`, 필드, `optionCount`, `imageCount`를 반환합니다. |
| `GET` | `/v1/picker-catalogs/:nodeType` | 피커 하나의 옵션입니다. `?detail=full`은 각 옵션의 `description`과 `promptHint`를 추가하고, `?category=`는 단일 필드 피커를 필터링하며, `?field=`는 다중 필드 피커의 필드 하나를 반환합니다. |
| `GET` | `/v1/catalogs` | 배포 환경이 큐레이션한 모든 카탈로그를 한 번에 반환합니다. `data`는 배포 환경이 카탈로그 팩을 등록했을 때만 있습니다. |
| `POST` | `/v1/text-to-picker` | “AI 채우기”입니다. 자유롭게 쓴 장면 설명에서 여러 피커의 ID를 고릅니다. `llmModel`과 `reasoningEffort`가 없으면 Claude Opus 5.5를 `high`로 실행하며, 채우는 피커 수와 관계없이 28크레딧이 듭니다. |

모든 옵션에는 `id`, `label`, 그리고 프롬프트에 쓰는 짧은 구문인 `term`이 있으며, 이미지가 있는 옵션에는 `imageUrl`도 있습니다. 카탈로그는 `@nodaro/prompts` npm 패키지에 데이터로도 포함되어 있습니다. 전체 형식은 [피커 카탈로그](https://nodaro.ai/docs/developers/picker-catalogs)를 읽어 보세요. 터미널에서는 `nodaro pickers list`, `nodaro pickers get mood --full`, `nodaro pickers analyze "<text>"`를 사용하세요.

## 구조화된 LLM 출력
`POST /v1/llm/structured`는 내가 제공한 JSON Schema에 맞춰 답을 강제하는 언어 모델 호출을 한 번 실행하고, 검증한 뒤 객체로 반환합니다. 모델 등급에 따라 크레딧으로 청구됩니다.

```json
{
"system": "You write production plans.",
"input": "A rainy chase through Rome.",
"jsonSchema": {
"type": "object",
"properties": { "title": { "type": "string" } },
"required": ["title"]
}
}
```

응답은 `{ jobId, output, usage: { inputTokens, outputTokens } }`이며, `output`은 내 스키마의 형태를 따릅니다.

- **필드:** `system`, `input`, `jsonSchema`, 그리고 선택 사항인 `schemaName`(최대 64자), `llmModel`, `reasoningEffort`, `maxRetries`, `origin`, `advancedMode`, `temperature`, `maxTokens`입니다. `system`과 `input`은 각각 최대 100,000자이며, `input`은 최소 1자가 필요합니다.
- **모델:** `llmModel`이 없으면 Gemini 3.6 Flash에서 실행됩니다.
- **스키마:** 루트는 일반 객체 스키마여야 하며, 최대 64KB, 깊이 20단계까지입니다. `properties`, `required`, `additionalProperties`, `items`, 기본 타입, `enum`, `const`, 숫자와 길이의 범위, `multipleOf`, `exclusiveMinimum`, `description`을 사용할 수 있습니다. `anyOf`와 `oneOf`는 루트 아래에서만 받아들여집니다. `not`, `if`, `then`, `else`, `dependent` 키워드, 외부 `$ref`, 루트의 조합 키워드는 `400`으로 응답합니다. 루트 아래에서 `required` 분기를 `anyOf`로 묶으면 받아들여지지만 강제되지는 않으므로, 여러 필드에 걸친 규칙은 직접 확인하세요.
- **재시도:** `maxRetries`는 0~3이며 기본값은 2입니다. 유효하지 않은 답이 검증 오류와 함께 모델로 다시 보내지는 횟수입니다.
- **샘플링:** `maxTokens`는 모든 호출에 적용되며 모델 자체 한도를 넘을 수 없습니다. `temperature`는 `advancedMode: true`를 함께 보내지 않으면 무시되며, 이 모드는 크레딧이 한 단계 올라갑니다. 프리미엄 모델은 premium-direct 가격으로 올라갑니다.
- **소요 시간:** 이 호출은 동기식이며 몇 분이 걸릴 수 있습니다. 시도마다 두 실행 경로 각각에서 최대 240초가 걸릴 수 있으므로, 최악의 경우 기본 `maxRetries`에서 24분, 최댓값에서 32분이 걸립니다. HTTP 클라이언트의 시간 제한을 늘리거나 아래의 작업 방식을 사용하세요. SDK의 기본 `timeoutMs`인 60초는 너무 짧습니다.
- **오류:** `400 validation_error`, `401`, `402`, `500 internal_error`, 재시도를 모두 쓴 뒤의 `502 llm_error`, `503 provider_unavailable`입니다.

SDK에서는 `client.llm.structured(body)`를 호출하세요.

### 작업으로 실행
`POST /v1/llm/structured/jobs`는 같은 본문을 받고 즉시 `{ jobId }`로 응답합니다. `GET /v1/jobs/:id/status`를 폴링하세요. `completed`이면 `output_data`가 `{ output, inputTokens, outputTokens }`이고, `failed`이면 `error_message`가 이유를 알려 줍니다. `GET /v1/jobs?type=llm-structured&origin=<your app>`으로 초안을 다시 찾을 수 있습니다. 추가 필드 세 개를 받습니다.

- `label`: 작업의 표시 이름으로, 최대 120자입니다.
- `videoUrl`: 동영상에서 초안을 만듭니다. 동영상은 먼저 내가 소유한 별도 작업으로 동영상 분석 가격에 분석되며, 분석 결과가 내 `input`에 추가됩니다. 분석이 진행되는 동안 `output_data.stage`는 `analyzing`이고, 그다음 `drafting`이 됩니다.
- `videoAnalysis`: 그 분석에 쓰는 `{ llmModel?, selectionMode? }`입니다.

`POST /v1/jobs/:id/cancel`로 초안을 취소하면 실행 중인 분석도 취소됩니다. 분석이 거부되면 초안에 예약된 크레딧이 모두 해제됩니다. 언어 모델 호출을 nodaro.ai로 보내는 설치에서는 작업 방식이 `503 provider_unavailable`로 응답하므로, 그곳에서는 동기식 호출을 사용하세요. SDK에서는 `client.llm.structuredJob(body)`를 호출하세요.

## 자주 묻는 질문

### Nodaro API로 이미지를 생성하려면 어떻게 하나요?

POST /v1/generate-image에 프롬프트와 공급자를 보내세요. 응답에는 jobId가 들어 있습니다. 상태가 completed가 될 때까지 GET /v1/jobs/:id/status를 폴링한 다음 output_data.imageUrl을 읽으세요. SDK에서는 client.nodes.runAndWait가 두 단계를 모두 처리합니다.

### API에서 어떤 노드를 실행할 수 있나요?

GET /v1/nodes가 나열하는 모든 노드 타입을 실행할 수 있습니다. 경로는 POST /v1/ 뒤에 노드 타입을 붙인 것이며, 노드의 설정을 JSON 본문으로 보냅니다. 일부 텍스트 노드는 /v1/llm-chat/generate처럼 더 긴 경로를 사용합니다.

### direction 객체란 무엇인가요?

{ "shotSize": "wide-shot", "timeOfDay": "golden-hour" }처럼 피커 ID를 한 단계로 나열한 맵으로, 이미지나 동영상 생성과 함께 보냅니다. Nodaro가 ID마다 검증된 자체 문구를 프롬프트에 써 넣습니다. 유효한 ID는 GET /v1/picker-catalogs에서 가져옵니다.

### API에서 캐릭터를 일관되게 유지하려면 어떻게 하나요?

캐릭터의 이미지를 connectedReferences에 넣고, source는 wired-character로, defaultName은 캐릭터 이름으로 보내세요. 경로가 이미지를 첨부하고 정체성 지시를 프롬프트에 써 넣습니다. 에디터와 같은 방식입니다.

### 조회 엔드포인트에 토큰이 필요한가요?

아니요. GET /v1/nodes, GET /v1/models, GET /v1/picker-catalogs, GET /v1/catalogs는 공개되어 있으며 5분 동안 캐시됩니다. 카탈로그 데이터만 제공하며, 내 계정에 관한 정보는 절대 노출하지 않습니다.

### 프롬프트와 direction ID를 합친 길이가 모델에 너무 길면 어떻게 되나요?

Nodaro는 프롬프트가 한도에 들어갈 때까지 연출 구절을 끝에서부터 하나씩 먼저 제거합니다. 피사체 구절은 마지막에 제거되며, 공간을 만들기 위해 내가 쓴 문장과 레퍼런스가 제거되는 일은 없습니다.
