# Recast

> Quote, buy and follow Recast runs from TypeScript, answer their picks, import an authored script, and change the music mix of a finished recast.

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

**`client.recast`** runs Recast: it regenerates an analyzed source video with your own cast, and it can also render a script you wrote, a "movie as JSON". You quote a run, buy it, follow it, and answer its picks for the cast, the sheets, the anchor frames and the music. The methods call the [Recast REST API](https://nodaro.ai/docs/developers/api/recast). See [Recast over MCP](https://nodaro.ai/docs/mcp/recast) for the authoring guide.

Recast runs on Nodaro Cloud. On a self-hosted install, these methods throw `NotFoundError`.

## Methods

| Method | What it does |
| --- | --- |
| [`authoringSkill()`](#authoringskill) | Read the script authoring guide |
| [`validateScript(script)`](#validatescriptscript) | Check an authored script |
| [`importScript(script, opts)`](#importscriptscript-opts) | Store an authored script as an analysis |
| [`estimate(input)`](#estimateinput) | Quote a run, for free |
| [`create(input)`](#createinput) | Buy the plan and create the run |
| [`get(recastId)`](#getrecastid) | Read the run's status and pending pick |
| [`start(recastId, opts?)`](#startrecastid-opts) | Start rendering a planned run |
| [`resolveGate(recastId, input)`](#resolvegaterecastid-input) | Answer a pending pick |
| [`estimateRescore(recastId, input)`](#estimaterescorerecastid-input) | Quote a new music mix or music track |
| [`rescore(recastId, input)`](#rescorerecastid-input) | Apply the quoted audio change |

## Authored scripts

### authoringSkill()

Returns the authoring guide, as Markdown (`GET /v1/video-analysis/authoring-skill`). It covers the script document, its vocabularies and limits, the audio rules, and a checked example. It is free.

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

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

Give the guide to the language model that writes your script.

### validateScript(script)

Checks an authored script and returns `{ valid, errors, warnings }` (`POST /v1/video-analysis/import/validate`). It is free and stores nothing.

```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: "The script document." },
}}
/>

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

Each error names the `path` that is wrong and usually carries a `hint` written for a language model. Fix and validate again until `valid` is `true`.

### importScript(script, opts)

Stores a valid script as a completed analysis job (`POST /v1/video-analysis/import`). Use its `jobId` as the `analysisJobId` of a recast. It is free.

```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: "A valid script document." },
rightsAttested: { type: 'true', required: true, description: "You confirm you own the work. Without it, the import fails with a ForbiddenError." },
}}
/>

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

An authored recast renders exactly as written, so import only work you own. `json` is your document with the fields the server adds; prefer it over your input. Importing the same script again returns `created: false`.

## The run

### estimate(input)

Quotes a run in credits (`POST /v1/recast/estimate`). It is free. The body is the same as `create()`, without `workflowId`, `rightsAttested` and `clientCapabilities`.

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

<TypeTable
type={{
analysisJobId: { type: 'string', required: true, description: "The video analysis or imported script to recast." },
fidelity: { type: 'string', description: "How closely the run follows the source. An authored script uses faithful." },
resolution: { type: 'string', description: "The output resolution." },
segmentSec: { type: 'number', description: "The segment length setting." },
renderMethod: { type: 'string', description: "The render method." },
interactive: { type: 'boolean', description: "Stop at the picks for your choices." },
provider: { type: 'string', description: "The video model." },
}}
/>

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

### create(input)

Creates the run and **buys the plan** (`POST /v1/recast`). Quote it with `estimate()` first.

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

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "An existing workflow you own. The run is attached to it." },
analysisJobId: { type: 'string', required: true, description: "The video analysis or imported script to recast." },
rightsAttested: { type: 'boolean', description: "You confirm you have the rights to the source." },
clientCapabilities: { type: 'string[]', description: "The kinds of picks your client can answer, such as sheet-gate. Picks you do not declare are decided automatically." },
'...': { type: 'EstimateRecastInput', description: "The fields of estimate()." },
}}
/>

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

Without `workflowId`, the call fails with `400 workflow_id_required`; an unknown or foreign workflow fails with a 404.

### get(recastId)

Reads the run (`GET /v1/recast/:id`). Poll it to follow progress. On an interactive run, `interactive.next` names the pending step or pick.

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

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "The recast id." },
}}
/>

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

The snapshot has `status`, `interactive`, `capabilities` and, for a finished run with separate audio layers, `audio`. See [Change the music mix](#change-the-music-mix).

### start(recastId, opts?)

Starts rendering a run in the `planned` state (`POST /v1/recast/:id/start`). The plan's quote already covered it, and a repeated call changes nothing.

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

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "The recast id." },
segmentSec: { type: 'number', description: "The segment length setting." },
provider: { type: 'string', description: "The video model." },
}}
/>

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

### resolveGate(recastId, input)

Answers a pending pick on an interactive run (`POST /v1/recast/:id/select`). The platform advances every other step itself, so a client only polls `get()` and answers picks. The pick itself is free.

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

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "The recast id." },
gate: { type: '"cast" | "sheet" | "anchors" | "music"', description: "The pick you answer." },
picks: { type: 'unknown', description: "Your choice for a cast or sheet pick." },
segment: { type: 'number', description: "The segment the pick belongs to." },
anchorPicks: { type: '{ start?: number; end?: number }', description: "Your anchor frames for an anchors pick." },
section: { type: 'number', description: "The music section the pick belongs to." },
musicPick: { type: 'number | string', description: "Your choice for a music pick." },
finishAuto: { type: 'boolean', description: "Hand this and every remaining pick to the automatic critic." },
}}
/>

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

A pick opens only for the kinds your `create()` declared in `clientCapabilities`. The platform decides the others automatically.

## Change the music mix

A finished recast can keep its music and the video's own sound as separate layers. When `get()` returns `capabilities.audioLayers` set to `1`, the `audio` manifest describes them:

- **`revision`** identifies the current audio. Every change needs it.
- **`present`** lists the layers the recast has, `music` and `video`.
- **`layers`** holds preview files for the layers that exist.
- **`bakedEffectiveGain`** describes the levels of the current download.
- **`pendingRescore`** describes a change still in progress. It survives a page reload.

Never make up a revision, and never guess the mix from a missing preview.

### estimateRescore(recastId, input)

Quotes an audio change: a new mix, a new music track, or both (`POST /v1/recast/:id/estimate-rescore`). It is free and returns `{ credits, audioRevision, noOp }`.

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

<TypeTable
type={{
recastId: { type: 'string', required: true, description: "The recast id." },
expectedAudioRevision: { type: 'string', required: true, description: "The audio.revision you read." },
mix: { type: '{ music?: { gain, muted }, video?: { gain, muted } }', description: "The complete mix you want, with a gain and a mute switch per layer." },
audioUrl: { type: 'string', description: "A new music track to replace the music." },
sections: { type: 'Array<{ index: number; brief: string }>', description: "Music sections to generate again, each with a short brief. Use either audioUrl or sections." },
}}
/>

### rescore(recastId, input)

Applies the audio change (`POST /v1/recast/:id/rescore`). Send the same operation you quoted, plus a new `requestId`, a UUID. Reuse that `requestId` only to retry the same request after a network failure.

```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: "The recast id." },
requestId: { type: 'string', required: true, description: "A new UUID for this change." },
'...': { type: 'EstimateRecastRescoreInput', description: "The same operation you sent to 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() })
}
}
```

The change runs as a job. Poll `get(recastId)` until `audio.pendingRescore` disappears and `audio.revision` changes. A change that would do nothing returns `noOp: true` without a job.

- An operation holds a mix, one music replacement (`audioUrl` or `sections`), or a replacement with its mix.
- With a replacement, send the complete mix you want. Leaving it out works only for the exact older default levels, and otherwise fails with `409 legacy_mix_mismatch`.
- A stale `expectedAudioRevision` fails with `409 stale_audio_revision`, and a second change while one runs fails with `409 rescore_in_progress`. Read the recast again, then quote again.
- An invalid operation fails with a 400, such as `validation_error`, `unknown_section`, `duplicate_section` or `all_audio_silent`.
- A quote never reserves credits, and it returns the price even when your balance is too low.

## Frequently asked questions

### What does Recast do?

Recast regenerates an analyzed source video with your own cast. Nodaro plans the run from the analysis, renders it segment by segment, and lets you pick the cast, the sheets, the anchor frames and the music along the way.

### Does creating a recast cost credits?

Yes. create buys the plan, so quote it first with client.recast.estimate, which is free. Answering a pick with resolveGate is free.

### Can I recast my own script instead of a real video?

Yes. Validate the script with validateScript until it is valid, then importScript with rightsAttested set to true. The import returns a jobId you use as the analysisJobId of a new recast.

### How do I change the music level of a finished recast?

Read the recast with get, quote the new mix with estimateRescore, then send the same operation to rescore with a new requestId. Poll get until the audio revision changes.
