Skip to content
Media

Voice Creation

generateVoice() creates a voice id. Pass that id as voice to generateSpeech().

Quick setup

This designs a voice from a description with ElevenLabs, and plays the previews in the browser. It takes about 5 minutes.

  1. Install the packages:
shell
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-elevenlabs
  1. Put your key in .env:
shell
ELEVENLABS_API_KEY=your-elevenlabs-api-key
  1. Add a POST route at /api/generate/voice:
ts
import {
  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)
}
  1. Call the route from a component. There is no voice-specific hook, so use the generic useGeneration hook:
tsx
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.

How it works

You can make a voice in two ways:

  • Design: you describe the voice in words. The provider makes it up.
  • Clone: you give a clip of a real speaker. The provider makes a voice from it.

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.

Speak with the new voice

Put the voice id into generateSpeech():

ts
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'

Keep a voice for later

A designed voice is a preview by default, and previews expire. To keep the voice in the library of the provider, pass a name:

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

Guide a design with a reference clip

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:

  • eleven_ttv_v3 still needs a prompt.
  • modelOptions.promptStrength sets how much of the voice comes from the description, and how much from the clip.
  • promptStrength: 0 gives a voice that comes almost fully from the clip.
ts
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.

Advanced

Find a voice you made earlier

If you store the voiceId, you do not need this. If you lose it, listVoices() reads the voice list of the account:

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

  • premade: the stock voices of the provider.
  • generated: designed from a description.
  • cloned: made from reference audio.
  • professional: a curated or paid tier.

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:

  • GeminiTTSVoices from @tanstack/ai-gemini.
  • The OpenAITTSVoice type from @tanstack/ai-openai.

On those adapters, listVoices() throws, and the message names these exports.

Provider support

ProviderDesignReference audioAdapter
ElevenLabsYesYes, on eleven_ttv_v3elevenlabsVoiceDesign()

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.

Options

OptionTypeDescription
adapterVoiceAdapterThe voice adapter, made with a model
promptstringA text description of the voice
referenceAudiostring | File | Blob | ArrayBufferA clip of the speaker to clone
namestringThe name to store the voice under
descriptionstringA description stored with the voice
modelOptionsobjectOptions for this provider, typed per adapter
streambooleanReturn 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.

Result shape

generateVoice() returns a VoiceResult with id, model, and voices. It can also have previewText and usage.

Each voice in voices has:

  • voiceId: pass it to generateSpeech() as voice.
  • saved: true if the provider keeps the voice.
  • status: 'ready' on every adapter, because each adapter finishes the voice before it returns.
  • audio, format, contentType, duration, and language, when the provider reports them.

Model ids and provider options are in the ElevenLabs adapter page.