How FluidVoice's Audio History System Works with Budget Controls and ZIP Export

FluidVoice maintains separate WAV recordings for every transcription entry, automatically pruning old files to respect user-defined gigabyte budgets while supporting full ZIP export with JSON manifests.

The audio history system in the FluidVoice repository preserves raw microphone recordings alongside transcription entries, enabling replay and audit functionality. It implements a three-layer architecture that decouples audio storage from transcription data and user settings, ensuring reliable persistence while respecting storage constraints.

Core Architecture

The system relies on three specialized stores that handle distinct responsibilities:

This separation ensures the UI never interacts directly with the file system, operating exclusively through these store interfaces.

Storing Audio Recordings

When dictation completes, TranscriptionHistoryStore.addEntry receives optional DictationAudioMetadata. If present, the store invokes DictationAudioHistoryStore.save(snapshot:entryID:timestamp:model:) to persist the raw audio.

WAV File Format and Storage Location

The method writes a 44-byte WAV header followed by raw PCM samples to a file named <timestamp>_<entryID>.wav in the application support directory:


~/Library/Application Support/FluidVoice/DictationAudioHistory/

The resulting metadata—including file name, size, duration, and sample rate—is stored on the TranscriptionHistoryEntry, creating a persistent link between the transcription text and its corresponding audio file.

Budget Controls and Storage Management

FluidVoice enforces storage limits through a configurable budget system that automatically prunes old recordings when limits are exceeded.

User Configuration

The budget UI resides in SettingsView and persists values through SettingsStore:

  • Budget value: Stored as a Double (gigabytes) in UserDefaults under the key AudioHistoryBudgetGB
  • Byte limit: Computed on-demand via SettingsStore.audioHistoryBudgetBytes:
var audioHistoryBudgetBytes: Int64 {
    DictationAudioHistoryStore.bytes(forGigabytes: self.audioHistoryBudgetGB)
}

Automatic Pruning Logic

Every time addEntry saves new audio, TranscriptionHistoryStore.pruneAudioToBudget() executes:

  1. Reads current usage via DictationAudioHistoryStore.audioUsageBytes()
  2. If usage exceeds the byte limit, iterates entries newest-to-oldest
  3. Deletes audio files via deleteAudio(fileName:) until usage fits under budget
  4. Removes orphaned files through DictationAudioHistoryStore.deleteUnreferencedAudioFiles

The UI displays current consumption using formatted strings:

Text("Audio history: \(DictationAudioHistoryStore.formattedGigabytes(self.audioHistoryUsageBytes)) / \(Self.audioBudgetText(for: SettingsStore.shared.audioHistoryBudgetGB)) GB Budget")

ZIP Export Functionality

The system supports exporting audio archives either as complete backups or individual entry pairs, triggered from SettingsView (exportAudioArchive) or per-entry actions (exportPair).

Export Process

The DictationAudioHistoryStore.exportAudioArchive(entries:to:) method implements the following workflow:

  1. Filter entries – Retains only entries with existing audio files
  2. Create staging directory – Generates temporary folder under the system temporary directory
  3. Copy audio files – Copies WAV files to staging/audio/ with deterministic names (<timestamp>_<entryID>.wav)
  4. Build manifest – Writes manifest.jsonl containing JSON lines with relative paths, raw transcripts, final transcripts, and technical metadata
  5. Archive creation – Executes /usr/bin/zip subprocess:
process.arguments = ["-qr", destinationURL.path, "manifest.jsonl", "audio"]
  1. Cleanup – Removes temporary staging directory via defer { try? self.fileManager.removeItem(at: staging) }

Manifest Structure

The internal AudioManifestRow struct encodes each entry with JSONEncoder(sortedKeys:), producing records containing:

  • Audio file path (relative to archive)
  • raw_transcript and final_transcript text
  • Recording duration, sample rate, channel count
  • Application version and model name

Practical Implementation Examples

Checking Current Usage and Budget

let usedBytes = DictationAudioHistoryStore.shared.audioUsageBytes()
let usedGB = DictationAudioHistoryStore.formattedGigabytes(usedBytes)
let budgetGB = SettingsStore.shared.audioHistoryBudgetGB

print("Audio usage: \(usedGB) / \(budgetGB) GB")

Updating Storage Budget

// Set budget to 2 GB (triggers pruning on next addEntry)
SettingsStore.shared.audioHistoryBudgetGB = 2.0

Manual Pruning

// Force immediate pruning to respect current budget
let prunedCount = TranscriptionHistoryStore.shared.pruneAudioToBudget()
print("Pruned \(prunedCount) old audio files")

Exporting Complete Archive

let entries = TranscriptionHistoryStore.shared.entries
let destination = FileManager.default.temporaryDirectory
    .appendingPathComponent("FluidVoice_Export.zip")

do {
    try DictationAudioHistoryStore.shared.exportAudioArchive(
        entries: entries,
        to: destination
    )
    print("Exported archive to \(destination.path)")
} catch {
    print("Export failed: \(error)")
}

Exporting Single Entry Pair

if let entry = TranscriptionHistoryStore.shared.entries.first {
    let pairURL = FileManager.default.temporaryDirectory
        .appendingPathComponent("EntryPair.zip")
    
    try DictationAudioHistoryStore.shared.exportPair(
        entry: entry,
        to: pairURL
    )
}

Summary

  • FluidVoice stores raw dictation audio as WAV files in ~/Library/Application Support/FluidVoice/DictationAudioHistory/, linked to transcription entries via DictationAudioMetadata
  • Budget controls convert user-defined gigabyte limits to byte values via SettingsStore.audioHistoryBudgetBytes, automatically pruning oldest recordings when usage exceeds limits through TranscriptionHistoryStore.pruneAudioToBudget()
  • ZIP export creates portable archives containing audio files and JSON line manifests via /usr/bin/zip, with automatic cleanup of temporary staging directories
  • Architecture decouples concerns across three stores: DictationAudioHistoryStore handles files, TranscriptionHistoryStore manages entry relationships, and SettingsStore persists budget configuration

Frequently Asked Questions

How does FluidVoice handle storage limits when the audio history budget is exceeded?

When the budget is exceeded, TranscriptionHistoryStore.pruneAudioToBudget() automatically deletes the oldest audio files first until usage falls below the configured limit. This process runs every time a new entry is added, checking DictationAudioHistoryStore.audioUsageBytes() against the byte limit derived from SettingsStore.audioHistoryBudgetGB. Orphaned files without corresponding entries are also removed via deleteUnreferencedAudioFiles().

What format does FluidVoice use for exported audio archives?

Exported archives are standard ZIP files containing a manifest.jsonl file and an audio/ directory. The manifest uses JSON Lines format with sorted keys, documenting each recording's relative path, raw transcript, final transcript, timestamp, duration, and technical metadata. Audio files maintain their original WAV format with PCM encoding and 44-byte headers.

Can users export individual transcription entries with their audio?

Yes, the system supports single-entry export through DictationAudioHistoryStore.exportPair(entry:to:). This method creates a ZIP archive containing both the individual WAV file and a corresponding manifest entry, useful for sharing specific recordings or debugging particular transcription sessions.

Where does FluidVoice store the audio history budget preference?

The budget preference persists in UserDefaults under the key AudioHistoryBudgetGB as a Double value representing gigabytes. SettingsStore exposes this as audioHistoryBudgetGB and provides the computed audioHistoryBudgetBytes property that converts the gigabyte value to bytes for comparison against actual disk usage during pruning operations.

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 →