# 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.

Source: https://nodaro.ai/docs/developers/sdk/voices-and-audio

**`client.voices`** works with ElevenLabs voices: it lists and searches voices, changes the voice in a recording, gives each speaker a new voice, designs new voices, and dubs audio and video into other languages. **`client.audio`** offers the audio building blocks on their own: separation, voice isolation, effects, mixing, volume, joining and transcription. Every generation method returns a `jobId`; poll it with [`client.jobs.getStatus()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions). The methods call the [Voice and media REST API](https://nodaro.ai/docs/developers/api/voice-and-media).

## Methods

| Method | What it does |
| --- | --- |
| [`voices.list()`](#voiceslist) | List the premade voices |
| [`voices.searchLibrary(params?)`](#voicessearchlibraryparams) | Search the community Voice Library |
| [`voices.listClones()`](#voiceslistclones) | List your existing voice clones |
| [`voices.deleteClone(id)`](#voicesdeletecloneid) | Delete one of your voice clones |
| [`voices.createClone()` and `createCloneFromFile()`](#voicescreateclone-and-createclonefromfile) | Retired |
| [`voices.change(input)`](#voiceschangeinput) | Replace the voice in a recording or a video |
| [`voices.recast(input)`](#voicesrecastinput) | Give each speaker a different voice |
| [`voices.analyze(input)`](#voicesanalyzeinput) | Detect the speakers before a recast |
| [`voices.exportMix(input)`](#voicesexportmixinput) | Render a video from mixed voice stems |
| [`voices.design(input)`](#voicesdesigninput) | Create a new voice from a description |
| [`voices.remix(input)`](#voicesremixinput) | Speak text in a voice you describe |
| [`voices.dub(input)`](#voicesdubinput) | Dub audio or video into another language |
| [`voices.textToDialogue(input)`](#voicestexttodialogueinput) | Voice a multi-speaker script as one audio file |
| [`audio.separate(input)`](#audioseparateinput) | Split a track into stems |
| [`audio.isolate(input)`](#audioisolateinput) | Keep the main voice and remove noise |
| [`audio.applyFx(input)`](#audioapplyfxinput) | Add reverb, echo, telephone or megaphone |
| [`audio.mix(input)`](#audiomixinput) | Layer several tracks into one |
| [`audio.adjustVolume(input)`](#audioadjustvolumeinput) | Change the level, normalize, or fade |
| [`audio.combine(input)`](#audiocombineinput) | Join audio segments end to end |
| [`audio.transcribe(input)`](#audiotranscribeinput) | Turn speech into text, with word timings |

## client.voices: voices

### voices.list()

Lists the premade ElevenLabs voices (`GET /v1/voices`). When the server has no ElevenLabs key configured, it returns a curated set instead.

```ts
list(): Promise<Voice[]>
```

```ts
const voices = await client.voices.list()
```

### voices.searchLibrary(params?)

Searches the community Voice Library (`GET /v1/voices/library`). Every parameter is optional, and empty values are left out so the server defaults apply. `hasMore` in the answer tells you whether another page exists.

```ts
searchLibrary(params?: VoiceLibraryParams): Promise<{ voices: Voice[]; hasMore: boolean }>
```

<TypeTable
type={{
search: { type: 'string', description: "Words to search for." },
gender: { type: 'string', description: "Filter by gender." },
age: { type: 'string', description: "Filter by age." },
accent: { type: 'string', description: "Filter by accent." },
language: { type: 'string', description: "Filter by language code, such as en." },
category: { type: 'string', description: "Filter by category." },
use_cases: { type: 'string', description: "Filter by use case." },
descriptives: { type: 'string', description: "Filter by descriptive tags." },
featured: { type: 'boolean', description: "Only featured voices." },
sort: { type: 'string', description: "The order, such as trending." },
page: { type: 'number', default: '0', description: "The page, counted from 0." },
page_size: { type: 'number', default: '30', description: "Voices per page, 1 to 100." },
}}
/>

```ts
const { voices, hasMore } = await client.voices.searchLibrary({ search: "deep", language: "en" })
const voice = voices[0]

await client.nodes.run("text-to-speech", {
text: "Hello!",
voice: voice.voice_id,
voiceType: "library",
...(voice.recommendedProvider ? { provider: voice.recommendedProvider } : {}),
})
```

Each voice may carry two hints about the speech models it was verified on:

- **`recommendedProvider`** is the best model for the voice. Apps without a model picker should send it as `provider` to [Text to Speech](https://nodaro.ai/docs/nodes/audio/text-to-speech), so the voice sounds like its library preview.
- **`verifiedProviders`** lists every model the voice is verified on. Apps with a model picker should replace the user's choice only when it is not in this list.

### voices.listClones()

Lists your voice clones (`GET /v1/voice-clones`). Clones made before cloning was retired still work as voice ids everywhere a voice is accepted.

```ts
listClones(): Promise<VoiceClone[]>
```

```ts
const clones = await client.voices.listClones()
```

### voices.deleteClone(id)

Deletes one of your voice clones (`DELETE /v1/voice-clones/:id`).

```ts
deleteClone(id: string): Promise<void>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The clone id." },
}}
/>

```ts
await client.voices.deleteClone(cloneId)
```

### voices.createClone() and createCloneFromFile()

Voice cloning is no longer offered on Nodaro. It was retired in September 2026. Both methods remain on the client and are marked deprecated. They fail with a `NodaroError` whose `code` is `voice_cloning_retired`, with status 410. For a new custom voice, use [`design()`](#voicesdesigninput).

## client.voices: voice changer

### voices.change(input)

Replaces the voice in a recording, or in a whole talking video, with another voice (`POST /v1/voice-changer`), as the [Voice Changer](https://nodaro.ai/docs/nodes/audio/voice-changer) node does. With `videoUrl`, the server takes the audio out of the video, changes the voice, and puts the new voice back on the original picture.

```ts
change(input: {
voiceId: string
audioUrl?: string
videoUrl?: string
model?: string
stability?: number
similarityBoost?: number
style?: number
useSpeakerBoost?: boolean
seed?: number
removeBackgroundNoise?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
voiceId: { type: 'string', required: true, description: "The new voice: a premade voice name or an ElevenLabs voice id." },
audioUrl: { type: 'string', description: "The recording to change. Give audioUrl or videoUrl. When both are sent, the video wins." },
videoUrl: { type: 'string', description: "A video whose voice to change." },
model: { type: 'string', description: "A speech-to-speech model override." },
stability: { type: 'number', description: "Voice stability, 0 to 1." },
similarityBoost: { type: 'number', description: "How closely to match the target voice, 0 to 1." },
style: { type: 'number', default: '0', description: "Style exaggeration, 0 to 1. Above 0 it strengthens the delivery, at some cost to speed and stability." },
useSpeakerBoost: { type: 'boolean', description: "Sharpen the likeness to the target voice, a little slower." },
seed: { type: 'number', description: "A whole number that makes the output repeatable." },
removeBackgroundNoise: { type: 'boolean', description: "true returns a clean voice only. false keeps the music and effects under the new voice." },
}}
/>

```ts
const { jobId } = await client.voices.change({
videoUrl: "https://example.com/talking.mp4",
voiceId: "Aria",
})
// the finished job's output_data has videoUrl and audioUrl
```

### voices.recast(input)

Gives each detected speaker in a recording a different voice (`POST /v1/voice-changer-pro`), as the [Voice Changer Pro](https://nodaro.ai/docs/nodes/audio/voice-changer-pro) node does. It runs on Nodaro Cloud, costs credits, and runs as a job.

```ts
recast(input: VoiceChangerProInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
orderedVoices: { type: 'Array<VoiceChangerProVoice | null>', required: true, description: "1 to 8 entries, one per detected speaker, in order. Each is a voice id, an object with per-voice settings, or null to keep that speaker's own voice. At least one entry must not be null." },
audioUrl: { type: 'string', description: "The recording. Give audioUrl or videoUrl." },
videoUrl: { type: 'string', description: "A video whose speakers to recast. The new voices are put back on the original picture." },
model: { type: 'string', description: "A speech-to-speech model override." },
preserveBackground: { type: 'boolean', default: 'true', description: "Mix the music and effects back under the new voices. false returns the voices only." },
separationQuality: { type: '"fast" | "best"', default: '"fast"', description: "fast is quicker and keeps more of the voice. best separates voice and music more finely." },
removeBackgroundNoise: { type: 'boolean', description: "Also remove noise from the result." },
musicVolumeMode: { type: '"match" | "normalize" | "manual"', default: '"match"', description: "The level of the kept background: the original level, normalized, or musicVolume." },
musicVolume: { type: 'number', description: "The background level in percent, 0 to 200, with musicVolumeMode manual." },
voiceFx: { type: '{ preset, wetDryMix?, delayMs?, decay? }', description: "An effect on all the new voices, applied before the background returns." },
output: { type: '"video" | "stems"', default: '"video"', description: "video renders the finished result. stems returns separate unmixed tracks, to mix yourself and render with exportMix()." },
analysis: { type: 'VcpAnalysis', description: "The result of an earlier analyze() job, to skip speaker detection." },
}}
/>

Each entry of `orderedVoices` is one of:

- **A voice id**: a premade voice name, such as `"Rachel"`, or an ElevenLabs voice id.
- **`null`**: a keep slot. That speaker keeps their own voice, while later speakers are still recast. Keep slots cost nothing, because only recast speakers are priced.
- **An object** with `voiceId` and settings for that speaker: `stability`, `similarityBoost` and `style` from 0 to 1, `useSpeakerBoost`, and a `seed` from 0 to 4,294,967,295. `volumeMode` is `"match"` (the default, the original speaker's level), `"normalize"` or `"manual"` with `volume` in percent from 0 to 200. `engine: "v3"` speaks the line again from its transcript, with `[audio tags]` support, instead of converting the recorded speech.

Speaker 0 gets `orderedVoices[0]`, speaker 1 gets `orderedVoices[1]`, and so on. Speakers after the last entry keep their own voice. Voice and music are always separated first. `preserveBackground` only decides whether the music comes back.

`voiceFx.preset` is a reverb space (`room`, `bathroom`, `car`, `hall`, `concert-hall`, `church`, `cave`, `arena` or `outdoor`), `telephone`, `megaphone`, `echo` or `custom`. Reverb presets use `wetDryMix` from 0 to 100. `echo` and `custom` use `delayMs` from 20 to 2,000 and `decay` from 0 to 1.

```ts
// Recast speakers 1 and 3, and keep speaker 2's own voice
const { jobId } = await client.voices.recast({
audioUrl: "https://example.com/panel.mp3",
orderedVoices: ["Rachel", null, "Aria"],
})

// Repeatable voices, a hall reverb, and finer separation
const { jobId: tuned } = await client.voices.recast({
audioUrl: "https://example.com/dialogue.mp3",
orderedVoices: [
{ voiceId: "Rachel", seed: 12345, stability: 0.6 },
{ voiceId: "Aria", seed: 67890, volumeMode: "manual", volume: 120 },
],
voiceFx: { preset: "hall", wetDryMix: 35 },
separationQuality: "best",
})
```

In video mode, the finished job's `output_data` has `videoUrl` and `audioUrl`.

### voices.analyze(input)

Detects the speakers in a clip **without recasting it** (`POST /v1/voice-changer-pro/analyze`). It separates voice from music once and finds who speaks when. It runs on Nodaro Cloud, at a flat price, as a job.

```ts
analyze(input: {
audioUrl?: string
videoUrl?: string
separationQuality?: "fast" | "best"
suggestTitle?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', description: "The recording. Give exactly one of audioUrl and videoUrl." },
videoUrl: { type: 'string', description: "A video to analyze." },
separationQuality: { type: '"fast" | "best"', description: "The separation quality, as in recast()." },
suggestTitle: { type: 'boolean', description: "Also propose a title for the clip." },
}}
/>

The finished job's `output_data` is a `VcpAnalysis`: the separated `vocalsUrl` and `backgroundUrl`, the detected `speakers` (each with `id`, time `segments`, `firstStartSec`, `wordCount` and a text `snippet`), `languageCode` with `languageProbability`, and `suggestedTitle` when asked. Pass it as `analysis` to every later `recast()`, so the recast reuses the separated tracks and does not pay for detection again.

### voices.exportMix(input)

Renders the final video from a mixed set of stems (`POST /v1/voice-changer-pro/export`). It is the last step after `recast({ output: "stems" })`. It runs on Nodaro Cloud, at a flat price, as a job.

```ts
exportMix(input: {
videoUrl: string
tracks: Array<{ url: string; gain: number; muted: boolean; kind?: "voice" | "background" }>
voiceFx?: { preset: AudioFxPreset; wetDryMix?: number; delayMs?: number; decay?: number }
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
videoUrl: { type: 'string', required: true, description: "The video. Its picture is copied without re-encoding." },
tracks: { type: 'VcpExportTrack[]', required: true, description: "Up to 16 tracks, at least one not muted. Each has a url, a gain from 0 to 200, muted, and an optional kind." },
voiceFx: { type: '{ preset, wetDryMix?, delayMs?, decay? }', description: "An effect on the voice tracks only, never on a background track." },
}}
/>

The interactive flow analyzes once, recasts to stems, then mixes and renders. Here it is with a small polling helper:

```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, 2_000))
}
}

const { jobId: analyzeJob } = await client.voices.analyze({ videoUrl })
const analysis = (await outputOf(analyzeJob)) as VcpAnalysis

const { jobId: recastJob } = await client.voices.recast({
videoUrl,
orderedVoices: ["Rachel", null, "Aria"],
output: "stems",
analysis,
})
const stems = await outputOf(recastJob)

const { jobId: exportJob } = await client.voices.exportMix({
videoUrl,
tracks: [
{ url: stems.tracks[0].url, gain: 100, muted: false },
{ url: stems.tracks[1].url, gain: 90, muted: false },
{ url: stems.backgroundUrl, gain: 70, muted: false, kind: "background" },
],
voiceFx: { preset: "hall", wetDryMix: 25 },
})
const { videoUrl: finalVideo } = await outputOf(exportJob)
```

The gains, mutes and effect are applied when the video renders, and the picture is copied as it is. The export therefore matches your preview, and you can change the mix as often as you like before you export. A mix with every track muted is refused with a 400.

## client.voices: create and translate

### voices.design(input)

Creates a new synthetic voice from a text description (`POST /v1/voice-design`), as the [Voice Design](https://nodaro.ai/docs/nodes/audio/voice-design) node does. The finished job carries an audio preview and the id of the new voice, which you can use anywhere a voice is accepted.

```ts
design(input: {
text: string
voiceDescription: string
model?: string
loudness?: number
guidanceScale?: number
seed?: number
quality?: number
shouldEnhance?: boolean
userPrompt?: string
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
text: { type: 'string', required: true, description: "The line the preview speaks, 100 to 1,000 characters." },
voiceDescription: { type: 'string', required: true, description: "The voice you want, such as a warm, low voice of an older narrator." },
model: { type: 'string', description: "A voice design model override." },
loudness: { type: 'number', description: "The loudness, -1 to 1." },
guidanceScale: { type: 'number', description: "How closely to follow the description, 0 to 100." },
seed: { type: 'number', description: "A seed for a repeatable result." },
quality: { type: 'number', description: "The quality setting." },
shouldEnhance: { type: 'boolean', description: "Let the model expand your description." },
userPrompt: { type: 'string', description: "Your original request, kept with the job." },
}}
/>

```ts
const { jobId } = await client.voices.design({
text: "Welcome back. Tonight we follow the river north, into the mountains where the story began.",
voiceDescription: "A calm, deep voice of an older male narrator with a slight British accent",
})
```

### voices.remix(input)

Speaks text in a voice described in plain words, without creating a voice (`POST /v1/voice-remix`), as the [Voice Remix](https://nodaro.ai/docs/nodes/audio/voice-remix) node does.

```ts
remix(input: { text: string; voiceDescription: string; userPrompt?: string }): Promise<{ jobId: string }>
```

<TypeTable
type={{
text: { type: 'string', required: true, description: "The text to speak, 1 to 5,000 characters." },
voiceDescription: { type: 'string', required: true, description: "The voice to use, described in words." },
userPrompt: { type: 'string', description: "Your original request, kept with the job." },
}}
/>

```ts
const { jobId } = await client.voices.remix({
text: "Your order is on its way.",
voiceDescription: "A cheerful young woman, fast and upbeat",
})
```

### voices.dub(input)

Dubs audio, or a whole video, into another language while keeping each speaker's voice (`POST /v1/dubbing`), as the [Dubbing](https://nodaro.ai/docs/nodes/audio/dubbing) node does. A video dub returns `output_data.videoUrl`, the dubbed clip, and `output_data.audioUrl`, the dubbed track alone.

```ts
dub(input: DubbingInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
targetLanguage: { type: 'string', required: true, description: "The language to dub into, as a code such as es or pt-BR." },
audioUrl: { type: 'string', description: "An audio source. Give exactly one of audioUrl, videoUrl and sourceUrl." },
videoUrl: { type: 'string', description: "A video source. The result is a dubbed video." },
sourceUrl: { type: 'string', description: "A public YouTube, TikTok or direct link, fetched for you." },
sourceLanguage: { type: 'string', description: "The spoken language. Detected when omitted." },
numSpeakers: { type: 'number', default: '0', description: "The number of speakers, 1 to 20, or 0 to detect it. A known number improves separation." },
disableVoiceCloning: { type: 'boolean', description: "Use stock voices instead of each speaker's own voice." },
dropBackgroundAudio: { type: 'boolean', description: "Leave out the music and effects." },
startTime: { type: 'number', description: "Dub only from this point, in seconds." },
endTime: { type: 'number', description: "Dub only up to this point, in seconds." },
highestResolution: { type: 'boolean', description: "Keep the source resolution on a video dub." },
useProfanityFilter: { type: 'boolean', description: "Filter profanity." },
targetAccent: { type: 'string', description: "An accent for the dub. Experimental." },
watermark: { type: 'boolean', description: "Add the model maker's watermark to a video dub." },
}}
/>

```ts
const { jobId } = await client.voices.dub({
videoUrl: "https://example.com/interview.mp4",
targetLanguage: "es",
numSpeakers: 2,
})
```

The price depends on the minutes of the dubbed span, with a minimum of 1 minute. A span is at most 30 minutes, so use `startTime` and `endTime` for longer sources.

### voices.textToDialogue(input)

Voices a script with several speakers as **one** audio file (`POST /v1/text-to-dialogue`), as the [Text to Dialogue](https://nodaro.ai/docs/nodes/audio/text-to-dialogue) node does with [ElevenLabs Dialogue v3](https://nodaro.ai/docs/models/audio/elevenlabs-dialogue-v3).

```ts
textToDialogue(input: {
dialogue: Array<{ text: string; voice: string }>
stability?: 0 | 0.5 | 1
languageCode?: string
seed?: number
applyTextNormalization?: "auto" | "on" | "off"
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
dialogue: { type: 'Array<{ text: string; voice: string }>', required: true, description: "The lines in speaking order. voice is a premade voice name or an ElevenLabs voice id. Text may carry audio tags such as [laughs]." },
stability: { type: '0 | 0.5 | 1', description: "The delivery stability." },
languageCode: { type: 'string', description: "An ISO 639-1 language hint. Detected when omitted." },
seed: { type: 'number', description: "0 to 4,294,967,295 for a repeatable result. Omit it for a random one." },
applyTextNormalization: { type: '"auto" | "on" | "off"', description: "Whether to spell out numbers and abbreviations." },
}}
/>

```ts
const { jobId } = await client.voices.textToDialogue({
dialogue: [
{ text: "Did you hear that?", voice: "Rachel" },
{ text: "[whispers] Stay behind me.", voice: "Callum" },
],
})
```

A script can hold at most 5,000 characters, and fewer than 2,000 give the best quality. It can use at most 10 different voices. Library voices, designed voices and existing clones all work, mixed as you like. The finished job's `output_data.audioUrl` is the file.

## client.audio

The audio building blocks that Voice Changer Pro uses internally, available one by one. Every method returns a `jobId`.

### audio.separate(input)

Splits a track into stems (`POST /v1/audio-separation`), as the [Audio Separation](https://nodaro.ai/docs/nodes/audio/audio-separation) node does.

```ts
separate(input: { audioUrl: string; mode?: "vocal_instrumental" | "stems"; quality?: "auto" | "fast" | "best" }): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "The track to split." },
mode: { type: '"vocal_instrumental" | "stems"', default: '"vocal_instrumental"', description: "vocal_instrumental splits the voice from the music and effects. stems returns drums, bass and the other parts." },
quality: { type: '"auto" | "fast" | "best"', description: "The separation quality." },
}}
/>

```ts
const { jobId } = await client.audio.separate({ audioUrl: songUrl })
// output: vocalUrl and instrumentalUrl, or one URL per stem in stems mode
```

### audio.isolate(input)

Keeps the main voice and removes background noise (`POST /v1/audio-isolation`), as the [Voice Extractor](https://nodaro.ai/docs/nodes/audio/voice-extractor) node does.

```ts
isolate(input: { audioUrl: string }): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "The recording to clean." },
}}
/>

```ts
const { jobId } = await client.audio.isolate({ audioUrl: interviewUrl })
```

### audio.applyFx(input)

Adds a reverb, echo, telephone or megaphone effect (`POST /v1/audio-fx`), as the [Audio FX](https://nodaro.ai/docs/nodes/audio/audio-fx) node does. The presets are the same as the voice changer's `voiceFx`.

```ts
applyFx(input: {
audioUrl: string
preset?: AudioFxPreset
mix?: number
delayMs?: number
decay?: number
eqLow?: number
eqHigh?: number
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "The track to process." },
preset: { type: 'AudioFxPreset', description: "A reverb space such as room, hall or church, or telephone, megaphone, echo or custom." },
mix: { type: 'number', description: "The reverb wet and dry balance, 0 to 100." },
delayMs: { type: 'number', description: "The echo delay, for echo and custom." },
decay: { type: 'number', description: "The echo decay, for echo and custom." },
eqLow: { type: 'number', description: "The low cut or boost in dB, for telephone and megaphone." },
eqHigh: { type: 'number', description: "The high cut or boost in dB, for telephone and megaphone." },
}}
/>

```ts
const { jobId } = await client.audio.applyFx({ audioUrl: lineUrl, preset: "telephone" })
```

### audio.mix(input)

Layers several tracks into one (`POST /v1/mix-audio`), as the [Mix Audio](https://nodaro.ai/docs/nodes/audio/mix-audio) node does.

```ts
mix(input: { audioUrls: string[]; trackVolumes?: number[] }): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrls: { type: 'string[]', required: true, description: "2 to 20 tracks, played together." },
trackVolumes: { type: 'number[]', description: "A level per track in percent, 0 to 200, in the same order." },
}}
/>

```ts
const { jobId } = await client.audio.mix({ audioUrls: [voiceUrl, musicUrl], trackVolumes: [100, 35] })
```

### audio.adjustVolume(input)

Changes the level of an audio file, or of a video's audio (`POST /v1/adjust-volume`), as the [Adjust Volume](https://nodaro.ai/docs/nodes/audio/adjust-volume) node does.

```ts
adjustVolume(input: {
audioUrl?: string
videoUrl?: string
volume?: number
normalize?: boolean
fadeIn?: number
fadeOut?: number
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', description: "An audio file. Give audioUrl or videoUrl." },
videoUrl: { type: 'string', description: "A video whose audio to change." },
volume: { type: 'number', default: '100', description: "The level in percent." },
normalize: { type: 'boolean', description: "Bring the loudness to a standard level." },
fadeIn: { type: 'number', description: "A fade-in, in seconds." },
fadeOut: { type: 'number', description: "A fade-out, in seconds." },
}}
/>

```ts
const { jobId } = await client.audio.adjustVolume({ audioUrl: musicUrl, volume: 60, fadeOut: 3 })
```

### audio.combine(input)

Joins audio segments end to end (`POST /v1/combine-audio`), as the [Combine Audio](https://nodaro.ai/docs/nodes/audio/combine-audio) node does.

```ts
combine(input: { segments: Array<{ url: string; startTime?: number; endTime?: number }> }): Promise<{ jobId: string }>
```

<TypeTable
type={{
segments: { type: 'Array<{ url: string; startTime?: number; endTime?: number }>', required: true, description: "The segments in order. Each can use only part of its file, from startTime to endTime in seconds." },
}}
/>

```ts
const { jobId } = await client.audio.combine({
segments: [{ url: introUrl }, { url: episodeUrl, startTime: 4 }, { url: outroUrl }],
})
```

### audio.transcribe(input)

Turns the speech in an audio or video file into text (`POST /v1/transcribe`), as the [Transcribe](https://nodaro.ai/docs/nodes/audio/transcribe) node does.

```ts
transcribe(input: {
audioUrl: string
provider?: "elevenlabs-stt" | "incredibly-fast-whisper" | "whisper"
language?: string
diarize?: boolean
tagAudioEvents?: boolean
wordTimestamps?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "The audio or video file." },
provider: { type: '"elevenlabs-stt" | "incredibly-fast-whisper" | "whisper"', default: '"whisper"', description: "The engine. See the table below." },
language: { type: 'string', description: "Force a language. Omit it to detect the language." },
diarize: { type: 'boolean', description: "Label who speaks each word. elevenlabs-stt only." },
tagAudioEvents: { type: 'boolean', description: "Tag laughter, applause and other sounds. elevenlabs-stt only." },
wordTimestamps: { type: 'boolean', description: "Ask for per-word timings." },
}}
/>

| `provider` | Word timings | Notes |
| --- | --- | --- |
| [`elevenlabs-stt`](https://nodaro.ai/docs/models/audio/elevenlabs-stt) | Always | The only engine that honors `diarize` and `tagAudioEvents`. |
| [`incredibly-fast-whisper`](https://nodaro.ai/docs/models/audio/incredibly-fast-whisper) | Only with `wordTimestamps: true` | Without the flag, the job succeeds and is charged, with phrase segments and an empty `words` list. |
| [`whisper`](https://nodaro.ai/docs/models/audio/whisper) | Never | Phrase segments only. `wordTimestamps: true` is refused with `400 validation_error` before any credits are spent. |

Omitting `provider` uses `whisper`, so `wordTimestamps: true` without a `provider` also gets the 400. For word timings, name `elevenlabs-stt`, or `incredibly-fast-whisper` with `wordTimestamps: true`. Kinetic captions need them.

```ts
const { jobId } = await client.audio.transcribe({ audioUrl: talkUrl, provider: "elevenlabs-stt" })
```

The finished job's `output_data` is a `TranscribeJobOutput`:

| Field | Units | Content |
| --- | --- | --- |
| `text` | | The whole transcript as one string. |
| `language` | | The detected or requested language code. |
| `words` | milliseconds | One entry per word: `{ text, startMs, endMs, speaker? }`. Empty when the engine was not asked for word timings. |
| `json` | milliseconds | The normalized transcript, `{ version, language, words, segments? }`, which [`client.edit`](https://nodaro.ai/docs/developers/sdk/editing) takes. |
| `segments` | **seconds** | The raw phrase ranges, from the older engines only. `elevenlabs-stt` returns none, so read `words`. |

`words` has exactly the shape of the `captions` that [`client.media.addCaptions()`](https://nodaro.ai/docs/developers/sdk/media-and-uploads#mediaaddcaptionsinput) takes, so you can correct a transcript and burn it as captions.

## Frequently asked questions

### How do I change the voice in a video with the Nodaro SDK?

Call client.voices.change with the videoUrl and a voiceId. The server replaces the voice and puts it back on the original video. Poll the job for output_data.videoUrl.

### How do I give each speaker in a recording a different voice?

Call client.voices.recast with orderedVoices, one entry per detected speaker in order. Use null for a speaker who keeps their own voice. It runs on Nodaro Cloud.

### Can I still clone a voice with the SDK?

No. Voice cloning was retired in September 2026, and createClone now fails with 410 voice_cloning_retired. Existing clones still work. Design a new voice with client.voices.design instead.

### Which transcription engine returns word timings?

elevenlabs-stt always returns word timings. incredibly-fast-whisper returns them only with wordTimestamps set to true, and whisper never does.
