# Recast

> TypeScript로 Recast 실행의 견적을 받고, 구매하고, 추적합니다. 선택에 답하고, 직접 쓴 스크립트를 가져오고, 완성된 Recast의 음악 믹스를 바꿉니다.

Source: https://nodaro.ai/ko/docs/developers/sdk/recast

<strong>`client.recast`</strong>는 Recast를 실행합니다. 분석된 원본 동영상을 내 출연진으로 다시 생성하며, 내가 쓴 스크립트, 즉 “JSON으로 쓴 영화”를 렌더링할 수도 있습니다. 실행의 견적을 받고, 구매하고, 추적하며, 출연진, 시트, 스틸, 음악을 고르는 선택에 답합니다. 이 메서드들은 [Recast REST API](https://nodaro.ai/docs/developers/api/recast)를 호출합니다. 작성 가이드는 [MCP에서 Recast 사용](https://nodaro.ai/docs/mcp/recast)을 참조하세요.

Recast는 Nodaro Cloud에서 실행됩니다. 셀프 호스팅 설치에서는 이 메서드들이 `NotFoundError`를 발생시킵니다.

## 메서드
| 메서드 | 하는 일 |
| --- | --- |
| [`authoringSkill()`](#authoringskill) | 스크립트 작성 가이드를 읽습니다 |
| [`validateScript(script)`](#validatescriptscript) | 직접 쓴 스크립트를 검사합니다 |
| [`importScript(script, opts)`](#importscriptscript-opts) | 직접 쓴 스크립트를 분석으로 저장합니다 |
| [`estimate(input)`](#estimateinput) | 실행의 견적을 무료로 받습니다 |
| [`create(input)`](#createinput) | 계획을 구매하고 실행을 만듭니다 |
| [`get(recastId)`](#getrecastid) | 실행의 상태와 대기 중인 선택을 읽습니다 |
| [`start(recastId, opts?)`](#startrecastid-opts) | 계획된 실행의 렌더링을 시작합니다 |
| [`resolveGate(recastId, input)`](#resolvegaterecastid-input) | 대기 중인 선택에 답합니다 |
| [`estimateRescore(recastId, input)`](#estimaterescorerecastid-input) | 새 음악 믹스나 음악 트랙의 견적을 받습니다 |
| [`rescore(recastId, input)`](#rescorerecastid-input) | 견적을 받은 오디오 변경을 적용합니다 |

## 직접 쓴 스크립트
### authoringSkill()
작성 가이드를 Markdown으로 반환합니다(`GET /v1/video-analysis/authoring-skill`). 스크립트 문서, 그 어휘와 제한, 오디오 규칙, 검증된 예시를 다룹니다. 무료입니다.

```ts
authoringSkill(): Promise<string>
```

```ts
const guide = await client.recast.authoringSkill()
```

이 가이드를 내 스크립트를 쓰는 언어 모델에 전달하세요.

### validateScript(script)
직접 쓴 스크립트를 검사하고 `{ valid, errors, warnings }`를 반환합니다(`POST /v1/video-analysis/import/validate`). 무료이며, 아무것도 저장하지 않습니다.

```ts
validateScript(script: Record<string, unknown>): Promise<{
valid: boolean
errors: Array<{ path: string; message: string; hint?: string }>
warnings: string[]
}>
```

<TypeTable
type={{
script: { type: 'Record<string, unknown>', required: true, description: "스크립트 문서입니다." },
}}
/>

```ts
let check = await client.recast.validateScript(script)
while (!check.valid) {
script = await fixWithModel(script, check.errors) // each error has a path, a message and usually a hint
check = await client.recast.validateScript(script)
}
```

각 오류는 잘못된 `path`를 알려 주며, 대개 언어 모델을 위해 쓴 `hint`가 함께 있습니다. `valid`가 `true`가 될 때까지 고치고 다시 검증하세요.

### importScript(script, opts)
유효한 스크립트를 완료된 분석 작업으로 저장합니다(`POST /v1/video-analysis/import`). 반환된 `jobId`를 Recast의 `analysisJobId`로 사용하세요. 무료입니다.

```ts
importScript(script: Record<string, unknown>, opts: { rightsAttested: true }): Promise<{
jobId: string
created: boolean
warnings: string[]
json: Record<string, unknown>
}>
```

<TypeTable
type={{
script: { type: 'Record<string, unknown>', required: true, description: "유효한 스크립트 문서입니다." },
rightsAttested: { type: 'true', required: true, description: "내가 그 작품을 소유하고 있음을 확인합니다. 이 값이 없으면 가져오기가 ForbiddenError로 실패합니다." },
}}
/>

```ts
const { jobId: analysisJobId, json } = await client.recast.importScript(script, { rightsAttested: true })
```

직접 쓴 스크립트로 만든 Recast는 쓴 그대로 렌더링되므로, 내가 소유한 작품만 가져오세요. `json`은 서버가 추가한 필드가 들어간 내 문서입니다. 내가 보낸 입력보다 이 값을 우선해서 사용하세요. 같은 스크립트를 다시 가져오면 `created: false`가 반환됩니다.

## 실행
### estimate(input)
실행의 견적을 크레딧으로 받습니다(`POST /v1/recast/estimate`). 무료입니다. 본문은 `create()`와 같지만, `workflowId`, `rightsAttested`, `clientCapabilities`는 포함하지 않습니다.

```ts
estimate(input: EstimateRecastInput): Promise<{ totalCredits?: number; breakdown?: Record<string, number> }>
```

<TypeTable
type={{
analysisJobId: { type: 'string', required: true, description: "Recast할 동영상 분석 또는 가져온 스크립트입니다." },
fidelity: { type: 'string', description: "실행이 원본을 얼마나 충실히 따를지입니다. 직접 쓴 스크립트에는 faithful을 사용합니다." },
resolution: { type: 'string', description: "출력 해상도입니다." },
segmentSec: { type: 'number', description: "렌더링이 장면을 파트로 묶는 방식입니다. 서버는 여기에 팩 이름을 받습니다. scenes-max(이음매가 가장 적음), scenes(더 짧은 파트), max(모델이 허용하는 가장 긴 파트) 중 하나입니다. 아래 참고를 확인하세요." },
renderMethod: { type: 'string', description: "렌더링 방식입니다." },
interactive: { type: 'boolean', description: "내가 고를 수 있도록 선택 지점에서 멈춥니다." },
provider: { type: 'string', description: "동영상 모델입니다." },
}}
/>

```ts
const quote = await client.recast.estimate({ analysisJobId, fidelity: "faithful" })
console.log(quote.totalCredits, quote.breakdown)
```

**SDK 2.17.0의 `segmentSec`.** Recast 라우트는 `segmentSec`에 팩 이름을 받습니다. `"scenes-max"`, `"scenes"`, `"max"` 중 하나입니다. SDK 2.17.0은 이 필드의 타입을 숫자로 지정하므로, `segmentSec: "scenes" as unknown as number`처럼 캐스트해서 이름을 전달하거나, 필드를 생략해 서버의 기본값을 사용하세요. `10` 같은 숫자는 400으로 거부됩니다. `create()`와 `start()`도 마찬가지입니다.

### create(input)
실행을 만들고 **계획을 구매합니다**(`POST /v1/recast`). 먼저 `estimate()`로 견적을 받으세요.

```ts
create(input: CreateRecastInput): Promise<{ recastId: string }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "내가 소유한 기존 워크플로입니다. 실행이 이 워크플로에 연결됩니다." },
analysisJobId: { type: 'string', required: true, description: "Recast할 동영상 분석 또는 가져온 스크립트입니다." },
rightsAttested: { type: 'boolean', description: "원본에 대한 권리가 있음을 확인합니다." },
clientCapabilities: { type: 'string[]', description: "내 클라이언트가 답할 수 있는 선택의 종류입니다. 예: sheet-gate. 선언하지 않은 선택은 자동으로 결정됩니다." },
'...': { type: 'EstimateRecastInput', description: "estimate()의 필드입니다." },
}}
/>

```ts
const { recastId } = await client.recast.create({
workflowId,
analysisJobId,
fidelity: "faithful",
rightsAttested: true,
interactive: true,
clientCapabilities: ["sheet-gate"],
})
```

`workflowId`가 없으면 호출이 `400 workflow_id_required`로 실패하고, 알 수 없는 워크플로나 다른 사람의 워크플로는 404로 실패합니다.

### get(recastId)
실행을 읽습니다(`GET /v1/recast/:id`). 진행 상황을 추적하려면 이 메서드를 폴링하세요. 대화형 실행에서는 `interactive.next`가 대기 중인 단계나 선택을 알려 줍니다.

```ts
get(recastId: string): Promise<RecastRunSnapshot>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "Recast ID입니다." },
}}
/>

```ts
const run = await client.recast.get(recastId)
console.log(run.status, run.interactive)
```

스냅샷에는 `status`, `interactive`, `capabilities`가 있으며, 오디오 레인이 분리된 완료된 실행에는 `audio`도 있습니다. [음악 믹스 변경](#change-the-music-mix)을 참조하세요.

### start(recastId, opts?)
`planned` 상태인 실행의 렌더링을 시작합니다(`POST /v1/recast/:id/start`). 계획의 견적에 이미 포함되어 있으며, 여러 번 호출해도 아무것도 바뀌지 않습니다.

```ts
start(recastId: string, opts?: { segmentSec?: number; provider?: string }): Promise<{ gvpJobId?: string }>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "Recast ID입니다." },
segmentSec: { type: 'number', description: "렌더링이 장면을 파트로 묶는 방식입니다. estimate()와 같이 팩 이름을 지정합니다." },
provider: { type: 'string', description: "동영상 모델입니다." },
}}
/>

```ts
const { gvpJobId } = await client.recast.start(recastId)
```

### resolveGate(recastId, input)
대화형 실행에서 대기 중인 선택에 답합니다(`POST /v1/recast/:id/select`). 그 밖의 모든 단계는 플랫폼이 스스로 진행하므로, 클라이언트는 `get()`을 폴링하고 선택에 답하기만 하면 됩니다. 선택 자체는 무료입니다.

```ts
resolveGate(recastId: string, input: ResolveRecastGateInput): Promise<Record<string, unknown>>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "Recast ID입니다." },
gate: { type: '"cast" | "sheet" | "anchors" | "music"', description: "답할 선택입니다." },
picks: { type: 'unknown', description: "cast 또는 sheet 선택에 대한 내 선택입니다." },
segment: { type: 'number', description: "선택이 속한 세그먼트입니다." },
anchorPicks: { type: '{ start?: number; end?: number }', description: "anchors 선택에서 고른 내 스틸입니다." },
section: { type: 'number', description: "선택이 속한 음악 섹션입니다." },
musicPick: { type: 'number | string', description: "music 선택에 대한 내 선택입니다." },
finishAuto: { type: 'boolean', description: "이 선택과 남은 모든 선택을 자동 평가 AI에 맡깁니다." },
}}
/>

```ts
await client.recast.resolveGate(recastId, { gate: "music", musicPick: 1 })
await client.recast.resolveGate(recastId, { finishAuto: true })
```

선택은 `create()`의 `clientCapabilities`에 선언한 종류에 대해서만 열립니다. 나머지는 플랫폼이 자동으로 결정합니다.

## 음악 믹스 변경
완성된 Recast는 음악과 동영상 자체의 사운드를 별도의 레인으로 유지할 수 있습니다. `get()`이 `capabilities.audioLayers`를 `1`로 반환하면, `audio` 매니페스트가 그 레인들을 설명합니다.

- <strong>`revision`</strong>은 현재 오디오를 식별합니다. 모든 변경에 필요합니다.
- <strong>`present`</strong>는 Recast에 있는 레인인 `music`과 `video`를 나열합니다.
- <strong>`layers`</strong>에는 존재하는 레인의 미리보기 파일이 들어 있습니다.
- <strong>`bakedEffectiveGain`</strong>은 현재 다운로드의 레벨을 설명합니다.
- <strong>`pendingRescore`</strong>는 아직 진행 중인 변경을 설명합니다. 페이지를 새로 고쳐도 유지됩니다.

리비전을 지어내지 말고, 미리보기가 없다는 이유로 믹스를 짐작하지 마세요.

### estimateRescore(recastId, input)
오디오 변경의 견적을 받습니다. 새 믹스, 새 음악 트랙, 또는 둘 다입니다(`POST /v1/recast/:id/estimate-rescore`). 무료이며 `{ credits, audioRevision, noOp }`를 반환합니다.

```ts
estimateRescore(recastId: string, input: EstimateRecastRescoreInput): Promise<{ credits: number; audioRevision: string; noOp: boolean }>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "Recast ID입니다." },
expectedAudioRevision: { type: 'string', required: true, description: "내가 읽은 audio.revision입니다." },
mix: { type: '{ music?: { gain, muted }, video?: { gain, muted } }', description: "원하는 완전한 믹스이며, 레인마다 gain과 음소거 스위치가 있습니다." },
audioUrl: { type: 'string', description: "음악을 대체할 새 음악 트랙입니다." },
sections: { type: 'Array<{ index: number; brief: string }>', description: "다시 생성할 음악 섹션이며, 각각 짧은 brief가 있습니다. audioUrl과 sections 중 하나만 사용하세요." },
}}
/>

### rescore(recastId, input)
오디오 변경을 적용합니다(`POST /v1/recast/:id/rescore`). 견적을 받은 것과 같은 변경 내용에 UUID인 새 `requestId`를 더해 보내세요. 그 `requestId`는 네트워크 오류 후 같은 요청을 다시 시도할 때만 재사용하세요.

```ts
rescore(recastId: string, input: EstimateRecastRescoreInput & { requestId: string }): Promise<
| { recastId: string; jobId: string }
| { recastId: string; noOp: true; audioRevision: string }
>
```

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "Recast ID입니다." },
requestId: { type: 'string', required: true, description: "이 변경에 쓸 새 UUID입니다." },
'...': { type: 'EstimateRecastRescoreInput', description: "estimateRescore에 보낸 것과 같은 변경 내용입니다." },
}}
/>

```ts
const { total: available } = await client.credits.balance()
const run = await client.recast.get(recastId)

if (run.capabilities?.audioLayers === 1 && run.audio) {
const operation = {
expectedAudioRevision: run.audio.revision,
mix: {
music: { gain: 55, muted: false },
video: { gain: 100, muted: false },
},
}
const quote = await client.recast.estimateRescore(recastId, operation)
if (!quote.noOp && available >= quote.credits) {
await client.recast.rescore(recastId, { ...operation, requestId: crypto.randomUUID() })
}
}
```

변경은 작업으로 실행됩니다. `audio.pendingRescore`가 사라지고 `audio.revision`이 바뀔 때까지 `get(recastId)`를 폴링하세요. 아무것도 바꾸지 않는 변경은 작업 없이 `noOp: true`를 반환합니다.

- 한 번의 변경 내용에는 믹스, 음악 교체 하나(`audioUrl` 또는 `sections`), 또는 믹스를 포함한 교체가 들어갑니다.
- 교체할 때는 원하는 완전한 믹스를 보내세요. 믹스를 생략해도 되는 것은 예전 기본 레벨과 정확히 같을 때뿐이며, 그 밖에는 `409 legacy_mix_mismatch`로 실패합니다.
- 오래된 `expectedAudioRevision`은 `409 stale_audio_revision`으로 실패하고, 변경이 진행 중일 때 두 번째 변경을 보내면 `409 rescore_in_progress`로 실패합니다. Recast를 다시 읽은 다음, 견적을 다시 받으세요.
- 잘못된 변경 내용은 `validation_error`, `unknown_section`, `duplicate_section`, `all_audio_silent` 같은 400 오류로 실패합니다.
- 견적은 크레딧을 예약하지 않으며, 잔액이 부족해도 가격을 반환합니다.

## 자주 묻는 질문

### Recast는 무엇을 하나요?

Recast는 분석된 원본 동영상을 내 출연진으로 다시 생성합니다. Nodaro는 분석을 바탕으로 실행을 계획하고, 세그먼트별로 렌더링하며, 그 과정에서 출연진, 시트, 스틸, 음악을 고를 수 있게 합니다.

### Recast를 만들면 크레딧이 드나요?

네. create가 계획을 구매하므로, 먼저 무료인 client.recast.estimate로 견적을 받으세요. resolveGate로 선택에 답하는 것은 무료입니다.

### 실제 동영상 대신 내가 쓴 스크립트를 Recast할 수 있나요?

네. validateScript로 스크립트가 유효해질 때까지 검증한 다음, rightsAttested를 true로 설정해 importScript를 호출하세요. 가져오기는 jobId를 반환하며, 이 값을 새 Recast의 analysisJobId로 사용합니다.

### 완성된 Recast의 음악 볼륨을 바꾸려면 어떻게 하나요?

get으로 Recast를 읽고, estimateRescore로 새 믹스의 견적을 받은 다음, 같은 변경 내용을 새 requestId와 함께 rescore로 보내세요. 오디오 리비전이 바뀔 때까지 get을 폴링하세요.
