This guide helps you migrate from other Text-to-Speech (TTS) providers to ElevenLabs, or upgrade between ElevenLabs API versions and models.
Table of Contents#
- Migration Overview
- Migrating from Other TTS Providers
- ElevenLabs Model Migrations
- Voice Migration Strategies
- API Version Migration
- Data Migration
- Testing and Validation
- Rollback Procedures
Migration Overview#
Pre-Migration Checklist#
Before starting any migration:
- Audit current TTS usage (volume, features, voices used)
- Identify all integration points in your codebase
- Document current voice configurations and mappings
- Set up ElevenLabs account with appropriate subscription tier
- Create test environment for validation
- Plan rollback strategy
- Estimate quota requirements
- Communicate migration timeline to stakeholders
Migration Phases#
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ Migration Process Overview │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: Assessment Phase 2: Setup Phase 3: Implementation │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ • Audit usage │ │ • Create account │ │ • Update code │ │
│ │ • Map features │────▶│ • Select voices │───▶│ • Voice mapping │ │
│ │ • Document APIs │ │ • Configure keys │ │ • Error handling │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ Phase 6: Cleanup Phase 5: Cutover Phase 4: Testing │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ • Remove old │ │ • Switch traffic │ │ • A/B testing │ │
│ │ • Update docs │◀────│ • Monitor closely│◀───│ • Quality check │ │
│ │ • Archive config │ │ • Verify metrics │ │ • Load testing │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Migrating from Other TTS Providers#
From Amazon Polly#
Feature Mapping#
| Amazon Polly Feature | ElevenLabs Equivalent | Notes |
|---|---|---|
| Standard voices | Multilingual v2 | Higher quality in ElevenLabs |
| Neural voices | Turbo v2.5 | Similar latency, better quality |
| SSML support | Native text + audio tags | Different syntax |
| Lexicons | Pronunciation dictionaries | Via API |
| Speech marks | Timestamps API | Different format |
| Long Audio Synthesis | Projects API | For content > 5000 chars |
Voice Mapping#
typescript
// Amazon Polly to ElevenLabs voice mapping
const POLLY_TO_ELEVENLABS_VOICES: Record<string, string> = {
// US English Female
Joanna: 'Rachel', // Professional, clear
Kendra: 'Domi', // Warm, friendly
Kimberly: 'Bella', // Conversational
Salli: 'Elli', // Young adult
Ivy: 'Charlotte', // Child-like → Young professional
// US English Male
Matthew: 'Adam', // Deep, authoritative
Joey: 'Josh', // Casual, friendly
Justin: 'Sam', // Young adult
// British English
Amy: 'Dorothy', // British female
Emma: 'Emily', // British female
Brian: 'Clyde', // British male
// Other languages - use ElevenLabs auto-detect
Celine: 'auto', // French → Multilingual v2
Hans: 'auto', // German → Multilingual v2
Mizuki: 'auto', // Japanese → Multilingual v2
};
// Migration adapter
class PollyToElevenLabsAdapter {
private elevenLabs: ElevenLabsClient;
constructor(apiKey: string) {
this.elevenLabs = new ElevenLabsClient({ apiKey });
}
async synthesizeSpeech(params: PollyParams): Promise<Buffer> {
const voiceId = this.mapVoice(params.VoiceId);
const text = this.convertSSML(params.Text, params.TextType);
return this.elevenLabs.textToSpeech({
voiceId,
text,
modelId: this.selectModel(params),
voiceSettings: this.mapVoiceSettings(params),
});
}
private mapVoice(pollyVoice: string): string {
const mapped = POLLY_TO_ELEVENLABS_VOICES[pollyVoice];
if (!mapped || mapped === 'auto') {
// Use a default multilingual voice
return 'pNInz6obpgDQGcFmaJgB'; // Adam
}
return this.getVoiceIdByName(mapped);
}
private convertSSML(text: string, textType: string): string {
if (textType !== 'ssml') return text;
// Convert Polly SSML to ElevenLabs format
let converted = text
// Remove SSML wrapper
.replace(/<speak>/g, '')
.replace(/<\/speak>/g, '')
// Convert breaks
.replace(/<break time="(\d+)ms"\/>/g, '<break time="$1ms" />')
.replace(/<break strength="(\w+)"\/>/g, (_, strength) => {
const msMap: Record<string, string> = {
none: '0ms',
'x-weak': '100ms',
weak: '200ms',
medium: '400ms',
strong: '600ms',
'x-strong': '1000ms',
};
return `<break time="${msMap[strength] || '400ms'}" />`;
})
// Convert prosody
.replace(/<prosody rate="(\w+)">/g, '')
.replace(/<\/prosody>/g, '')
// Convert emphasis
.replace(/<emphasis level="(\w+)">/g, '')
.replace(/<\/emphasis>/g, '')
// Remove unsupported tags
.replace(/<amazon:effect[^>]*>/g, '')
.replace(/<\/amazon:effect>/g, '')
.replace(/<phoneme[^>]*>[^<]*<\/phoneme>/g, (match) => {
// Extract the word from phoneme tag
const word = match.match(/>([^<]*)</)?.[1] || '';
return word;
});
return converted.trim();
}
private selectModel(params: PollyParams): string {
// Neural voices → Turbo for low latency
if (params.Engine === 'neural') {
return 'eleven_turbo_v2_5';
}
// Standard voices → Multilingual for quality
return 'eleven_multilingual_v2';
}
private mapVoiceSettings(params: PollyParams): VoiceSettings {
return {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
};
}
}
SSML Conversion Reference#
typescript
// Polly SSML to ElevenLabs conversion
const SSML_CONVERSIONS = {
// Pauses
polly: '<break time="500ms"/>',
elevenlabs: '<break time="500ms" />',
// Emphasis (not directly supported, use stability)
polly: '<emphasis level="strong">important</emphasis>',
elevenlabs: 'important', // Adjust via voice settings
// Prosody (not directly supported)
polly: '<prosody rate="slow">slowly</prosody>',
elevenlabs: 'slowly...', // Use ellipsis for pacing
// Whisper (Polly-specific, no equivalent)
polly: '<amazon:effect name="whispered">secret</amazon:effect>',
elevenlabs: 'secret', // No direct equivalent
// Phonemes
polly: '<phoneme alphabet="ipa" ph="pɪˈkɑːn">pecan</phoneme>',
elevenlabs: 'pecan', // Use pronunciation dictionary instead
};
From Google Cloud TTS#
Feature Mapping#
| Google Cloud TTS Feature | ElevenLabs Equivalent | Notes |
|---|---|---|
| Standard voices | Multilingual v2 | Better quality |
| WaveNet voices | Multilingual v2 | Comparable quality |
| Neural2 voices | Turbo v2.5 | Similar performance |
| Studio voices | Voice Design | Create custom voices |
| SSML support | Audio tags | Different syntax |
| Audio profiles | Output format settings | Similar control |
| Custom Voice | Voice Cloning | ElevenLabs more flexible |
| Long Audio | Projects API | For extended content |
Voice Mapping#
typescript
const GOOGLE_TO_ELEVENLABS_VOICES: Record<string, string> = {
// US English Female
'en-US-Standard-C': 'Rachel',
'en-US-Standard-E': 'Domi',
'en-US-Standard-F': 'Bella',
'en-US-Standard-G': 'Elli',
'en-US-Standard-H': 'Charlotte',
'en-US-Wavenet-C': 'Rachel',
'en-US-Wavenet-E': 'Domi',
'en-US-Wavenet-F': 'Bella',
'en-US-Neural2-C': 'Rachel',
'en-US-Neural2-E': 'Domi',
'en-US-Neural2-F': 'Bella',
// US English Male
'en-US-Standard-A': 'Adam',
'en-US-Standard-B': 'Josh',
'en-US-Standard-D': 'Arnold',
'en-US-Standard-I': 'Sam',
'en-US-Standard-J': 'Antoni',
'en-US-Wavenet-A': 'Adam',
'en-US-Wavenet-B': 'Josh',
'en-US-Wavenet-D': 'Arnold',
'en-US-Neural2-A': 'Adam',
'en-US-Neural2-D': 'Arnold',
// British English
'en-GB-Standard-A': 'Dorothy',
'en-GB-Standard-B': 'Clyde',
'en-GB-Wavenet-A': 'Dorothy',
'en-GB-Wavenet-B': 'Clyde',
'en-GB-Neural2-A': 'Dorothy',
'en-GB-Neural2-B': 'Clyde',
};
class GoogleTTSToElevenLabsAdapter {
private elevenLabs: ElevenLabsClient;
constructor(apiKey: string) {
this.elevenLabs = new ElevenLabsClient({ apiKey });
}
async synthesizeSpeech(request: GoogleTTSRequest): Promise<Buffer> {
const voiceId = this.mapVoice(request.voice);
const text = this.convertSSML(request.input);
const outputFormat = this.mapAudioConfig(request.audioConfig);
return this.elevenLabs.textToSpeech({
voiceId,
text,
modelId: this.selectModel(request.voice),
outputFormat,
voiceSettings: this.mapVoiceSettings(request),
});
}
private mapVoice(voice: GoogleVoice): string {
const key = `${voice.languageCode}-${voice.name}`;
const mapped = GOOGLE_TO_ELEVENLABS_VOICES[key];
if (mapped) {
return this.getVoiceIdByName(mapped);
}
// Fallback: use language to select appropriate voice
return this.selectVoiceByLanguage(voice.languageCode, voice.ssmlGender);
}
private convertSSML(input: GoogleTTSInput): string {
if (input.text) return input.text;
if (!input.ssml) return '';
let text = input.ssml
// Remove SSML wrapper
.replace(/<speak>/g, '')
.replace(/<\/speak>/g, '')
// Convert breaks
.replace(/<break time="(\d+(?:\.\d+)?)(s|ms)"\/>/g, (_, time, unit) => {
const ms = unit === 's' ? parseFloat(time) * 1000 : parseFloat(time);
return `<break time="${Math.round(ms)}ms" />`;
})
// Convert say-as
.replace(/<say-as interpret-as="([^"]+)"[^>]*>([^<]*)<\/say-as>/g, '$2')
// Convert sub (substitution)
.replace(/<sub alias="([^"]+)">([^<]*)<\/sub>/g, '$1')
// Remove audio tags (not supported)
.replace(/<audio[^>]*>.*?<\/audio>/gs, '')
// Remove par/seq (timing)
.replace(/<par>/g, '')
.replace(/<\/par>/g, '')
.replace(/<seq>/g, '')
.replace(/<\/seq>/g, '')
// Remove mark tags
.replace(/<mark name="[^"]*"\/>/g, '');
return text.trim();
}
private mapAudioConfig(config: GoogleAudioConfig): string {
const encodingMap: Record<string, string> = {
MP3: 'mp3_44100_128',
MP3_64_KBPS: 'mp3_44100_64',
OGG_OPUS: 'pcm_44100', // Convert later
LINEAR16: 'pcm_16000',
MULAW: 'ulaw_8000',
};
return encodingMap[config.audioEncoding] || 'mp3_44100_128';
}
private selectModel(voice: GoogleVoice): string {
// Neural2 and Studio voices → Turbo for speed
if (voice.name?.includes('Neural2') || voice.name?.includes('Studio')) {
return 'eleven_turbo_v2_5';
}
// WaveNet and Standard → Multilingual for quality
return 'eleven_multilingual_v2';
}
}
From Azure Cognitive Services#
Feature Mapping#
| Azure TTS Feature | ElevenLabs Equivalent | Notes |
|---|---|---|
| Standard voices | Multilingual v2 | Better quality |
| Neural voices | Turbo v2.5 | Similar latency |
| Custom Neural Voice | Voice Cloning | ElevenLabs easier |
| SSML support | Audio tags | Different syntax |
| Viseme (lip sync) | Not directly supported | Use external tools |
| Word boundary events | Timestamps API | Different format |
| Audio Content Creation | Voice Design | Similar capability |
| Speaking styles | Voice settings | Map to stability/style |
Voice Mapping#
typescript
const AZURE_TO_ELEVENLABS_VOICES: Record<string, string> = {
// US English Female Neural
'en-US-JennyNeural': 'Rachel',
'en-US-AriaNeural': 'Domi',
'en-US-SaraNeural': 'Bella',
'en-US-JaneNeural': 'Charlotte',
'en-US-NancyNeural': 'Elli',
// US English Male Neural
'en-US-GuyNeural': 'Adam',
'en-US-DavisNeural': 'Josh',
'en-US-JasonNeural': 'Arnold',
'en-US-TonyNeural': 'Antoni',
// British English Neural
'en-GB-SoniaNeural': 'Dorothy',
'en-GB-RyanNeural': 'Clyde',
'en-GB-LibbyNeural': 'Emily',
// Multilingual voices
'en-US-JennyMultilingualNeural': 'Rachel', // + multilingual model
'en-US-RyanMultilingualNeural': 'Adam', // + multilingual model
};
// Azure speaking styles to ElevenLabs settings mapping
const STYLE_TO_SETTINGS: Record<string, VoiceSettings> = {
cheerful: { stability: 0.4, similarity_boost: 0.8, style: 0.3 },
sad: { stability: 0.7, similarity_boost: 0.6, style: 0.5 },
angry: { stability: 0.3, similarity_boost: 0.9, style: 0.8 },
fearful: { stability: 0.6, similarity_boost: 0.5, style: 0.4 },
friendly: { stability: 0.5, similarity_boost: 0.75, style: 0.2 },
newscast: { stability: 0.8, similarity_boost: 0.7, style: 0 },
customerservice: { stability: 0.6, similarity_boost: 0.7, style: 0.1 },
shouting: { stability: 0.3, similarity_boost: 0.9, style: 0.9 },
whispering: { stability: 0.9, similarity_boost: 0.5, style: 0 },
default: { stability: 0.5, similarity_boost: 0.75, style: 0 },
};
class AzureTTSToElevenLabsAdapter {
private elevenLabs: ElevenLabsClient;
constructor(apiKey: string) {
this.elevenLabs = new ElevenLabsClient({ apiKey });
}
async synthesizeSpeech(ssml: string): Promise<Buffer> {
const parsed = this.parseAzureSSML(ssml);
return this.elevenLabs.textToSpeech({
voiceId: this.mapVoice(parsed.voice),
text: parsed.text,
modelId: this.selectModel(parsed),
voiceSettings: this.mapStyle(parsed.style),
});
}
private parseAzureSSML(ssml: string): ParsedAzureSSML {
const result: ParsedAzureSSML = {
voice: '',
text: '',
style: 'default',
rate: 1.0,
pitch: 0,
};
// Extract voice name
const voiceMatch = ssml.match(/<voice name="([^"]+)">/);
if (voiceMatch) {
result.voice = voiceMatch[1];
}
// Extract style
const styleMatch = ssml.match(/<mstts:express-as style="([^"]+)"[^>]*>/);
if (styleMatch) {
result.style = styleMatch[1];
}
// Extract text (remove all tags)
result.text = ssml
.replace(/<speak[^>]*>/g, '')
.replace(/<\/speak>/g, '')
.replace(/<voice[^>]*>/g, '')
.replace(/<\/voice>/g, '')
.replace(/<mstts:[^>]*>/g, '')
.replace(/<\/mstts:[^>]*>/g, '')
.replace(/<prosody[^>]*>/g, '')
.replace(/<\/prosody>/g, '')
.replace(/<break[^>]*\/>/g, (match) => {
const timeMatch = match.match(/time="(\d+)ms"/);
if (timeMatch) {
return `<break time="${timeMatch[1]}ms" />`;
}
return '';
})
.replace(/<[^>]+>/g, '')
.trim();
return result;
}
private mapVoice(azureVoice: string): string {
const mapped = AZURE_TO_ELEVENLABS_VOICES[azureVoice];
if (mapped) {
return this.getVoiceIdByName(mapped);
}
// Parse language and gender from Azure voice name
const match = azureVoice.match(/^(\w{2}-\w{2})-(\w+)Neural$/);
if (match) {
const [, locale, name] = match;
return this.selectVoiceByLocale(locale, name);
}
// Default fallback
return 'pNInz6obpgDQGcFmaJgB'; // Adam
}
private mapStyle(style: string): VoiceSettings {
return STYLE_TO_SETTINGS[style] || STYLE_TO_SETTINGS.default;
}
private selectModel(parsed: ParsedAzureSSML): string {
// Multilingual voices → Multilingual model
if (parsed.voice.includes('Multilingual')) {
return 'eleven_multilingual_v2';
}
// Default to Turbo for speed
return 'eleven_turbo_v2_5';
}
}
From OpenAI TTS#
Feature Mapping#
| OpenAI TTS Feature | ElevenLabs Equivalent | Notes |
|---|---|---|
| tts-1 model | Turbo v2.5 | Similar speed |
| tts-1-hd model | Multilingual v2 | Both high quality |
| 6 voices | 100+ voices | Much more variety |
| Streaming | Streaming API | Similar capability |
| No voice cloning | Voice Cloning | ElevenLabs advantage |
| No SSML | Audio tags | ElevenLabs advantage |
Voice Mapping#
typescript
const OPENAI_TO_ELEVENLABS_VOICES: Record<string, string> = {
alloy: 'Rachel', // Neutral, balanced
echo: 'Adam', // Deep, resonant
fable: 'Antoni', // Warm, engaging
onyx: 'Arnold', // Deep, authoritative
nova: 'Bella', // Young, energetic
shimmer: 'Domi', // Clear, expressive
};
class OpenAITTSToElevenLabsAdapter {
private elevenLabs: ElevenLabsClient;
constructor(apiKey: string) {
this.elevenLabs = new ElevenLabsClient({ apiKey });
}
async createSpeech(params: OpenAITTSParams): Promise<Buffer> {
const voiceId = this.mapVoice(params.voice);
return this.elevenLabs.textToSpeech({
voiceId,
text: params.input,
modelId: this.selectModel(params.model),
outputFormat: this.mapFormat(params.response_format),
voiceSettings: {
stability: params.speed ? this.speedToStability(params.speed) : 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
});
}
async createSpeechStream(
params: OpenAITTSParams
): AsyncGenerator<Uint8Array> {
const voiceId = this.mapVoice(params.voice);
return this.elevenLabs.textToSpeechStream({
voiceId,
text: params.input,
modelId: this.selectModel(params.model),
outputFormat: this.mapFormat(params.response_format),
});
}
private mapVoice(openaiVoice: string): string {
const mapped = OPENAI_TO_ELEVENLABS_VOICES[openaiVoice];
return mapped ? this.getVoiceIdByName(mapped) : 'pNInz6obpgDQGcFmaJgB'; // Adam as default
}
private selectModel(openaiModel: string): string {
return openaiModel === 'tts-1'
? 'eleven_turbo_v2_5'
: 'eleven_multilingual_v2';
}
private mapFormat(format?: string): string {
const formatMap: Record<string, string> = {
mp3: 'mp3_44100_128',
opus: 'pcm_44100', // Convert after
aac: 'mp3_44100_128', // Use MP3
flac: 'pcm_44100', // Convert after
wav: 'pcm_44100',
pcm: 'pcm_16000',
};
return formatMap[format || 'mp3'] || 'mp3_44100_128';
}
private speedToStability(speed: number): number {
// OpenAI speed: 0.25 to 4.0, default 1.0
// Map to stability: higher speed = lower stability
// speed 0.25 → stability 0.8
// speed 1.0 → stability 0.5
// speed 4.0 → stability 0.2
return Math.max(0.1, Math.min(1.0, 1.1 - speed * 0.2));
}
}
OpenAI to ElevenLabs Code Migration#
typescript
// Before (OpenAI)
import OpenAI from 'openai';
const openai = new OpenAI();
const mp3 = await openai.audio.speech.create({
model: 'tts-1',
voice: 'alloy',
input: 'Hello, world!',
});
const buffer = Buffer.from(await mp3.arrayBuffer());
// After (ElevenLabs)
import { ElevenLabsClient } from '@oshun/elevenlabs-client';
const elevenlabs = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
const buffer = await elevenlabs.textToSpeech({
voiceId: 'Rachel', // or voice ID
text: 'Hello, world!',
modelId: 'eleven_turbo_v2_5',
});
ElevenLabs Model Migrations#
Upgrading to Multilingual v2#
When migrating from older models to Multilingual v2:
Compatibility Matrix#
| Previous Model | Migration Path | Breaking Changes |
|---|---|---|
| eleven_monolingual | Direct replacement | None |
| eleven_multilingual | Direct replacement | Minor quality |
| eleven_turbo_v2 | Quality vs. speed | Latency increase |
Migration Steps#
typescript
// 1. Update model ID in configuration
const CONFIG_MIGRATION = {
old: {
modelId: 'eleven_monolingual_v1',
},
new: {
modelId: 'eleven_multilingual_v2',
},
};
// 2. Adjust voice settings for new model
const VOICE_SETTINGS_MIGRATION = {
old: {
stability: 0.5,
similarity_boost: 0.75,
},
new: {
stability: 0.5,
similarity_boost: 0.75,
style: 0, // New parameter
use_speaker_boost: true, // New parameter
},
};
// 3. Test with existing voices
async function testMigration(
voiceId: string,
testTexts: string[]
): Promise<MigrationTestResult> {
const results: MigrationTestResult = {
voiceId,
tests: [],
};
for (const text of testTexts) {
const oldAudio = await generateWithModel(
voiceId,
text,
'eleven_monolingual_v1'
);
const newAudio = await generateWithModel(
voiceId,
text,
'eleven_multilingual_v2'
);
results.tests.push({
text,
oldLatency: oldAudio.latency,
newLatency: newAudio.latency,
oldSize: oldAudio.size,
newSize: newAudio.size,
});
}
return results;
}
Upgrading to Turbo v2.5#
For latency-sensitive applications migrating to Turbo v2.5:
Performance Comparison#
| Metric | Turbo v2 | Turbo v2.5 | Change |
|---|---|---|---|
| Average latency | 300ms | 250ms | -17% |
| First byte | 150ms | 100ms | -33% |
| Quality (MOS) | 4.2 | 4.4 | +5% |
| Language support | 5 | 32 | +540% |
Migration Code#
typescript
// Turbo v2 to v2.5 migration
const TURBO_MIGRATION = {
modelIdUpdate: {
old: 'eleven_turbo_v2',
new: 'eleven_turbo_v2_5',
},
// Voice settings remain compatible
voiceSettings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
};
// Gradual rollout implementation
class TurboMigrationController {
private rolloutPercentage: number = 0;
setRolloutPercentage(percentage: number) {
this.rolloutPercentage = Math.max(0, Math.min(100, percentage));
}
selectModel(requestId: string): string {
// Deterministic selection based on request ID
const hash = this.hashCode(requestId);
const bucket = Math.abs(hash % 100);
if (bucket < this.rolloutPercentage) {
return 'eleven_turbo_v2_5';
}
return 'eleven_turbo_v2';
}
private hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return hash;
}
}
Voice Migration Strategies#
Preserving Voice Consistency#
When migrating, maintaining voice consistency is critical:
typescript
// Voice consistency migration
class VoiceMigrationManager {
private voiceMapping: Map<string, VoiceMapping>;
async createMapping(
oldProvider: string,
oldVoiceId: string
): Promise<VoiceMapping> {
// 1. Get characteristics of old voice
const characteristics = await this.analyzeVoiceCharacteristics(
oldProvider,
oldVoiceId
);
// 2. Find best matching ElevenLabs voice
const candidates = await this.findMatchingVoices(characteristics);
// 3. Allow human review
const mapping: VoiceMapping = {
oldProvider,
oldVoiceId,
newVoiceId: candidates[0].voiceId,
confidence: candidates[0].matchScore,
alternatives: candidates.slice(1, 4),
characteristics,
needsReview: candidates[0].matchScore < 0.8,
};
this.voiceMapping.set(`${oldProvider}:${oldVoiceId}`, mapping);
return mapping;
}
private async analyzeVoiceCharacteristics(
provider: string,
voiceId: string
): Promise<VoiceCharacteristics> {
// Generate sample audio with known text
const sampleText = 'The quick brown fox jumps over the lazy dog.';
const audio = await this.generateSample(provider, voiceId, sampleText);
// Analyze audio characteristics
return {
pitch: this.analyzePitch(audio),
speed: this.analyzeSpeed(audio),
tone: this.analyzeTone(audio),
gender: this.detectGender(audio),
age: this.estimateAge(audio),
accent: this.detectAccent(audio),
};
}
private async findMatchingVoices(
characteristics: VoiceCharacteristics
): Promise<VoiceCandidate[]> {
const allVoices = await this.elevenLabs.getVoices();
const candidates = allVoices.map((voice) => ({
voiceId: voice.voice_id,
name: voice.name,
matchScore: this.calculateMatchScore(voice, characteristics),
labels: voice.labels,
}));
return candidates.sort((a, b) => b.matchScore - a.matchScore).slice(0, 10);
}
private calculateMatchScore(
voice: Voice,
target: VoiceCharacteristics
): number {
let score = 1.0;
// Gender match (required)
if (voice.labels?.gender !== target.gender) {
score *= 0.3;
}
// Age match
const ageMatch = this.matchAge(voice.labels?.age, target.age);
score *= ageMatch;
// Accent match
if (voice.labels?.accent) {
const accentMatch = this.matchAccent(voice.labels.accent, target.accent);
score *= accentMatch;
}
return score;
}
}
Voice Cloning for Custom Voices#
If you have custom voices from another provider:
typescript
// Migrate custom voice to ElevenLabs via cloning
class CustomVoiceMigrator {
async migrateCustomVoice(
sourceProvider: string,
sourceVoiceId: string,
voiceName: string
): Promise<ClonedVoice> {
// 1. Generate diverse samples from source
const samples = await this.generateSourceSamples(
sourceProvider,
sourceVoiceId
);
// 2. Clone voice on ElevenLabs
const clonedVoice = await this.elevenLabs.cloneVoice({
name: voiceName,
description: `Migrated from ${sourceProvider}`,
files: samples,
labels: {
migrated_from: sourceProvider,
original_id: sourceVoiceId,
migration_date: new Date().toISOString(),
},
});
// 3. Validate clone quality
const validation = await this.validateClone(
sourceProvider,
sourceVoiceId,
clonedVoice.voice_id
);
if (!validation.acceptable) {
// Try with different samples or Professional Voice Cloning
return this.attemptProfessionalCloning(
sourceProvider,
sourceVoiceId,
voiceName
);
}
return clonedVoice;
}
private async generateSourceSamples(
provider: string,
voiceId: string
): Promise<Buffer[]> {
// Generate samples with varied content
const sampleTexts = [
// Declarative
'The weather today is sunny with a slight breeze from the west.',
// Interrogative
'What time does the meeting start tomorrow morning?',
// Exclamatory
'What an incredible achievement! Congratulations to the entire team!',
// Emotional range
'I understand your concerns, and I want to help find a solution.',
"This is absolutely fantastic news! I couldn't be happier!",
// Technical content
'The system processes approximately one thousand requests per second.',
];
const samples: Buffer[] = [];
for (const text of sampleTexts) {
const audio = await this.generateFromProvider(provider, voiceId, text);
samples.push(audio);
}
return samples;
}
}
API Version Migration#
Handling API Changes#
typescript
// API version compatibility layer
class ElevenLabsVersionAdapter {
private apiVersion: string;
constructor(targetVersion: string = 'v1') {
this.apiVersion = targetVersion;
}
// Normalize request format across versions
normalizeRequest(request: any): NormalizedRequest {
// Handle deprecated parameters
if (request.voice_settings?.speaking_rate !== undefined) {
// speaking_rate was deprecated, map to stability
console.warn('voice_settings.speaking_rate is deprecated');
request.voice_settings.stability = this.mapSpeakingRateToStability(
request.voice_settings.speaking_rate
);
delete request.voice_settings.speaking_rate;
}
// Ensure required parameters
if (!request.model_id) {
request.model_id = 'eleven_multilingual_v2';
}
// Add new required parameters
if (!request.voice_settings?.use_speaker_boost) {
request.voice_settings = {
...request.voice_settings,
use_speaker_boost: true,
};
}
return request as NormalizedRequest;
}
// Normalize response format
normalizeResponse(response: any): NormalizedResponse {
// Handle response format changes
return {
audio: response.audio || response.audio_base64,
contentType: response.content_type || 'audio/mpeg',
characterCount: response.character_count || response.characters_used,
historyItemId: response.history_item_id,
};
}
}
Deprecation Handling#
typescript
// Monitor and handle deprecated features
class DeprecationMonitor {
private deprecationWarnings: Map<string, DeprecationWarning> = new Map();
checkForDeprecations(request: any): DeprecationWarning[] {
const warnings: DeprecationWarning[] = [];
// Check deprecated models
const deprecatedModels = ['eleven_monolingual_v1', 'eleven_english_v1'];
if (deprecatedModels.includes(request.model_id)) {
warnings.push({
type: 'model',
feature: request.model_id,
message: `Model ${request.model_id} is deprecated`,
replacement: 'eleven_multilingual_v2',
removalDate: '2025-06-01',
});
}
// Check deprecated voice settings
if (request.voice_settings?.speaking_rate !== undefined) {
warnings.push({
type: 'parameter',
feature: 'voice_settings.speaking_rate',
message: 'speaking_rate parameter is deprecated',
replacement: 'Use stability parameter instead',
removalDate: '2025-03-01',
});
}
// Log warnings
warnings.forEach((w) => this.logDeprecation(w));
return warnings;
}
private logDeprecation(warning: DeprecationWarning) {
const key = `${warning.type}:${warning.feature}`;
if (!this.deprecationWarnings.has(key)) {
this.deprecationWarnings.set(key, warning);
console.warn(
`DEPRECATION WARNING: ${warning.message}. ` +
`Use ${warning.replacement} instead. ` +
`Will be removed on ${warning.removalDate}.`
);
}
}
}
Data Migration#
History Migration#
typescript
// Migrate audio history from other providers
class HistoryMigrator {
async migrateHistory(
sourceProvider: string,
sourceHistory: SourceHistoryItem[]
): Promise<MigrationResult> {
const results: MigrationResult = {
total: sourceHistory.length,
migrated: 0,
skipped: 0,
failed: 0,
items: [],
};
for (const item of sourceHistory) {
try {
// Re-generate audio with ElevenLabs
const voiceId = this.mapVoice(sourceProvider, item.voiceId);
const audio = await this.elevenLabs.textToSpeech({
voiceId,
text: item.text,
modelId: 'eleven_multilingual_v2',
});
// Store with metadata
const historyItem = await this.storeWithMetadata(audio, {
originalProvider: sourceProvider,
originalId: item.id,
originalTimestamp: item.timestamp,
originalVoiceId: item.voiceId,
text: item.text,
});
results.migrated++;
results.items.push({
sourceId: item.id,
newId: historyItem.id,
status: 'migrated',
});
} catch (error) {
results.failed++;
results.items.push({
sourceId: item.id,
status: 'failed',
error: error.message,
});
}
}
return results;
}
}
Configuration Migration#
typescript
// Migrate configuration settings
interface MigrationConfig {
sourceProvider: string;
targetDefaults: {
modelId: string;
voiceSettings: VoiceSettings;
outputFormat: string;
};
voiceMapping: Record<string, string>;
}
const createMigrationConfig = (sourceProvider: string): MigrationConfig => {
const configs: Record<string, MigrationConfig> = {
'amazon-polly': {
sourceProvider: 'amazon-polly',
targetDefaults: {
modelId: 'eleven_multilingual_v2',
voiceSettings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
outputFormat: 'mp3_44100_128',
},
voiceMapping: POLLY_TO_ELEVENLABS_VOICES,
},
'google-cloud': {
sourceProvider: 'google-cloud',
targetDefaults: {
modelId: 'eleven_multilingual_v2',
voiceSettings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
outputFormat: 'mp3_44100_128',
},
voiceMapping: GOOGLE_TO_ELEVENLABS_VOICES,
},
azure: {
sourceProvider: 'azure',
targetDefaults: {
modelId: 'eleven_multilingual_v2',
voiceSettings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
outputFormat: 'mp3_44100_128',
},
voiceMapping: AZURE_TO_ELEVENLABS_VOICES,
},
openai: {
sourceProvider: 'openai',
targetDefaults: {
modelId: 'eleven_turbo_v2_5',
voiceSettings: {
stability: 0.5,
similarity_boost: 0.75,
style: 0,
use_speaker_boost: true,
},
outputFormat: 'mp3_44100_128',
},
voiceMapping: OPENAI_TO_ELEVENLABS_VOICES,
},
};
return configs[sourceProvider] || configs['openai'];
};
Testing and Validation#
Migration Testing Framework#
typescript
// Comprehensive migration testing
class MigrationTester {
async runMigrationTests(
migrationConfig: MigrationConfig
): Promise<MigrationTestReport> {
const report: MigrationTestReport = {
timestamp: new Date().toISOString(),
sourceProvider: migrationConfig.sourceProvider,
tests: [],
};
// Test 1: Voice mapping coverage
report.tests.push(await this.testVoiceMappingCoverage(migrationConfig));
// Test 2: Audio quality comparison
report.tests.push(await this.testAudioQuality(migrationConfig));
// Test 3: Latency comparison
report.tests.push(await this.testLatency(migrationConfig));
// Test 4: Feature parity
report.tests.push(await this.testFeatureParity(migrationConfig));
// Test 5: Error handling
report.tests.push(await this.testErrorHandling(migrationConfig));
// Calculate overall score
report.overallScore = this.calculateOverallScore(report.tests);
report.recommendation = this.getRecommendation(report.overallScore);
return report;
}
private async testAudioQuality(config: MigrationConfig): Promise<TestResult> {
const testCases = [
{ text: 'Hello, world!', category: 'short' },
{
text: 'The quick brown fox jumps over the lazy dog.',
category: 'medium',
},
{
text:
'In the beginning, there was silence. Then came the voice, ' +
'clear and resonant, carrying with it the weight of meaning ' +
'and the lightness of intention.',
category: 'long',
},
];
const results: QualityTestResult[] = [];
for (const testCase of testCases) {
// Generate with both providers
const sourceAudio = await this.generateFromSource(config, testCase.text);
const targetAudio = await this.generateFromTarget(config, testCase.text);
// Compare quality metrics
const comparison = await this.compareAudioQuality(
sourceAudio,
targetAudio
);
results.push({
text: testCase.text,
category: testCase.category,
sourceMetrics: comparison.source,
targetMetrics: comparison.target,
difference: comparison.difference,
acceptable: comparison.difference.mos >= -0.5,
});
}
const passed = results.every((r) => r.acceptable);
return {
name: 'Audio Quality Comparison',
passed,
score: results.filter((r) => r.acceptable).length / results.length,
details: results,
};
}
private async testLatency(config: MigrationConfig): Promise<TestResult> {
const iterations = 10;
const sourceLatencies: number[] = [];
const targetLatencies: number[] = [];
const testText = 'This is a latency test.';
for (let i = 0; i < iterations; i++) {
// Measure source latency
const sourceStart = performance.now();
await this.generateFromSource(config, testText);
sourceLatencies.push(performance.now() - sourceStart);
// Measure target latency
const targetStart = performance.now();
await this.generateFromTarget(config, testText);
targetLatencies.push(performance.now() - targetStart);
}
const sourceAvg = sourceLatencies.reduce((a, b) => a + b) / iterations;
const targetAvg = targetLatencies.reduce((a, b) => a + b) / iterations;
const difference = ((targetAvg - sourceAvg) / sourceAvg) * 100;
// Accept up to 20% latency increase
const passed = difference <= 20;
return {
name: 'Latency Comparison',
passed,
score: passed ? 1 : Math.max(0, 1 - (difference - 20) / 100),
details: {
sourceAverageMs: sourceAvg,
targetAverageMs: targetAvg,
differencePercent: difference,
sourceSamples: sourceLatencies,
targetSamples: targetLatencies,
},
};
}
}
A/B Testing Framework#
typescript
// A/B testing for gradual migration
class MigrationABTest {
private testConfig: ABTestConfig;
private metrics: MetricsCollector;
constructor(config: ABTestConfig) {
this.testConfig = config;
this.metrics = new MetricsCollector();
}
async processRequest(request: TTSRequest): Promise<TTSResponse> {
// Determine test group
const group = this.assignToGroup(request.userId || request.requestId);
// Generate with appropriate provider
const startTime = performance.now();
let response: TTSResponse;
if (group === 'control') {
response = await this.generateWithSource(request);
} else {
response = await this.generateWithTarget(request);
}
const endTime = performance.now();
// Collect metrics
this.metrics.record({
requestId: request.requestId,
group,
latency: endTime - startTime,
audioSize: response.audio.length,
characterCount: request.text.length,
voiceId: request.voiceId,
timestamp: new Date().toISOString(),
});
return response;
}
private assignToGroup(identifier: string): 'control' | 'treatment' {
const hash = this.hashString(identifier);
const bucket = Math.abs(hash % 100);
return bucket < this.testConfig.treatmentPercentage
? 'treatment'
: 'control';
}
async getTestResults(): Promise<ABTestResults> {
const controlMetrics = this.metrics.getGroupMetrics('control');
const treatmentMetrics = this.metrics.getGroupMetrics('treatment');
return {
control: {
sampleSize: controlMetrics.count,
avgLatency: controlMetrics.avgLatency,
p95Latency: controlMetrics.p95Latency,
errorRate: controlMetrics.errorRate,
},
treatment: {
sampleSize: treatmentMetrics.count,
avgLatency: treatmentMetrics.avgLatency,
p95Latency: treatmentMetrics.p95Latency,
errorRate: treatmentMetrics.errorRate,
},
statisticalSignificance: this.calculateSignificance(
controlMetrics,
treatmentMetrics
),
recommendation: this.getRecommendation(controlMetrics, treatmentMetrics),
};
}
}
Rollback Procedures#
Implementing Rollback#
typescript
// Rollback support for failed migrations
class MigrationRollback {
private rollbackState: RollbackState;
async enableRollback(migrationId: string): Promise<void> {
this.rollbackState = {
migrationId,
enabled: true,
rollbackPercentage: 0,
startTime: new Date().toISOString(),
};
}
async initiateRollback(reason: string): Promise<RollbackResult> {
console.warn(
`Initiating rollback for migration ${this.rollbackState.migrationId}`
);
console.warn(`Reason: ${reason}`);
// Gradual rollback
const rollbackSteps = [10, 25, 50, 75, 100];
for (const percentage of rollbackSteps) {
this.rollbackState.rollbackPercentage = percentage;
// Wait and monitor
await this.sleep(30000); // 30 seconds between steps
const metrics = await this.getHealthMetrics();
if (metrics.errorRate > 5) {
// Something wrong with rollback, pause
return {
success: false,
stoppedAt: percentage,
reason: 'Error rate increased during rollback',
};
}
}
return {
success: true,
completedAt: new Date().toISOString(),
reason,
};
}
shouldUseOriginalProvider(requestId: string): boolean {
if (!this.rollbackState?.enabled) {
return false;
}
// Deterministic rollback based on request ID
const hash = this.hashString(requestId);
const bucket = Math.abs(hash % 100);
return bucket < this.rollbackState.rollbackPercentage;
}
}
// Usage in main service
class TTSService {
private migrationRollback: MigrationRollback;
private sourceProvider: TTSProvider;
private targetProvider: ElevenLabsClient;
async synthesize(request: TTSRequest): Promise<Buffer> {
// Check if rollback is active
if (this.migrationRollback.shouldUseOriginalProvider(request.requestId)) {
return this.sourceProvider.synthesize(request);
}
try {
return await this.targetProvider.textToSpeech({
voiceId: this.mapVoice(request.voiceId),
text: request.text,
modelId: 'eleven_multilingual_v2',
});
} catch (error) {
// On persistent errors, consider triggering rollback
if (this.shouldTriggerRollback(error)) {
await this.migrationRollback.initiateRollback(error.message);
}
// Fallback to original provider
return this.sourceProvider.synthesize(request);
}
}
private shouldTriggerRollback(error: Error): boolean {
// Trigger rollback on critical errors
const criticalErrors = [
'account_suspended',
'quota_exceeded',
'service_unavailable',
];
return criticalErrors.some((e) => error.message.includes(e));
}
}
Rollback Checklist#
Before initiating rollback:
- Document the issue triggering rollback
- Notify stakeholders
- Verify original provider is operational
- Enable feature flag for rollback
- Monitor metrics during rollback
- Collect data for post-mortem
After rollback:
- Verify service is stable
- Document lessons learned
- Create action items for retry
- Update migration plan
- Schedule post-mortem meeting