arcane-os 0.94.0 → 0.96.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 +36 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-model-assets.mjs +74 -0
- package/browser-runtime/core/client.mjs +195 -0
- package/docs/reference/README.md +9 -0
- package/docs/reference/ai/browser-speech.md +61 -10
- package/docs/reference/core-client.md +35 -0
- package/docs/reference/core-native-packaging.md +38 -6
- package/docs/reference/inventory/package-api.json +133 -5
- package/docs/reference/inventory/runtime-modules.json +39 -13
- package/docs/reference/model-assets.md +164 -3
- package/docs/reference/native-magpie.md +98 -0
- package/docs/reference/native-nemotron.md +204 -0
- package/docs/reference/native-speech.md +250 -5
- package/docs/reference/runtime-modules.md +157 -11
- package/docs/reference/sdk-api.md +247 -4
- package/package.json +7 -3
- 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/host.mjs +1 -0
- package/src/core/services/local-ai.mjs +46 -12
- package/src/core/services/model-assets.mjs +491 -59
- package/src/core/services/speech.mjs +237 -4
- 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/nemo-models.mjs +160 -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 +693 -0
- package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
- package/src/local-ai/nemotron/native/main.cpp +282 -0
- package/src/native/macos-provider.mjs +14 -7
- package/src/native/portable-assembly.mjs +180 -0
- package/src/native/portable-provider.mjs +17 -161
- package/src/native/publish-output.mjs +101 -0
- package/src/native/windows-provider.mjs +12 -6
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {arcaneLogging} from 'arcane-os/logging';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
|
|
6
|
+
/** Prepare exact native speech for the existing utterance queue, without starting synthesis. */
|
|
7
|
+
export default function prepareNativeSpeechPlayback({speech, request, getAudioContext, signal = null}) {
|
|
8
|
+
if (!is.function(speech?.synthesizeStream) || !is.function(getAudioContext)) {
|
|
9
|
+
throw new TypeError('Native speech playback requires streaming speech and an AudioContext owner.');
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
kind: 'native-speech',
|
|
13
|
+
input: request.input,
|
|
14
|
+
voice: request.voice,
|
|
15
|
+
language: request.languageCode ?? request.language_code ?? request.language,
|
|
16
|
+
speed: request.speed ?? 1,
|
|
17
|
+
play(options = {}) {
|
|
18
|
+
return startPlayback(speech, request, getAudioContext, signal, options);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function startPlayback(speech, request, getAudioContext, preparationSignal, {signal = null, onState = null} = {}) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const signals = new Set([preparationSignal, signal].filter(Boolean));
|
|
26
|
+
const segments = [];
|
|
27
|
+
const failures = [];
|
|
28
|
+
const completion = deferred();
|
|
29
|
+
const release = deferred();
|
|
30
|
+
let context = null;
|
|
31
|
+
let playbackState = 'queued';
|
|
32
|
+
let playbackError = null;
|
|
33
|
+
let paused = false;
|
|
34
|
+
let settled = false;
|
|
35
|
+
let synthesisSettled = true;
|
|
36
|
+
let releaseSettled = false;
|
|
37
|
+
let nextStart = 0;
|
|
38
|
+
let playbackReady = null;
|
|
39
|
+
|
|
40
|
+
function publish(nextState, error = playbackError) {
|
|
41
|
+
playbackState = nextState;
|
|
42
|
+
playbackError = error;
|
|
43
|
+
if (!is.function(onState)) return;
|
|
44
|
+
function observerFailed(cause) {
|
|
45
|
+
arcaneLogging.error('Native speech playback observer failed.', cause);
|
|
46
|
+
}
|
|
47
|
+
try { Promise.resolve(onState({state: nextState, error})).catch(observerFailed); }
|
|
48
|
+
catch (cause) { observerFailed(cause); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function retainFailure(error) {
|
|
52
|
+
if (!failures.includes(error)) failures.push(error);
|
|
53
|
+
playbackError = failures.length === 1 ? failures[0]
|
|
54
|
+
: new AggregateError(failures, 'Native speech playback and cleanup failed.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function releaseIfFinished() {
|
|
58
|
+
if (!settled || !synthesisSettled || releaseSettled) return;
|
|
59
|
+
releaseSettled = true;
|
|
60
|
+
if (failures.length) release.reject(playbackError);
|
|
61
|
+
else release.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function removeSegment(segment) {
|
|
65
|
+
const position = segments.indexOf(segment);
|
|
66
|
+
if (position >= 0) segments.splice(position, 1);
|
|
67
|
+
segment.buffer = null;
|
|
68
|
+
segment.ended.resolve();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detachSource(segment, stop) {
|
|
72
|
+
const node = segment.node;
|
|
73
|
+
if (!node) return;
|
|
74
|
+
segment.node = null;
|
|
75
|
+
node.onended = null;
|
|
76
|
+
if (stop && segment.started) {
|
|
77
|
+
try { node.stop(); }
|
|
78
|
+
catch (error) { retainFailure(error); }
|
|
79
|
+
}
|
|
80
|
+
segment.started = false;
|
|
81
|
+
try { node.disconnect(); }
|
|
82
|
+
catch (error) { retainFailure(error); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wakePlayback() {
|
|
86
|
+
if (!settled && (paused || context?.state !== 'running')) return;
|
|
87
|
+
playbackReady?.resolve();
|
|
88
|
+
playbackReady = null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function finish(completed, error = null) {
|
|
92
|
+
if (error) retainFailure(error);
|
|
93
|
+
if (settled) {
|
|
94
|
+
if (error) publish('error');
|
|
95
|
+
releaseIfFinished();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
settled = true;
|
|
99
|
+
for (const requestSignal of signals) requestSignal.removeEventListener('abort', stop);
|
|
100
|
+
context?.removeEventListener('statechange', contextChanged);
|
|
101
|
+
if (!completed && !controller.signal.aborted) {
|
|
102
|
+
controller.abort(error ?? cancellation());
|
|
103
|
+
}
|
|
104
|
+
for (const segment of [...segments]) {
|
|
105
|
+
detachSource(segment, true);
|
|
106
|
+
removeSegment(segment);
|
|
107
|
+
}
|
|
108
|
+
wakePlayback();
|
|
109
|
+
publish(playbackError ? 'error' : completed ? 'complete' : 'stopped');
|
|
110
|
+
if (playbackError) completion.reject(playbackError);
|
|
111
|
+
else completion.resolve(completed);
|
|
112
|
+
releaseIfFinished();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function updatePlaybackState() {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
publish(paused ? 'paused'
|
|
118
|
+
: context?.state === 'running' && segments.length ? 'playing' : 'waiting-for-audio');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function contextChanged() {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
if (context.state === 'closed') {
|
|
124
|
+
finish(false, playbackFailure('The shared audio output closed during native speech.'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
wakePlayback();
|
|
128
|
+
updatePlaybackState();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function schedule(segment) {
|
|
132
|
+
if (settled || paused || segment.node) return;
|
|
133
|
+
const node = context.createBufferSource();
|
|
134
|
+
segment.node = node;
|
|
135
|
+
node.buffer = segment.buffer;
|
|
136
|
+
node.connect(context.destination);
|
|
137
|
+
segment.startAt = Math.max(context.currentTime, nextStart);
|
|
138
|
+
nextStart = segment.startAt + segment.buffer.duration - segment.offset;
|
|
139
|
+
node.onended = function segmentEnded() {
|
|
140
|
+
if (segment.node !== node || settled) return;
|
|
141
|
+
detachSource(segment, false);
|
|
142
|
+
removeSegment(segment);
|
|
143
|
+
if (failures.length) finish(false);
|
|
144
|
+
else if (synthesisSettled && !segments.length) finish(true);
|
|
145
|
+
else updatePlaybackState();
|
|
146
|
+
};
|
|
147
|
+
node.start(segment.startAt, segment.offset);
|
|
148
|
+
segment.started = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function receiveAudio({pcm, format, sampleRate, channels}) {
|
|
152
|
+
try {
|
|
153
|
+
await playAudio(pcm, format, sampleRate, channels);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error !== controller.signal.reason) finish(false, error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function playAudio(pcm, format, sampleRate, channels) {
|
|
161
|
+
controller.signal.throwIfAborted();
|
|
162
|
+
if (format !== 's16le' || !(pcm instanceof Uint8Array)) {
|
|
163
|
+
throw playbackFailure('Native speech requires signed 16-bit little-endian PCM.');
|
|
164
|
+
}
|
|
165
|
+
// PCM sample framing belongs to this audio transport, not request admission.
|
|
166
|
+
const frames = pcm.byteLength / (2 * channels);
|
|
167
|
+
if (!Number.isInteger(frames)) {
|
|
168
|
+
throw playbackFailure('Native speech returned an incomplete PCM frame.');
|
|
169
|
+
}
|
|
170
|
+
if (!frames) return;
|
|
171
|
+
const buffer = context.createBuffer(channels, frames, sampleRate);
|
|
172
|
+
const samples = new DataView(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
173
|
+
for (let channel = 0; channel < channels; channel += 1) {
|
|
174
|
+
const output = buffer.getChannelData(channel);
|
|
175
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
176
|
+
output[frame] = samples.getInt16((frame * channels + channel) * 2, true) / 32768;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const preceding = segments[segments.length - 1];
|
|
180
|
+
const segment = {buffer, offset: 0, startAt: 0, node: null, started: false, ended: deferred()};
|
|
181
|
+
segments.push(segment);
|
|
182
|
+
schedule(segment);
|
|
183
|
+
updatePlaybackState();
|
|
184
|
+
controller.signal.throwIfAborted();
|
|
185
|
+
if (paused || context.state !== 'running') {
|
|
186
|
+
playbackReady ??= deferred();
|
|
187
|
+
await playbackReady.promise;
|
|
188
|
+
}
|
|
189
|
+
// Keep the current and next chunk scheduled. The awaited callback applies
|
|
190
|
+
// playback backpressure without another utterance queue or a complete clip.
|
|
191
|
+
if (preceding) await preceding.ended.promise;
|
|
192
|
+
controller.signal.throwIfAborted();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function stop() {
|
|
196
|
+
if (settled) return false;
|
|
197
|
+
finish(false);
|
|
198
|
+
return playbackError === null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pause() {
|
|
202
|
+
if (settled || paused) return false;
|
|
203
|
+
paused = true;
|
|
204
|
+
const now = context?.currentTime ?? 0;
|
|
205
|
+
for (const segment of [...segments]) {
|
|
206
|
+
if (segment.node) {
|
|
207
|
+
segment.offset = Math.min(segment.buffer.duration,
|
|
208
|
+
segment.offset + Math.max(0, now - segment.startAt));
|
|
209
|
+
detachSource(segment, true);
|
|
210
|
+
}
|
|
211
|
+
if (segment.offset >= segment.buffer.duration) removeSegment(segment);
|
|
212
|
+
}
|
|
213
|
+
nextStart = now;
|
|
214
|
+
if (failures.length) { finish(false); return false; }
|
|
215
|
+
if (synthesisSettled && !segments.length) { finish(true); return false; }
|
|
216
|
+
publish('paused');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resume() {
|
|
221
|
+
if (settled || !paused) return false;
|
|
222
|
+
paused = false;
|
|
223
|
+
try {
|
|
224
|
+
nextStart = context?.currentTime ?? 0;
|
|
225
|
+
for (const segment of segments) schedule(segment);
|
|
226
|
+
wakePlayback();
|
|
227
|
+
updatePlaybackState();
|
|
228
|
+
return true;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
finish(false, error);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const control = {
|
|
236
|
+
finished: completion.promise,
|
|
237
|
+
released: release.promise,
|
|
238
|
+
get state() { return playbackState; },
|
|
239
|
+
get error() { return playbackError; },
|
|
240
|
+
pause,
|
|
241
|
+
resume,
|
|
242
|
+
stop
|
|
243
|
+
};
|
|
244
|
+
for (const requestSignal of signals) requestSignal.addEventListener('abort', stop, {once: true});
|
|
245
|
+
publish('queued');
|
|
246
|
+
if ([...signals].some(function alreadyAborted(requestSignal) { return requestSignal.aborted; })) stop();
|
|
247
|
+
if (!settled) {
|
|
248
|
+
try {
|
|
249
|
+
context = getAudioContext();
|
|
250
|
+
if (context.state === 'closed') throw playbackFailure('The shared audio output is closed.');
|
|
251
|
+
context.addEventListener('statechange', contextChanged);
|
|
252
|
+
synthesisSettled = false;
|
|
253
|
+
updatePlaybackState();
|
|
254
|
+
const synthesis = Promise.resolve().then(function synthesizeOwnedUtterance() {
|
|
255
|
+
controller.signal.throwIfAborted();
|
|
256
|
+
return speech.synthesizeStream(request, {signal: controller.signal, onAudio: receiveAudio});
|
|
257
|
+
});
|
|
258
|
+
synthesis.then(function synthesisCompleted() {
|
|
259
|
+
synthesisSettled = true;
|
|
260
|
+
if (!settled && !segments.length) finish(true);
|
|
261
|
+
releaseIfFinished();
|
|
262
|
+
}, function synthesisFailed(error) {
|
|
263
|
+
synthesisSettled = true;
|
|
264
|
+
if (cancelledByOwner(error, controller.signal)) finish(false);
|
|
265
|
+
else finish(false, error);
|
|
266
|
+
releaseIfFinished();
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
finish(false, error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return control;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function deferred() {
|
|
276
|
+
const result = {};
|
|
277
|
+
result.promise = new Promise(function retainCompletion(resolve, reject) {
|
|
278
|
+
result.resolve = resolve;
|
|
279
|
+
result.reject = reject;
|
|
280
|
+
});
|
|
281
|
+
result.promise.catch(function observeRetainedFailure() {});
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function cancellation() {
|
|
286
|
+
const error = new Error('The owning caller stopped this native utterance.');
|
|
287
|
+
error.name = 'AbortError';
|
|
288
|
+
error.code = 'ARCANE_AI_REQUEST_ABORTED';
|
|
289
|
+
return error;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function cancelledByOwner(error, signal) {
|
|
293
|
+
if (!signal.aborted) return false;
|
|
294
|
+
if (error === signal.reason) return true;
|
|
295
|
+
// Core represents the matching request cancellation with its transport error.
|
|
296
|
+
// A separate failure or cleanup aggregate remains observable.
|
|
297
|
+
return error?.code === 'ARCANE_REQUEST_ABORTED' && !error.cause && !error.errors;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function playbackFailure(message) {
|
|
301
|
+
const error = new Error(message);
|
|
302
|
+
error.code = 'ARCANE_AI_SPEECH_PLAYBACK_FAILED';
|
|
303
|
+
return error;
|
|
304
|
+
}
|
|
@@ -18,10 +18,21 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
|
|
|
18
18
|
#buffer = null;
|
|
19
19
|
#view = null;
|
|
20
20
|
#stopped = false;
|
|
21
|
+
#pcmMode = false;
|
|
22
|
+
#pcmBuffer = null;
|
|
23
|
+
#pcmOffset = 0;
|
|
24
|
+
#pcmFrames = 0;
|
|
25
|
+
#observedActivity = false;
|
|
26
|
+
#observedFrames = 0;
|
|
21
27
|
|
|
22
28
|
constructor(options) {
|
|
23
29
|
super();
|
|
24
30
|
const configuration = options.processorOptions;
|
|
31
|
+
this.#pcmMode = configuration.mode === 'pcm';
|
|
32
|
+
if (this.#pcmMode) {
|
|
33
|
+
this.#pcmFrames = Math.max(1, Math.round(configuration.pcmChunkMs * sampleRate / 1000));
|
|
34
|
+
this.#pcmBuffer = new Float32Array(this.#pcmFrames);
|
|
35
|
+
}
|
|
25
36
|
this.#preRoll = new Float32Array(
|
|
26
37
|
Math.round(configuration.preRollMs * sampleRate / 1000)
|
|
27
38
|
);
|
|
@@ -35,11 +46,23 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
|
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
#receiveControl(event) {
|
|
38
|
-
if (this.#stopped
|
|
49
|
+
if (this.#stopped) return;
|
|
50
|
+
if (event.data.type === 'flush') {
|
|
51
|
+
try {
|
|
52
|
+
this.#flushAnalysis();
|
|
53
|
+
if (this.#pcmMode) this.#emitPCM();
|
|
54
|
+
this.port.postMessage({type: 'flushed', id: event.data.id});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this.#fail(error);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (event.data.type !== 'stop') return;
|
|
39
61
|
this.#stopped = true;
|
|
40
62
|
try {
|
|
41
63
|
this.#flushAnalysis();
|
|
42
|
-
this.#
|
|
64
|
+
if (this.#pcmMode) this.#emitPCM();
|
|
65
|
+
else this.#emit('stop');
|
|
43
66
|
this.#release();
|
|
44
67
|
this.port.postMessage(
|
|
45
68
|
{type: 'stopped'}
|
|
@@ -74,6 +97,25 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
|
|
|
74
97
|
#flushAnalysis() {
|
|
75
98
|
if (!this.#analysisCount) return;
|
|
76
99
|
const activity = Math.sqrt(this.#analysisEnergy / this.#analysisCount) > this.#activityThreshold;
|
|
100
|
+
if (this.#pcmMode) {
|
|
101
|
+
if (activity !== this.#observedActivity) {
|
|
102
|
+
this.#observedActivity = activity;
|
|
103
|
+
this.port.postMessage({
|
|
104
|
+
type: 'activity',
|
|
105
|
+
active: activity,
|
|
106
|
+
audioTime: this.#observedFrames / sampleRate
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (let index = 0; index < this.#analysisCount; index += 1) {
|
|
110
|
+
this.#pcmBuffer[this.#pcmOffset] = this.#analysisSamples[index];
|
|
111
|
+
this.#pcmOffset += 1;
|
|
112
|
+
if (this.#pcmOffset === this.#pcmFrames) this.#emitPCM();
|
|
113
|
+
}
|
|
114
|
+
this.#observedFrames += this.#analysisCount;
|
|
115
|
+
this.#analysisCount = 0;
|
|
116
|
+
this.#analysisEnergy = 0;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
77
119
|
// Classification selects boundaries only. Every original sample still
|
|
78
120
|
// enters capture in order, including a partial final analysis window.
|
|
79
121
|
for (let index = 0; index < this.#analysisCount; index += 1) {
|
|
@@ -123,6 +165,22 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
|
|
|
123
165
|
this.#preRollLength = Math.min(this.#preRollLength + 1, this.#preRoll.length);
|
|
124
166
|
}
|
|
125
167
|
|
|
168
|
+
#emitPCM() {
|
|
169
|
+
if (!this.#pcmOffset) return;
|
|
170
|
+
const samples = this.#pcmOffset === this.#pcmFrames
|
|
171
|
+
? this.#pcmBuffer
|
|
172
|
+
: this.#pcmBuffer.slice(0, this.#pcmOffset);
|
|
173
|
+
this.#sequence += 1;
|
|
174
|
+
this.port.postMessage({
|
|
175
|
+
type: 'pcm',
|
|
176
|
+
audio: samples.buffer,
|
|
177
|
+
sampleRate,
|
|
178
|
+
sequence: this.#sequence
|
|
179
|
+
}, [samples.buffer]);
|
|
180
|
+
this.#pcmBuffer = new Float32Array(this.#pcmFrames);
|
|
181
|
+
this.#pcmOffset = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
126
184
|
#beginSegment() {
|
|
127
185
|
// Preserve all requested pre-roll even when it exceeds chunkMs. After a
|
|
128
186
|
// periodic boundary, retain quiet audio until activity or the pause so
|
|
@@ -206,6 +264,8 @@ class ArcaneVoiceCaptureProcessor extends AudioWorkletProcessor {
|
|
|
206
264
|
}
|
|
207
265
|
|
|
208
266
|
#release() {
|
|
267
|
+
this.#pcmBuffer = null;
|
|
268
|
+
this.#pcmOffset = 0;
|
|
209
269
|
this.#buffer = null;
|
|
210
270
|
this.#view = null;
|
|
211
271
|
this.#preRoll = new Float32Array(0);
|
package/src/core/host.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export function resolveNativeLaunchContext({appId, context = {}} = {}) {
|
|
|
39
39
|
const resolved = {
|
|
40
40
|
...context,
|
|
41
41
|
stateRoot,
|
|
42
|
+
modelsDirectory: context.modelsDirectory === undefined ? path.join(root, 'Models') : context.modelsDirectory,
|
|
42
43
|
workspaceRoot: context.workspaceRoot === undefined ? path.join(root, 'Workspace') : context.workspaceRoot
|
|
43
44
|
};
|
|
44
45
|
if (context.sharedHost !== undefined) {
|
|
@@ -121,7 +121,7 @@ function collectCompletion(completion, chunk) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Core owns native processes; applications own runtime and model selection. */
|
|
124
|
-
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare, executionDevices} = {}) {
|
|
124
|
+
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare, executionDevices, autoStart = true} = {}) {
|
|
125
125
|
const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
|
|
126
126
|
const lifetime = new AbortController();
|
|
127
127
|
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
@@ -130,6 +130,8 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
130
130
|
const owner = {};
|
|
131
131
|
const events = createArcaneEventSource(owner, {source: 'core-local-ai', eventTypes: ['localai.state']});
|
|
132
132
|
let context;
|
|
133
|
+
let preparing;
|
|
134
|
+
let prepared = false;
|
|
133
135
|
let modelAssetOwner;
|
|
134
136
|
let closed = false;
|
|
135
137
|
let onnx;
|
|
@@ -738,17 +740,20 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
738
740
|
});
|
|
739
741
|
}
|
|
740
742
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
async
|
|
748
|
-
|
|
743
|
+
function prepareEngines(requestSignal) {
|
|
744
|
+
lifetimeSignal.throwIfAborted();
|
|
745
|
+
requestSignal?.throwIfAborted();
|
|
746
|
+
if (prepared) return Promise.resolve(snapshot());
|
|
747
|
+
if (preparing) return awaitReadiness(preparing, requestSignal ?? lifetimeSignal);
|
|
748
|
+
const operationSignal = requestSignal ? AbortSignal.any([lifetimeSignal, requestSignal]) : lifetimeSignal;
|
|
749
|
+
const task = Promise.resolve().then(async function prepareSelectedEngines() {
|
|
750
|
+
operationSignal.throwIfAborted();
|
|
749
751
|
if (prepare) {
|
|
750
|
-
runtimes = await prepare({signal:
|
|
751
|
-
|
|
752
|
+
runtimes = await prepare({signal: operationSignal, onEvent: function preparationProgress(event) {
|
|
753
|
+
context?.emit('localai.preparation', event);
|
|
754
|
+
return onEvent?.(event);
|
|
755
|
+
}});
|
|
756
|
+
operationSignal.throwIfAborted();
|
|
752
757
|
onnxRecord = runtimes.find(function installedONNX(runtime) { return runtime.id === 'onnx'; });
|
|
753
758
|
}
|
|
754
759
|
for (const requirement of config.runtimes) {
|
|
@@ -764,11 +769,40 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
764
769
|
state: {id: requirement.id, installed: runtimes.some(function installed(record) { return record.id === requirement.id && record.executable; }), available: false, state: 'starting'}
|
|
765
770
|
};
|
|
766
771
|
engines.set(engine.id, engine);
|
|
767
|
-
startEngine(engine);
|
|
772
|
+
try { startEngine(engine); }
|
|
773
|
+
catch (error) {
|
|
774
|
+
engines.delete(engine.id);
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
768
777
|
}
|
|
769
778
|
publish();
|
|
779
|
+
prepared = true;
|
|
780
|
+
});
|
|
781
|
+
preparing = task;
|
|
782
|
+
jobs.add(task);
|
|
783
|
+
function releasePreparation() {
|
|
784
|
+
jobs.delete(task);
|
|
785
|
+
if (preparing === task) preparing = null;
|
|
786
|
+
}
|
|
787
|
+
task.then(releasePreparation, releasePreparation);
|
|
788
|
+
return task;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const service = {
|
|
792
|
+
name: 'local-ai',
|
|
793
|
+
current: snapshot,
|
|
794
|
+
getONNXRuntime() {
|
|
795
|
+
return requireONNX();
|
|
796
|
+
},
|
|
797
|
+
async start(currentContext) {
|
|
798
|
+
context = currentContext;
|
|
799
|
+
if (autoStart === false) return;
|
|
800
|
+
return prepareEngines(lifetimeSignal);
|
|
770
801
|
},
|
|
771
802
|
methods: {
|
|
803
|
+
'localai.prepare': function prepareSelectedRuntime(_parameters, request) {
|
|
804
|
+
return prepareEngines(request.signal).then(snapshot);
|
|
805
|
+
},
|
|
772
806
|
'localai.services.recover': async function recoverServices({runtimes: selected = [...engines.keys(), ...(onnxSelected ? ['onnx'] : [])]} = {}, request) {
|
|
773
807
|
request.signal.throwIfAborted();
|
|
774
808
|
const selectedEngines = selected.map(function selectedEngine(id) {
|