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

Editing

Edit podcasts and long videos from TypeScript. Detect silence, sync several recordings, plan cuts from a transcript, and render an edit decision list.

client.edit holds the editing tools for podcasts and long videos: it finds silence, measures how far apart several recordings are, plans a cut from a transcript, and renders an edit decision list (EDL) into a finished video or audio file. Four methods start a job and return { jobId }, which you poll with client.jobs.getStatus(). A fifth, remapTranscript(), runs locally without a request.

Methods

MethodEndpointWhat it does
edit.silenceDetect(input)POST /v1/silence-detectFind the silent ranges of an audio or video source
edit.audioSync(input)POST /v1/audio-syncMeasure the time offset between 2 to 6 recordings
edit.editPlan(input)POST /v1/edit-planPlan a tightened cut, short clips or chapters from a transcript
edit.applyEdl(input)POST /v1/apply-edlRender an EDL into a video or an audio file
edit.remapTranscript(edl, transcript)None, localMove a transcript's timings onto the edited timeline

Edit a podcast from code

Transcribe the recording

Run client.audio.transcribe() with an engine that returns word timings. Its output_data.json is the transcript the plan needs.

Find the silence

Run silenceDetect() on the master recording. Its output_data.json holds the silent ranges.

Plan the cut

Run editPlan() in tighten mode with the transcript, the sources and the silence. Read the plan with unwrapEditPlanOutput().

Render it

Pass the plan to applyEdl(). The finished job holds the edited video or audio.

Upload VideoThe episodeTranscribeWord timingsSilence DetectEdit PlanTightenApply EDL
The same editing chain as nodes: transcribe and find silence, plan a tighter cut, then render it.
import { unwrapEditPlanOutput } from "@nodaro/sdk"

async function outputOf(jobId: string): Promise<any> {
  for (;;) {
    const { data } = await client.jobs.getStatus(jobId)
    if (data.status === "completed") return data.output_data
    if (data.status === "failed" || data.status === "cancelled") throw new Error(data.error_message ?? data.status)
    await new Promise((resolve) => setTimeout(resolve, 3_000))
  }
}

// 1. Transcribe with word timings
const tr = await client.audio.transcribe({ audioUrl: masterUrl, provider: "elevenlabs-stt" })
const transcript = (await outputOf(tr.jobId)).json

// 2. Find the silence
const sd = await client.edit.silenceDetect({ audioUrl: masterUrl, thresholdDb: -35 })
const silence = (await outputOf(sd.jobId)).json

// 3. Plan a tighter cut
const plan = await client.edit.editPlan({
  mode: "tighten",
  planTier: "standard",
  transcript,
  sources: [{ id: "ep", url: masterUrl, kind: "video", role: "master-audio" }],
  silence,
})
const edl = unwrapEditPlanOutput(await outputOf(plan.jobId))

// 4. Render the plan
const render = await client.edit.applyEdl({ edl, output: "video", quality: "final" })
const { videoUrl } = await outputOf(render.jobId)

The same steps exist as nodes: Transcribe, Silence Detect, Edit Plan and Apply EDL.

client.edit

edit.silenceDetect(input)

Finds the silent ranges of an audio or video source. It runs on the server without an AI model.

silenceDetect(input: SilenceDetectInput): Promise<{ jobId: string }>

Prop

Type

const { jobId } = await client.edit.silenceDetect({ audioUrl: masterUrl, minSilenceMs: 900 })

The finished job's output_data.json is a SilenceRanges object: { version, ranges: [{ startMs, endMs }], durationMs }. Pass the whole object as silence to editPlan().

edit.audioSync(input)

Measures how far apart the clocks of 2 to 6 recordings of one conversation are, from their sound, as the Audio Sync node does. It runs on the server without an AI model. It costs 10 × (sources − 1) credits: 10 for 2 sources, 30 for 4 and 50 for 6.

audioSync(input: AudioSyncInput): Promise<{ jobId: string }>

Prop

Type

const { jobId } = await client.edit.audioSync({
  sources: [
    { id: "mic", url: micUrl },
    { id: "camA", url: camAUrl },
  ],
  reference: "mic",
})

The finished job's output_data.json is an AudioSyncResult:

{
  version: number
  reference: string // the source every offset is measured against
  offsets: Array<{
    sourceId: string
    offsetMs: number              // referenceMs = sourceMs + offsetMs
    confidence: number            // 0 to 1; below 0.5 a note asks you to check by ear
    driftMsPerHour: number | null // measured, never corrected; null when the overlap was too short
  }>
  notes: string[] // low confidence, drift above 33 ms over the overlap, no shared sound
}

With the master recording as reference, each offsetMs is exactly the offsetMs of that source in your EDL. A malformed request is refused with a NodaroError whose code is validation_error, before any credits are reserved. That covers fewer than 2 or more than 6 sources, a repeated id, or a reference that is not one of the ids.

edit.editPlan(input)

Plans an edit from a timed transcript, as the Edit Plan node does. It writes one of three kinds of plan: a tighter cut of the whole recording, a set of short clips, or chapters.

editPlan(input: EditPlanInput): Promise<{ jobId: string }>

Prop

Type

import { unwrapEditPlanOutput } from "@nodaro/sdk"

const { jobId } = await client.edit.editPlan({
  mode: "clips",
  planTier: "standard",
  transcript,
  sources: [{ id: "ep", url: masterUrl, kind: "video", role: "master-audio" }],
  silence,
  count: 5,
  targetAspect: "9:16",
})
const { data } = await client.jobs.getStatus(jobId) // poll until completed
const clips = unwrapEditPlanOutput(data.output_data) // one EDL per clip

Read the finished job's output with unwrapEditPlanOutput(). It returns an Edl in tighten mode, an array of Edl in clips mode, and a ChapterSet in chapters mode.

On a self-hosted install, this method needs a Nodaro Cloud connection. Without one, it fails with 503 nodaro_connection_required.

edit.applyEdl(input)

Renders an edit decision list into a video or an audio file, as the Apply EDL node does.

applyEdl(input: ApplyEdlInput): Promise<{ jobId: string }>

Prop

Type

const { jobId } = await client.edit.applyEdl({ edl, output: "video", quality: "proxy", crossfadeMs: 80 })

The EDL is checked before any credits are reserved. An unknown source, a segment without picture in a video edit, or more than 180 minutes of output in one render fails with a NodaroError whose code is invalid_edl. The length is measured after crossfades. The message names the problem, for example that the edit renders 200 minutes and must be split into parts of at most 180 minutes.

edit.remapTranscript(edl, transcript)

Moves a transcript onto the timeline of an edit, locally, without a request. It drops the words inside cut ranges, clips the words that cross a cut, and shifts every timing to the edited output. applyEdl() does the same on the server when you pass it a transcript.

remapTranscript(edl: Edl, transcript: Transcript): Transcript

Prop

Type

const editedTranscript = client.edit.remapTranscript(edl, transcript)
// caption the edited video without another transcription

Use it to caption an edit without a render or a second transcription. It is also the faster choice for a large transcript when you only need the new timings.

Frequently asked questions

Last updated on

On this page