forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbatch_recording.dart
More file actions
156 lines (136 loc) · 7.16 KB
/
Copy pathbatch_recording.dart
File metadata and controls
156 lines (136 loc) · 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import 'package:omi/backend/schema/bt_device/bt_device.dart';
/// Marker stored in [Wal.device] for recordings produced by offline/batch mode.
/// Lets the conversations list show *only* batch recordings — never the device
/// SD-card/flash sync WALs or realtime offline buffers (which live on the Sync page).
const String batchRecordingDevice = 'omibatch';
/// Marker for recordings drained from the Limitless pendant's flash. Starts with
/// [batchRecordingDevice] so the recordings scanner matches it; contains
/// `limitless` so the backend tags the conversation `source=limitless`.
const String limitlessBatchRecordingDevice = 'omibatchlimitless';
/// Marker for phone-mic recordings captured via explicit Transcribe Later. Starts
/// with [batchRecordingDevice] so the recordings scanner matches it; the backend
/// maps the `omibatchphone` substring to `source=phone`. These stay strictly
/// manual — the user chose Transcribe Later, so they never auto-upload.
const String phoneBatchRecordingDevice = 'omibatchphone';
/// Marker for phone-mic recordings captured by the automatic offline fallback
/// (the user wanted a live conversation but had no connectivity). Starts with
/// [phoneBatchRecordingDevice] (hence also [batchRecordingDevice], so both the
/// scanner and the backend `source=phone` mapping still match), with an `auto`
/// suffix that flags it for silent auto-upload once connectivity returns.
const String phoneBatchAutoRecordingDevice = 'omibatchphoneauto';
/// True if [fileName] is an automatic offline-fallback phone-mic recording
/// (as opposed to an explicit Transcribe Later one). Only these are eligible for
/// silent auto-upload; every other recording stays manual.
bool isAutoPhoneBatchRecording(String fileName) => fileName.startsWith('audio_${phoneBatchAutoRecordingDevice}_');
/// Per-file auto-upload failure cap: after this many consecutive failures for a
/// file we stop retrying it until the app relaunches (the count lives in memory).
const int autoPhoneUploadMaxFailures = 3;
/// Whether the silent auto-upload of offline-fallback recordings may run right
/// now. Mirrors the gates in `SyncProvider._autoUploadPendingPhoneFiles`:
/// custom-STT users sync manually (with confirmation), the auto-sync opt-out is
/// respected, and a second upload never starts while one is already in flight.
bool canAutoUploadPhoneRecordings({
required bool useCustomStt,
required bool autoSyncOfflineRecordings,
required bool isUploading,
}) =>
!useCustomStt && autoSyncOfflineRecordings && !isUploading;
/// The next offline-fallback recording to auto-upload from [fileNames], or null
/// when none is eligible. Only auto-marker files qualify (explicit Transcribe
/// Later recordings stay manual); [busyNames] (uploading or processing) are
/// skipped, and a file at/over its [failureCounts] cap is skipped until the next
/// app launch. Pure so the selection/backoff rules are unit testable without the
/// provider's heavy singletons.
String? selectNextAutoPhoneUpload(
List<String> fileNames, {
required Set<String> busyNames,
required Map<String, int> failureCounts,
}) {
for (final name in fileNames) {
if (!isAutoPhoneBatchRecording(name)) continue;
if (busyNames.contains(name)) continue;
if ((failureCounts[name] ?? 0) >= autoPhoneUploadMaxFailures) continue;
return name;
}
return null;
}
/// Which capture mode a phone-mic session starts in. Fixed for the whole
/// session at streamRecording(); there is no mid-session switching.
enum PhoneMicSessionMode { live, batchExplicit, batchAuto }
/// Pure decision table for CaptureController.streamRecording. [hasNetwork] is
/// network connectivity (ConnectivityService.isConnected) — never device
/// connectivity. Explicit Transcribe Later suppresses the automatic offline
/// fallback, so an offline start with the toggle on is batchExplicit (the file
/// keeps the manual marker and is not auto-uploaded on reconnect).
PhoneMicSessionMode selectPhoneMicSessionMode({
required bool supportsBatch,
required bool batchModeEnabled,
required bool hasNetwork,
}) {
if (!supportsBatch) return PhoneMicSessionMode.live;
if (batchModeEnabled) return PhoneMicSessionMode.batchExplicit;
if (!hasNetwork) return PhoneMicSessionMode.batchAuto;
return PhoneMicSessionMode.live;
}
/// Metadata parsed from a batch recording filename written by the native layer:
///
/// audio_{device}_{codec}_{sampleRate}_{channel}_fs{frameSize}_{timestamp}.bin
///
/// Mirrors how the backend `/v2/sync-local-files` pipeline interprets the name:
/// codec is detected from the `_pcm16_`/`_pcm8_` markers (otherwise opus), the
/// frame size from `_fs<n>`, and the timestamp from the trailing segment
/// (milliseconds are normalized to seconds). Kept pure so it can be unit tested.
class BatchRecordingInfo {
/// Recording start time, unix seconds.
final int timerStart;
final BleAudioCodec codec;
final int frameSize;
final int sampleRate;
const BatchRecordingInfo({
required this.timerStart,
required this.codec,
required this.frameSize,
this.sampleRate = 16000,
});
/// Returns null if [name] is not a parseable, finalized batch `.bin` filename
/// (e.g. a `.bin.part` in-progress file, or an unrelated file).
static BatchRecordingInfo? fromFileName(String name) {
if (!name.startsWith('audio_') || !name.endsWith('.bin')) return null;
final base = name.substring(0, name.length - 4); // strip ".bin"
final ts = int.tryParse(base.split('_').last);
if (ts == null) return null;
final timerStart = ts > 100000000000 ? ts ~/ 1000 : ts; // ms -> s
final fsMatch = RegExp(r'_fs(\d+)').firstMatch(name);
final frameSize = fsMatch != null ? int.parse(fsMatch.group(1)!) : 160;
final srMatch = RegExp(r'_(\d+)_\d+_fs\d+_\d+\.bin$').firstMatch(name);
final sampleRate = srMatch != null ? int.parse(srMatch.group(1)!) : 16000;
final BleAudioCodec codec;
if (name.contains('_pcm16_')) {
codec = BleAudioCodec.pcm16;
} else if (name.contains('_pcm8_')) {
codec = BleAudioCodec.pcm8;
} else {
codec = frameSize == 320 ? BleAudioCodec.opusFS320 : BleAudioCodec.opus;
}
return BatchRecordingInfo(timerStart: timerStart, codec: codec, frameSize: frameSize, sampleRate: sampleRate);
}
/// Exact duration in seconds from the decoded frame count. Each length-prefixed
/// frame decodes to [frameSize] samples (the backend uses the same), so the audio
/// duration is `frames * frameSize / sampleRate` — independent of opus VBR bitrate.
/// Preferred over [estimateSeconds], which only sees the byte size.
int secondsFromFrameCount(int frames) {
if (sampleRate <= 0 || frameSize <= 0) return 1;
return (frames * frameSize / sampleRate).round().clamp(1, 24 * 3600);
}
/// Rough duration in seconds from file size — for display/stats only. The
/// backend recomputes the exact duration from the decoded audio. Accounts for
/// ~16 kbps opus plus the 4-byte per-frame length prefix, or raw PCM rates.
int estimateSeconds(int sizeBytes) {
final bytesPerSec = codec == BleAudioCodec.pcm16
? 32200
: codec == BleAudioCodec.pcm8
? 16100
: 2400;
return (sizeBytes / bytesPerSec).round().clamp(1, 24 * 3600);
}
}