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
| Method | Endpoint | What it does |
|---|---|---|
edit.silenceDetect(input) | POST /v1/silence-detect | Find the silent ranges of an audio or video source |
edit.audioSync(input) | POST /v1/audio-sync | Measure the time offset between 2 to 6 recordings |
edit.editPlan(input) | POST /v1/edit-plan | Plan a tightened cut, short clips or chapters from a transcript |
edit.applyEdl(input) | POST /v1/apply-edl | Render an EDL into a video or an audio file |
edit.remapTranscript(edl, transcript) | None, local | Move 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.
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 clipRead 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): TranscriptProp
Type
const editedTranscript = client.edit.remapTranscript(edl, transcript)
// caption the edited video without another transcriptionUse 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
Related
Voices and audio
Media and uploads
Edit Plan
Apply EDL
Silence Detect
Last updated on
Voices and audio
Browse voices, change and recast voices, design new voices, dub video, and separate, mix and transcribe audio from TypeScript with the Nodaro SDK.
3D scenes
Generate an editable 3D scene from a prompt, edit it, render it to MP4, and run 3D Render Pro from TypeScript with client.scene3d and the 3D scene nodes.