# ElevenLabs Troubleshooting Guide

This guide covers common issues when working with the ElevenLabs integration and
their solutions.

## Table of Contents

- [Authentication Issues](#authentication-issues)
- [Rate Limiting](#rate-limiting)
- [Quota and Usage Issues](#quota-and-usage-issues)
- [Voice Issues](#voice-issues)
- [Audio Quality Issues](#audio-quality-issues)
- [Streaming Issues](#streaming-issues)
- [Voice Cloning Issues](#voice-cloning-issues)
- [Model-Specific Issues](#model-specific-issues)
- [Network and Connectivity](#network-and-connectivity)
- [Performance Issues](#performance-issues)
- [Debugging Tools](#debugging-tools)

## Authentication Issues

### Error: "Invalid API Key" (401)

**Symptoms:**

- `ElevenLabsProviderError` with code `INVALID_API_KEY`
- HTTP 401 Unauthorized response

**Causes:**

1. Missing API key
2. Invalid or expired API key
3. API key with insufficient permissions

**Solutions:**

```typescript
// 1. Verify API key is set
if (!process.env.ELEVENLABS_API_KEY) {
  throw new Error('ELEVENLABS_API_KEY environment variable not set');
}

// 2. Test API key validity
try {
  const provider = new ElevenLabsProvider({
    apiKey: process.env.ELEVENLABS_API_KEY,
  });
  await provider.getUsage(); // Simple test call
  console.log('API key is valid');
} catch (error) {
  if (error.code === 'INVALID_API_KEY') {
    console.error('API key is invalid or expired');
    // Regenerate at: https://elevenlabs.io/api
  }
}
```

**Verification steps:**

1. Log into ElevenLabs dashboard
2. Navigate to Profile > API Key
3. Regenerate key if necessary
4. Update `ELEVENLABS_API_KEY` in your environment

### Error: "Unauthorized" for Voice Cloning

**Symptoms:**

- Can synthesize but cannot clone voices
- 403 Forbidden on clone endpoints

**Cause:** Voice cloning requires paid subscription tier

**Solution:**

```typescript
// Check subscription level
const subscription = await provider.getSubscription();
if (subscription.tier === 'free') {
  console.error('Voice cloning requires paid subscription');
  console.log('Upgrade at: https://elevenlabs.io/subscription');
}
```

## Rate Limiting

### Error: "Rate Limited" (429)

**Symptoms:**

- `ElevenLabsProviderError` with code `RATE_LIMITED`
- HTTP 429 Too Many Requests
- Requests fail in bursts

**Rate Limits by Tier:**

| Tier       | Requests/min | Concurrent |
| ---------- | ------------ | ---------- |
| Free       | 20           | 2          |
| Starter    | 100          | 5          |
| Creator    | 500          | 10         |
| Pro        | 1000         | 20         |
| Enterprise | Custom       | Custom     |

**Solutions:**

```typescript
// 1. Built-in retry with exponential backoff
const provider = new ElevenLabsProvider({
  apiKey: process.env.ELEVENLABS_API_KEY,
  maxRetries: 5,
  retryDelayMs: 1000, // Base delay
});

// 2. Manual rate limiting with queue
import pLimit from 'p-limit';

const limit = pLimit(5); // 5 concurrent requests
const texts = ['text1', 'text2', 'text3' /* ... */];

const results = await Promise.all(
  texts.map((text) =>
    limit(() =>
      provider.synthesize({
        text,
        voice_id: 'voice_id',
      })
    )
  )
);

// 3. Add delays between requests
async function synthesizeWithDelay(
  texts: string[],
  delayMs: number = 200
): Promise<Buffer[]> {
  const results: Buffer[] = [];
  for (const text of texts) {
    const response = await provider.synthesize({
      text,
      voice_id: 'voice_id',
    });
    results.push(response.audio);
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  return results;
}
```

### Handling Rate Limit Headers

```typescript
// The provider automatically handles Retry-After headers
// For custom handling:
try {
  await provider.synthesize({ text, voice_id });
} catch (error) {
  if (error.code === 'RATE_LIMITED') {
    const retryAfter = error.retryAfter || 60; // seconds
    console.log(`Rate limited. Retry after ${retryAfter}s`);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    // Retry the request
  }
}
```

## Quota and Usage Issues

### Error: "Quota Exceeded"

**Symptoms:**

- `ElevenLabsProviderError` with code `QUOTA_EXCEEDED`
- Cannot synthesize despite valid API key

**Diagnosis:**

```typescript
// Check current usage
const usage = await provider.getUsage();
console.log(`Characters used: ${usage.character_count}`);
console.log(`Character limit: ${usage.character_limit}`);
console.log(`Remaining: ${usage.character_limit - usage.character_count}`);
console.log(`Reset date: ${usage.next_reset_date}`);

if (usage.character_count >= usage.character_limit) {
  console.error('Quota exceeded');
  if (usage.can_extend_character_limit) {
    console.log('You can extend your limit in the dashboard');
  }
}
```

**Solutions:**

1. Wait for monthly reset
2. Upgrade subscription tier
3. Purchase character add-on
4. Optimize text to reduce character count

### Optimizing Character Usage

```typescript
// 1. Remove unnecessary whitespace
const optimizedText = text.replace(/\s+/g, ' ').trim();

// 2. Use abbreviations for common words
const abbreviations: Record<string, string> = {
  'for example': 'e.g.',
  'that is': 'i.e.',
  approximately: 'approx.',
};

// 3. Estimate cost before synthesis
function estimateCharacterCost(text: string): number {
  // ElevenLabs counts all characters including spaces
  return text.length;
}

const cost = estimateCharacterCost(text);
const remaining = usage.character_limit - usage.character_count;
if (cost > remaining) {
  throw new Error(`Text too long: ${cost} chars, only ${remaining} remaining`);
}
```

## Voice Issues

### Error: "Voice Not Found"

**Symptoms:**

- `ElevenLabsProviderError` with code `VOICE_NOT_FOUND`
- HTTP 404 on synthesis request

**Causes:**

1. Invalid voice ID
2. Voice was deleted
3. Voice belongs to different account
4. Cloned voice not yet processed

**Solutions:**

```typescript
// 1. Validate voice ID before use
async function validateVoiceId(voiceId: string): Promise<boolean> {
  try {
    await provider.getVoice(voiceId);
    return true;
  } catch (error) {
    if (error.code === 'VOICE_NOT_FOUND') {
      console.error(`Voice ${voiceId} not found`);
      return false;
    }
    throw error;
  }
}

// 2. List available voices
const voices = await provider.listVoices();
const voiceIds = voices.map((v) => v.voice_id);
if (!voiceIds.includes(targetVoiceId)) {
  console.error('Voice not in available list');
  console.log(
    'Available voices:',
    voices.map((v) => `${v.name}: ${v.voice_id}`)
  );
}

// 3. Use fallback voice
const FALLBACK_VOICE_ID = 'EXAVITQu4vr4xnSDxMaL'; // Sarah

async function synthesizeWithFallback(text: string, voiceId: string) {
  try {
    return await provider.synthesize({ text, voice_id: voiceId });
  } catch (error) {
    if (error.code === 'VOICE_NOT_FOUND') {
      console.warn(`Voice ${voiceId} not found, using fallback`);
      return await provider.synthesize({ text, voice_id: FALLBACK_VOICE_ID });
    }
    throw error;
  }
}
```

### Voice Sounds Different Than Expected

**Symptoms:**

- Voice output doesn't match preview
- Voice quality inconsistent

**Causes:**

1. Different model being used
2. Voice settings not applied
3. Text context affecting delivery

**Solutions:**

```typescript
// 1. Ensure consistent model
const response = await provider.synthesize({
  text,
  voice_id: 'voice_id',
  model_id: 'eleven_v3', // Explicitly set model
});

// 2. Apply optimal voice settings
const response = await provider.synthesize({
  text,
  voice_id: 'voice_id',
  voice_settings: {
    stability: 0.75, // Consistent delivery
    similarity_boost: 0.85, // Match original voice
    style: 0.0, // Neutral style
    use_speaker_boost: true,
  },
});

// 3. Provide context for better intonation
const response = await provider.synthesize({
  text: 'Current sentence to speak.',
  voice_id: 'voice_id',
  previous_text: 'Context from before.', // Helps with intonation
  next_text: 'What comes after.', // Helps with pacing
});
```

## Audio Quality Issues

### Audio Sounds Robotic or Unnatural

**Causes:**

1. Using legacy model
2. Inappropriate voice settings
3. Text formatting issues

**Solutions:**

```typescript
// 1. Use latest model for best quality
const response = await provider.synthesize({
  text,
  voice_id: 'voice_id',
  model_id: 'eleven_v3', // Most natural sounding
});

// 2. Adjust stability for more natural variation
voice_settings: {
  stability: 0.5,        // Lower = more variation
  similarity_boost: 0.7,
  style: 0.3            // Some expressiveness
}

// 3. Add natural pauses with audio tags
const naturalText = text
  .replace(/\. /g, '. <break time="0.3s" /> ')
  .replace(/\? /g, '? <break time="0.4s" /> ')
  .replace(/, /g, ', <break time="0.1s" /> ');
```

### Audio Has Artifacts or Glitches

**Causes:**

1. Text contains problematic characters
2. Very long text without breaks
3. Special characters not handled

**Solutions:**

```typescript
// 1. Clean text before synthesis
function cleanTextForSynthesis(text: string): string {
  return (
    text
      // Remove control characters
      .replace(/[\x00-\x1F\x7F]/g, '')
      // Normalize quotes
      .replace(/[""]/g, '"')
      .replace(/['']/g, "'")
      // Remove emojis (unless supported)
      .replace(
        /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu,
        ''
      )
      // Normalize whitespace
      .replace(/\s+/g, ' ')
      .trim()
  );
}

// 2. Split long text at natural boundaries
function splitAtSentences(text: string, maxLength: number = 500): string[] {
  const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
  const chunks: string[] = [];
  let current = '';

  for (const sentence of sentences) {
    if ((current + sentence).length > maxLength) {
      if (current) chunks.push(current.trim());
      current = sentence;
    } else {
      current += sentence;
    }
  }
  if (current) chunks.push(current.trim());

  return chunks;
}
```

### Wrong Output Format

**Symptoms:**

- File won't play
- Wrong sample rate
- Unexpected file size

**Solutions:**

```typescript
// Explicitly specify format
const response = await provider.synthesize({
  text,
  voice_id: 'voice_id',
  output_format: 'mp3_44100_128', // Clear format specification
});

// Verify output
const audioBuffer = response.audio;
console.log(`Output size: ${audioBuffer.length} bytes`);
console.log(`Expected MP3 header: ${audioBuffer.slice(0, 3).toString()}`);
// MP3 should start with 'ID3' or 0xFF 0xFB
```

## Streaming Issues

### WebSocket Connection Fails

**Symptoms:**

- Cannot establish streaming connection
- Connection drops immediately
- Timeout errors

**Causes:**

1. Network/firewall blocking WebSocket
2. Invalid configuration
3. Server temporarily unavailable

**Solutions:**

```typescript
// 1. Verify WebSocket URL
const provider = new ElevenLabsProvider({
  apiKey: process.env.ELEVENLABS_API_KEY,
  websocketUrl: 'wss://api.elevenlabs.io/v1/text-to-speech',
  websocketTimeoutSeconds: 30, // Increase timeout
});

// 2. Add connection error handling
provider.on('streaming-error', (event) => {
  console.error(`Stream error: ${event.error}`);

  if (event.error.includes('connection')) {
    // Network issue - check proxy settings
    console.log('Check firewall/proxy for WebSocket support');
  }
});

// 3. Implement reconnection logic
async function streamWithReconnect(
  text: string,
  maxRetries: number = 3
): Promise<void> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const session = await provider.startStreaming({
        voice_id: 'voice_id',
        model_id: 'eleven_flash_v2_5',
      });
      await provider.sendStreamingText(session.sessionId, text);
      return;
    } catch (error) {
      console.error(`Stream attempt ${attempt + 1} failed:`, error);
      if (attempt < maxRetries - 1) {
        await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
      }
    }
  }
  throw new Error('Failed to establish stream after retries');
}
```

### Stream Audio Choppy or Delayed

**Causes:**

1. Network latency
2. Buffer underrun
3. Wrong model for real-time use

**Solutions:**

```typescript
// 1. Use Flash model for lowest latency
const session = await provider.startStreaming({
  voice_id: 'voice_id',
  model_id: 'eleven_flash_v2_5', // ~75ms latency
});

// 2. Optimize latency settings
const session = await provider.startStreaming({
  voice_id: 'voice_id',
  model_id: 'eleven_flash_v2_5',
  optimize_streaming_latency: 4, // Maximum latency optimization
});

// 3. Pre-buffer audio chunks
const audioBuffer: Buffer[] = [];
const MIN_BUFFER_SIZE = 3; // Wait for 3 chunks before playing

provider.on('streaming-audio', (event) => {
  audioBuffer.push(event.audio);

  if (audioBuffer.length >= MIN_BUFFER_SIZE) {
    // Start playing
    playNextChunk();
  }
});
```

## Voice Cloning Issues

### Clone Request Fails

**Symptoms:**

- 400 Bad Request on clone
- "Invalid audio file" error

**Causes:**

1. Audio file too short/long
2. Wrong audio format
3. Poor audio quality

**Requirements:**

- **Minimum duration:** 30 seconds per file
- **Maximum files:** 25 for instant clone
- **Formats:** MP3, WAV, M4A
- **Quality:** Clear speech, minimal background noise

**Solutions:**

```typescript
// 1. Validate audio files before upload
import { parseBuffer } from 'music-metadata';

async function validateCloneAudio(buffer: Buffer): Promise<void> {
  const metadata = await parseBuffer(buffer);

  // Check duration
  if (metadata.format.duration && metadata.format.duration < 30) {
    throw new Error('Audio must be at least 30 seconds');
  }

  // Check format
  const validFormats = ['mp3', 'wav', 'm4a', 'mpeg'];
  if (!validFormats.includes(metadata.format.container?.toLowerCase() || '')) {
    throw new Error(`Invalid format. Use: ${validFormats.join(', ')}`);
  }

  // Check for mono/stereo
  if (
    metadata.format.numberOfChannels &&
    metadata.format.numberOfChannels > 2
  ) {
    throw new Error('Audio must be mono or stereo');
  }
}

// 2. Normalize audio before upload
// Use ffmpeg to standardize format
// ffmpeg -i input.wav -ar 44100 -ac 1 -b:a 128k output.mp3
```

### Cloned Voice Sounds Different

**Causes:**

1. Insufficient training data
2. Background noise in samples
3. Multiple speakers in samples

**Solutions:**

```typescript
// 1. Use more/better samples for professional clone
const clonedVoice = await provider.cloneVoiceProfessional({
  name: 'Custom Voice',
  files: highQualitySamples, // 30+ minutes recommended
  description: 'Clear speech with consistent tone',
  remove_background_noise: true,
});

// 2. Use optimal settings for cloned voices
const response = await provider.synthesize({
  text,
  voice_id: clonedVoice.voice_id,
  voice_settings: {
    stability: 0.85, // Higher for cloned voices
    similarity_boost: 0.9, // High to match original
    style: 0.0, // Minimal style deviation
  },
});
```

## Model-Specific Issues

### Audio Tags Not Working

**Cause:** Audio tags only work with `eleven_v3` model

**Solution:**

```typescript
// Verify model supports audio tags
const text = 'Hello <break time="1s" /> world';

const response = await provider.synthesize({
  text,
  voice_id: 'voice_id',
  model_id: 'eleven_v3', // Required for audio tags
});

// Check model capabilities
const models = await provider.getModels();
const v3Model = models.find((m) => m.model_id === 'eleven_v3');
console.log('Audio tags supported:', v3Model?.can_use_audio_tags);
```

### Model Not Available

**Symptoms:**

- "Model not found" error
- 400 Bad Request with model ID

**Solutions:**

```typescript
// 1. List available models
const models = await provider.getModels();
console.log(
  'Available models:',
  models.map((m) => m.model_id)
);

// 2. Use model availability check
async function getAvailableModel(
  preferred: string,
  fallback: string
): Promise<string> {
  const models = await provider.getModels();
  const modelIds = models.map((m) => m.model_id);

  if (modelIds.includes(preferred)) return preferred;
  if (modelIds.includes(fallback)) return fallback;

  throw new Error(`Neither ${preferred} nor ${fallback} available`);
}

const modelId = await getAvailableModel('eleven_v3', 'eleven_multilingual_v2');
```

## Network and Connectivity

### Timeout Errors

**Symptoms:**

- Request timeout after 30 seconds
- Connection reset errors

**Solutions:**

```typescript
// 1. Increase timeout for long text
const provider = new ElevenLabsProvider({
  apiKey: process.env.ELEVENLABS_API_KEY,
  timeout: 60000, // 60 seconds
});

// 2. Split long text into chunks
const MAX_CHUNK_LENGTH = 2500;

async function synthesizeLongText(text: string): Promise<Buffer> {
  const chunks = splitAtSentences(text, MAX_CHUNK_LENGTH);
  const audioChunks: Buffer[] = [];

  for (const chunk of chunks) {
    const response = await provider.synthesize({
      text: chunk,
      voice_id: 'voice_id',
    });
    audioChunks.push(response.audio);
  }

  return Buffer.concat(audioChunks);
}
```

### SSL/TLS Errors

**Symptoms:**

- Certificate errors
- Handshake failures

**Solutions:**

```typescript
// For development/debugging only
// NOT recommended for production
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// Better: Update Node.js CA certificates
// npm install -g ca-certificates
```

## Performance Issues

### Slow Response Times

**Diagnosis:**

```typescript
// Enable request logging
const provider = new ElevenLabsProvider({
  apiKey: process.env.ELEVENLABS_API_KEY,
  logRequests: true,
});

// Check statistics
const stats = provider.getStats();
console.log(`Average latency: ${stats.averageLatencyMs}ms`);
console.log(`Total requests: ${stats.totalRequests}`);
console.log(`By model:`, stats.requestsByModel);
```

**Solutions:**

```typescript
// 1. Use faster models for real-time
model_id: 'eleven_flash_v2_5';

// 2. Optimize latency
optimize_streaming_latency: 4;

// 3. Pre-warm voice cache
await provider.listVoices(); // Cache voices
await provider.getVoice('frequently_used_voice'); // Pre-fetch

// 4. Parallel processing for multiple texts
const results = await Promise.all(
  texts.map((text) =>
    provider.synthesize({
      text,
      voice_id: 'voice_id',
      model_id: 'eleven_flash_v2_5',
    })
  )
);
```

## Debugging Tools

### Enable Verbose Logging

```typescript
// Configure provider with logging
const provider = new ElevenLabsProvider({
  apiKey: process.env.ELEVENLABS_API_KEY,
  logRequests: true,
});

// Listen to all events
provider.on('synthesis-start', (e) => console.log('Start:', e));
provider.on('synthesis-complete', (e) => console.log('Complete:', e));
provider.on('synthesis-error', (e) => console.error('Error:', e));
provider.on('rate-limited', (e) => console.warn('Rate limited:', e));
```

### Health Check Endpoint

```typescript
async function elevenLabsHealthCheck(): Promise<{
  status: 'healthy' | 'degraded' | 'down';
  details: Record<string, unknown>;
}> {
  try {
    const start = Date.now();

    // Test basic connectivity
    const usage = await provider.getUsage();
    const apiLatency = Date.now() - start;

    // Test voice list
    const voicesStart = Date.now();
    const voices = await provider.listVoices();
    const voiceLatency = Date.now() - voicesStart;

    return {
      status: 'healthy',
      details: {
        apiLatencyMs: apiLatency,
        voiceLatencyMs: voiceLatency,
        voicesAvailable: voices.length,
        charactersRemaining: usage.character_limit - usage.character_count,
        quotaResetDate: usage.next_reset_date,
      },
    };
  } catch (error) {
    return {
      status: error.code === 'RATE_LIMITED' ? 'degraded' : 'down',
      details: {
        error: error.message,
        code: error.code,
      },
    };
  }
}
```

### Debug Request/Response

```typescript
// Log raw HTTP for debugging
import { createLogger } from '@oshun/logging';

const logger = createLogger({ service: 'elevenlabs-debug' });

// Wrap provider methods for debugging
const originalSynthesize = provider.synthesize.bind(provider);
provider.synthesize = async (request) => {
  logger.debug('Synthesis request', {
    text_length: request.text.length,
    voice_id: request.voice_id,
    model_id: request.model_id,
  });

  try {
    const response = await originalSynthesize(request);
    logger.debug('Synthesis response', {
      audio_size: response.audio.length,
      latency_ms: response.latencyMs,
    });
    return response;
  } catch (error) {
    logger.error('Synthesis failed', {
      error: error.message,
      code: error.code,
    });
    throw error;
  }
};
```

## Getting Help

### Support Resources

- **ElevenLabs Documentation:** https://docs.elevenlabs.io
- **API Status Page:** https://status.elevenlabs.io
- **Discord Community:** https://discord.gg/elevenlabs
- **Support Email:** support@elevenlabs.io

### Information to Include in Support Requests

1. API key prefix (first 8 characters)
2. Error code and message
3. Request parameters (without sensitive data)
4. Timestamp and timezone
5. Subscription tier
6. Node.js version
7. Provider version

```typescript
// Generate debug info for support
async function getDebugInfo(): Promise<string> {
  const usage = await provider.getUsage();
  const stats = provider.getStats();

  return JSON.stringify(
    {
      nodeVersion: process.version,
      timestamp: new Date().toISOString(),
      usage: {
        charactersUsed: usage.character_count,
        limit: usage.character_limit,
      },
      stats: {
        totalRequests: stats.totalRequests,
        successRate:
          (stats.successfulRequests / stats.totalRequests) * 100 + '%',
        avgLatency: stats.averageLatencyMs + 'ms',
      },
    },
    null,
    2
  );
}
```
