# Cast Swap

> REST, SDK, MCP로 4~30초 동영상 속 인물이나 장소를 내 레퍼런스 이미지로 교체하세요. 움직임, 구도, 소리는 그대로 유지됩니다.

Source: https://nodaro.ai/ko/docs/developers/api/cast-swap

**Cast Swap API**는 짧은 동영상 속 인물, 장소 또는 둘 다를 내 레퍼런스 이미지로 교체합니다. 동영상의 움직임, 구도, 타이밍, 소리는 그대로 유지됩니다. 실행 견적을 무료로 받고, 실행을 시작한 뒤, 다른 작업처럼 그 작업을 추적하세요.

Cast Swap은 Nodaro Cloud에서만 실행됩니다. 셀프 호스팅 설치에서는 두 라우트 모두 `404`로 응답합니다. 라우트는 Bearer 토큰을 받습니다. [인증](https://nodaro.ai/docs/developers/api/authentication)을 참조하세요.

## 엔드포인트
| 메서드 | 경로 | 하는 일 | 비용 |
| --- | --- | --- | --- |
| `POST` | `/v1/cast-swap/estimate` | 실행 견적을 받습니다. 아무것도 만들거나 예약하지 않습니다. | 무료 |
| `POST` | `/v1/cast-swap` | 실행을 시작합니다. `202 { jobId }`로 응답합니다. `GET /v1/jobs/:id`로 작업을 추적하세요. | 견적을 받은 실행 금액 |

## 요청 본문
두 라우트는 같은 본문을 받습니다.

| 필드 | 하는 일 |
| --- | --- |
| `videoUrl` | 필수입니다. 4~30초 길이의 원본 클립입니다. |
| `roles` | 필수입니다. 인물 역할 최대 6개와 장소 역할 1개입니다. 아래를 참조하세요. |
| `postPrompt` | 선택 사항입니다. 모델에 전달할 내 문구로, 최대 4,000자입니다. 역할은 `{role:N}`으로 언급하며, N은 빈 역할을 포함해 `roles`에서 1부터 셉니다. |
| `preview` | 선택 사항입니다. `true`이면 `previewStart`부터 클립의 4초를 렌더링합니다. |
| `previewStart` | 선택 사항입니다. 미리보기가 시작되는 지점(초)입니다. 기본값은 0입니다. |
| `quality` | 선택 사항입니다. 현재 유일한 값인 `"draft"`입니다. |
| `sessionId`, `origin` | 선택 사항입니다. 결과를 보관할 세션과 내 앱의 출처 레이블입니다. |

역할은 동영상 속 누군가나 배경을 지정하며, 한 사람이나 한 장소의 이미지를 0~3장 받습니다.

- **인물:** `{ kind: "person", name, images, body?, outfit?, hair? }`. `name`은 동영상 속 누구인지를 말합니다. 예: `"the woman dancing"`. `body`, `outfit`, `hair`는 각각 내 이미지에서 가져오는 기본값 `"new"`, 또는 동영상에서 유지하는 `"original"`입니다.
- **장소:** `{ kind: "location", name?, images }`. 기본 이름은 `"the setting"`입니다.

이미지가 없는(비어 있거나 `null`인) 역할은 그 인물이나 장소를 동영상 그대로 유지합니다. 이미지가 있는 역할이 하나 이상 있거나 `postPrompt`가 있어야 합니다.

```json
{
"videoUrl": "https://example.com/clip.mp4",
"roles": [
{ "kind": "person", "name": "the woman dancing", "images": ["https://example.com/me.jpg"], "outfit": "original" }
],
"preview": true
}
```

## 견적
`POST /v1/cast-swap/estimate`는 `{ totalCredits, breakdown: { castSwap, seedance }, settlesLower }`로 응답합니다.

- `totalCredits`는 실행 자체인 `breakdown.castSwap`과, 실행이 호출하는 동영상 모델인 `breakdown.seedance`를 더한 값입니다.
- `settlesLower: true`는 결과의 길이를 모델이 정한다는 뜻이므로, 최종 청구액이 견적보다 낮을 것으로 예상됩니다.

## 반복 요청
실행을 재시도할 때는 8~200자의 같은 `Idempotency-Key` 헤더를 보내세요. 약 10초 안에 반복된 요청은 `X-Dedup-Hit: 1` 헤더와 함께 `200 { jobId, deduped: true }`로 응답합니다. 그 뒤에 반복된 요청은 첫 실행의 작업으로 `202 { jobId }`라고 응답합니다. 어느 경우든 새로 시작되거나 청구되는 것은 없습니다. 키가 없으면 요청마다 새 실행이 됩니다.

## 오류
모든 거부는 아무것도 청구되기 전에 일어납니다. 본문은 `{ error: { code, message } }`입니다.

| 상태 | `code` | 발생 시점 |
| --- | --- | --- |
| `400` | `validation_error` | 본문이 유효하지 않습니다. 이미지가 있는 역할도 `postPrompt`도 없거나, 역할이 너무 많거나, 한 역할의 이미지가 3장을 넘거나, `postPrompt`가 4,000자를 넘거나, `origin`이 유효하지 않은 경우입니다. |
| `400` | `raw_reference_token` | 역할의 `name`이나 `postPrompt`에 원시 레퍼런스 토큰 `{image:`, `{video:`, `{audio:`가 대소문자와 관계없이 들어 있습니다. |
| `400` | `invalid_role_mention` | `postPrompt`가 1부터 역할 수까지의 범위를 벗어난 N으로 `{role:N}`을 언급합니다. |
| `400` | `unsafe_url` | `videoUrl`이나 역할 이미지가 공개 http 또는 https 주소가 아닙니다. |
| `400` | `video_unreadable` | 클립의 길이를 읽을 수 없습니다. 재생할 수 있는 동영상이 아닙니다. |
| `400` | `clip_out_of_range` | 클립이 4초보다 짧거나 30초보다 깁니다. |
| `400` | `preview_out_of_range` | 미리보기가 클립의 끝을 넘습니다. `previewStart`에 4를 더한 값이 클립 길이 이하여야 합니다. |
| `401` | `unauthorized` | 인증된 사용자가 없습니다. |
| `402` | `insufficient_credits` | 내 크레딧으로 실행 비용을 감당할 수 없습니다. `POST /v1/cast-swap` 전용입니다. 워크스페이스에서는 거부한 한도에 따라 `budget_exceeded`, `member_cap_exceeded`, `user_allowance_exceeded`로 응답할 수도 있습니다. |
| `404` | `not_found` | `sessionId`가 내가 쓸 수 없는 세션을 가리킵니다. `POST /v1/cast-swap` 전용입니다. |
| `422` | `job_blocked` | 배포 환경의 작업 정책이 요청을 막았습니다. `POST /v1/cast-swap` 전용입니다. |
| `503` | `host_too_old` | 이 플랫폼 버전에서는 Cast Swap을 실행할 수 없습니다. |
| `503` | `estimate_unavailable` | 지금은 동영상 모델의 가격을 견적 낼 수 없습니다. 잠시 후 다시 시도하세요. `/estimate` 전용입니다. |

## SDK에서 사용
`client.castSwap`은 두 라우트를 감쌉니다. `@nodaro/sdk/cast-swap`으로도 가져올 수 있습니다.

```ts
run(params: CastSwapParams, options?: { idempotencyKey?: string }): Promise<{ jobId: string; deduped?: boolean }>
estimate(params: CastSwapParams): Promise<CastSwapEstimate>
```

`CastSwapParams`는 위의 요청 본문입니다. `CastSwapEstimate`는 견적입니다. `run()`은 `idempotencyKey`를 그대로 `Idempotency-Key` 헤더로 보냅니다. 없으면 헤더를 보내지 않습니다.

```ts
const params: CastSwapParams = {
videoUrl,
roles: [{ kind: "person", name: "the woman dancing", images: [myPhotoUrl], outfit: "original" }],
preview: true,
}
const quote = await client.castSwap.estimate(params)
const { jobId } = await client.castSwap.run(params, { idempotencyKey: crypto.randomUUID() })
```

`client.jobs.get(jobId)`로 작업을 추적하세요. 미리보기 작업은 모든 입력을 `input_data`에 보관하므로, `preview: false`로 다시 실행해 같은 선택으로 클립 전체를 렌더링할 수 있습니다.

## MCP에서 사용
MCP 도구는 [`cast_swap`](https://nodaro.ai/docs/mcp/tools/video#cast_swap)입니다. `confirm`이 없으면 견적을 반환하고 아무것도 청구하지 않습니다. 사용자가 가격을 수락한 뒤 `confirm: true`로 호출하면 실행을 시작하고 작업 ID를 반환합니다.

## 자주 묻는 질문

### Cast Swap은 무엇을 하나요?

Cast Swap은 짧은 동영상 속 인물, 장소 또는 둘 다를 내 레퍼런스 이미지로 교체합니다. 동영상의 움직임, 구도, 타이밍, 소리는 그대로 유지됩니다.

### 원본 동영상은 얼마나 길 수 있나요?

4~30초입니다. 더 짧거나 긴 클립은 아무것도 청구되기 전에 clip_out_of_range로 거부됩니다.

### Cast Swap 실행 비용은 어떻게 알 수 있나요?

실행할 때와 같은 본문으로 POST /v1/cast-swap/estimate를 호출하세요. 무료이며, 실행 자체와 실행이 호출하는 동영상 모델의 내역과 함께 totalCredits를 반환합니다.

### 클립 전체를 렌더링하기 전에 결과를 미리 볼 수 있나요?

네. preview를 true로 보내면 previewStart부터 클립의 4초를 렌더링합니다. 미리보기는 비용이 더 적고, 그 작업은 모든 입력을 보관하므로 preview를 false로 해서 다시 실행할 수 있습니다.

### Cast Swap은 셀프 호스팅 설치에서도 작동하나요?

아니요. Cast Swap은 Nodaro Cloud에서만 실행됩니다. 셀프 호스팅 설치에서는 두 라우트 모두 404로 응답합니다.
