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

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

MethodWhat 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:

  • recommendedProvider is the best model for the voice. Apps without a model picker should send it as provider to 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.

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 audioUrl

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

// 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 mode

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

providerWord timingsNotes
elevenlabs-sttAlwaysThe only engine that honors diarize and tagAudioEvents.
incredibly-fast-whisperOnly with wordTimestamps: trueWithout the flag, the job succeeds and is charged, with phrase segments and an empty words list.
whisperNeverPhrase 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:

FieldUnitsContent
textThe whole transcript as one string.
languageThe detected or requested language code.
wordsmillisecondsOne entry per word: { text, startMs, endMs, speaker? }. Empty when the engine was not asked for word timings.
jsonmillisecondsThe normalized transcript, { version, language, words, segments? }, which client.edit takes.
segmentssecondsThe 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

Last updated on

On this page