# TTS Voice Metadata System

## Overview

This directory contains JSON files that define voice metadata for all TTS providers. Each file describes the available voices for a specific provider, including voice characteristics, preview URLs, download information, and more.

## File Structure

```
voices/
├── README.md           # This file
├── openai.json         # OpenAI voices
├── piper.json          # Piper TTS voices
├── deepinfra.json      # DeepInfra TTS voices
└── os_native.json      # OS-native TTS (platform-dependent)
```

## JSON Format

Each JSON file follows this structure:

```json
{
  "provider": "provider_id",
  "providerName": "Human-Readable Provider Name",
  "hasVoiceDownloads": true/false,
  "metadata": {
    // Provider-specific metadata
  },
  "voices": [
    {
      "id": "voice_id",
      "name": "Voice Display Name",
      "language": "en-US",
      "languageDisplay": "English (US)",
      "gender": "male/female/neutral",
      "model": "model_name",           // Optional: for multi-model providers
      "speaker": "speaker_id",         // Optional: for multi-speaker models
      "previewUrl": "https://...",     // URL to audio preview
      "description": "Voice characteristics",
      "requiresDownload": true/false,
      "downloadUrl": "https://...",    // For downloadable voices
      "downloadSize": 12345678,        // Size in bytes
      "metadata": {
        // Voice-specific metadata
      }
    }
  ]
}
```

## Voice Metadata Fields

### Provider Fields

- **provider**: Unique identifier for the provider (e.g., "openai", "piper", "deepinfra")
- **providerName**: Human-readable name for the provider
- **hasVoiceDownloads**: Whether this provider requires downloading voice models
- **metadata**: Provider-specific metadata (models, URLs, etc.)

### Voice Fields

#### Required Fields
- **id**: Unique identifier for the voice within the provider
- **name**: Human-readable display name
- **language**: ISO language code (e.g., "en-US", "es-ES")
- **languageDisplay**: Human-readable language name
- **requiresDownload**: Whether this voice needs to be downloaded before use

#### Optional Fields
- **gender**: Voice gender ("male", "female", "neutral", "unknown")
- **model**: Model name for multi-model providers (e.g., DeepInfra)
- **speaker**: Speaker ID for multi-speaker models
- **previewUrl**: URL to audio preview file (hosted on R2/CDN)
- **description**: Text description of voice characteristics
- **downloadUrl**: Download URL for voice models (Piper only)
- **downloadSize**: Size in bytes for downloadable voices
- **metadata**: Additional voice-specific data

## Preview Generation

Piper previews are generated using the Flutter preview generator app:

```bash
flutter run -d <device> -t lib/generate_previews_main.dart
```

This writes `preview.mp3` inside each voice directory under the Piper voices root.  
Requirements: `sherpa-onnx` native libs available on the device and `ffmpeg` on the host.

For API providers (OpenAI, DeepInfra), preview files are managed externally and uploaded to R2.

Generate API previews with the CLI script:

```bash
python tool/generate_api_previews.py --provider openai
python tool/generate_api_previews.py --provider deepinfra
```

Output structure:
`/usr/local/cluster/projects/gogochat-assets/voices/<provider>/<lang>/<voice_id>/preview.mp3`

Required environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export DEEPINFRA_API_KEY="..."
```

### Upload to R2 Storage

After generating previews, run the assets upload script to push them to R2.

## Usage in Code

### Loading Voice Metadata

```dart
import 'package:gogo_chat/src/services/tts_voice_manager.dart';

// Initialize the voice manager
final voiceManager = TTSVoiceManager.instance;
await voiceManager.initialize();

// Get all voices for a provider
final piperVoices = voiceManager.getProviderVoices('piper');

// Get voices by language
final englishVoices = voiceManager.getVoicesByLanguage('piper', 'en');

// Find a specific voice
final voice = voiceManager.findVoiceById('en_US-lessac-medium');

// Check if provider has downloads
final hasDownloads = voiceManager.hasVoiceDownloads('piper');
```

### Using with TTS Services

```dart
import 'package:gogo_chat/src/services/tts_service_factory.dart';

// Get voice metadata from a service
final ttsService = TTSServiceFactory.getInstance(type: TTSServiceType.piper);
final voices = await ttsService.getVoiceMetadata();

// Filter to English voices
final englishVoices = voices.where((v) => v.language.startsWith('en')).toList();

// Use a voice
await ttsService.speak('Hello!', voice: 'en_US-lessac-medium');
```

## Provider-Specific Notes

### OpenAI

- No downloads required
- All voices support multi-language
- Two models: `tts-1` (faster) and `tts-1-hd` (higher quality)
- 6 voices: alloy, echo, fable, onyx, nova, shimmer

### Piper

- Requires downloading voice models (10-50 MB each)
- Models are in ONNX format; shared `espeak-ng-data` is downloaded on demand
- Quality levels: low, medium, high
- Many languages supported
- Download URLs point to the assets worker (HMAC-protected)

### DeepInfra

- No downloads required
- Each voice is a separate model
- Aura series voices (Asteria, Luna, Stella, etc.)
- Model IDs follow pattern: `deepinfra/aura-{name}-en`

### OS Native

- Uses platform-specific TTS
- Voices are discovered at runtime
- No downloads or previews available
- Voice quality and availability varies by device

## Adding a New Voice

1. Edit the appropriate JSON file (e.g., `piper.json`)
2. Add a new voice object with all required fields
3. Generate a preview (Piper) using the Flutter preview generator:
   ```bash
   flutter run -d <device> -t lib/generate_previews_main.dart
   ```
4. Upload previews to R2 storage
5. For Piper voices, previews are served via the assets worker and do not use `previewUrl` in JSON

## Adding a New Provider

1. Create a new JSON file (e.g., `newprovider.json`)
2. Follow the JSON format above
3. Create a new TTS service implementation
4. Update `TTSVoiceManager` to load the new provider
5. Generate previews for all voices
6. Upload previews to R2

## Preview URL Format

Preview URLs are **not stored in JSON files**. The Flutter app dynamically constructs
them based on the provider, model, language, and speaker.

**Dynamic URL Pattern:**
```
{assetsUrl}/voices/{provider}/{model}/{language}/{speaker}/preview.mp3
```

**Examples:**
- OpenAI voice in Spanish: `https://gogoai.app/api/assets/voices/openai/gpt-4o-mini-tts/es/alloy/preview.mp3`
- DeepInfra Japanese voice: `https://gogoai.app/api/assets/voices/deepinfra/kokoro/ja/jf_alpha/preview.mp3`
- Piper English voice: `https://gogoai.app/api/assets/voices/piper/vits/{lang}/{voiceId}/preview.mp3`

**Path Components:**
- **provider**: `openai`, `deepinfra`, `piper`
- **model**: From voice metadata (`voice.model` field)
  - OpenAI: `gpt-4o-mini-tts`
  - DeepInfra: `kokoro`
  - Piper: `vits` (or model name if specified)
- **language**: Language code (`en`, `es`, `fr`, etc.)
- **speaker**: Voice identifier without model suffix
  - OpenAI: `alloy`, `ash`, `coral` (extracted from `voice.key`)
  - DeepInfra: `af_alloy`, `jf_alpha` (full voice ID)
  - Piper: Full voice ID (e.g., `en_US-lessac-medium`)

**Logic:**
1. Check if voice supports user's language (via `supportedLanguages` field)
2. If supported, use user's language; otherwise use voice's primary language
3. Extract model from `voice.model` field
4. Extract speaker from `voice.key` field (or voice ID for Piper/DeepInfra)
5. Construct URL using pattern above
6. All preview requests require HMAC authentication headers

## Maintenance

### Updating Voice Metadata

When a provider adds new voices:

1. Update the appropriate JSON file
2. Generate previews for the new voices
3. Upload previews to R2
4. Test the voices in the app

### Verifying Preview URLs

Check that all preview URLs are accessible:

```bash
# Simple script to verify URLs
for file in *.json; do
  echo "Checking $file..."
  jq -r '.voices[].previewUrl | select(. != null)' "$file" | while read url; do
    curl -I "$url" 2>&1 | grep "200 OK" || echo "Failed: $url"
  done
done
```

## Testing

Test voice metadata loading:

```dart
import 'package:gogo_chat/src/services/tts_voice_manager.dart';

void main() async {
  final voiceManager = TTSVoiceManager.instance;
  await voiceManager.initialize();

  // Verify all providers loaded
  final providers = voiceManager.getAllProviders();
  print('Loaded providers: $providers');

  // Check voice counts
  for (final provider in providers) {
    final voices = voiceManager.getProviderVoices(provider);
    print('$provider: ${voices?.voices.length ?? 0} voices');
  }
}
```
