Disciplines · Integrations

ElevenLabs Migration Guide

Before starting any migration:

10sections3 minread

On this page

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#

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