generateVoice() creates a voice id. Pass that id as voice to generateSpeech().
This designs a voice from a description with ElevenLabs, and plays the previews in the browser. It takes about 5 minutes.
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-elevenlabspnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-elevenlabsyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-elevenlabsbun add @tanstack/ai @tanstack/ai-react @tanstack/ai-elevenlabsELEVENLABS_API_KEY=your-elevenlabs-api-keyimport {
generateVoice,
generationParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { elevenlabsVoiceDesign } from '@tanstack/ai-elevenlabs'
export async function POST(request: Request) {
const { input, threadId } = await generationParamsFromRequest('voice', request)
if (typeof input.prompt !== 'string') {
return new Response('Describe the voice in `prompt`.', { status: 400 })
}
const stream = generateVoice({
adapter: elevenlabsVoiceDesign('eleven_ttv_v3'),
prompt: input.prompt,
threadId,
stream: true,
})
return toServerSentEventsResponse(stream)
}import { fetchServerSentEvents, useGeneration } from '@tanstack/ai-react'
import type { VoiceResult } from '@tanstack/ai'
export function VoiceDesigner() {
const { generate, result, isLoading, error } = useGeneration<
{ prompt: string },
VoiceResult
>({
connection: fetchServerSentEvents('/api/generate/voice'),
threadId: 'generation',
})
return (
<div>
<button
onClick={() =>
generate({
prompt: 'A warm, gravelly narrator in his sixties with a slight Irish lilt',
})
}
disabled={isLoading}
>
{isLoading ? 'Designing...' : 'Design a voice'}
</button>
{error && <p role="alert">Error: {error.message}</p>}
{result?.voices.map((voice) => (
<div key={voice.voiceId}>
<p>Voice id: {voice.voiceId}</p>
{voice.audio && (
<audio
src={`data:${voice.contentType ?? 'audio/mpeg'};base64,${voice.audio}`}
controls
/>
)}
</div>
))}
</div>
)
}Click Design a voice. You get a few candidate voices. Each one has a preview to play and a voice id.
You can make a voice in two ways:
Both ways return a list of voices. Each voice has a voiceId, and usually a short preview in audio (base64). To speak with a voice, pass its voiceId to generateSpeech() as voice.
A longer, more specific description gives a better voice. Name the age, the accent, the pace, and the mood.
To call the route from a TanStack Start server function instead, see Transports in full. The useGeneration options are in Generation Hooks.
Put the voice id into generateSpeech():
import { generateSpeech, generateVoice } from '@tanstack/ai'
import { elevenlabsSpeech, elevenlabsVoiceDesign } from '@tanstack/ai-elevenlabs'
const designed = await generateVoice({
adapter: elevenlabsVoiceDesign('eleven_ttv_v3'),
prompt: 'A warm, gravelly narrator in his sixties',
threadId: 'thread-1',
})
const [best] = designed.voices
if (!best) throw new Error('The provider returned no voices.')
const speech = await generateSpeech({
adapter: elevenlabsSpeech('eleven_v4'),
text: 'Once upon a time, in a village at the edge of the sea...',
threadId: 'thread-1',
voice: best.voiceId,
})
console.log(speech.contentType) // 'audio/mpeg'A designed voice is a preview by default, and previews expire. To keep the voice in the library of the provider, pass a name:
import { generateVoice } from '@tanstack/ai'
import { elevenlabsVoiceDesign } from '@tanstack/ai-elevenlabs'
const result = await generateVoice({
adapter: elevenlabsVoiceDesign('eleven_ttv_v3'),
prompt: 'A bright, upbeat product demo host',
threadId: 'thread-1',
name: 'Demo Host',
description: 'Bright, upbeat, mid-30s',
})
const [saved] = result.voices
if (!saved) throw new Error('The provider returned no voices.')
console.log(saved.saved) // true
console.log(saved.voiceId) // A permanent voice id. Store it.Store voiceId in your database. Then every later generateSpeech() call uses it, and you do not design the same voice again.
Providers do not all treat name the same way. ElevenLabs keeps a voice only when you give a name. To see what happened, read saved on each returned voice.
Some models take a clip of a real speaker as a reference. Pass the clip as referenceAudio.
On ElevenLabs, the clip guides the design. It is not a direct copy:
import { readFile } from 'node:fs/promises'
import { generateVoice } from '@tanstack/ai'
import { elevenlabsVoiceDesign } from '@tanstack/ai-elevenlabs'
const clip = await readFile('./narrator.mp3')
const result = await generateVoice({
adapter: elevenlabsVoiceDesign('eleven_ttv_v3'),
prompt: 'The same speaker, but younger and more energetic',
threadId: 'thread-1',
// Use `new Blob([clip])`, not `clip.buffer`. A Buffer can be a view into a
// shared pool, so its `.buffer` can hold more than the bytes of the file.
referenceAudio: new Blob([clip]),
name: 'Young Narrator',
})referenceAudio accepts a base64 string, a base64 data URL, a Blob, a File, or an ArrayBuffer. A remote URL is refused. Read the file yourself and pass the bytes.
Warning
Clone a voice only if the speaker agrees. Most providers make this a condition of their terms.
Your app can now make its own voices and speak with them.
If you store the voiceId, you do not need this. If you lose it, listVoices() reads the voice list of the account:
import { listVoices } from '@tanstack/ai'
import { elevenlabsSpeech } from '@tanstack/ai-elevenlabs'
const { voices } = await listVoices({
adapter: elevenlabsSpeech('eleven_v4'),
origins: ['generated', 'cloned'],
})
for (const voice of voices) {
console.log(voice.voiceId, voice.name)
}Each voice has voiceId. It also has name, origin, description, previewUrl, and labels when the provider reports them. origin is one of:
Leave out origins to get all voices, including the stock voices.
Only providers with a voice list per account support listVoices(). On providers with a fixed list, the package exports that list, so you do not need a network call:
On those adapters, listVoices() throws, and the message names these exports.
| Provider | Design | Reference audio | Adapter |
|---|---|---|---|
| ElevenLabs | Yes | Yes, on eleven_ttv_v3 | elevenlabsVoiceDesign() |
ElevenLabs is the only provider with a generateVoice() adapter. Its voice models are eleven_ttv_v3 and eleven_multilingual_ttv_v2. Only eleven_ttv_v3 takes reference audio.
OpenAI, Gemini, and Cloudflare have a fixed voice list. They have no generateVoice() adapter, and they will not get one.
xAI, BytePlus, and fal.ai each have a voice cloning API. No adapter for them exists yet.
| Option | Type | Description |
|---|---|---|
| adapter | VoiceAdapter | The voice adapter, made with a model |
| prompt | string | A text description of the voice |
| referenceAudio | string | File | Blob | ArrayBuffer | A clip of the speaker to clone |
| name | string | The name to store the voice under |
| description | string | A description stored with the voice |
| modelOptions | object | Options for this provider, typed per adapter |
| stream | boolean | Return a stream of events for an SSE route |
Give prompt, or referenceAudio, or both. ElevenLabs always needs prompt, because its design endpoint requires a description.
generateVoice() returns a VoiceResult with id, model, and voices. It can also have previewText and usage.
Each voice in voices has:
Model ids and provider options are in the ElevenLabs adapter page.