Skip to content

Feature: Use local WAL files for audio playback when 'Store Audio on Phone' is enabled #6182

@omi-discord-vector

Description

@omi-discord-vector

Problem

When "Store Audio on Phone" is enabled, local WAL files are kept on device but NOT used for playback. The app always streams audio from the cloud (GCS), even when a local copy exists.

Current behavior:

// app/lib/widgets/conversation_audio_player_widget.dart
final urls = getConversationAudioUrls(
  conversationId: widget.conversation.id,
  audioFileIds: audioFileIds,
  format: 'wav',
);
// Always generates: v1/sync/audio/{conversationId}/{audioFileId}?format=wav
// Always streams from GCS

Why this is wasteful:

  • Downloads same audio multiple times if replayed
  • Wastes bandwidth and cloud egress costs
  • Doesn't work offline (even though local file exists)
  • "Store Audio on Phone" setting has no playback benefit

Proposed Solution

Check for local WAL files first, fall back to cloud streaming if missing.

Playback flow:

Click play → Check if WAL exists locally
  → If yes: Play from local file (instant, offline-capable)
  → If no: Stream from cloud (current behavior)

Benefits:

  • Save bandwidth for users
  • Reduce cloud egress costs for Omi
  • Offline playback when local copy exists
  • Instant playback (no network latency)
  • "Store Audio on Phone" setting becomes actually useful for playback

Implementation

1. Add local file check in audio API:

// app/lib/backend/http/api/audio.dart
Future<List<String>> getConversationAudioUrls({
  required String conversationId,
  required List<String> audioFileIds,
  String format = 'wav',
}) async {
  List<String> urls = [];
  
  for (String audioFileId in audioFileIds) {
    // Check if local WAL file exists
    String? localPath = await getLocalWalPath(conversationId, audioFileId);
    
    if (localPath != null && await File(localPath).exists()) {
      // Use local file
      urls.add('file://$localPath');
    } else {
      // Fall back to cloud streaming
      urls.add(getAudioStreamUrl(
        conversationId: conversationId,
        audioFileId: audioFileId,
        format: format,
      ));
    }
  }
  
  return urls;
}

2. Helper to locate WAL files:

Future<String?> getLocalWalPath(String conversationId, String audioFileId) async {
  if (!SharedPreferencesUtil().unlimitedLocalStorageEnabled) {
    return null; // Only use local files when setting is enabled
  }
  
  // Look up WAL directory and check for matching file
  // Return path if exists, null otherwise
}

3. Audio player already supports file:// URIs:

  • just_audio package handles both HTTP and local file URIs
  • No changes needed in conversation_audio_player_widget.dart

Edge Cases

What if local file is corrupt/incomplete?

  • Catch playback errors
  • Fall back to cloud streaming
  • Optionally delete corrupt local file

What if user disables "Store Audio on Phone"?

  • Only check local files when unlimitedLocalStorageEnabled == true
  • Otherwise skip local check (current cloud-only behavior)

What if file format differs?

  • WAL files are Opus, playback uses WAV
  • Either: transcode on-the-fly (heavy)
  • Or: also store WAV locally when syncing
  • Or: update player to support Opus directly (most efficient)

Alternative: Smart caching

Instead of requiring "Store Audio on Phone" to be enabled, implement automatic caching:

  • Download played audio to cache directory
  • Expire after N days or when storage fills
  • Transparent to user (no setting required)
  • Similar to how images/videos are cached

Related Code

  • Audio player: app/lib/widgets/conversation_audio_player_widget.dart
  • Audio API: app/lib/backend/http/api/audio.dart
  • WAL sync: app/lib/services/wals/local_wal_sync.dart
  • Preferences: app/lib/backend/preferences.dart (line 336: unlimitedLocalStorageEnabled)
  • Capture provider: app/lib/providers/capture_provider.dart (line 580: WAL condition)

Requested by: l4w1 (Discord user 261726922696818689)
Context: Discord #general-vector-chat, 2026-03-30

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions