# How FluidVoice Downloads and Caches ASR Models: A Complete Technical Breakdown

> Discover how FluidVoice downloads and caches ASR models with a detailed technical breakdown. Learn about the two-layer architecture for efficient offline ASR.

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

---

**FluidVoice downloads ASR models through a two-layer architecture where `ASRService` orchestrates the operation and model-specific providers handle the actual network fetch, streaming the file to a persistent cache directory in `~/Library/Application Support/FluidVoice/Models/` for subsequent offline use.**

FluidVoice's automatic speech recognition (ASR) infrastructure relies on a clean separation between model-agnostic orchestration and provider-specific implementation. This article explains exactly how the open-source macOS dictation app downloads, caches, and retrieves speech-to-text models based on the source code in `altic-dev/FluidVoice`.

---

## The Entry Point: ASRService Orchestration

All model download operations in FluidVoice flow through `ASRService`, a singleton service that manages the complete ASR lifecycle. When a user selects a speech model—or when the UI triggers an explicit download—the sequence begins with model selection and provider instantiation.

### Model Selection and Provider Creation

The currently active model lives in `SettingsStore.shared.selectedSpeechModel`. The `ASRService` creates an appropriate transcription provider through its private `getProvider(for:)` method:

```swift
private func getProvider(for model: SettingsStore.SpeechModel) -> TranscriptionProvider

```

This method returns concrete implementations such as `WhisperProvider`, `FluidAudioProvider`, or `ParakeetRealtimeProvider` depending on the selected model type. Source: [[`ASRService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/ASRService.swift)](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ASRService.swift#L78-L105).

### Initiating the Download

The public entry point `ASRService.downloadModel(_:source:progressHandler:)` enforces single-operation semantics and publishes state for UI binding:

```swift
try await provider.prepare(progressHandler: { progress in … })

```

Key responsibilities at this stage include:
- Recording the model ID in `downloadingModelId`
- Setting `modelPreparationPhase = .preparingDownload`
- Ensuring only one model operation runs concurrently

Source: [[`ASRService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/ASRService.swift)](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ASRService.swift#L107-L120).

---

## Provider-Level Download Implementation

Each concrete provider implements the `TranscriptionProvider` protocol's `prepare(progressHandler:)` method. The **WhisperProvider** implementation demonstrates FluidVoice's download and caching pattern.

### Streaming Download with Atomic Placement

In [`WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/WhisperProvider.swift), the download process constructs a Hugging Face asset URL and delegates to `ProgressiveFileDownloader`:

```swift
let (downloadedURL, response) = try await ProgressiveFileDownloader.download(
    url, progressHandler: progressHandler)
try FileManager.default.moveItem(at: downloadedURL, to: destination)

```

Critical implementation details:
- **Temporary file handling**: Data streams to a temporary location first
- **Atomic move**: Only upon successful completion does the file relocate to its final destination
- **Destination path**: `~/Library/Application Support/FluidVoice/Models/<model-id>`

Source: [[`WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/WhisperProvider.swift)](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/WhisperProvider.swift#L343-L409).

---

## Caching Strategy and Cache Hits

FluidVoice's ASR model caching eliminates redundant network requests through explicit existence checks and state publishing.

### Cache Validation

Before any network operation, providers call `modelsExistOnDisk()`. If the cached file is present and valid, `provider.prepare` skips the download entirely and proceeds directly to model loading.

### UI-Bound Cache State

`ASRService` exposes three key properties for interface binding:
- `modelsExistOnDisk`: Boolean indicating cache presence
- `isDownloadingModel`: Active download status
- `downloadProgress`: Fractional completion (0.0–1.0)

The computed property `modelStatusMessage` surfaces this state to users:

```swift
var modelStatusMessage: String {
    if self.isAsrReady { return "Model ready" }
    …
    if self.modelsExistOnDisk { return "Model cached, needs loading" }
}

```

Source: [[`ASRService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/ASRService.swift)](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ASRService.swift#L22-L32).

---

## Loading Cached Models into Memory

When a user initiates dictation, `ASRService.ensureReadyTask` invokes `provider.prepare` again. This time the provider detects the cached file and loads it—without network activity—into the appropriate runtime:
- **Whisper models**: Loaded via Core ML or GGML inference
- **FluidAudio/Parakeet models**: Initialized through the FluidAudio runtime

The cache directory persists across app launches, enabling immediate availability after first download.

---

## Cancellation and Cache Consistency

FluidVoice handles interrupted downloads gracefully. If `cancelModelDownload()` is invoked, providers may trigger `provider.clearCache()` to remove partial files, preventing corrupted cache entries.

---

## Practical Code Examples

### Force Download with Progress Tracking

```swift
Button("Download Model") {
    Task {
        do {
            try await ASRService.shared.downloadModel(
                SettingsStore.shared.selectedSpeechModel,
                progressHandler: { fraction in
                    print("Download progress: \(fraction * 100)%")
                }
            )
        } catch {
            print("Download failed: \(error)")
        }
    }
}

```

### Check Cache State Programmatically

```swift
if ASRService.shared.modelsExistOnDisk {
    print("Model is already cached – loading directly")
} else {
    print("Model not present – will download on first use")
}

```

### Cancel Ongoing Download

```swift
ASRService.shared.cancelModelDownload()

```

---

## Key Source Files

| File | Responsibility |
|------|--------------|
| [`Sources/Fluid/Services/ASRService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ASRService.swift) | Central orchestration of model selection, download state management, and provider coordination |
| [`Sources/Fluid/Services/WhisperProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/WhisperProvider.swift) | Whisper-specific download implementation with Hugging Face integration |
| [`Sources/Fluid/Services/FluidAudioProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/FluidAudioProvider.swift) | Parakeet (FluidAudio) model handling |
| [`Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Services/ExternalCoreMLTranscriptionProvider.swift) | Cohere Transcribe Core ML model caching |
| [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) | User model preference persistence (`selectedSpeechModel`) |

These files collectively implement the complete ASR model download and caching lifecycle in FluidVoice.

---

## Summary

- **Orchestration layer**: `ASRService` manages all model operations, enforces single-download semantics, and publishes observable state
- **Provider abstraction**: Each ASR backend implements its own `prepare(progressHandler:)` for model-specific fetch logic
- **Atomic caching**: Downloads stream to temporary files, then move atomically to `~/Library/Application Support/FluidVoice/Models/<model-id>`
- **Cache validation**: Subsequent preparations check disk existence before network requests
- **Graceful interruption**: Cancellation APIs prevent partial file corruption

---

## Frequently Asked Questions

### Where does FluidVoice store downloaded ASR models?

FluidVoice caches ASR models in `~/Library/Application Support/FluidVoice/Models/<model-id>`, a directory within the macOS application support folder that persists across app launches and system restarts.

### Can FluidVoice work offline after downloading models?

Yes. Once a model is cached via `modelsExistOnDisk`, `ASRService` and its providers load the local file directly into memory without network activity, enabling complete offline dictation.

### How does FluidVoice handle interrupted or failed downloads?

Providers may invoke `clearCache()` to remove partial files, and `ASRService` exposes `cancelModelDownload()` for user-initiated cancellation. The atomic move pattern ensures only complete, verified downloads reach the cache directory.

### Which ASR models does FluidVoice support for download?

The codebase includes providers for Whisper (via `WhisperProvider`), Parakeet/FluidAudio (via `FluidAudioProvider`), and Cohere Transcribe (via `ExternalCoreMLTranscriptionProvider`), with extensibility for additional backends through the `TranscriptionProvider` protocol.