arcane-os 0.94.0 → 0.95.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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +195 -0
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/native-magpie.md +98 -0
- package/docs/reference/native-nemotron.md +204 -0
- package/docs/reference/native-speech.md +145 -1
- package/package.json +5 -1
- package/runtime/arcane/modules/AI.js +114 -14
- package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
- package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
- package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
- package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
- package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
- package/src/core/services/speech.mjs +235 -2
- package/src/local-ai/magpie/build.mjs +36 -0
- package/src/local-ai/magpie/helper.mjs +254 -0
- package/src/local-ai/magpie/index.mjs +488 -0
- package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
- package/src/local-ai/magpie/native/main.cpp +270 -0
- package/src/local-ai/nemotron/build.mjs +53 -0
- package/src/local-ai/nemotron/helper.mjs +308 -0
- package/src/local-ai/nemotron/index.mjs +637 -0
- package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
- package/src/local-ai/nemotron/native/main.cpp +282 -0
|
@@ -4,8 +4,8 @@ import {arcaneLogging} from 'arcane-os/logging';
|
|
|
4
4
|
const is = new Is(false);
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Browser microphone capture with activity-selected
|
|
8
|
-
*
|
|
7
|
+
* Browser microphone capture with activity-selected WAV clips or continuous PCM.
|
|
8
|
+
* PCM callbacks settle in capture order without delaying the audio worklet.
|
|
9
9
|
* stop() delivers the final active clip; cancel() abandons pending delivery.
|
|
10
10
|
*/
|
|
11
11
|
export default class ContinuousVoiceCapture {
|
|
@@ -13,6 +13,8 @@ export default class ContinuousVoiceCapture {
|
|
|
13
13
|
#onSegment;
|
|
14
14
|
#onError;
|
|
15
15
|
#onState;
|
|
16
|
+
#onPCM;
|
|
17
|
+
#onActivity;
|
|
16
18
|
#session = null;
|
|
17
19
|
|
|
18
20
|
constructor(
|
|
@@ -21,7 +23,11 @@ export default class ContinuousVoiceCapture {
|
|
|
21
23
|
quietMs = 2000,
|
|
22
24
|
chunkMs = 30000,
|
|
23
25
|
activityThreshold = 0.02,
|
|
26
|
+
mode = 'segments',
|
|
27
|
+
pcmChunkMs = 100,
|
|
24
28
|
onSegment,
|
|
29
|
+
onPCM,
|
|
30
|
+
onActivity,
|
|
25
31
|
onError,
|
|
26
32
|
onState
|
|
27
33
|
} = {}
|
|
@@ -38,8 +44,16 @@ export default class ContinuousVoiceCapture {
|
|
|
38
44
|
if (!is.finite(activityThreshold) || activityThreshold < 0) {
|
|
39
45
|
throw new RangeError('activityThreshold must be a finite nonnegative amplitude.');
|
|
40
46
|
}
|
|
41
|
-
|
|
47
|
+
if (!['segments', 'pcm'].includes(mode)) {
|
|
48
|
+
throw new TypeError('Voice capture mode must be segments or pcm.');
|
|
49
|
+
}
|
|
50
|
+
if (!is.finite(pcmChunkMs) || pcmChunkMs <= 0) {
|
|
51
|
+
throw new RangeError('pcmChunkMs must be a finite positive duration.');
|
|
52
|
+
}
|
|
53
|
+
this.#options = {preRollMs, quietMs, chunkMs, activityThreshold, mode, pcmChunkMs};
|
|
42
54
|
this.#onSegment = onSegment;
|
|
55
|
+
this.#onPCM = onPCM;
|
|
56
|
+
this.#onActivity = onActivity;
|
|
43
57
|
this.#onError = onError;
|
|
44
58
|
this.#onState = onState;
|
|
45
59
|
}
|
|
@@ -47,10 +61,11 @@ export default class ContinuousVoiceCapture {
|
|
|
47
61
|
async start(
|
|
48
62
|
{
|
|
49
63
|
mediaConstraints = {audio: true},
|
|
64
|
+
sampleRate,
|
|
50
65
|
signal
|
|
51
66
|
} = {}
|
|
52
67
|
) {
|
|
53
|
-
if (
|
|
68
|
+
if (this.#session || signal?.aborted) return false;
|
|
54
69
|
|
|
55
70
|
const session = {
|
|
56
71
|
phase: 'starting',
|
|
@@ -62,8 +77,12 @@ export default class ContinuousVoiceCapture {
|
|
|
62
77
|
node: null,
|
|
63
78
|
cleanup: [],
|
|
64
79
|
errors: [],
|
|
80
|
+
pcmDelivery: Promise.resolve(),
|
|
81
|
+
flushes: new Map(),
|
|
82
|
+
flushSequence: 0,
|
|
65
83
|
resolveStartup: null,
|
|
66
|
-
resolveStop: null
|
|
84
|
+
resolveStop: null,
|
|
85
|
+
rejectStop: null
|
|
67
86
|
};
|
|
68
87
|
session.startupCancelled = new Promise(
|
|
69
88
|
function ownStartupCancellation(resolve) {
|
|
@@ -71,10 +90,12 @@ export default class ContinuousVoiceCapture {
|
|
|
71
90
|
}
|
|
72
91
|
);
|
|
73
92
|
session.stopped = new Promise(
|
|
74
|
-
function ownCaptureStop(resolve) {
|
|
93
|
+
function ownCaptureStop(resolve, reject) {
|
|
75
94
|
session.resolveStop = resolve;
|
|
95
|
+
session.rejectStop = reject;
|
|
76
96
|
}
|
|
77
97
|
);
|
|
98
|
+
session.stopped.catch(function observeCaptureStopFailure() {});
|
|
78
99
|
this.#session = session;
|
|
79
100
|
|
|
80
101
|
const owner = this;
|
|
@@ -101,7 +122,7 @@ export default class ContinuousVoiceCapture {
|
|
|
101
122
|
this.#notify(session, this.#onState, 'starting');
|
|
102
123
|
if (!this.#starting(session)) return false;
|
|
103
124
|
|
|
104
|
-
const preparation = this.#prepare(session, mediaConstraints);
|
|
125
|
+
const preparation = this.#prepare(session, mediaConstraints, sampleRate);
|
|
105
126
|
return Promise.race(
|
|
106
127
|
[preparation, session.startupCancelled]
|
|
107
128
|
);
|
|
@@ -136,27 +157,46 @@ export default class ContinuousVoiceCapture {
|
|
|
136
157
|
|
|
137
158
|
cancel() {
|
|
138
159
|
const session = this.#session;
|
|
139
|
-
if (!session) return;
|
|
160
|
+
if (!session) return Promise.resolve();
|
|
140
161
|
session.cancelled = true;
|
|
141
162
|
this.#settle(session, 'stopped');
|
|
163
|
+
return session.stopped;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
flush() {
|
|
167
|
+
const session = this.#session;
|
|
168
|
+
if (!session || session.phase !== 'listening') return session?.pcmDelivery ?? Promise.resolve();
|
|
169
|
+
session.flushSequence += 1;
|
|
170
|
+
const id = session.flushSequence;
|
|
171
|
+
const task = new Promise(function ownCaptureFlush(resolve, reject) {
|
|
172
|
+
session.flushes.set(id, {resolve, reject});
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
session.node.port.postMessage({type: 'flush', id});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
session.flushes.delete(id);
|
|
178
|
+
this.#fail(session, error);
|
|
179
|
+
return Promise.reject(error);
|
|
180
|
+
}
|
|
181
|
+
return task;
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
destroy() {
|
|
145
|
-
this.cancel();
|
|
185
|
+
return this.cancel();
|
|
146
186
|
}
|
|
147
187
|
|
|
148
188
|
#starting(session) {
|
|
149
189
|
return this.#session === session && session.phase === 'starting';
|
|
150
190
|
}
|
|
151
191
|
|
|
152
|
-
async #prepare(session, mediaConstraints) {
|
|
192
|
+
async #prepare(session, mediaConstraints, sampleRate) {
|
|
153
193
|
try {
|
|
154
194
|
if (!is.function(globalThis.AudioContext)
|
|
155
195
|
|| !is.function(globalThis.AudioWorkletNode)
|
|
156
196
|
|| !is.function(globalThis.navigator?.mediaDevices?.getUserMedia)) {
|
|
157
197
|
throw new Error('Continuous voice capture requires microphone access and AudioWorklet.');
|
|
158
198
|
}
|
|
159
|
-
const context = new globalThis.AudioContext();
|
|
199
|
+
const context = new globalThis.AudioContext(sampleRate === undefined ? undefined : {sampleRate});
|
|
160
200
|
session.context = context;
|
|
161
201
|
if (!context.audioWorklet) {
|
|
162
202
|
throw new Error('This browser does not expose AudioWorklet microphone processing.');
|
|
@@ -226,6 +266,22 @@ export default class ContinuousVoiceCapture {
|
|
|
226
266
|
durationMs: message.durationMs
|
|
227
267
|
}
|
|
228
268
|
);
|
|
269
|
+
} else if (message.type === 'pcm') {
|
|
270
|
+
owner.#deliverPCM(session, {
|
|
271
|
+
samples: new Float32Array(message.audio),
|
|
272
|
+
sampleRate: message.sampleRate,
|
|
273
|
+
channels: 1,
|
|
274
|
+
sequence: message.sequence
|
|
275
|
+
});
|
|
276
|
+
} else if (message.type === 'activity') {
|
|
277
|
+
owner.#notify(session, owner.#onActivity, {
|
|
278
|
+
active: message.active,
|
|
279
|
+
audioTime: message.audioTime
|
|
280
|
+
});
|
|
281
|
+
} else if (message.type === 'flushed') {
|
|
282
|
+
const flush = session.flushes.get(message.id);
|
|
283
|
+
session.flushes.delete(message.id);
|
|
284
|
+
if (flush) session.pcmDelivery.then(flush.resolve, flush.reject);
|
|
229
285
|
} else if (message.type === 'stopped') {
|
|
230
286
|
owner.#settle(session, 'stopped');
|
|
231
287
|
} else if (message.type === 'error') {
|
|
@@ -321,6 +377,23 @@ export default class ContinuousVoiceCapture {
|
|
|
321
377
|
await context.resume();
|
|
322
378
|
}
|
|
323
379
|
|
|
380
|
+
#deliverPCM(session, value) {
|
|
381
|
+
const owner = this;
|
|
382
|
+
// Capture and inference run concurrently. This ordered delivery chain
|
|
383
|
+
// retains every sample while the consumer catches up; no activity gate
|
|
384
|
+
// selects or discards audio in continuous mode.
|
|
385
|
+
session.pcmDelivery = session.pcmDelivery.then(async function deliverCapturedPCM() {
|
|
386
|
+
if (!session.cancelled && is.function(owner.#onPCM)) {
|
|
387
|
+
await owner.#onPCM(value);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// Observe failure without converting the delivery/flush contract to success.
|
|
391
|
+
session.pcmDelivery.catch(function reportPCMDeliveryFailure(error) {
|
|
392
|
+
if (!session.settled) owner.#fail(session, error);
|
|
393
|
+
else owner.#reportError(session, error, true);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
324
397
|
#notify(session, callback, value) {
|
|
325
398
|
if (session.cancelled
|
|
326
399
|
|| (this.#session && this.#session !== session)
|
|
@@ -424,23 +497,47 @@ export default class ContinuousVoiceCapture {
|
|
|
424
497
|
}
|
|
425
498
|
}
|
|
426
499
|
const context = session.context;
|
|
500
|
+
let contextClosing = Promise.resolve();
|
|
427
501
|
if (context && context.state !== 'closed') {
|
|
428
|
-
const owner = this;
|
|
429
|
-
function observeContextCloseFailure(error) {
|
|
430
|
-
owner.#reportError(session, error);
|
|
431
|
-
}
|
|
432
502
|
try {
|
|
433
|
-
Promise.resolve(context.close())
|
|
503
|
+
contextClosing = Promise.resolve(context.close());
|
|
434
504
|
} catch (error) {
|
|
435
505
|
session.errors.push(error);
|
|
436
506
|
}
|
|
437
507
|
}
|
|
438
508
|
session.resolveStartup(false);
|
|
439
|
-
for (const
|
|
440
|
-
|
|
509
|
+
for (const flush of session.flushes.values()) {
|
|
510
|
+
if (session.errors.length) flush.reject(session.errors[0]);
|
|
511
|
+
else session.pcmDelivery.then(flush.resolve, flush.reject);
|
|
441
512
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
513
|
+
session.flushes.clear();
|
|
514
|
+
// stop() drains callbacks already accepted before the worklet's stop
|
|
515
|
+
// acknowledgment. cancel() explicitly abandons subsequent delivery;
|
|
516
|
+
// both operations join the context's actual asynchronous close.
|
|
517
|
+
const owner = this;
|
|
518
|
+
Promise.allSettled(
|
|
519
|
+
[session.pcmDelivery, contextClosing]
|
|
520
|
+
).then(
|
|
521
|
+
function finishCapturedCleanup(results) {
|
|
522
|
+
for (const result of results) {
|
|
523
|
+
if (result.status === 'rejected' && !session.errors.includes(result.reason)) {
|
|
524
|
+
session.errors.push(result.reason);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (owner.#session === session) owner.#session = null;
|
|
528
|
+
if (session.errors.length) {
|
|
529
|
+
session.rejectStop(
|
|
530
|
+
session.errors.length === 1 ? session.errors[0] : new AggregateError(
|
|
531
|
+
session.errors,
|
|
532
|
+
'Voice capture delivery or cleanup failed.'
|
|
533
|
+
)
|
|
534
|
+
);
|
|
535
|
+
} else {
|
|
536
|
+
session.resolveStop();
|
|
537
|
+
}
|
|
538
|
+
for (const error of session.errors) owner.#reportError(session, error, true);
|
|
539
|
+
owner.#notify(session, owner.#onState, session.errors.length ? 'interrupted' : state);
|
|
540
|
+
}
|
|
541
|
+
);
|
|
445
542
|
}
|
|
446
543
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import ContinuousVoiceCapture from './ContinuousVoiceCapture.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One microphone lifetime and one scoped Core ASR stream.
|
|
5
|
+
* Recognition callbacks receive (result, {requestStop}). They request a drain
|
|
6
|
+
* without awaiting it, then return; the external owner joins stop() or done.
|
|
7
|
+
* Awaiting outer stop()/done/forceEndpoint() from a recognition callback waits on that
|
|
8
|
+
* callback itself. Segments do not submit chat turns.
|
|
9
|
+
*/
|
|
10
|
+
export default function createNativeSpeechCapture({speech, model, ...options}) {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
let stream;
|
|
13
|
+
let started = false;
|
|
14
|
+
let stopping;
|
|
15
|
+
let settled = false;
|
|
16
|
+
let cancelled = false;
|
|
17
|
+
let cleanup;
|
|
18
|
+
let detachSignal;
|
|
19
|
+
let resolveDone;
|
|
20
|
+
let rejectDone;
|
|
21
|
+
const done = new Promise(function ownNativeCapture(resolve, reject) {
|
|
22
|
+
resolveDone = resolve;
|
|
23
|
+
rejectDone = reject;
|
|
24
|
+
});
|
|
25
|
+
// The caller still receives rejection through done; this observer prevents
|
|
26
|
+
// a synchronous device failure from becoming an unhandled rejection first.
|
|
27
|
+
done.catch(function observeNativeCaptureFailure() {});
|
|
28
|
+
|
|
29
|
+
function finish(error, result) {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
detachSignal?.();
|
|
33
|
+
if (error) rejectDone(error);
|
|
34
|
+
else resolveDone(result);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fail(error) {
|
|
38
|
+
if (settled || cleanup) return cleanup;
|
|
39
|
+
return endCapture(error, error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function endCapture(reason, primaryError) {
|
|
43
|
+
if (settled || cleanup) return cleanup ?? done;
|
|
44
|
+
const captureCleanup = capture.cancel();
|
|
45
|
+
// Join the explicit native cancellation operation, not the immediately
|
|
46
|
+
// aborted browser request, before releasing the capture/provider lane.
|
|
47
|
+
// Native cancellation proceeds while capture joins its pending PCM.
|
|
48
|
+
const nativeCleanup = Promise.resolve().then(function cancelNativeStream() {
|
|
49
|
+
return stream?.cancel(reason);
|
|
50
|
+
});
|
|
51
|
+
cleanup = Promise.allSettled(
|
|
52
|
+
[captureCleanup, nativeCleanup]
|
|
53
|
+
).then(
|
|
54
|
+
function nativeCaptureReleased(results) {
|
|
55
|
+
const errors = primaryError ? [primaryError] : [];
|
|
56
|
+
for (const result of results) {
|
|
57
|
+
if (result.status === 'rejected' && !errors.includes(result.reason)) errors.push(result.reason);
|
|
58
|
+
}
|
|
59
|
+
const error = errors.length > 1
|
|
60
|
+
? new AggregateError(errors, 'Native capture failed during cleanup.')
|
|
61
|
+
: errors[0];
|
|
62
|
+
finish(error, results[1].value);
|
|
63
|
+
if (error) return options.onError?.(error);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
cleanup.catch(function observeCaptureErrorCallback(error) {
|
|
67
|
+
console.error('Native speech capture error callback failed.', error);
|
|
68
|
+
});
|
|
69
|
+
controller.abort(reason);
|
|
70
|
+
return cleanup;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const capture = new ContinuousVoiceCapture({
|
|
74
|
+
mode: 'pcm',
|
|
75
|
+
pcmChunkMs: options.pcmChunkMs ?? 100,
|
|
76
|
+
activityThreshold: options.activityThreshold ?? 0.02,
|
|
77
|
+
async onPCM(record) {
|
|
78
|
+
try {
|
|
79
|
+
await stream.ready;
|
|
80
|
+
controller.signal.throwIfAborted();
|
|
81
|
+
const sampleRate = options.streamOptions?.sampleRate ?? options.sampleRate ?? 16000;
|
|
82
|
+
if (record.sampleRate !== sampleRate) {
|
|
83
|
+
throw new Error(`Microphone PCM sample rate ${record.sampleRate} differs from native stream rate ${sampleRate}.`);
|
|
84
|
+
}
|
|
85
|
+
await stream.push(record.samples);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// The owned cancellation ends an in-flight browser request;
|
|
88
|
+
// that expected abort is distinct from actual delivery failure.
|
|
89
|
+
if (controller.signal.aborted && (
|
|
90
|
+
error === controller.signal.reason || error?.name === 'AbortError' || error?.code === 'ARCANE_CANCELLED'
|
|
91
|
+
)) return;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onActivity: options.onActivity,
|
|
96
|
+
onState: options.onState,
|
|
97
|
+
onError: fail
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function cancel(reason) {
|
|
101
|
+
if (settled) return done;
|
|
102
|
+
cancelled = true;
|
|
103
|
+
endCapture(reason);
|
|
104
|
+
return done;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stop() {
|
|
108
|
+
if (stopping || settled) return stopping ?? done;
|
|
109
|
+
if (!stream) return cancel();
|
|
110
|
+
async function drainNativeCapture() {
|
|
111
|
+
await capture.stop();
|
|
112
|
+
if (!controller.signal.aborted) await stream.finish();
|
|
113
|
+
return done;
|
|
114
|
+
}
|
|
115
|
+
stopping = drainNativeCapture().catch(
|
|
116
|
+
function nativeCaptureStopFailed(error) {
|
|
117
|
+
fail(error);
|
|
118
|
+
return done;
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
return stopping;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function requestStop() {
|
|
125
|
+
// A callback requests the same terminal drain without joining itself.
|
|
126
|
+
// done retains its success or failure for the external owning caller.
|
|
127
|
+
stop().catch(function observeRequestedCaptureStop() {});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const callbackControls = {requestStop};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
done,
|
|
134
|
+
async start({signal, mediaConstraints} = {}) {
|
|
135
|
+
if (started || settled) return false;
|
|
136
|
+
started = true;
|
|
137
|
+
function externalCancellation() { cancel(signal.reason); }
|
|
138
|
+
if (signal) {
|
|
139
|
+
signal.addEventListener('abort', externalCancellation, {once: true});
|
|
140
|
+
detachSignal = function removeCaptureSignal() {
|
|
141
|
+
signal.removeEventListener('abort', externalCancellation);
|
|
142
|
+
};
|
|
143
|
+
if (signal.aborted) {
|
|
144
|
+
cancel(signal.reason);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
stream = speech.openStream({model, sampleRate: options.sampleRate ?? 16000, ...options.streamOptions}, {
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
onProgress: options.onProgress,
|
|
152
|
+
onRequest: options.onRequest,
|
|
153
|
+
async onUpdate(result) {
|
|
154
|
+
if (controller.signal.aborted) return;
|
|
155
|
+
await options.onUpdate?.(result, callbackControls);
|
|
156
|
+
// These are ASR snapshot callbacks, not semantic turn-end
|
|
157
|
+
// decisions. Preserve every field and replacement partial.
|
|
158
|
+
if (result.final) await options.onSegment?.(result, callbackControls);
|
|
159
|
+
else await options.onInterim?.(result, callbackControls);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
stream.completion.then(async function nativeStreamFinished(result) {
|
|
163
|
+
if (cleanup) return;
|
|
164
|
+
await capture.cancel();
|
|
165
|
+
if (cleanup) return cleanup;
|
|
166
|
+
finish(undefined, result);
|
|
167
|
+
}, fail).catch(function captureObserverFailed(error) {
|
|
168
|
+
return fail(error);
|
|
169
|
+
});
|
|
170
|
+
// Microphone permission remains in the initiating user gesture;
|
|
171
|
+
// its samples queue while the native stream becomes ready.
|
|
172
|
+
const capturing = capture.start({
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
sampleRate: options.streamOptions?.sampleRate ?? options.sampleRate ?? 16000,
|
|
175
|
+
mediaConstraints: mediaConstraints ?? options.mediaConstraints ?? {audio: {echoCancellation: true}}
|
|
176
|
+
});
|
|
177
|
+
const [ready] = await Promise.all([capturing, stream.ready]);
|
|
178
|
+
if (!ready && !settled) cancel();
|
|
179
|
+
return ready && !settled;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (!cancelled) await fail(error);
|
|
182
|
+
else await cleanup;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
async forceEndpoint() {
|
|
187
|
+
if (!started || settled || stopping) return [];
|
|
188
|
+
await capture.flush();
|
|
189
|
+
controller.signal.throwIfAborted();
|
|
190
|
+
return stream.forceEndpoint();
|
|
191
|
+
},
|
|
192
|
+
stop,
|
|
193
|
+
requestStop,
|
|
194
|
+
cancel,
|
|
195
|
+
destroy: cancel,
|
|
196
|
+
current() { return stream?.current() ?? null; }
|
|
197
|
+
};
|
|
198
|
+
}
|