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

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
Concrete providers Built-in implementations like WhisperProvider that wrap specific engines Sources/Fluid/Services/WhisperProvider.swift
SettingsStore Persists SavedProvider structs and the currently selected provider ID 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
DictationProviderRoute Helper that constructs the concrete provider instance from stored configuration 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

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.

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

// 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →