# How to Add a New TTS Provider to Muse Alongside ElevenLabs: A Complete Implementation Guide

> Add a new TTS provider to Muse by implementing the TTSProvider interface and registering it in Koin. Follow this complete guide for seamless integration.

- Repository: [Ko Shin/muse](https://github.com/kkoshin/muse)
- Tags: how-to-guide
- Published: 2026-03-05

---

**To add a new TTS provider to the Muse app, implement the `TTSProvider` interface with three suspend functions, register your class in the Koin DI module, and optionally wrap it with `GroupedTTSProvider` for fallback support.**

The Muse repository (`kkoshin/muse`) uses a clean abstraction layer that makes adding a new TTS provider straightforward without modifying existing UI or business logic. Whether you want to integrate a local speech engine, a different cloud API, or a mock for testing, the architecture allows you to add a new TTS provider alongside the default ElevenLabs integration through simple interface implementation and dependency injection.

## Understanding the TTSProvider Contract

The core contract for text-to-speech functionality is defined in **[`TTSProvider.kt`](https://github.com/kkoshin/muse/blob/main/TTSProvider.kt)** located at `muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/provider/`. Any class adding a new TTS provider must implement this interface with three suspend functions:

- **`generate(voiceId: String, text: String): Result<TTSResult>`** – Produces an audio stream as an `okio.Source` wrapped in a result.
- **`queryQuota(): Result<CharacterQuota>`** – Returns remaining character limits or a dummy value if the service doesn't enforce quotas.
- **`queryVoices(): Result<List<Voice>>`** – Lists available voices with metadata including accent, age, gender, and preview URLs.

The default ElevenLabs implementation, **`ElevenLabProcessor`**, lives in `muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/manager/` and serves as a reference for how to structure network calls and error handling.

## Step‑by‑Step Implementation Guide

### Step 1: Create Your Custom Provider Class

Create a new Kotlin file in the vendor package that implements `TTSProvider`. The file location should mirror existing providers, such as [`muse/src/commonMain/kotlin/io/github/kkoshin/muse/tts/vendor/MyCustomTTSProvider.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/tts/vendor/MyCustomTTSProvider.kt).

```kotlin
package io.github.kkoshin.muse.tts.vendor

import io.github.kkoshin.muse.core.provider.*
import io.github.kkoshin.muse.tts.*

class MyCustomTTSProvider(
    private val apiKey: String
) : TTSProvider {

    override suspend fun generate(voiceId: String, text: String): Result<TTSResult> {
        // Implement your API call here
        // Convert response bytes to okio.Source
        // return Result.success(TTSResult(source, SupportedAudioType.MP3, metadata))
        TODO("Replace with real implementation")
    }

    override suspend fun queryQuota(): Result<CharacterQuota> =
        Result.success(CharacterQuota.empty)

    override suspend fun queryVoices(): Result<List<Voice>> =
        Result.success(listOf(
            Voice(
                voiceId = "custom-voice-1",
                name = "Custom Neural Voice",
                description = "High-quality synthetic voice",
                previewUrl = "",
                accent = Voice.Accent.Other,
                age = Voice.Age.Other,
                useCase = null,
                gender = Voice.Gender.Other,
                descriptive = null
            )
        ))
}

```

For a concrete reference on structuring mock implementations or handling edge cases, examine **[`MockTTSProvider.kt`](https://github.com/kkoshin/muse/blob/main/MockTTSProvider.kt)** in `muse/src/androidDebug/kotlin/io/github/kkoshin/muse/tts/vendor/`.

### Step 2: Register Your Provider in Koin

The dependency injection graph is configured in **[`appModule.kt`](https://github.com/kkoshin/muse/blob/main/appModule.kt)** at `muse/src/androidMain/kotlin/io/github/kkoshin/muse/`. You have two registration strategies when adding a new TTS provider:

**Option A: Replace the Default Provider**

Use this approach if you want your implementation to be the sole TTS engine used throughout the app:

```kotlin
val appModule = module {
    includes(baseModule)
    
    single<TTSProvider> { 
        MyCustomTTSProvider(apiKey = "your-api-key-here") 
    }
    
    // Keep other ElevenLabs services unchanged
    single<AudioIsolationProvider> { ElevenLabProcessor(get(), get()) }
    single<SoundEffectProvider> { ElevenLabProcessor(get(), get()) }
    single<STTProvider> { ElevenLabProcessor(get(), get()) }
}

```

**Option B: Combine Providers with GroupedTTSProvider**

Use **`GroupedTTSProvider`** (located in `muse/src/androidDebug/kotlin/io/github/kkoshin/muse/tts/vendor/`) to enable fallback behavior or user selection between multiple engines:

```kotlin
val appModule = module {
    includes(baseModule)

    single<TTSProvider> {
        GroupedTTSProvider(
            providers = listOf(
                MyCustomTTSProvider(apiKey = "your-key-here"),
                ElevenLabProcessor(get(), get())   // fallback to ElevenLabs
            )
        )
    }

    single<AudioIsolationProvider> { ElevenLabProcessor(get(), get()) }
    single<SoundEffectProvider> { ElevenLabProcessor(get(), get()) }
    single<STTProvider> { ElevenLabProcessor(get(), get()) }
}

```

`GroupedTTSProvider` iterates over the provider list in order, returning the first successful result from `generate()` and aggregating quotas across all registered engines.

### Step 3: Verify Injection Points

Once registered in Koin, your new provider automatically injects into all existing TTS consumers without code changes. Key injection points include:

- **`SpeechProcessorManager`** – Orchestrates speech generation workflows
- **`ExportViewModel`** – Handles audio export functionality

Both classes declare dependencies on the `TTSProvider` interface, so Koin resolves them to your registered implementation at runtime.

### Step 4: Optional UI Configuration

If you want users to toggle between providers, expose the provider list through a `ProviderRegistry` or store the active provider ID in `MutableState` persisted to DataStore. The `GroupedTTSProvider` already supports multiple providers; you can filter its internal list based on user preferences stored in your configuration layer.

## Testing Your Implementation

The debug build provides a reference testing pattern through **`MockTTSProvider`** and its corresponding DI module **[`mockAppModule.kt`](https://github.com/kkoshin/muse/blob/main/mockAppModule.kt)** at `muse/src/androidDebug/kotlin/io/github/kkoshin/muse/`. 

To test your new TTS provider without consuming real API quota:

1. Create a test implementation that returns static `ByteArray` data in `generate()`
2. Register it in a test-specific Koin module
3. Inject into `SpeechProcessorManager` to verify integration

The mock module demonstrates how to override production bindings with test doubles using Koin's module isolation.

## Summary

- **Implement `TTSProvider`** with `generate()`, `queryQuota()`, and `queryVoices()` methods to define your engine's contract.
- **Register in [`appModule.kt`](https://github.com/kkoshin/muse/blob/main/appModule.kt)** using either direct binding or `GroupedTTSProvider` for multi-engine fallback support.
- **Leverage existing injection** – `SpeechProcessorManager` and `ExportViewModel` automatically receive your implementation through Koin's interface resolution.
- **Reference `MockTTSProvider`** for testing patterns and structure when adding a new TTS provider to the Muse codebase.

## Frequently Asked Questions

### What methods must I implement when adding a new TTS provider?

You must implement three suspend functions defined in the `TTSProvider` interface: `generate(voiceId, text)` to return a `Result<TTSResult>` containing the audio stream, `queryQuota()` to return character limits or `CharacterQuota.empty`, and `queryVoices()` to return a list of available `Voice` objects with metadata.

### Can I use multiple TTS providers simultaneously in Muse?

Yes. Wrap your providers in **`GroupedTTSProvider`**, which attempts each engine in sequence until one succeeds. This allows you to add a new TTS provider as a primary option while keeping ElevenLabs as a fallback, or to offer user-selectable voice engines within the same session.

### Where should I place my custom TTS provider files?

Follow the existing package structure by placing your implementation in `muse/src/commonMain/kotlin/io/github/kkoshin/muse/tts/vendor/` for shared code, or `muse/src/androidMain/...` for Android-specific implementations. Mirror the file organization of [`MockTTSProvider.kt`](https://github.com/kkoshin/muse/blob/main/MockTTSProvider.kt) for consistency.

### How do I test my new TTS provider without making real API calls?

Use the debug build's **`MockTTSProvider`** as a template to create a test double that returns static audio data or empty sources. Register your mock in a test-specific Koin module (similar to [`mockAppModule.kt`](https://github.com/kkoshin/muse/blob/main/mockAppModule.kt)) to override production bindings during unit or integration tests.