Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/elevenlabs-rest-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-elevenlabs': minor
---

feat: add REST adapters to @tanstack/ai-elevenlabs and migrate realtime to the renamed SDK

Extends `@tanstack/ai-elevenlabs` (previously realtime-only) with three tree-shakeable REST adapters built on the official `@elevenlabs/elevenlabs-js` SDK (v2.44+):

- `elevenlabsSpeech()` β€” text-to-speech on `eleven_v3`, `eleven_multilingual_v2`, `eleven_flash_*`, `eleven_turbo_*`
- `elevenlabsAudio()` β€” music (`music_v1`, with structured composition plans) and sound effects (`eleven_text_to_sound_v2`/`v1`) via a single adapter that dispatches by model
- `elevenlabsTranscription()` β€” Scribe v1/v2 speech-to-text with diarization, keyterm biasing, PII redaction, and word-level timestamps

Also migrates the existing realtime adapter off the deprecated `@11labs/client` onto the renamed `@elevenlabs/client` package.
36 changes: 30 additions & 6 deletions examples/ts-react-chat/.env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
# OpenAI API Key
# OpenAI API Key (chat, images, video, speech, transcription, summarize)
# Get yours at: https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-...
OPENAI_API_KEY=

# ElevenLabs API Key (for realtime voice)
# Anthropic API Key (chat)
# Get yours at: https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY=

# Google Gemini API Key (chat, audio, speech)
# Get yours at: https://aistudio.google.com/apikey
GOOGLE_API_KEY=

# xAI / Grok API Key (chat)
# Get yours at: https://console.x.ai
XAI_API_KEY=

# Groq API Key (chat)
# Get yours at: https://console.groq.com/keys
GROQ_API_KEY=

# OpenRouter API Key (chat, images)
# Get yours at: https://openrouter.ai/keys
OPENROUTER_API_KEY=

# fal.ai Key (audio, speech, transcription)
# Get yours at: https://fal.ai/dashboard/keys
FAL_KEY=

# ElevenLabs API Key (realtime voice)
# Get yours at: https://elevenlabs.io/app/settings/api-keys
ELEVENLABS_API_KEY=xi-...
ELEVENLABS_API_KEY=

# ElevenLabs Agent ID (for realtime voice)
# ElevenLabs Agent ID (realtime voice)
# Create an agent at: https://elevenlabs.io/app/conversational-ai
ELEVENLABS_AGENT_ID=...
ELEVENLABS_AGENT_ID=
101 changes: 98 additions & 3 deletions examples/ts-react-chat/src/lib/audio-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
* and audio generation flows.
*/

export type SpeechProviderId = 'openai' | 'gemini' | 'fal' | 'grok'
export type SpeechProviderId =
| 'openai'
| 'gemini'
| 'fal'
| 'grok'
| 'elevenlabs'

export interface SpeechProviderConfig {
id: SpeechProviderId
Expand Down Expand Up @@ -68,9 +73,21 @@ export const SPEECH_PROVIDERS: ReadonlyArray<SpeechProviderConfig> = [
],
placeholder: 'Enter text for Grok speech…',
},
{
id: 'elevenlabs',
label: 'ElevenLabs',
model: 'eleven_multilingual_v2',
voices: [
{ id: '21m00Tcm4TlvDq8ikWAM', label: 'Rachel' },
{ id: 'AZnzlk1XvdvUeBnXmlld', label: 'Domi' },
{ id: 'EXAVITQu4vr4xnSDxMaL', label: 'Bella' },
{ id: 'pNInz6obpgDQGcFmaJgB', label: 'Adam' },
],
placeholder: 'Enter text to synthesize with ElevenLabs…',
},
]

export type TranscriptionProviderId = 'openai' | 'fal' | 'grok'
export type TranscriptionProviderId = 'openai' | 'fal' | 'grok' | 'elevenlabs'

export interface TranscriptionProviderConfig {
id: TranscriptionProviderId
Expand Down Expand Up @@ -99,9 +116,21 @@ export const TRANSCRIPTION_PROVIDERS: ReadonlyArray<TranscriptionProviderConfig>
model: 'grok-stt',
description: 'xAI speech-to-text with word-level timestamps.',
},
{
id: 'elevenlabs',
label: 'ElevenLabs Scribe',
model: 'scribe_v1',
description:
'ElevenLabs Scribe with diarization, keyterm biasing, and PII redaction.',
},
]

export type AudioProviderId = 'gemini-lyria' | 'fal-audio' | 'fal-sfx'
export type AudioProviderId =
| 'gemini-lyria'
| 'fal-audio'
| 'fal-sfx'
| 'elevenlabs-music'
| 'elevenlabs-sfx'

export interface AudioProviderConfig {
id: AudioProviderId
Expand Down Expand Up @@ -244,4 +273,70 @@ export const AUDIO_PROVIDERS: ReadonlyArray<AudioProviderConfig> = [
},
],
},
{
id: 'elevenlabs-music',
label: 'ElevenLabs Music',
model: 'music_v1',
models: [{ id: 'music_v1', label: 'Music v1' }],
description:
'ElevenLabs Music β€” free-form prompts or structured composition plans.',
placeholder: 'An upbeat synthwave track with driving drums and arpeggios',
defaultDuration: 15,
samplePrompts: [
{
label: 'Synthwave drive',
prompt:
'An upbeat synthwave track with driving drums, warm analog pads, and glittering arpeggios.',
},
{
label: 'Cinematic reveal',
prompt:
'A cinematic reveal score with soaring strings, low brass stabs, and a sudden timpani hit.',
},
{
label: 'Acoustic campfire',
prompt:
'A gentle acoustic campfire tune: fingerpicked guitar, soft harmonica, and distant crickets.',
},
{
label: 'Angry kazoo orchestra',
prompt:
'A furious kazoo orchestra performing an operatic aria about overdue library books.',
},
],
},
{
id: 'elevenlabs-sfx',
label: 'ElevenLabs SFX',
model: 'eleven_text_to_sound_v2',
models: [
{ id: 'eleven_text_to_sound_v2', label: 'Text-to-Sound v2' },
{ id: 'eleven_text_to_sound_v1', label: 'Text-to-Sound v1' },
],
description:
'ElevenLabs text-to-sound for short effects, 0.5–30 seconds per clip.',
placeholder: 'A whoosh followed by a deep bass impact',
defaultDuration: 5,
samplePrompts: [
{
label: 'Trailer whoosh',
prompt: 'A cinematic whoosh followed by a deep sub-bass impact.',
},
{
label: 'Sword unsheathe',
prompt:
'The crisp metallic ring of a sword being drawn from a leather scabbard.',
},
{
label: 'UI confirmation',
prompt:
'A short, satisfying UI confirmation tone with a subtle sparkle tail.',
},
{
label: 'Anxious toaster',
prompt:
'A small kitchen toaster having an anxiety attack: frantic clicks, steam, and a plaintive ding.',
},
],
},
]
12 changes: 12 additions & 0 deletions examples/ts-react-chat/src/lib/server-audio-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import { openaiSpeech, openaiTranscription } from '@tanstack/ai-openai'
import { geminiAudio, geminiSpeech } from '@tanstack/ai-gemini'
import { falAudio, falSpeech, falTranscription } from '@tanstack/ai-fal'
import {
elevenlabsAudio,
elevenlabsSpeech,
elevenlabsTranscription,
} from '@tanstack/ai-elevenlabs'
import { grokSpeech, grokTranscription } from '@tanstack/ai-grok'
import type {
AnyAudioAdapter,
Expand Down Expand Up @@ -48,6 +53,8 @@ export function buildSpeechAdapter(provider: SpeechProviderId): AnyTTSAdapter {
return falSpeech(config.model)
case 'grok':
return grokSpeech(config.model as 'grok-tts')
case 'elevenlabs':
return elevenlabsSpeech(config.model)
}
}

Expand All @@ -62,6 +69,8 @@ export function buildTranscriptionAdapter(
return falTranscription(config.model)
case 'grok':
return grokTranscription(config.model as 'grok-stt')
case 'elevenlabs':
return elevenlabsTranscription(config.model)
}
}

Expand All @@ -79,6 +88,9 @@ export function buildAudioAdapter(
case 'fal-audio':
case 'fal-sfx':
return falAudio(model)
case 'elevenlabs-music':
case 'elevenlabs-sfx':
return elevenlabsAudio(model)
}
}

Expand Down
12 changes: 9 additions & 3 deletions examples/ts-react-chat/src/lib/server-fns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,21 @@ function rethrowAudioAdapterError(err: unknown): never {
}

const SPEECH_PROVIDER_SCHEMA = z
.enum(['openai', 'gemini', 'fal', 'grok'])
.enum(['openai', 'gemini', 'fal', 'grok', 'elevenlabs'])
.optional()

const TRANSCRIPTION_PROVIDER_SCHEMA = z
.enum(['openai', 'fal', 'grok'])
.enum(['openai', 'fal', 'grok', 'elevenlabs'])
.optional()

const AUDIO_PROVIDER_SCHEMA = z
.enum(['gemini-lyria', 'fal-audio', 'fal-sfx'])
.enum([
'gemini-lyria',
'fal-audio',
'fal-sfx',
'elevenlabs-music',
'elevenlabs-sfx',
])
.optional()

// =============================================================================
Expand Down
18 changes: 7 additions & 11 deletions examples/ts-react-chat/src/lib/use-realtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { realtimeClientTools } from '@/lib/realtime-tools'
type Provider = 'openai' | 'elevenlabs' | 'grok'

const getRealtimeTokenFn = createServerFn({ method: 'POST' })
.inputValidator((data: { provider: Provider; agentId?: string }) => {
.inputValidator((data: { provider: Provider; language?: string }) => {
if (!data.provider) throw new Error('Provider is required')
return data
})
Expand All @@ -26,14 +26,10 @@ const getRealtimeTokenFn = createServerFn({ method: 'POST' })
}

if (data.provider === 'elevenlabs') {
const agentId = data.agentId || process.env.ELEVENLABS_AGENT_ID
if (!agentId) {
throw new Error(
'ElevenLabs agent ID is required. Set ELEVENLABS_AGENT_ID or pass agentId in request body.',
)
}
return realtimeToken({
adapter: elevenlabsRealtimeToken({ agentId }),
adapter: elevenlabsRealtimeToken({
...(data.language ? { overrides: { language: data.language } } : {}),
}),
})
}

Expand All @@ -59,15 +55,15 @@ function adapterForProvider(provider: Provider) {

export function useRealtime({
provider,
agentId,
language,
voice,
outputModalities,
temperature,
maxOutputTokens,
semanticEagerness,
}: {
provider: Provider
agentId: string
language?: string
voice?: string
outputModalities?: Array<'audio' | 'text'>
temperature?: number
Expand All @@ -79,7 +75,7 @@ export function useRealtime({
getRealtimeTokenFn({
data: {
provider,
...(provider === 'elevenlabs' && agentId ? { agentId } : {}),
...(provider === 'elevenlabs' && language ? { language } : {}),
},
}),
adapter: adapterForProvider(provider),
Expand Down
8 changes: 7 additions & 1 deletion examples/ts-react-chat/src/routes/api.generate.audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import {
} from '../lib/server-audio-adapters'

const AUDIO_PROVIDER_SCHEMA = z
.enum(['gemini-lyria', 'fal-audio', 'fal-sfx'])
.enum([
'gemini-lyria',
'fal-audio',
'fal-sfx',
'elevenlabs-music',
'elevenlabs-sfx',
])
.optional()

const AUDIO_BODY_SCHEMA = z.object({
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-react-chat/src/routes/api.generate.speech.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../lib/server-audio-adapters'

const SPEECH_PROVIDER_SCHEMA = z
.enum(['openai', 'gemini', 'fal', 'grok'])
.enum(['openai', 'gemini', 'fal', 'grok', 'elevenlabs'])
.optional()

const SPEECH_BODY_SCHEMA = z.object({
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-react-chat/src/routes/api.transcribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../lib/server-audio-adapters'

const TRANSCRIPTION_PROVIDER_SCHEMA = z
.enum(['openai', 'fal', 'grok'])
.enum(['openai', 'fal', 'grok', 'elevenlabs'])
.optional()

const TRANSCRIBE_BODY_SCHEMA = z.object({
Expand Down
Loading
Loading