Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
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.
- Install the packages:
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-elevenlabs- Put your key in .env:
ELEVENLABS_API_KEY=your-elevenlabs-api-key- Add a POST route at /api/generate/voice:
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)
}- Call the route from a component. There is no voice-specific hook, so use the generic useGeneration hook:
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():
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:
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.
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:
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#
| 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.
Options#
| 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.
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.