---
title: "TTS Generation"
description: "How text-to-speech generation works in Voicebox"
---

## Overview

Voicebox uses Qwen3-TTS for voice cloning and text-to-speech generation. The TTS module handles model loading, voice prompt creation, and audio synthesis.

## Architecture

The TTS system is built around the `TTSModel` class which manages:

**Model Loading:** Lazy loading with automatic HuggingFace Hub download.

**Voice Prompts:** Converting reference audio into embeddings.

**Generation:** Synthesizing speech from text using voice prompts.

## TTSModel Class

```python
class TTSModel:
    def __init__(self, model_size: str = "1.7B"):
        self.model = None
        self.model_size = model_size
        self.device = self._get_device()  # cuda, mps, or cpu
```

### Device Selection

The model automatically selects the best available device:

```python
def _get_device(self) -> str:
    if torch.cuda.is_available():
        return "cuda"
    elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
        return "cpu"  # MPS can have issues, use CPU for stability
    return "cpu"
```

## Model Loading

Models are downloaded from HuggingFace Hub on first use:

```python
def load_model(self, model_size: Optional[str] = None):
    # Model IDs on HuggingFace Hub
    hf_model_map = {
        "1.7B": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
        "0.6B": "Qwen/Qwen3-TTS-12Hz-0.6B-Base",
    }
    
    # Load with progress tracking
    with tracker.patch_download():
        self.model = Qwen3TTSModel.from_pretrained(
            model_path,
            device_map=self.device,
            torch_dtype=torch.bfloat16,  # float32 on CPU
        )
```

### Async Loading

Loading runs in a thread pool to avoid blocking the event loop:

```python
async def load_model_async(self, model_size: Optional[str] = None):
    if self.model is not None and self._current_model_size == model_size:
        return
    await asyncio.to_thread(self.load_model, model_size)
```

## Voice Prompt Creation

Voice prompts are created from reference audio and cached for reuse:

```python
async def create_voice_prompt(
    self,
    audio_path: str,
    reference_text: str,
    use_cache: bool = True,
) -> Tuple[dict, bool]:
    await self.load_model_async()
    
    # Check cache
    if use_cache:
        cache_key = get_cache_key(audio_path, reference_text)
        cached = get_cached_voice_prompt(cache_key)
        if cached:
            return cached, True
    
    # Create prompt (blocking, run in thread pool)
    voice_prompt = await asyncio.to_thread(
        self.model.create_voice_clone_prompt,
        ref_audio=audio_path,
        ref_text=reference_text,
    )
    
    # Cache the result
    cache_voice_prompt(cache_key, voice_prompt)
    return voice_prompt, False
```

### Combining Multiple Samples

When a profile has multiple samples, they're combined:

```python
async def combine_voice_prompts(
    self,
    audio_paths: List[str],
    reference_texts: List[str],
) -> Tuple[np.ndarray, str]:
    combined_audio = []
    
    for audio_path in audio_paths:
        audio, sr = load_audio(audio_path)
        audio = normalize_audio(audio)
        combined_audio.append(audio)
    
    # Concatenate and normalize
    mixed = np.concatenate(combined_audio)
    mixed = normalize_audio(mixed)
    
    # Combine texts
    combined_text = " ".join(reference_texts)
    
    return mixed, combined_text
```

## Speech Generation

The core generation function:

```python
async def generate(
    self,
    text: str,
    voice_prompt: dict,
    language: str = "en",
    seed: Optional[int] = None,
    instruct: Optional[str] = None,
) -> Tuple[np.ndarray, int]:
    await self.load_model_async()
    
    def _generate_sync():
        # Set seed for reproducibility
        if seed is not None:
            torch.manual_seed(seed)
        
        # Generate audio
        wavs, sample_rate = self.model.generate_voice_clone(
            text=text,
            voice_clone_prompt=voice_prompt,
            instruct=instruct,  # Natural language delivery control
        )
        return wavs[0], sample_rate
    
    # Run in thread pool
    return await asyncio.to_thread(_generate_sync)
```

### Instruct Feature

The `instruct` parameter allows natural language control over speech delivery:

```python
# Examples:
instruct = "Speak slowly and clearly"
instruct = "Sound excited and enthusiastic"
instruct = "Whisper softly"
```

## Caching Strategy

Voice prompts are cached to avoid recomputation:

```python
def get_cache_key(audio_path: str, reference_text: str) -> str:
    """Generate cache key from audio hash and text."""
    audio_hash = hashlib.md5(Path(audio_path).read_bytes()).hexdigest()
    text_hash = hashlib.md5(reference_text.encode()).hexdigest()
    return f"{audio_hash}_{text_hash}"
```

Cache is stored in `data/cache/voice_prompts/`.

## Memory Management

### Unloading Models

Free VRAM/RAM when not needed:

```python
def unload_model(self):
    if self.model is not None:
        del self.model
        self.model = None
        
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
```

### Model Switching

When switching between model sizes (1.7B ↔ 0.6B):

```python
# Unload existing model first
if self.model is not None and self._current_model_size != model_size:
    self.unload_model()
```

## Generation Flow

1. **Request** → Validate text and profile ID
2. **Profile** → Load profile samples from database
3. **Voice Prompt** → Create or retrieve cached prompt
4. **Generate** → Run TTS inference
5. **Save** → Write audio to generations directory
6. **Record** → Create history entry in database
7. **Response** → Return audio path and metadata

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/generate` | Generate speech from text |
| GET | `/audio/{id}` | Serve generated audio file |

### Request Schema

```json
{
  "profile_id": "uuid",
  "text": "Text to synthesize",
  "language": "en",
  "seed": 42,
  "model_size": "1.7B",
  "instruct": "Speak clearly"
}
```

### Response Schema

```json
{
  "id": "generation_uuid",
  "profile_id": "profile_uuid",
  "text": "Text to synthesize",
  "language": "en",
  "audio_path": "/path/to/audio.wav",
  "duration": 3.5,
  "seed": 42,
  "instruct": "Speak clearly",
  "created_at": "2024-01-15T10:30:00Z"
}
```

## Performance Considerations

### GPU Acceleration

- CUDA provides fastest inference
- MPS (Apple Silicon) has stability issues, uses CPU fallback
- CPU inference is slower but always works

### Batch Size

Currently generates one utterance at a time. For long texts, consider:
- Splitting into sentences
- Sequential generation
- Concatenating results

### Memory Usage

| Model | VRAM/RAM Required |
|-------|-------------------|
| 0.6B | ~2GB |
| 1.7B | ~6GB |
