How FluidVoice Handles Text-to-Speech Generation: A Deep Dive into the Swift Implementation

FluidVoice generates spoken audio from text by wrapping Apple's AVSpeechSynthesizer in a service called AppleSpeechProvider, with an alternative streaming path via FluidAudioProvider for the Parakeet Flash engine.

This article examines how the altic-dev/FluidVoice repository implements text-to-speech (TTS) generation in Swift. The codebase abstracts TTS behind provider protocols, allowing the application to switch between Apple's native speech synthesis and a custom low-latency streaming engine without changing calling code.

The Two TTS Engines in FluidVoice

FluidVoice supports two distinct text-to-speech implementations:

  • Apple Speech — Uses the system AVSpeechSynthesizer for standard macOS TTS
  • Parakeet Flash — Uses a streaming EOU (End-Of-Utterance) model via FluidAudioProvider for real-time, low-latency synthesis

The VoiceEngineSettingsViewModel class determines which engine is active based on user preferences stored in SettingsStore.

Apple Speech Provider: The Native TTS Path

File Location and Core Class

The native TTS implementation lives in Sources/Fluid/Services/AppleSpeechProvider.swift. This class wraps AVSpeechSynthesizer and exposes a unified speak method.

How AppleSpeechProvider Works

When your code needs to vocalize text through the Apple engine, the flow follows four steps:

  1. Request initiation — Call AppleSpeechProvider.speak(_:withVoice:rate:completion:)
  2. Utterance construction — Create an AVSpeechUtterance with the text, voice identifier, and rate
  3. Synthesis execution — Pass the utterance to a shared AVSpeechSynthesizer instance
  4. Callback propagation — Forward delegate callbacks to the completion handler

Code Example: Basic TTS with AppleSpeechProvider

let provider = AppleSpeechProvider.shared
provider.speak(
    "Hello, world!",
    withVoice: AVSpeechSynthesisVoice(language: "en-US"),
    rate: 0.5
) { finished in
    print("Finished speaking: \(finished)")
}

The rate parameter accepts values between 0.0 and 1.0, where 0.5 represents normal speaking speed. The completion closure receives true when speech finishes naturally or false if interrupted.

FluidAudioProvider: The Streaming TTS Path

For users selecting Parakeet Flash, FluidVoice switches to Sources/Fluid/Services/FluidAudioProvider.swift. This provider implements the same TextToSpeechProvider protocol as AppleSpeechProvider, maintaining API consistency.

The Parakeet pipeline uses FluidAudio's streaming architecture to generate speech with minimal latency—critical for real-time dictation workflows. Despite the different underlying technology, callers interact with identical method signatures:

let streamingProvider = FluidAudioProvider.shared
streamingProvider.speak(
    "Streaming speech output",
    withVoice: selectedVoice,
    rate: selectedRate
) { _ in
    // Completion handling identical to AppleSpeechProvider
}

Engine Selection and Settings Persistence

VoiceEngineSettingsViewModel

The Sources/Fluid/UI/AISettings/VoiceEngineSettingsViewModel.swift file centralizes engine management. This view model:

  • Exposes the current voiceEngine selection as a published property
  • Maps enum values (.appleSpeech or .parakeetFlash) to concrete provider instances
  • Provides convenience methods for immediate speech requests

SettingsStore

User preferences persist through Sources/Fluid/Persistence/SettingsStore.swift. This class stores:

  • Selected voice engine (.appleSpeech or .parakeetFlash)
  • Voice-specific parameters (language, rate, pitch)
  • Custom voice identifiers for third-party voices

Runtime Engine Switching

let settings = SettingsStore.shared
let engine = settings.voiceEngine

let ttsProvider: TextToSpeechProvider = 
    (engine == .appleSpeech) ? AppleSpeechProvider.shared
                              : FluidAudioProvider.shared

ttsProvider.speak(text, withVoice: selectedVoice, rate: selectedRate) { _ in
    // Engine-agnostic completion handling
}

This pattern keeps the rest of the codebase decoupled from TTS implementation details.

UI Integration

The Sources/Fluid/UI/AISettingsView+SpeechRecognition.swift file renders the settings interface for TTS configuration. Users can:

  • Toggle between Apple Speech and Parakeet Flash
  • Adjust speech rate with a slider
  • Preview voices with instant playback

The view binds directly to VoiceEngineSettingsViewModel, ensuring UI changes immediately affect subsequent synthesis requests.

Provider Protocol Design

Both TTS implementations conform to a shared TextToSpeechProvider protocol (implied by the identical APIs). This protocol likely specifies:

protocol TextToSpeechProvider {
    func speak(
        _ text: String,
        withVoice voice: AVSpeechSynthesisVoice?,
        rate: Float,
        completion: @escaping (Bool) -> Void
    )
}

This abstraction enables the view model to return either provider type without exposing implementation specifics to callers.

Summary

  • FluidVoice implements text-to-speech through two providers: AppleSpeechProvider for native macOS synthesis and FluidAudioProvider for streaming Parakeet Flash generation
  • Engine selection happens at runtime via VoiceEngineSettingsViewModel, with preferences stored in SettingsStore
  • Both providers share identical APIs, allowing engine-agnostic calling code throughout the application
  • The native path constructs AVSpeechUtterance objects and delegates to AVSpeechSynthesizer
  • The streaming path uses FluidAudio's low-latency EOU model for real-time speech generation

Frequently Asked Questions

How does FluidVoice choose between Apple Speech and Parakeet Flash?

The VoiceEngineSettingsViewModel checks SettingsStore.shared.voiceEngine when initializing the appropriate provider. This enum value persists across app launches, so users retain their preferred engine.

Can I use custom voices with FluidVoice's TTS?

Yes. Both providers accept optional AVSpeechSynthesisVoice parameters. The Apple engine supports any system-installed voice, while the Parakeet pipeline can use voice models compatible with the FluidAudio streaming framework.

What happens if speech synthesis is interrupted?

The completion closure receives false when synthesis doesn't finish naturally—whether from user cancellation, audio session interruption, or errors. The Apple provider forwards these states directly from AVSpeechSynthesizer delegate callbacks.

Where does FluidVoice store TTS configuration?

All voice engine settings live in Sources/Fluid/Persistence/SettingsStore.swift. This includes the active engine selection, speech rate, and voice identifiers. The store persists data across sessions so user preferences survive app restarts.

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 →