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.libraryFacets(params?) | Count the Voice Library voices for each filter value |
voices.listFavorites(), favorite(input) and unfavorite(voiceId) | List, star and unstar your favorite voices |
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.pitchTempo(input) | Shift the pitch, or change the tempo without changing the pitch |
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()On a deployment that serves voice previews from its own host, each voice's preview_url, here and in searchLibrary(), is a same-site path (/v1/voices/<id>/preview?…) instead of the provider's address. Resolve it against the API's origin to play it. See Voice previews from the deployment's own host.
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.
Both hints follow the order ElevenLabs v4, ElevenLabs v3, ElevenLabs v4 Turbo, Turbo v2.5, then Multilingual v2. recommendedProvider is the first entry of verifiedProviders. Check whether a model is in the list, never compare against a fixed set of values: a speech model added later joins the list when it ships. Both hints name only models that the install offers. A voice with no verified model there carries neither field, so keep the user's current model.
Each voice also carries what the library says about its sound and its languages, so a card can read "Cute · Spanish" without a second call:
descriptiveis the library's one-word tone, such ascalm,deeporcute, or""when it states none. It is the value adescriptivesfilter takes. The known values areVOICE_LIBRARY_DESCRIPTIVESin@nodaro/shared/pipeline.languageandlocaleare the primary language as the library codes it (en,he) and its locale (en-US), or""when unstated.verifiedLanguageslists every language code the voice is verified for, the primary first. Thelanguagefilter matches this list: a voice in English that is verified for Spanish is listed underlanguage: "es".
The use_cases values are VOICE_LIBRARY_USE_CASES in @nodaro/shared/pipeline. Both filters pass the value through as sent, so a value the library adds later works before it is listed. Older servers leave the new fields out.
voices.libraryFacets(params?)
Counts the Voice Library voices for each group (use_cases), tone (descriptives), language, gender, age, accent and category, for the filters you hold (GET /v1/voices/library/facets). One public call, with no credits, fills a whole picker screen: a count on every group, and only the tones and languages that have voices.
libraryFacets(params?: VoiceLibraryFacetsParams): Promise<VoiceLibraryFacets>params are the filters of searchLibrary() without paging and sort: search, gender, age, accent, language, category, use_cases, descriptives and featured.
{
total: 681, // voices matching every filter sent
use_cases: [{ value: "characters_animation", count: 681 }, { value: "social_media", count: 930 }, …],
languages: [{ value: "en", count: 681 }, { value: "es", count: 496 }, …],
descriptives: [{ value: "confident", count: 73 }, { value: "casual", count: 64 }, …],
genders: […], ages: […], accents: […], categories: […],
approximate: false,
updatedAt: "2026-11-09T06:00:00.000Z",
}totalapplies every filter. Each facet's counts apply every filter except that facet's own, so a client can show every group's count while one is chosen. Values with no voices are left out. The lists are sorted by count, then by value, and the values are the library's own spellings: pass them back as filters.languagescounts a voice under every language it is verified for, so those counts add up to more thantotal.- The counts come from the platform's own copy of the library, read on a schedule.
updatedAtsays when, so a count can trail the listing by a few voices. Withsearch, the counts are a lower estimate andapproximateistrue. - For a few minutes after a deployment first reads the library, the method throws with
503 voice_library_warming. Retry after theRetry-Afterseconds. A deployment with no speech key answers zero counts withkeyMissing: true.
const facets = await client.voices.libraryFacets({ use_cases: "characters_animation", language: "en" })
const tones = facets.descriptives // only tones that have voices, most first
const hebrew = facets.languages.find((l) => l.value === "he")?.count ?? 0voices.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.listFavorites(), favorite(input) and unfavorite(voiceId)
List, star and unstar your favorite voices (GET /v1/voices/favorites, PUT and DELETE /v1/voices/favorites/:voiceId). A favorite is a premade voice, a Voice Library voice or one of your clones. It is starred from any client and read back by all of them, newest first.
listFavorites(): Promise<VoiceFavorite[]>
favorite(input: VoiceFavoriteInput): Promise<VoiceFavorite>
unfavorite(voiceId: string): Promise<void>Prop
Type
- A favorite carries the
voiceIdandvoiceTypethat a text-to-speech run takes asvoiceandvoiceType, and thenameandpreviewUrlit was starred with. A favorites list renders without a second lookup. - Both writes are idempotent. Starring an already-starred voice refreshes its snapshot, and unstarring a voice that is not there succeeds.
- Past 500 favorites,
favorite()throws with409 limit_reached. - OAuth apps need
assets:readfor the list andassets:writefor the writes.
const { voices } = await client.voices.searchLibrary({ search: "calm narrator", language: "en" })
const pick = voices[0]
await client.voices.favorite({ voiceId: pick.voice_id, voiceType: "library", name: pick.name, previewUrl: pick.preview_url })
const favorites = await client.voices.listFavorites()
await client.nodes.run("text-to-speech", { text: "Hello!", voice: favorites[0].voiceId, voiceType: favorites[0].voiceType })
await client.voices.unfavorite(pick.voice_id)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 ElevenLabs v3, with[audio tags]support, instead of converting the recorded speech.engine: "v4"does the same with ElevenLabs v4, takes anystabilityfrom 0 to 1 and appliessimilarityBoost.
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 0 and 2, and keep speaker 1'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.
A sourceUrl is fetched by the dubbing service for you, so it is an import of that link, under the content rights declaration. Pass the confirmationId that contentRights.confirm() answered for it. Without one, a deployment that requires a confirmation refuses the dub with 403 before the job is created. A key or an agent whose account holds a standing authorization (contentRights.authorize()) needs none.
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 or ElevenLabs Dialogue v4.
textToDialogue(input: {
dialogue: Array<{ text: string; voice: string }>
provider?: "elevenlabs-dialogue" | "elevenlabs-dialogue-v4"
stability?: number
similarityBoost?: number
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 on Dialogue v3 and 10,000 on Dialogue v4, 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. On both models, output_data.transcript holds the timings: words[] with startMs, endMs and speaker, and one segments[] entry per line, where speaker is the line's voice. Pass transcript.words to addCaptions with autoTranscribe: false to caption the dialogue without a second transcription.
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[]
duck?: { under: number; amount?: number; thresholdDb?: number; ratio?: number; attackMs?: number; releaseMs?: number }
endAt?: "longest" | "voice"
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.audio.mix({ audioUrls: [voiceUrl, musicUrl], trackVolumes: [100, 35] })
// The music dips under the voice (track 0) while it speaks.
const ducked = await client.audio.mix({ audioUrls: [voiceUrl, musicUrl], duck: { under: 0, amount: 80 } })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.pitchTempo(input)
Shifts the pitch, or changes the tempo without changing the pitch (POST /v1/pitch-tempo), as the Pitch & Tempo node does. Pass exactly one of audioUrl or videoUrl. A video keeps its picture, and its tempo stays at 1.
pitchTempo(input: {
audioUrl?: string
videoUrl?: string
semitones?: number
tempo?: number
preserveFormants?: boolean
}): Promise<{ jobId: string }>Prop
Type
The run costs 22 credits per started minute of the input, up to 30 minutes.
const { jobId } = await client.audio.pitchTempo({ audioUrl: voiceUrl, semitones: -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" | "nemotron-3-5-transcribe"
language?: string
diarize?: boolean
tagAudioEvents?: boolean
wordTimestamps?: boolean
targetLanguage?: string
confirmationId?: string
}): Promise<{ jobId: string }>Prop
Type
An audioUrl that links to a video's page on a social platform is an import of that link: its sound is fetched first. Pass the confirmationId that contentRights.confirm() answered for that link. Without one, a deployment that requires a confirmation refuses the request with 403 before the job is created. A key or an agent whose account holds a standing authorization (contentRights.authorize()) needs none. A direct file link needs none.
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. |
nemotron-3-5-transcribe | Only with wordTimestamps: true | Nemotron 3.5 Transcribe detects the language itself (language is not used) and adds a speaker per word. The one engine that translates: targetLanguage returns a translation, with no word timings. Beside wordTimestamps: true, or on another engine, it is a 400 validation_error. Priced per started minute of the audio, measured first and charged for the length the model decoded. It takes a direct https file link, up to 60 minutes and 200 MB. |
Omitting provider uses whisper, so wordTimestamps: true without a provider also gets the 400. For word timings, name elevenlabs-stt, or incredibly-fast-whisper or nemotron-3-5-transcribe 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
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.
Related
Media and uploads
Editing
Voice and media
Voice Changer Pro
Transcribe
Last updated on
Media and uploads
Upload files, list your media library, import videos from links, trim clips, burn captions and composite images and videos from TypeScript with the Nodaro SDK.
Editing
Edit podcasts and long videos from TypeScript. Tighten an episode, cut clips, chapters or a trailer, sync and switch cameras, and render an edit decision list.