Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

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.

Available on Nodaro Cloud

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. See Recast over MCP for the authoring guide.

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

Methods

MethodWhat it does
authoringSkill()Read the script authoring guide
validateScript(script)Check an authored script
importScript(script, opts)Store an authored script as an analysis
estimate(input)Quote a run, for free
create(input)Buy the plan and create the run
get(recastId)Read the run's status and pending pick
start(recastId, opts?)Start rendering a planned run
resolveGate(recastId, input)Answer a pending pick
estimateRescore(recastId, input)Quote a new music mix or music track
rescore(recastId, 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.

authoringSkill(): Promise<string>
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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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.

get(recastId: string): Promise<RecastRunSnapshot>

Prop

Type

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.

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.

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

Prop

Type

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.

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

Prop

Type

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 }.

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

Prop

Type

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.

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

Prop

Type

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

Last updated on

On this page