# Editing

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

Source: https://nodaro.ai/docs/developers/sdk/editing

**`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()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions). A fifth, `remapTranscript()`, runs locally without a request.

## Methods

| Method | Endpoint | What it does |
| --- | --- | --- |
| [`edit.silenceDetect(input)`](#editsilencedetectinput) | `POST /v1/silence-detect` | Find the silent ranges of an audio or video source |
| [`edit.audioSync(input)`](#editaudiosyncinput) | `POST /v1/audio-sync` | Measure the time offset between 2 to 6 recordings |
| [`edit.editPlan(input)`](#editeditplaninput) | `POST /v1/edit-plan` | Plan a tightened cut, short clips or chapters from a transcript |
| [`edit.applyEdl(input)`](#editapplyedlinput) | `POST /v1/apply-edl` | Render an EDL into a video or an audio file |
| [`edit.remapTranscript(edl, transcript)`](#editremaptranscriptedl-transcript) | None, local | Move a transcript's timings onto the edited timeline |

## Edit a podcast from code

### Transcribe the recording

Run [`client.audio.transcribe()`](https://nodaro.ai/docs/developers/sdk/voices-and-audio#audiotranscribeinput) 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.

Workflow: The same editing chain as nodes: transcribe and find silence, plan a tighter cut, then render it.

- Upload Video → Transcribe
- Upload Video → Silence Detect
- Transcribe → Edit Plan
- Silence Detect → Edit Plan
- Edit Plan → Apply EDL

```ts

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](https://nodaro.ai/docs/nodes/audio/transcribe), [Silence Detect](https://nodaro.ai/docs/nodes/audio/silence-detect), [Edit Plan](https://nodaro.ai/docs/nodes/video/edit-plan) and [Apply EDL](https://nodaro.ai/docs/nodes/video/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.

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

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "An audio or video source." },
thresholdDb: { type: 'number', default: '-35', description: "The loudness below which audio counts as silence, in dBFS, 0 or lower." },
minSilenceMs: { type: 'number', default: '700', description: "The shortest silence to report, in milliseconds." },
padMs: { type: 'number', default: '120', description: "Padding kept around speech, in milliseconds. It shrinks each silent range." },
workflowId: { type: 'string', description: "A workflow to list this run under, in its run history." },
}}
/>

```ts
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](https://nodaro.ai/docs/nodes/audio/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.

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

<TypeTable
type={{
sources: { type: 'Array<{ id: string; url: string }>', required: true, description: "2 to 6 recordings. Each id, 1 to 200 characters and unique, comes back as the sourceId of its offset. Use the ids your EDL uses for the same recordings." },
reference: { type: 'string', description: "The id of the source every offset is measured against. Its own offset is 0. The default is the first source." },
workflowId: { type: 'string', description: "A workflow to list this run under." },
}}
/>

```ts
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`:

```ts
{
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](https://nodaro.ai/docs/nodes/video/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.

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

<TypeTable
type={{
mode: { type: '"tighten" | "clips" | "chapters"', required: true, description: "tighten removes pauses and filler, clips cuts short clips, chapters splits the recording into chapters." },
planTier: { type: '"economy" | "standard" | "premium"', required: true, description: "The model tier. It sets the quality and the price." },
transcript: { type: 'Transcript', required: true, description: "The timed transcript: the output_data.json of a transcribe job." },
sources: { type: 'EditPlanSource[]', required: true, description: "1 to 6 sources. Each is { id, url, kind, role?, speakers?, offsetMs? }, where kind is video or audio." },
silence: { type: 'SilenceRanges', description: "The output_data.json of a silenceDetect job. Pass the whole object: one without ranges is ignored." },
instructions: { type: 'string', description: "Free-text editing directions." },
styleGuide: { type: 'string', description: "A style guide to follow." },
count: { type: 'number', description: "clips mode: how many clips to cut." },
targetDurationSec: { type: 'number', description: "clips mode: the target length of each clip." },
targetAspect: { type: '"16:9" | "9:16" | "1:1" | "4:5"', description: "The clip shape." },
platform: { type: 'string', description: "The platform the clips are for." },
workflowId: { type: 'string', description: "A workflow to list this run under." },
}}
/>

```ts

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](https://nodaro.ai/docs/self-hosting/cloud-connect). 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](https://nodaro.ai/docs/nodes/video/apply-edl) node does.

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

<TypeTable
type={{
edl: { type: 'Edl', required: true, description: "The edit decision list to render. Media comes from the url of each of its sources." },
sources: { type: 'string[]', description: "Replacement media URLs for the EDL's sources, in the same order." },
transcript: { type: 'Transcript', description: "A transcript to move onto the edited timeline. The result is returned in the job's json output." },
output: { type: '"video" | "audio"', default: '"video"', description: "What to render." },
quality: { type: '"proxy" | "final"', default: '"final"', description: "proxy renders a quick preview. final renders the full quality." },
crossfadeMs: { type: 'number', default: '0', description: "A crossfade on every cut without its own transition, in milliseconds. 0 means hard cuts." },
workflowId: { type: 'string', description: "A workflow to list this run under." },
}}
/>

```ts
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`.

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

<TypeTable
type={{
edl: { type: 'Edl', required: true, description: "The edit." },
transcript: { type: 'Transcript', required: true, description: "The transcript of the original recording." },
}}
/>

```ts
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

### How do I cut the pauses out of a podcast with the Nodaro SDK?

Transcribe the recording, run client.edit.silenceDetect, then client.edit.editPlan in tighten mode with the transcript and the silence ranges. Render the returned plan with client.edit.applyEdl.

### What is an EDL?

An edit decision list: the sources of an edit and the segments to keep, in order, with their transitions. client.edit.editPlan writes one, and client.edit.applyEdl renders it into a video or an audio file.

### How long can one EDL render be?

At most 180 minutes of output per render. A longer edit is refused with invalid_edl before any credits are reserved. Split it into parts.

### What does client.edit.audioSync cost?

10 credits per source after the first: 10 credits for 2 recordings, 30 for 4 and 50 for 6.
