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
| Method | Path | What it does | Cost |
|---|---|---|---|
POST | /v1/recast/estimate | Quote a run. | Free |
POST | /v1/recast | Create a run. This buys the plan. | The quoted plan |
GET | /v1/recast/:id | Poll a run and read the pending gate. | Free |
POST | /v1/recast/:id/start | Start rendering a planned run. | Covered by the plan |
POST | /v1/recast/:id/select | Answer a pending gate. | Free |
POST | /v1/recast/:id/estimate-rescore | Quote a new soundtrack or a new mix. | Free |
POST | /v1/recast/:id/rescore | Apply the quoted audio change. | The quoted price |
GET | /v1/video-analysis/authoring-skill | Get the guide for writing a script. | Free |
POST | /v1/video-analysis/import/validate | Validate a script. | Free |
POST | /v1/video-analysis/import | Import 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 --jsonProp
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:
gate | What you choose |
|---|---|
cast | One portrait for each cast member. |
sheet | For 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. |
anchors | The stills of a scene segment. |
music | The 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.
| Field | What it does |
|---|---|
gate | cast, sheet, anchors or music. |
picks | For cast and sheet: your choices, in the shape the pending gate shows. |
segment, anchorPicks | For anchors: the segment, and { start?, end? }, the chosen stills. |
section, musicPick | For music: the section, and the chosen track. |
finishAuto | true 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 }
}
}presentlists the audio lanes the take has:musicand, inbedmode, the originalvideosound.layerslists only the lanes with a preview file your browser can play. A lane missing fromlayerscan still be in the download.bakedEffectiveGainis the level of each lane in the current file, in percent.resultUrlin 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 }
}
}- Quote.
POST /v1/recast/:id/estimate-rescoreis free and returns{ credits, audioRevision, noOp }. It returns the price even when your balance is too low. - Apply.
POST /v1/recast/:id/rescoretakes the same body plus arequestId(a UUID) and the sameexpectedAudioRevision. It returns{ recastId, jobId }, or{ recastId, noOp: true, audioRevision }when nothing changes. A no-op reserves no credits and creates no job. - Follow. Poll the status.
audio.pendingRescoreshows 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:
| Part | What it holds |
|---|---|
meta | durationSec, width, height, aspectRatio (16:9 or 9:16, matching the width and height) and a required title, which names the project. |
look | Optional. The film's overall look. |
slots | The cast and the settings, each with a role: person, object or background. |
scenes | The 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 --jsonUse 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
| Status | Code | Meaning |
|---|---|---|
400 | workflow_id_required | POST /v1/recast was sent without workflowId. |
400 | validation_error, duplicate_section, unknown_section, all_audio_silent | The request or the audio operation is invalid. |
402 | insufficient_credits | The account cannot cover the plan or the audio change. |
403 | rights_attestation_required | A script import came without rightsAttested: true. |
404 | workflow_not_found | The workflow does not exist or is not yours. |
404 | not_found | The run does not exist, or the instance is self-hosted. |
409 | audio_layers_unavailable, audio_layer_unavailable, audio_preview_unavailable | The take has no revisioned audio, or the lane you named is missing or has no usable preview. |
409 | rescore_sections_unavailable, legacy_mix_mismatch | Music sections cannot be replaced on this take, or a replacement without a mix does not match the current levels. |
409 | stale_audio_revision, rescore_in_progress, idempotency_conflict | The audio changed, another change is running, or a requestId was reused for a different request. Read the status and retry. |
Frequently asked questions
Related
Recast
Video Analysis
Presets
Voice and media
Jobs
Last updated on
Prompt Wizard
Turn a rough idea into an optimized prompt over REST: ask for guided questions, build a prompt from the answers, or improve a prompt in one call.
Studio productions
Read and edit Studio productions over REST: apply atomic operations, generate stills and clips, land finished jobs, plan the export and share or copy a film.