Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
REST API

Recast

Recast an analyzed video with your own cast over REST: quote and buy a run, answer interactive gates, remix its audio, or import an authored script.

Available on Nodaro Cloud

The Recast API regenerates an analyzed video with your own cast. You quote the run, buy its plan, render it scene by scene, and, on an interactive run, pick the cast, the scene stills and the music along the way. You can also write a movie as a JSON script and import it, so a recast needs no source video at all. It is the engine behind recast.nodaro.ai.

Recast runs on Nodaro Cloud only; on self-hosted installs the routes answer 404. On a self-hosted install, break a video down with the Video Analysis node and regenerate its scenes with Generate Video in a workflow. The routes take a bearer token. See Authentication.

Endpoints

MethodPathWhat it doesCost
POST/v1/recast/estimateQuote a run.Free
POST/v1/recastCreate a run. This buys the plan.The quoted plan
GET/v1/recast/:idPoll a run and read the pending gate.Free
POST/v1/recast/:id/startStart rendering a planned run.Covered by the plan
POST/v1/recast/:id/selectAnswer a pending gate.Free
POST/v1/recast/:id/estimate-rescoreQuote a new soundtrack or a new mix.Free
POST/v1/recast/:id/rescoreApply the quoted audio change.The quoted price
GET/v1/video-analysis/authoring-skillGet the guide for writing a script.Free
POST/v1/video-analysis/import/validateValidate a script.Free
POST/v1/video-analysis/importImport a script as a completed analysis.Free

Quote and create a run

A run starts from an analysis job: either a video analyzed by the Video Analysis node, or an imported script. Quote it first. POST /v1/recast/estimate takes the settings you will create the run with and returns { totalCredits, breakdown }.

POST /v1/recast then creates the run and buys its plan. It returns { recastId }. The body needs workflowId, the id of a workflow you own that the run attaches to. Without it, the route answers 400 workflow_id_required; with an unknown or foreign id, 404 workflow_not_found.

curl -X POST https://app.nodaro.ai/v1/recast/estimate \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "analysisJobId": "6c1e8a3f-9b2d-4f5a-8e7c-3d1b9a5f2e6c", "resolution": "720p", "interactive": true }'

curl -X POST https://app.nodaro.ai/v1/recast \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflowId": "8d3f5b7a-1c9e-4a2d-b6f8-4e2a7c9d1b3f",
    "analysisJobId": "6c1e8a3f-9b2d-4f5a-8e7c-3d1b9a5f2e6c",
    "resolution": "720p",
    "interactive": true,
    "clientCapabilities": ["sheet-gate"]
  }'
import { createClient, StaticTokenAuth } from '@nodaro/sdk'

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

const quote = await client.recast.estimate({ analysisJobId, resolution: '720p', interactive: true })
console.log(quote.totalCredits, quote.breakdown)

const { recastId } = await client.recast.create({
  workflowId,
  analysisJobId,
  resolution: '720p',
  interactive: true,
  clientCapabilities: ['sheet-gate'],
})
nodaro recast estimate --analysis-job <jobId> --resolution 720p --json
nodaro recast create --workflow <workflowId> --analysis-job <jobId> --resolution 720p --json

Prop

Type

To reuse a set of render settings, save them as a recast-render preset. See Presets.

Follow a run

GET /v1/recast/:id returns { status, interactive?, capabilities?, audio? }. The status goes through planning, planned, generating and then completed or failed. A planned run waits for POST /v1/recast/:id/start, which starts rendering and returns { gvpJobId? }. The start route is idempotent and costs nothing more, because the plan's quote already covered the render.

curl https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f \
  -H "Authorization: Bearer $NODARO_API_KEY"

curl -X POST https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f/start \
  -H "Authorization: Bearer $NODARO_API_KEY"
const run = await client.recast.get(recastId)
if (run.status === 'planned') await client.recast.start(recastId)
nodaro recast status <recastId> --json
nodaro recast start <recastId>

Answer interactive gates

An interactive run is driven by the server: Nodaro advances every step that needs no choice, and you only poll and answer gates. When a gate is waiting, interactive.next in the status names it. Gates open in this order:

gateWhat you choose
castOne portrait for each cast member.
sheetFor a person only, when the run offers it: one of 3 identity sheets that share the chosen face, so you choose the body and the wardrobe.
anchorsThe stills of a scene segment.
musicThe music for a section of the film.

A gate opens only for the kinds your create declared in clientCapabilities, for example sheet-gate. Any other gate is decided automatically, so a client never sees a question it cannot answer.

Answer with POST /v1/recast/:id/select. The pick is free.

FieldWhat it does
gatecast, sheet, anchors or music.
picksFor cast and sheet: your choices, in the shape the pending gate shows.
segment, anchorPicksFor anchors: the segment, and { start?, end? }, the chosen stills.
section, musicPickFor music: the section, and the chosen track.
finishAutotrue hands this gate and every remaining one to the automatic reviewer.
await client.recast.resolveGate(recastId, { gate: 'cast', picks })
await client.recast.resolveGate(recastId, { gate: 'music', section: 0, musicPick: 1, finishAuto: true })

An abandoned interactive run is safe: it waits, then resolves itself when its deadline passes.

Change the soundtrack or the mix

After a take completes, you can replace its music or rebalance it without rendering the video again. This works only when the status carries capabilities.audioLayers: 1 and the take has an audio manifest:

interface RecastAudioManifestV1 {
  version: 1
  revision: string
  mode: 'bed' | 'replace'
  present: { music?: true; video?: true }
  layers: { music?: { url: string }; video?: { url: string } }
  bakedEffectiveGain: { music?: number; video?: number }
  pendingRescore?: {
    jobId: string
    requestId: string
    state: 'pending' | 'running'
    expectedAudioRevision: string
    requestedEffectiveGain: { music?: number; video?: number }
  }
}
  • present lists the audio lanes the take has: music and, in bed mode, the original video sound.
  • layers lists only the lanes with a preview file your browser can play. A lane missing from layers can still be in the download.
  • bakedEffectiveGain is the level of each lane in the current file, in percent.
  • resultUrl in the status is the only video URL you receive.

Quote, then apply

Quote and apply take the same operation. Send at most one music replacement, either audioUrl or one or more sections with a brief, plus the complete mix you want. A mix alone is also valid.

{
  "expectedAudioRevision": "server-revision",
  "sections": [{ "index": 0, "brief": "Sparse analogue pulse" }],
  "mix": {
    "music": { "gain": 60, "muted": false },
    "video": { "gain": 85, "muted": false }
  }
}
  1. Quote. POST /v1/recast/:id/estimate-rescore is free and returns { credits, audioRevision, noOp }. It returns the price even when your balance is too low.
  2. Apply. POST /v1/recast/:id/rescore takes the same body plus a requestId (a UUID) and the same expectedAudioRevision. It returns { recastId, jobId }, or { recastId, noOp: true, audioRevision } when nothing changes. A no-op reserves no credits and creates no job.
  3. Follow. Poll the status. audio.pendingRescore shows the operation, survives a reload, and disappears when the new revision is published or the operation fails. Read the status again before the next operation.

Gains are percentages from 0 to 200; a muted lane counts as 0. Address only the lanes in present, or music that this request adds. A replace-mode take has no video lane, and the result may not leave every lane silent. Reuse a requestId only to retry the identical request.

Send the complete mix with a music replacement. Omitting it works only when the result matches the fixed standard levels: music 35 and video 100 in bed mode, or music 100 in replace mode. Any other current level answers 409 legacy_mix_mismatch.

curl -X POST https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f/rescore \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "4f6a8c1e-3b5d-4e7f-9a2c-6d8b1f3e5a7c",
    "expectedAudioRevision": "server-revision",
    "mix": { "music": { "gain": 60, "muted": false }, "video": { "gain": 85, "muted": false } }
  }'
const status = await client.recast.get(recastId)
const revision = status.audio?.revision
if (status.capabilities?.audioLayers === 1 && revision) {
  const operation = {
    expectedAudioRevision: revision,
    mix: { music: { gain: 60, muted: false }, video: { gain: 85, muted: false } },
  }
  const quote = await client.recast.estimateRescore(recastId, operation)
  if (!quote.noOp) {
    await client.recast.rescore(recastId, { ...operation, requestId: crypto.randomUUID() })
  }
}

Import a script as a movie

You can write a movie as a JSON document, often with the help of a language model, and recast it with no source video. All three routes are free.

Read the authoring guide

GET /v1/video-analysis/authoring-skill returns the guide as Markdown: the document's fields, the allowed values, the limits, the audio rules and a validated example. Give it to the model that writes your script.

Validate until the script is valid

POST /v1/video-analysis/import/validate with { "script": { … } } returns { valid, errors, warnings }. Each error has a path, a message and usually a hint written for a repair loop. Fix each path and validate again until valid is true.

Import it

POST /v1/video-analysis/import with { "script": { … }, "rightsAttested": true } stores the script as a completed analysis and returns { jobId, created, warnings, json }. json is your document with the fields the server derives; keep it as the document of record. Importing the same script again returns the same jobId with created: false.

Recast it

Create a run with that jobId as analysisJobId, fidelity: "faithful" and rightsAttested: true.

rightsAttested: true is required: an authored recast renders exactly as written, brand names included, so it confirms the script is your own work. Without it, the import answers 403 rights_attestation_required.

The document has these parts:

PartWhat it holds
metadurationSec, width, height, aspectRatio (16:9 or 9:16, matching the width and height) and a required title, which names the project.
lookOptional. The film's overall look.
slotsThe cast and the settings, each with a role: person, object or background.
scenesThe scenes, numbered from 0 without gaps, each 8 seconds or less. The total runs from 4 seconds up to the platform's run limit.

A document over the run limit is refused, never cut short. Do not write sceneNumber, slotRefs or visualResolved: the server derives them and ignores your values. That is also why an analysis you copied from the editor with Copy JSON imports as it is.

curl https://app.nodaro.ai/v1/video-analysis/authoring-skill \
  -H "Authorization: Bearer $NODARO_API_KEY" > recast-authoring.md

curl -X POST https://app.nodaro.ai/v1/video-analysis/import \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"script\": $(cat script.json), \"rightsAttested\": true }"
const guide = await client.recast.authoringSkill()
const check = await client.recast.validateScript(script)
if (check.valid) {
  const { jobId } = await client.recast.importScript(script, { rightsAttested: true })
}
nodaro recast skill > recast-authoring.md
nodaro recast validate --file script.json
nodaro recast import --file script.json --rights-attested --json

Use it from MCP

AI assistants run the same loop with get_recast_authoring_skill, validate_recast_script, import_recast_script, start_recast, get_recast_status and resolve_recast_gate. start_recast shows the price first and spends only when called again to confirm. See Recast over MCP.

Errors

StatusCodeMeaning
400workflow_id_requiredPOST /v1/recast was sent without workflowId.
400validation_error, duplicate_section, unknown_section, all_audio_silentThe request or the audio operation is invalid.
402insufficient_creditsThe account cannot cover the plan or the audio change.
403rights_attestation_requiredA script import came without rightsAttested: true.
404workflow_not_foundThe workflow does not exist or is not yours.
404not_foundThe run does not exist, or the instance is self-hosted.
409audio_layers_unavailable, audio_layer_unavailable, audio_preview_unavailableThe take has no revisioned audio, or the lane you named is missing or has no usable preview.
409rescore_sections_unavailable, legacy_mix_mismatchMusic sections cannot be replaced on this take, or a replacement without a mix does not match the current levels.
409stale_audio_revision, rescore_in_progress, idempotency_conflictThe audio changed, another change is running, or a requestId was reused for a different request. Read the status and retry.

Frequently asked questions

Last updated on

On this page