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.
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(). The methods call the Voice and media REST API.
Methods
| Method | What it does |
|---|---|
voices.list() | List the premade voices |
voices.searchLibrary(params?) | Search the community Voice Library |
voices.listClones() | List your existing voice clones |
voices.deleteClone(id) | Delete one of your voice clones |
voices.createClone() and createCloneFromFile() | Retired |
voices.change(input) | Replace the voice in a recording or a video |
voices.recast(input) | Give each speaker a different voice |
voices.analyze(input) | Detect the speakers before a recast |
voices.exportMix(input) | Render a video from mixed voice stems |
voices.design(input) | Create a new voice from a description |
voices.remix(input) | Speak text in a voice you describe |
voices.dub(input) | Dub audio or video into another language |
voices.textToDialogue(input) | Voice a multi-speaker script as one audio file |
audio.separate(input) | Split a track into stems |
audio.isolate(input) | Keep the main voice and remove noise |
audio.applyFx(input) | Add reverb, echo, telephone or megaphone |
audio.mix(input) | Layer several tracks into one |
audio.adjustVolume(input) | Change the level, normalize, or fade |
audio.combine(input) | Join audio segments end to end |
audio.transcribe(input) | 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.
list(): Promise<Voice[]>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.
searchLibrary(params?: VoiceLibraryParams): Promise<{ voices: Voice[]; hasMore: boolean }>Prop
Type
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:
recommendedProvideris the best model for the voice. Apps without a model picker should send it asproviderto Text to Speech, so the voice sounds like its library preview.verifiedProviderslists 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.
listClones(): Promise<VoiceClone[]>const clones = await client.voices.listClones()voices.deleteClone(id)
Deletes one of your voice clones (DELETE /v1/voice-clones/:id).
deleteClone(id: string): Promise<void>Prop
Type
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().
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 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.
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 }>Prop
Type
const { jobId } = await client.voices.change({
videoUrl: "https://example.com/talking.mp4",
voiceId: "Aria",
})
// the finished job's output_data has videoUrl and audioUrlvoices.recast(input)
Gives each detected speaker in a recording a different voice (POST /v1/voice-changer-pro), as the Voice Changer Pro node does. It runs on Nodaro Cloud, costs credits, and runs as a job.
recast(input: VoiceChangerProInput): Promise<{ jobId: string }>Prop
Type
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
voiceIdand settings for that speaker:stability,similarityBoostandstylefrom 0 to 1,useSpeakerBoost, and aseedfrom 0 to 4,294,967,295.volumeModeis"match"(the default, the original speaker's level),"normalize"or"manual"withvolumein 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.
// 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.
analyze(input: {
audioUrl?: string
videoUrl?: string
separationQuality?: "fast" | "best"
suggestTitle?: boolean
}): Promise<{ jobId: string }>Prop
Type
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.
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 }>Prop
Type
The interactive flow analyzes once, recasts to stems, then mixes and renders. Here it is with a small polling helper:
import type { VcpAnalysis } 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, 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 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.
design(input: {
text: string
voiceDescription: string
model?: string
loudness?: number
guidanceScale?: number
seed?: number
quality?: number
shouldEnhance?: boolean
userPrompt?: string
}): Promise<{ jobId: string }>Prop
Type
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 node does.
remix(input: { text: string; voiceDescription: string; userPrompt?: string }): Promise<{ jobId: string }>Prop
Type
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 node does. A video dub returns output_data.videoUrl, the dubbed clip, and output_data.audioUrl, the dubbed track alone.
dub(input: DubbingInput): Promise<{ jobId: string }>Prop
Type
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 node does with ElevenLabs Dialogue v3.
textToDialogue(input: {
dialogue: Array<{ text: string; voice: string }>
stability?: 0 | 0.5 | 1
languageCode?: string
seed?: number
applyTextNormalization?: "auto" | "on" | "off"
}): Promise<{ jobId: string }>Prop
Type
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 node does.
separate(input: { audioUrl: string; mode?: "vocal_instrumental" | "stems"; quality?: "auto" | "fast" | "best" }): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.audio.separate({ audioUrl: songUrl })
// output: vocalUrl and instrumentalUrl, or one URL per stem in stems modeaudio.isolate(input)
Keeps the main voice and removes background noise (POST /v1/audio-isolation), as the Voice Extractor node does.
isolate(input: { audioUrl: string }): Promise<{ jobId: string }>Prop
Type
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 node does. The presets are the same as the voice changer's voiceFx.
applyFx(input: {
audioUrl: string
preset?: AudioFxPreset
mix?: number
delayMs?: number
decay?: number
eqLow?: number
eqHigh?: number
}): Promise<{ jobId: string }>Prop
Type
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 node does.
mix(input: { audioUrls: string[]; trackVolumes?: number[] }): Promise<{ jobId: string }>Prop
Type
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 node does.
adjustVolume(input: {
audioUrl?: string
videoUrl?: string
volume?: number
normalize?: boolean
fadeIn?: number
fadeOut?: number
}): Promise<{ jobId: string }>Prop
Type
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 node does.
combine(input: { segments: Array<{ url: string; startTime?: number; endTime?: number }> }): Promise<{ jobId: string }>Prop
Type
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 node does.
transcribe(input: {
audioUrl: string
provider?: "elevenlabs-stt" | "incredibly-fast-whisper" | "whisper"
language?: string
diarize?: boolean
tagAudioEvents?: boolean
wordTimestamps?: boolean
}): Promise<{ jobId: string }>Prop
Type
provider | Word timings | Notes |
|---|---|---|
elevenlabs-stt | Always | The only engine that honors diarize and tagAudioEvents. |
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 | 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.
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 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() takes, so you can correct a transcript and burn it as captions.
Frequently asked questions
Related
Media and uploads
Editing
Voice and media
Voice Changer Pro
Transcribe
Last updated on
Media and uploads
Upload files, list your media library, download social videos, trim clips, burn captions and composite images and videos from TypeScript with the Nodaro 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.