neoagent 3.1.1-beta.9 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +322 -364
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +26 -122
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +88081 -90036
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- package/server/services/voice/turnCoordinator.js +0 -51
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import 'dart:async';
|
|
2
|
-
import 'dart:typed_data';
|
|
3
|
-
|
|
4
|
-
/// Metadata plus bytes for one recording chunk awaiting upload.
|
|
5
|
-
class PendingChunk {
|
|
6
|
-
const PendingChunk({
|
|
7
|
-
required this.sourceKey,
|
|
8
|
-
required this.sequence,
|
|
9
|
-
required this.startMs,
|
|
10
|
-
required this.endMs,
|
|
11
|
-
required this.mimeType,
|
|
12
|
-
required this.bytes,
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
final String sourceKey;
|
|
16
|
-
final int sequence;
|
|
17
|
-
final int startMs;
|
|
18
|
-
final int endMs;
|
|
19
|
-
final String mimeType;
|
|
20
|
-
final Uint8List bytes;
|
|
21
|
-
|
|
22
|
-
Map<String, Object?> toMeta() => <String, Object?>{
|
|
23
|
-
'sourceKey': sourceKey,
|
|
24
|
-
'sequence': sequence,
|
|
25
|
-
'startMs': startMs,
|
|
26
|
-
'endMs': endMs,
|
|
27
|
-
'mimeType': mimeType,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
static PendingChunk fromMeta(Map<String, Object?> meta, Uint8List bytes) {
|
|
31
|
-
return PendingChunk(
|
|
32
|
-
sourceKey: '${meta['sourceKey']}',
|
|
33
|
-
sequence: (meta['sequence'] as num?)?.toInt() ?? 0,
|
|
34
|
-
startMs: (meta['startMs'] as num?)?.toInt() ?? 0,
|
|
35
|
-
endMs: (meta['endMs'] as num?)?.toInt() ?? 0,
|
|
36
|
-
mimeType: meta['mimeType'] as String? ?? 'application/octet-stream',
|
|
37
|
-
bytes: bytes,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/// Uploads a single chunk. Must throw on any failure so the queue retries.
|
|
43
|
-
typedef ChunkUploader = Future<void> Function(PendingChunk chunk);
|
|
44
|
-
|
|
45
|
-
typedef QueueLogger =
|
|
46
|
-
void Function(String event, {Map<String, Object?> data, Object? error});
|
|
47
|
-
|
|
48
|
-
/// In-memory, ordered, retry-until-success upload queue.
|
|
49
|
-
///
|
|
50
|
-
/// Chunks upload one at a time in enqueue order. A chunk that fails is kept and
|
|
51
|
-
/// retried (with backoff) until it succeeds or [dispose] is called, so a
|
|
52
|
-
/// transient network failure never silently drops audio while new chunks keep
|
|
53
|
-
/// buffering. Used on web, where no filesystem is available; the queue lives
|
|
54
|
-
/// only for the lifetime of the tab.
|
|
55
|
-
class MemoryChunkUploadQueue {
|
|
56
|
-
MemoryChunkUploadQueue({
|
|
57
|
-
required ChunkUploader uploader,
|
|
58
|
-
this.maxAttemptsPerPass = 3,
|
|
59
|
-
this.retryBackoff = const Duration(seconds: 2),
|
|
60
|
-
QueueLogger? logger,
|
|
61
|
-
}) : _uploader = uploader,
|
|
62
|
-
_logger = logger;
|
|
63
|
-
|
|
64
|
-
final ChunkUploader _uploader;
|
|
65
|
-
final int maxAttemptsPerPass;
|
|
66
|
-
final Duration retryBackoff;
|
|
67
|
-
final QueueLogger? _logger;
|
|
68
|
-
|
|
69
|
-
final List<PendingChunk> _pending = <PendingChunk>[];
|
|
70
|
-
bool _draining = false;
|
|
71
|
-
bool _disposed = false;
|
|
72
|
-
Timer? _retryTimer;
|
|
73
|
-
|
|
74
|
-
int get pendingCount => _pending.length;
|
|
75
|
-
|
|
76
|
-
void enqueue(PendingChunk chunk) {
|
|
77
|
-
if (_disposed) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
_pending.add(chunk);
|
|
81
|
-
unawaited(drain());
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
Future<void> drain() async {
|
|
85
|
-
if (_draining || _disposed) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
_draining = true;
|
|
89
|
-
try {
|
|
90
|
-
while (_pending.isNotEmpty && !_disposed) {
|
|
91
|
-
final chunk = _pending.first;
|
|
92
|
-
final uploaded = await _attemptUpload(chunk);
|
|
93
|
-
if (!uploaded) {
|
|
94
|
-
_scheduleRetry();
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
if (_pending.isNotEmpty && identical(_pending.first, chunk)) {
|
|
98
|
-
_pending.removeAt(0);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
} finally {
|
|
102
|
-
_draining = false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
Future<bool> _attemptUpload(PendingChunk chunk) async {
|
|
107
|
-
for (var attempt = 0; attempt < maxAttemptsPerPass && !_disposed; attempt += 1) {
|
|
108
|
-
try {
|
|
109
|
-
await _uploader(chunk);
|
|
110
|
-
return true;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
_logger?.call(
|
|
113
|
-
'chunk.upload.failed',
|
|
114
|
-
data: <String, Object?>{
|
|
115
|
-
'sourceKey': chunk.sourceKey,
|
|
116
|
-
'sequence': chunk.sequence,
|
|
117
|
-
'attempt': attempt + 1,
|
|
118
|
-
},
|
|
119
|
-
error: error,
|
|
120
|
-
);
|
|
121
|
-
await Future<void>.delayed(retryBackoff * (attempt + 1));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
void _scheduleRetry() {
|
|
128
|
-
_retryTimer?.cancel();
|
|
129
|
-
_retryTimer = Timer(retryBackoff, () => unawaited(drain()));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// Wait until every queued chunk has uploaded, or [timeout] elapses.
|
|
133
|
-
Future<void> flush({Duration timeout = const Duration(seconds: 30)}) async {
|
|
134
|
-
final deadline = DateTime.now().add(timeout);
|
|
135
|
-
while (_pending.isNotEmpty && !_disposed && DateTime.now().isBefore(deadline)) {
|
|
136
|
-
await drain();
|
|
137
|
-
if (_pending.isNotEmpty) {
|
|
138
|
-
await Future<void>.delayed(const Duration(milliseconds: 200));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Future<void> dispose() async {
|
|
144
|
-
_disposed = true;
|
|
145
|
-
_retryTimer?.cancel();
|
|
146
|
-
_retryTimer = null;
|
|
147
|
-
_pending.clear();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import 'dart:async';
|
|
2
|
-
import 'dart:convert';
|
|
3
|
-
import 'dart:io';
|
|
4
|
-
|
|
5
|
-
import 'recording_chunk_queue.dart';
|
|
6
|
-
|
|
7
|
-
/// Disk-backed, ordered, retry-until-success upload queue for desktop.
|
|
8
|
-
///
|
|
9
|
-
/// Each chunk is persisted atomically before upload, so a network outage or
|
|
10
|
-
/// backend hiccup mid-meeting never loses audio: the chunk stays on disk and is
|
|
11
|
-
/// re-sent on the next drain. Files are deleted only after a confirmed upload.
|
|
12
|
-
class DiskChunkUploadQueue {
|
|
13
|
-
DiskChunkUploadQueue({
|
|
14
|
-
required Directory directory,
|
|
15
|
-
required ChunkUploader uploader,
|
|
16
|
-
this.maxAttemptsPerPass = 3,
|
|
17
|
-
this.retryBackoff = const Duration(seconds: 2),
|
|
18
|
-
QueueLogger? logger,
|
|
19
|
-
}) : _dir = directory,
|
|
20
|
-
_uploader = uploader,
|
|
21
|
-
_logger = logger;
|
|
22
|
-
|
|
23
|
-
final Directory _dir;
|
|
24
|
-
final ChunkUploader _uploader;
|
|
25
|
-
final int maxAttemptsPerPass;
|
|
26
|
-
final Duration retryBackoff;
|
|
27
|
-
final QueueLogger? _logger;
|
|
28
|
-
|
|
29
|
-
bool _draining = false;
|
|
30
|
-
bool _disposed = false;
|
|
31
|
-
Timer? _retryTimer;
|
|
32
|
-
final Set<Future<void>> _pendingEnqueues = <Future<void>>{};
|
|
33
|
-
|
|
34
|
-
Future<void> enqueue(PendingChunk chunk) async {
|
|
35
|
-
if (_disposed) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
final write = _persistChunk(chunk);
|
|
39
|
-
_pendingEnqueues.add(write);
|
|
40
|
-
try {
|
|
41
|
-
await write;
|
|
42
|
-
} finally {
|
|
43
|
-
_pendingEnqueues.remove(write);
|
|
44
|
-
}
|
|
45
|
-
unawaited(drain());
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
Future<void> drain() async {
|
|
49
|
-
if (_draining || _disposed) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
_draining = true;
|
|
53
|
-
try {
|
|
54
|
-
while (!_disposed) {
|
|
55
|
-
final metaFiles = await _listMetaFiles();
|
|
56
|
-
if (metaFiles.isEmpty) {
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
final metaFile = metaFiles.first;
|
|
60
|
-
final audioFile = File(
|
|
61
|
-
metaFile.path.replaceFirst(RegExp(r'\.json$'), '.bin'),
|
|
62
|
-
);
|
|
63
|
-
if (!await audioFile.exists()) {
|
|
64
|
-
await _deleteQuietly(metaFile);
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
final PendingChunk chunk;
|
|
69
|
-
try {
|
|
70
|
-
final meta =
|
|
71
|
-
jsonDecode(await metaFile.readAsString()) as Map<String, dynamic>;
|
|
72
|
-
chunk = PendingChunk.fromMeta(meta, await audioFile.readAsBytes());
|
|
73
|
-
} catch (error) {
|
|
74
|
-
// Corrupt metadata: drop the pair rather than blocking the queue.
|
|
75
|
-
_logger?.call('chunk.read.failed', error: error);
|
|
76
|
-
await _deleteQuietly(metaFile);
|
|
77
|
-
await _deleteQuietly(audioFile);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
final uploaded = await _attemptUpload(chunk);
|
|
82
|
-
if (!uploaded) {
|
|
83
|
-
_scheduleRetry();
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
await _deleteQuietly(audioFile);
|
|
87
|
-
await _deleteQuietly(metaFile);
|
|
88
|
-
}
|
|
89
|
-
} finally {
|
|
90
|
-
_draining = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
Future<bool> _attemptUpload(PendingChunk chunk) async {
|
|
95
|
-
for (var attempt = 0; attempt < maxAttemptsPerPass && !_disposed; attempt += 1) {
|
|
96
|
-
try {
|
|
97
|
-
await _uploader(chunk);
|
|
98
|
-
return true;
|
|
99
|
-
} catch (error) {
|
|
100
|
-
_logger?.call(
|
|
101
|
-
'chunk.upload.failed',
|
|
102
|
-
data: <String, Object?>{
|
|
103
|
-
'sourceKey': chunk.sourceKey,
|
|
104
|
-
'sequence': chunk.sequence,
|
|
105
|
-
'attempt': attempt + 1,
|
|
106
|
-
},
|
|
107
|
-
error: error,
|
|
108
|
-
);
|
|
109
|
-
await Future<void>.delayed(retryBackoff * (attempt + 1));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
Future<List<File>> _listMetaFiles() async {
|
|
116
|
-
if (!await _dir.exists()) {
|
|
117
|
-
return const <File>[];
|
|
118
|
-
}
|
|
119
|
-
final entries = await _dir.list().toList();
|
|
120
|
-
final metaFiles = entries
|
|
121
|
-
.whereType<File>()
|
|
122
|
-
.where((file) => file.path.endsWith('.json'))
|
|
123
|
-
.toList()
|
|
124
|
-
..sort((a, b) => a.path.compareTo(b.path));
|
|
125
|
-
return metaFiles;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
Future<int> pendingCount() async => (await _listMetaFiles()).length;
|
|
129
|
-
|
|
130
|
-
Future<void> _persistChunk(PendingChunk chunk) async {
|
|
131
|
-
await _dir.create(recursive: true);
|
|
132
|
-
final base = '${chunk.sourceKey}-${chunk.sequence.toString().padLeft(6, '0')}';
|
|
133
|
-
final audioFile = File('${_dir.path}/$base.bin');
|
|
134
|
-
final metaFile = File('${_dir.path}/$base.json');
|
|
135
|
-
final audioTmp = File('${audioFile.path}.tmp');
|
|
136
|
-
final metaTmp = File('${metaFile.path}.tmp');
|
|
137
|
-
await audioTmp.writeAsBytes(chunk.bytes, flush: true);
|
|
138
|
-
await metaTmp.writeAsString(jsonEncode(chunk.toMeta()), flush: true);
|
|
139
|
-
await audioTmp.rename(audioFile.path);
|
|
140
|
-
await metaTmp.rename(metaFile.path);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Future<void> _waitForPendingEnqueues() async {
|
|
144
|
-
while (_pendingEnqueues.isNotEmpty && !_disposed) {
|
|
145
|
-
await Future.wait<void>(List<Future<void>>.from(_pendingEnqueues));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
void _scheduleRetry() {
|
|
150
|
-
_retryTimer?.cancel();
|
|
151
|
-
_retryTimer = Timer(retryBackoff, () => unawaited(drain()));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
Future<void> _deleteQuietly(File file) async {
|
|
155
|
-
try {
|
|
156
|
-
if (await file.exists()) {
|
|
157
|
-
await file.delete();
|
|
158
|
-
}
|
|
159
|
-
} catch (error) {
|
|
160
|
-
_logger?.call('chunk.delete.failed', error: error);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/// Wait until every persisted chunk has uploaded, or [timeout] elapses.
|
|
165
|
-
Future<void> flush({Duration timeout = const Duration(seconds: 30)}) async {
|
|
166
|
-
final deadline = DateTime.now().add(timeout);
|
|
167
|
-
while (!_disposed && DateTime.now().isBefore(deadline)) {
|
|
168
|
-
await _waitForPendingEnqueues();
|
|
169
|
-
await drain();
|
|
170
|
-
if (_pendingEnqueues.isEmpty && await pendingCount() == 0) {
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
await Future<void>.delayed(const Duration(milliseconds: 200));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
Future<void> dispose() async {
|
|
178
|
-
_disposed = true;
|
|
179
|
-
_retryTimer?.cancel();
|
|
180
|
-
_retryTimer = null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
Map<String, dynamic> buildWebScreenAndMicRecordingPayload() {
|
|
2
|
-
return <String, dynamic>{
|
|
3
|
-
'platform': 'web',
|
|
4
|
-
'screenAnalysisReady': true,
|
|
5
|
-
'sources': const <Map<String, dynamic>>[
|
|
6
|
-
<String, dynamic>{
|
|
7
|
-
'sourceKey': 'screen',
|
|
8
|
-
'sourceKind': 'screen-share',
|
|
9
|
-
'mediaKind': 'video',
|
|
10
|
-
'mimeType': 'video/webm',
|
|
11
|
-
'metadata': <String, dynamic>{
|
|
12
|
-
'analysisReady': true,
|
|
13
|
-
'transcribe': false,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
<String, dynamic>{
|
|
17
|
-
'sourceKey': 'system',
|
|
18
|
-
'sourceKind': 'system-audio',
|
|
19
|
-
'mediaKind': 'audio',
|
|
20
|
-
'mimeType': 'audio/webm',
|
|
21
|
-
'metadata': <String, dynamic>{
|
|
22
|
-
'transcribe': true,
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
<String, dynamic>{
|
|
26
|
-
'sourceKey': 'microphone',
|
|
27
|
-
'sourceKind': 'microphone',
|
|
28
|
-
'mediaKind': 'audio',
|
|
29
|
-
'mimeType': 'audio/webm',
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
Map<String, dynamic> buildWebMicrophoneRecordingPayload() {
|
|
36
|
-
return <String, dynamic>{
|
|
37
|
-
'platform': 'web',
|
|
38
|
-
'screenAnalysisReady': false,
|
|
39
|
-
'sources': const <Map<String, dynamic>>[
|
|
40
|
-
<String, dynamic>{
|
|
41
|
-
'sourceKey': 'microphone',
|
|
42
|
-
'sourceKind': 'microphone',
|
|
43
|
-
'mediaKind': 'audio',
|
|
44
|
-
'mimeType': 'audio/webm',
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
Map<String, dynamic> buildAndroidBackgroundRecordingPayload() {
|
|
51
|
-
return <String, dynamic>{
|
|
52
|
-
'platform': 'android',
|
|
53
|
-
'screenAnalysisReady': false,
|
|
54
|
-
'sources': const <Map<String, dynamic>>[
|
|
55
|
-
<String, dynamic>{
|
|
56
|
-
'sourceKey': 'microphone',
|
|
57
|
-
'sourceKind': 'microphone',
|
|
58
|
-
'mediaKind': 'audio',
|
|
59
|
-
'mimeType': 'audio/wav',
|
|
60
|
-
'metadata': <String, dynamic>{'backgroundCapable': true},
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
Map<String, dynamic> buildDesktopRecordingPayload() {
|
|
67
|
-
return <String, dynamic>{
|
|
68
|
-
'platform': 'desktop',
|
|
69
|
-
'screenAnalysisReady': false,
|
|
70
|
-
'sources': const <Map<String, dynamic>>[
|
|
71
|
-
<String, dynamic>{
|
|
72
|
-
'sourceKey': 'microphone',
|
|
73
|
-
'sourceKind': 'microphone',
|
|
74
|
-
'mediaKind': 'audio',
|
|
75
|
-
'mimeType': 'audio/wav',
|
|
76
|
-
'metadata': <String, dynamic>{
|
|
77
|
-
'backgroundCapable': true,
|
|
78
|
-
'foregroundRole': 'user',
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
<String, dynamic>{
|
|
82
|
-
'sourceKey': 'system',
|
|
83
|
-
'sourceKind': 'system-audio',
|
|
84
|
-
'mediaKind': 'audio',
|
|
85
|
-
'mimeType': 'audio/wav',
|
|
86
|
-
'metadata': <String, dynamic>{
|
|
87
|
-
'backgroundCapable': true,
|
|
88
|
-
'floatingToolbarCapable': true,
|
|
89
|
-
'globalHotkeyReady': true,
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
'capturePlan': 'desktop-dual-source',
|
|
94
|
-
};
|
|
95
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 desktop_audio_capture
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
# desktop_audio_capture
|
|
2
|
-
|
|
3
|
-
A Flutter plugin for capturing audio on desktop platforms (macOS, Linux, Windows). Supports capturing from microphone and system audio with features like audio data streaming, decibel monitoring, device management, and permission handling.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🎤 **Microphone Capture**: Capture audio from microphone with flexible configuration
|
|
8
|
-
- 🔊 **System Audio Capture**: Capture audio from system (system audio)
|
|
9
|
-
- 📊 **Decibel Monitoring**: Real-time audio level monitoring
|
|
10
|
-
- 🎛️ **Device Management**: List and manage available input devices
|
|
11
|
-
- ⚙️ **Configurable**: Configure sample rate, channels, bit depth, gain boost
|
|
12
|
-
- 🔐 **Permissions Handling**: Automatic permission handling for microphone and screen recording
|
|
13
|
-
|
|
14
|
-
## Platform Support
|
|
15
|
-
|
|
16
|
-
- ✅ **macOS 13.0+** (Ventura or later) - Fully tested and working
|
|
17
|
-
- ✅ **Linux** - Supported
|
|
18
|
-
- ✅ **Windows** - Supported
|
|
19
|
-
|
|
20
|
-
> **Note**:
|
|
21
|
-
> - macOS requires **macOS 13.0 (Ventura) or later** because System Audio Capture uses APIs available from macOS 13.0+
|
|
22
|
-
|
|
23
|
-
## Screenshots
|
|
24
|
-
|
|
25
|
-
### Home Screen
|
|
26
|
-

|
|
27
|
-
|
|
28
|
-
### Microphone Settings
|
|
29
|
-

|
|
30
|
-
|
|
31
|
-
### System Audio Settings
|
|
32
|
-

|
|
33
|
-
|
|
34
|
-
## Installation
|
|
35
|
-
|
|
36
|
-
Add the dependency to your `pubspec.yaml`:
|
|
37
|
-
|
|
38
|
-
```yaml
|
|
39
|
-
dependencies:
|
|
40
|
-
desktop_audio_capture: ^0.0.1
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Then run:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
flutter pub get
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Usage
|
|
50
|
-
|
|
51
|
-
### Microphone Capture
|
|
52
|
-
|
|
53
|
-
```dart
|
|
54
|
-
import 'package:desktop_audio_capture/audio_capture.dart';
|
|
55
|
-
|
|
56
|
-
// Create instance
|
|
57
|
-
final micCapture = MicAudioCapture(
|
|
58
|
-
config: MicAudioConfig(
|
|
59
|
-
sampleRate: 16000,
|
|
60
|
-
channels: 1,
|
|
61
|
-
bitDepth: 16,
|
|
62
|
-
gainBoost: 2.5,
|
|
63
|
-
inputVolume: 1.0,
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// Start capture
|
|
68
|
-
await micCapture.startCapture();
|
|
69
|
-
|
|
70
|
-
// Listen to audio stream
|
|
71
|
-
micCapture.audioStream?.listen((audioData) {
|
|
72
|
-
// Process audio data (Uint8List)
|
|
73
|
-
print('Received ${audioData.length} bytes');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Listen to decibel readings
|
|
77
|
-
micCapture.decibelStream?.listen((decibelData) {
|
|
78
|
-
print('Decibel: ${decibelData.decibel} dB');
|
|
79
|
-
print('Timestamp: ${decibelData.timestamp}');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Listen to status updates
|
|
83
|
-
micCapture.statusStream?.listen((status) {
|
|
84
|
-
print('Mic active: ${status.isActive}');
|
|
85
|
-
if (status.deviceName != null) {
|
|
86
|
-
print('Device: ${status.deviceName}');
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Stop capture
|
|
91
|
-
await micCapture.stopCapture();
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### System Audio Capture
|
|
95
|
-
|
|
96
|
-
```dart
|
|
97
|
-
import 'package:desktop_audio_capture/audio_capture.dart';
|
|
98
|
-
|
|
99
|
-
// Create instance
|
|
100
|
-
final systemCapture = SystemAudioCapture(
|
|
101
|
-
config: SystemAudioConfig(
|
|
102
|
-
sampleRate: 44100,
|
|
103
|
-
channels: 2,
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
// Start capture
|
|
108
|
-
await systemCapture.startCapture();
|
|
109
|
-
|
|
110
|
-
// Listen to audio stream
|
|
111
|
-
systemCapture.audioStream?.listen((audioData) {
|
|
112
|
-
// Process audio data (Uint8List)
|
|
113
|
-
print('Received ${audioData.length} bytes');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Listen to decibel readings
|
|
117
|
-
systemCapture.decibelStream?.listen((decibelData) {
|
|
118
|
-
print('Decibel: ${decibelData.decibel} dB');
|
|
119
|
-
print('Timestamp: ${decibelData.timestamp}');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Listen to status updates
|
|
123
|
-
systemCapture.statusStream?.listen((status) {
|
|
124
|
-
print('System audio active: ${status.isActive}');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Stop capture
|
|
128
|
-
await systemCapture.stopCapture();
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Device Management (Microphone)
|
|
132
|
-
|
|
133
|
-
```dart
|
|
134
|
-
// Check if input device is available
|
|
135
|
-
final hasDevice = await micCapture.hasInputDevice();
|
|
136
|
-
|
|
137
|
-
// Get list of available devices
|
|
138
|
-
final devices = await micCapture.getAvailableInputDevices();
|
|
139
|
-
for (final device in devices) {
|
|
140
|
-
print('Device: ${device.name}');
|
|
141
|
-
print('Type: ${device.type.toString()}');
|
|
142
|
-
print('Channels: ${device.channelCount}');
|
|
143
|
-
print('Default: ${device.isDefault}');
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
## API Reference
|
|
148
|
-
|
|
149
|
-
### MicAudioCapture
|
|
150
|
-
|
|
151
|
-
#### Methods
|
|
152
|
-
|
|
153
|
-
- `startCapture({MicAudioConfig? config})`: Start capturing from microphone
|
|
154
|
-
- `stopCapture()`: Stop capture
|
|
155
|
-
- `requestPermissions()`: Request microphone access permission
|
|
156
|
-
- `hasInputDevice()`: Check if input device is available
|
|
157
|
-
- `getAvailableInputDevices()`: Get list of available input devices
|
|
158
|
-
- `updateConfig(MicAudioConfig config)`: Update configuration
|
|
159
|
-
|
|
160
|
-
#### Streams
|
|
161
|
-
|
|
162
|
-
- `audioStream`: Stream of audio data (Uint8List)
|
|
163
|
-
- `decibelStream`: Stream of decibel readings (DecibelData)
|
|
164
|
-
- `statusStream`: Stream of status updates (MicAudioStatus)
|
|
165
|
-
|
|
166
|
-
#### Properties
|
|
167
|
-
|
|
168
|
-
- `isRecording`: Whether currently recording or not
|
|
169
|
-
|
|
170
|
-
### SystemAudioCapture
|
|
171
|
-
|
|
172
|
-
#### Methods
|
|
173
|
-
|
|
174
|
-
- `startCapture({SystemAudioConfig? config})`: Start capturing system audio
|
|
175
|
-
- `stopCapture()`: Stop capture
|
|
176
|
-
- `requestPermissions()`: Request screen recording permission (macOS)
|
|
177
|
-
- `updateConfig(SystemAudioConfig config)`: Update configuration
|
|
178
|
-
|
|
179
|
-
#### Streams
|
|
180
|
-
|
|
181
|
-
- `audioStream`: Stream of audio data (Uint8List)
|
|
182
|
-
- `decibelStream`: Stream of decibel readings (DecibelData)
|
|
183
|
-
- `statusStream`: Stream of status updates (SystemAudioStatus)
|
|
184
|
-
|
|
185
|
-
#### Properties
|
|
186
|
-
|
|
187
|
-
- `isRecording`: Whether currently recording or not
|
|
188
|
-
|
|
189
|
-
### MicAudioConfig
|
|
190
|
-
|
|
191
|
-
- `sampleRate` (int): Sample rate (default: 16000 Hz)
|
|
192
|
-
- `channels` (int): Number of audio channels (default: 1)
|
|
193
|
-
- `bitDepth` (int): Bit depth (default: 16)
|
|
194
|
-
- `gainBoost` (double): Gain boost multiplier (default: 2.5, range: 0.1-10.0)
|
|
195
|
-
- `inputVolume` (double): Input volume (default: 1.0, range: 0.0-1.0)
|
|
196
|
-
|
|
197
|
-
### SystemAudioConfig
|
|
198
|
-
|
|
199
|
-
- `sampleRate` (int): Sample rate (default: 16000 Hz)
|
|
200
|
-
- `channels` (int): Number of audio channels (default: 1)
|
|
201
|
-
|
|
202
|
-
### DecibelData
|
|
203
|
-
|
|
204
|
-
- `decibel` (double): Decibel value (-120 to 0 dB)
|
|
205
|
-
- `timestamp` (double): Unix timestamp in seconds
|
|
206
|
-
|
|
207
|
-
### MicAudioStatus
|
|
208
|
-
|
|
209
|
-
- `isActive` (bool): Whether microphone capture is currently active
|
|
210
|
-
- `deviceName` (String?): Name of the microphone device (if available)
|
|
211
|
-
|
|
212
|
-
### SystemAudioStatus
|
|
213
|
-
|
|
214
|
-
- `isActive` (bool): Whether system audio capture is currently active
|
|
215
|
-
|
|
216
|
-
### InputDevice
|
|
217
|
-
|
|
218
|
-
- `id` (String): Unique identifier of the device
|
|
219
|
-
- `name` (String): Human-readable name of the device
|
|
220
|
-
- `type` (InputDeviceType): Type of device (builtIn, bluetooth, external)
|
|
221
|
-
- `channelCount` (int): Number of audio channels supported
|
|
222
|
-
- `isDefault` (bool): Whether this device is the system default input device
|
|
223
|
-
|
|
224
|
-
## Permissions
|
|
225
|
-
|
|
226
|
-
### macOS (13.0+)
|
|
227
|
-
|
|
228
|
-
**System Requirements**: macOS 13.0 (Ventura) or later is required.
|
|
229
|
-
|
|
230
|
-
- **Microphone**: Requires microphone access permission in `Info.plist`:
|
|
231
|
-
```xml
|
|
232
|
-
<key>NSMicrophoneUsageDescription</key>
|
|
233
|
-
<string>This app needs access to the microphone to capture audio.</string>
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
- **System Audio**: Requires Screen Recording permission in System Preferences. Add to `Info.plist`:
|
|
237
|
-
```xml
|
|
238
|
-
<key>NSScreenCaptureDescription</key>
|
|
239
|
-
<string>This app needs Screen Recording permission to capture system audio.</string>
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
After adding to `Info.plist`, users need to grant permission in **System Preferences > Security & Privacy > Screen Recording**.
|
|
243
|
-
|
|
244
|
-
### Linux
|
|
245
|
-
|
|
246
|
-
- Requires access to audio devices (usually automatic)
|
|
247
|
-
|
|
248
|
-
### Windows
|
|
249
|
-
|
|
250
|
-
- Requires microphone access (usually automatic)
|
|
251
|
-
|
|
252
|
-
## Example
|
|
253
|
-
|
|
254
|
-
See detailed example in the [example](example/) directory.
|
|
255
|
-
|
|
256
|
-
## License
|
|
257
|
-
|
|
258
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
259
|
-
|
|
260
|
-
## Homepage
|
|
261
|
-
|
|
262
|
-
[https://github.com/hiuhuyn/desktop_audio_capture](https://github.com/hiuhuyn/desktop_audio_capture)
|