# How to Use a Custom Transcription Provider in FluidVoice: A Complete Implementation Guide

> Integrate your custom transcription provider into FluidVoice. This guide shows you how to create a Swift class, register it, and select it for seamless voice processing.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: how-to-guide
- Published: 2026-08-14

---

**You can use a custom transcription provider in FluidVoice by creating a Swift class that conforms to the `TranscriptionProvider` protocol, registering it in `SettingsStore`, and selecting it as the active provider.**

FluidVoice is an open-source Swift dictation app that abstracts speech-to-text functionality behind a clean protocol-based architecture. Whether you want to integrate a proprietary ASR engine, a self-hosted Whisper variant, or a cloud API not bundled with the app, the provider system lets you swap backends without touching the core transcription UI. This guide walks through the exact implementation steps based on the `altic-dev/FluidVoice` source code.

## Architecture Overview: How Providers Work

The transcription pipeline revolves around five key components defined in the `Sources/Fluid/` directory:

| Component | Purpose | Source File |
|-----------|---------|-------------|
| **`TranscriptionProvider`** | Protocol defining the contract: preparation, streaming transcription, final transcription, and cache management | [`Sources/Fluid/Services/TranscriptionProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/TranscriptionProvider.swift) |
| **Concrete providers** | Built-in implementations like `WhisperProvider` that wrap specific engines | [`Sources/Fluid/Services/WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/WhisperProvider.swift) |
| **`SettingsStore`** | Persists `SavedProvider` structs and the currently selected provider ID | [`Sources/Fluid/Settings/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Settings/SettingsStore.swift) |
| **`ASRService`** | Central service that resolves the selected provider via `DictationProviderRoute` and exposes it to the rest of the app | [`Sources/Fluid/Services/ASRService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ASRService.swift) |
| **`DictationProviderRoute`** | Helper that constructs the concrete provider instance from stored configuration | [`Sources/Fluid/Networking/DictationProviderRoute.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Networking/DictationProviderRoute.swift) |

At launch, `ASRService` fetches the selected provider ID from `SettingsStore.shared.selectedProviderID`, locates the matching entry in `savedProviders`, instantiates the corresponding class, and invokes `prepare()` before any audio processing begins.

## Step 1: Implement the TranscriptionProvider Protocol

Create a class that conforms to `TranscriptionProvider` in `Sources/Fluid/Services/` or your own module. The protocol requires several properties and methods, though many have default implementations you can inherit.

### Required Conformance Points

```swift
import Foundation

final class MyCustomProvider: TranscriptionProvider {
    // MARK: - Metadata
    
    /// Display name shown in the provider picker UI
    var name: String { "My Custom Provider" }
    
    /// Whether the provider can be used on this device (e.g., check for required binaries)
    var isAvailable: Bool { true }
    
    /// Whether models are loaded and ready for transcription
    var isReady: Bool = false
    
    // MARK: - Lifecycle
    
    /// Called before first transcription; report progress via the handler
    func prepare(progressHandler: ((ModelPreparationProgress) -> Void)?) async throws {
        progressHandler?(.preparingDownload)
        // Download or load models if needed
        progressHandler?(.loading)
        isReady = true
    }
    
    // MARK: - Core Transcription
    
    /// Transcribe raw audio samples (typically 16kHz mono Float32)
    func transcribe(_ samples: [Float]) async throws -> ASRTranscriptionResult {
        // Your speech-to-text implementation here
        let recognizedText = performSTT(samples)
        return ASRTranscriptionResult(text: recognizedText, confidence: 0.92)
    }
    
    // MARK: - Optional Capabilities
    
    /// Set to true if your provider natively handles file transcription efficiently
    var prefersNativeFileTranscription: Bool { false }
    
    /// Clear any cached models or temporary files
    func clearCache() async throws {
        // Implementation if your provider maintains on-disk cache
    }
    
    /// Unique identifier used for persistence and routing
    static func providerID() -> String { "my-custom" }
}

```

The `TranscriptionProvider` protocol also defines `transcribeStreaming` and `transcribeFinal` with default implementations that delegate to `transcribe(_:)`. Override these only if your backend supports incremental streaming results or optimized batch processing.

## Step 2: Register Your Custom Provider in SettingsStore

`SettingsStore` maintains an array of `SavedProvider` structs in `UserDefaults`. To make your provider selectable, append a `SavedProvider` instance with your stable ID.

```swift
import Foundation

// Run this registration once—typically on first app launch or during onboarding
func registerCustomProvider() {
    let customProvider = SettingsStore.SavedProvider(
        id: "my-custom",                    // Must match providerID() in your class
        name: "My Custom Provider",          // Display name in the UI picker
        userInfo: [                          // Optional configuration dictionary
            "apiEndpoint": "https://api.example.com/stt",
            "modelVersion": "v2.1"
        ]
    )
    
    let store = SettingsStore.shared
    
    // Avoid duplicates
    if !store.savedProviders.contains(where: { $0.id == customProvider.id }) {
        store.savedProviders.append(customProvider)
    }
}

```

The `userInfo` dictionary persists arbitrary configuration your provider might need. Access these values in your `TranscriptionProvider` implementation via `ASRService` resolution logic or by reading `SettingsStore` directly.

## Step 3: Activate the Provider

Users can select your provider through the built-in Settings UI, or you can activate it programmatically.

### Programmatic Activation

```swift
// Switch to your custom provider immediately
SettingsStore.shared.selectedProviderID = "my-custom"

// Force ASRService to reload the provider (if already running)
await ASRService.shared.setProvider(id: "my-custom")

```

The `ASRService.setProvider(id:)` method triggers `DictationProviderRoute` to instantiate your class and call `prepare()` before returning. Any transcription requests will now route through your implementation.

## Reference Implementation: WhisperProvider

Study [`Sources/Fluid/Services/WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/WhisperProvider.swift) for a production-grade example. Key patterns to observe:

- **Model download progress**: Whisper implements `prepare(progressHandler:)` with granular `ModelPreparationProgress` updates during ggml binary download
- **Cache management**: Implements `clearCache()` to purge downloaded models
- **Streaming support**: Overrides `transcribeStreaming` for real-time partial results during dictation
- **File optimization**: Sets `prefersNativeFileTranscription = true` to bypass sample buffer conversion for file-based transcription

## Troubleshooting Common Issues

- **Provider not appearing in UI**: Verify `savedProviders.append()` ran and the ID matches exactly between `SavedProvider.id` and your class's `providerID()`
- **prepare() not called**: Ensure `ASRService.shared.setProvider(id:)` or UI selection triggers the provider resolution path
- **Transcription silently fails**: Check `isAvailable` and `isReady` properties—`ASRService` skips providers that report unavailable

## Summary

- **Implement `TranscriptionProvider`** in a new class with your speech-to-text logic
- **Expose a stable ID** via `providerID()` that matches your `SavedProvider` registration
- **Register with `SettingsStore.shared.savedProviders`** to make the provider selectable
- **Activate via `selectedProviderID`** or `ASRService.shared.setProvider(id:)`
- **Study [`WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/WhisperProvider.swift)** for model handling, progress reporting, and cache patterns

## Frequently Asked Questions

### What methods must I implement for a minimal custom transcription provider?

At minimum, implement `name`, `isAvailable`, `isReady`, `prepare(progressHandler:)`, and `transcribe(_:)`. The protocol provides default implementations for `transcribeStreaming`, `transcribeFinal`, `clearCache()`, and `prefersNativeFileTranscription`, so you can start with basic synchronous transcription and add streaming later.

### How does FluidVoice handle provider configuration like API keys?

Store configuration in the `userInfo` dictionary of `SettingsStore.SavedProvider`. Your `TranscriptionProvider` implementation can access `SettingsStore.shared.savedProviders` to retrieve its own entry and read values like endpoints or credentials. For sensitive data, consider the Keychain instead of `UserDefaults`.

### Can my custom provider replace Whisper entirely?

Yes. Set `SettingsStore.shared.selectedProviderID` to your provider's ID, and `ASRService` will route all transcription—live dictation, meeting capture, and file processing—through your implementation. The UI will display your provider's `name` in the picker, and other app features remain unchanged.