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 granularModelPreparationProgressupdates during ggml binary download - Cache management: Implements
clearCache()to purge downloaded models - Streaming support: Overrides
transcribeStreamingfor real-time partial results during dictation - File optimization: Sets
prefersNativeFileTranscription = trueto 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 betweenSavedProvider.idand your class'sproviderID() - prepare() not called: Ensure
ASRService.shared.setProvider(id:)or UI selection triggers the provider resolution path - Transcription silently fails: Check
isAvailableandisReadyproperties—ASRServiceskips providers that report unavailable
Summary
- Implement
TranscriptionProviderin a new class with your speech-to-text logic - Expose a stable ID via
providerID()that matches yourSavedProviderregistration - Register with
SettingsStore.shared.savedProvidersto make the provider selectable - Activate via
selectedProviderIDorASRService.shared.setProvider(id:) - Study
WhisperProvider.swiftfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →