arcane-os 0.77.0 → 0.79.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 +40 -0
- package/README.md +6 -1
- package/browser-runtime/ai/core-onnx.mjs +6 -2
- package/browser-runtime/ai/dbopfs-model-resources.mjs +1 -1
- package/browser-runtime/ai/speech-worker-runtime.mjs +79 -3
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/publishing.md +20 -3
- package/docs/reference/ai/browser-speech.md +59 -2
- package/docs/reference/execution-devices.md +157 -0
- package/docs/reference/inventory/package-api.json +39 -11
- package/docs/reference/inventory/runtime-modules.json +9 -8
- package/docs/reference/local-ai.md +104 -10
- package/docs/reference/native-decisions.md +65 -10
- package/docs/reference/runtime-modules.md +78 -11
- package/package.json +4 -2
- package/runtime/arcane/modules/AI.js +30 -7
- package/runtime/arcane/modules/DBOPFS.js +24 -6
- package/runtime/arcane/modules/PreparedSpeech.js +111 -6
- package/src/core/services/decisions.mjs +51 -15
- package/src/core/services/execution-devices.mjs +32 -0
- package/src/core/services/local-ai.mjs +12 -5
- package/src/local-ai/decisions.mjs +93 -15
- package/src/local-ai/dev.mjs +7 -3
- package/src/local-ai/execution-devices-windows.ps1 +833 -0
- package/src/local-ai/execution-devices.mjs +288 -0
- package/src/local-ai/onnx-worker.mjs +149 -19
- package/src/local-ai/onnx.mjs +87 -27
- package/src/native/portable-layout.mjs +8 -1
|
@@ -13,6 +13,10 @@ function cancellation(message) {
|
|
|
13
13
|
|
|
14
14
|
function reportCleanupFailure(error) { console.error('Native decision cleanup failed.', error); }
|
|
15
15
|
|
|
16
|
+
function sameExecutionTarget(left, right) {
|
|
17
|
+
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
/**
|
|
17
21
|
* One explicitly activated Laya FP32 model using an existing native ONNX owner.
|
|
18
22
|
* It does not create another inference runtime or load on construction.
|
|
@@ -20,7 +24,7 @@ function reportCleanupFailure(error) { console.error('Native decision cleanup fa
|
|
|
20
24
|
export function createNativeDecisionModel({
|
|
21
25
|
onnx, modelAssets, workingDirectory, paths,
|
|
22
26
|
model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
|
|
23
|
-
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', signal
|
|
27
|
+
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
|
|
24
28
|
} = {}) {
|
|
25
29
|
if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
|
|
26
30
|
const owner = {};
|
|
@@ -33,14 +37,18 @@ export function createNativeDecisionModel({
|
|
|
33
37
|
let progress = null;
|
|
34
38
|
let lastError = null;
|
|
35
39
|
let execution = null;
|
|
40
|
+
let selectedTarget = executionTarget;
|
|
41
|
+
let pendingActivation;
|
|
36
42
|
|
|
37
43
|
function current() {
|
|
44
|
+
const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
|
|
38
45
|
return {
|
|
39
46
|
family: 'laya', model, revision, dtype, state,
|
|
40
|
-
loaded: state === 'ready', busy: state === 'loading' || Boolean(activation?.jobs.size),
|
|
47
|
+
loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
|
|
41
48
|
activeRequests: activation?.jobs.size ?? 0, progress,
|
|
42
49
|
error: lastError ? serializeCoreError(lastError) : null,
|
|
43
|
-
execution
|
|
50
|
+
execution,
|
|
51
|
+
pendingActivation: pending ? {executionTarget: pending.executionTarget} : null
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -141,8 +149,8 @@ export function createNativeDecisionModel({
|
|
|
141
149
|
return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
function beginActivation() {
|
|
145
|
-
const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false};
|
|
152
|
+
function beginActivation(target) {
|
|
153
|
+
const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false, executionTarget: target};
|
|
146
154
|
activation = entry;
|
|
147
155
|
state = 'loading';
|
|
148
156
|
progress = {phase: 'preparing-model'};
|
|
@@ -161,7 +169,7 @@ export function createNativeDecisionModel({
|
|
|
161
169
|
});
|
|
162
170
|
entry.onnxLoadStarted = true;
|
|
163
171
|
const loaded = onnx.load({
|
|
164
|
-
id: entry.id, model: files.model, sessionOptions, executionPreference,
|
|
172
|
+
id: entry.id, model: files.model, sessionOptions, executionPreference, executionTarget: entry.executionTarget,
|
|
165
173
|
signal: entry.controller.signal
|
|
166
174
|
});
|
|
167
175
|
const results = await Promise.allSettled([loaded, entry.tokenizer.ready]);
|
|
@@ -193,19 +201,76 @@ export function createNativeDecisionModel({
|
|
|
193
201
|
return entry;
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
async function
|
|
197
|
-
assertOpen(operationSignal);
|
|
198
|
-
if (releaseTask && !activation) await releaseTask;
|
|
199
|
-
assertOpen(operationSignal);
|
|
200
|
-
const entry = activation ?? beginActivation();
|
|
204
|
+
async function awaitActivation(entry, operationSignal) {
|
|
201
205
|
const detach = observeCancellation(entry, operationSignal);
|
|
202
206
|
try {
|
|
203
207
|
await entry.loading;
|
|
204
208
|
entry.controller.signal.throwIfAborted();
|
|
209
|
+
if (activation !== entry) throw cancellation('The decision activation was replaced.');
|
|
205
210
|
return current();
|
|
206
211
|
} finally { detach(); }
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
function observeLoad(request, operationSignal) {
|
|
215
|
+
function abort() { request.controller.abort(operationSignal.reason); }
|
|
216
|
+
operationSignal?.addEventListener('abort', abort, {once: true});
|
|
217
|
+
if (operationSignal?.aborted) abort();
|
|
218
|
+
return request.task.then(function loadedActivation() {
|
|
219
|
+
request.controller.signal.throwIfAborted();
|
|
220
|
+
request.entry.controller.signal.throwIfAborted();
|
|
221
|
+
if (activation !== request.entry) throw cancellation('The decision activation was replaced.');
|
|
222
|
+
return current();
|
|
223
|
+
}).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function load({signal: operationSignal, executionTarget: requestedTarget} = {}) {
|
|
227
|
+
assertOpen(operationSignal);
|
|
228
|
+
const target = requestedTarget === undefined ? selectedTarget : requestedTarget;
|
|
229
|
+
if (pendingActivation && !pendingActivation.controller.signal.aborted && sameExecutionTarget(pendingActivation.executionTarget, target)) {
|
|
230
|
+
return observeLoad(pendingActivation, operationSignal);
|
|
231
|
+
}
|
|
232
|
+
if (!pendingActivation && activation && sameExecutionTarget(activation.executionTarget, target)) {
|
|
233
|
+
return awaitActivation(activation, operationSignal);
|
|
234
|
+
}
|
|
235
|
+
const previous = pendingActivation;
|
|
236
|
+
const request = {executionTarget: target, controller: new AbortController()};
|
|
237
|
+
let precedingRelease;
|
|
238
|
+
// Retain the replacement before synchronous abort/state observers run.
|
|
239
|
+
// Only this model's retiring activation must finish before its successor.
|
|
240
|
+
request.task = Promise.resolve().then(async function loadSelectedTarget() {
|
|
241
|
+
request.controller.signal.throwIfAborted();
|
|
242
|
+
if (precedingRelease) await precedingRelease;
|
|
243
|
+
assertOpen(request.controller.signal);
|
|
244
|
+
if (pendingActivation !== request) throw cancellation('The decision activation request was replaced.');
|
|
245
|
+
request.entry ??= activation ?? beginActivation(request.executionTarget);
|
|
246
|
+
return awaitActivation(request.entry, request.controller.signal);
|
|
247
|
+
});
|
|
248
|
+
function settled(error) {
|
|
249
|
+
request.detach?.();
|
|
250
|
+
if (pendingActivation !== request) return;
|
|
251
|
+
pendingActivation = undefined;
|
|
252
|
+
if (!request.entry && !activation && !disposed && state === 'loading') {
|
|
253
|
+
state = request.controller.signal.aborted ? 'unloaded' : 'error';
|
|
254
|
+
progress = null;
|
|
255
|
+
lastError = request.controller.signal.aborted ? null : error;
|
|
256
|
+
}
|
|
257
|
+
publish();
|
|
258
|
+
}
|
|
259
|
+
request.task.then(function loaded() { settled(); }, function loadFailed(error) { settled(error); }).catch(reportCleanupFailure);
|
|
260
|
+
pendingActivation = request;
|
|
261
|
+
selectedTarget = target;
|
|
262
|
+
const reason = cancellation('The decision execution target was replaced.');
|
|
263
|
+
previous?.controller.abort(reason);
|
|
264
|
+
if (activation && !sameExecutionTarget(activation.executionTarget, target)) stop(activation, reason);
|
|
265
|
+
precedingRelease = !activation ? releaseTask : undefined;
|
|
266
|
+
if (!activation && !precedingRelease) {
|
|
267
|
+
request.entry = beginActivation(target);
|
|
268
|
+
request.detach = observeCancellation(request.entry, request.controller.signal);
|
|
269
|
+
}
|
|
270
|
+
publish();
|
|
271
|
+
return observeLoad(request, operationSignal);
|
|
272
|
+
}
|
|
273
|
+
|
|
209
274
|
async function evaluate(rows, {signal: operationSignal, runOptions} = {}) {
|
|
210
275
|
assertOpen(operationSignal);
|
|
211
276
|
const entry = activation;
|
|
@@ -247,21 +312,34 @@ export function createNativeDecisionModel({
|
|
|
247
312
|
}
|
|
248
313
|
|
|
249
314
|
function unload() {
|
|
250
|
-
|
|
251
|
-
|
|
315
|
+
const reason = cancellation('The native decision model was unloaded.');
|
|
316
|
+
const pending = pendingActivation;
|
|
317
|
+
pending?.controller.abort(reason);
|
|
318
|
+
const release = activation ? stop(activation, reason) : releaseTask ?? Promise.resolve(current());
|
|
319
|
+
if (!pending) return release;
|
|
320
|
+
return Promise.allSettled([release, pending.task]).then(function releasedActivation(results) {
|
|
321
|
+
if (results[0].status === 'rejected') throw results[0].reason;
|
|
322
|
+
return current();
|
|
323
|
+
});
|
|
252
324
|
}
|
|
253
325
|
|
|
254
326
|
function dispose() {
|
|
255
327
|
if (disposal) return disposal;
|
|
256
328
|
disposed = true;
|
|
257
329
|
signal?.removeEventListener('abort', lifetimeAborted);
|
|
330
|
+
const pending = pendingActivation;
|
|
331
|
+
const reason = cancellation('The native decision model was disposed.');
|
|
258
332
|
let release;
|
|
259
333
|
disposal = Promise.resolve().then(async function disposeNativeModel() {
|
|
260
|
-
try {
|
|
334
|
+
try {
|
|
335
|
+
const results = await Promise.allSettled([release, pending?.task]);
|
|
336
|
+
if (results[0].status === 'rejected') throw results[0].reason;
|
|
337
|
+
}
|
|
261
338
|
finally { state = 'disposed'; publish(); events.dispose(); }
|
|
262
339
|
return current();
|
|
263
340
|
});
|
|
264
|
-
|
|
341
|
+
pending?.controller.abort(reason);
|
|
342
|
+
release = activation ? stop(activation, reason) : releaseTask;
|
|
265
343
|
if (!activation && !release) { state = 'disposed'; publish(); }
|
|
266
344
|
return disposal;
|
|
267
345
|
}
|
package/src/local-ai/dev.mjs
CHANGED
|
@@ -4,19 +4,23 @@ import {createLocalAIService} from '../core/services/local-ai.mjs';
|
|
|
4
4
|
import {createLocalImageService} from '../core/services/image.mjs';
|
|
5
5
|
import {createModelAssetService} from '../core/services/model-assets.mjs';
|
|
6
6
|
import {createSpeechService} from '../core/services/speech.mjs';
|
|
7
|
+
import {createExecutionDeviceService} from '../core/services/execution-devices.mjs';
|
|
7
8
|
import {normalizeLocalAIConfig} from './config.mjs';
|
|
8
9
|
import {discoverLocalAIRuntimes} from './discover.mjs';
|
|
9
10
|
import {ensureLocalAIRuntimes} from './install.mjs';
|
|
10
11
|
import {createWhisperRuntime} from './whisper/index.mjs';
|
|
12
|
+
import {createExecutionDeviceCatalog} from './execution-devices.mjs';
|
|
11
13
|
|
|
12
14
|
/** Local engines share the ordinary app-service development transport. */
|
|
13
15
|
export function createDevelopmentLocalAI({
|
|
14
16
|
config, appRoot, directory, application, version, signal, onEvent,
|
|
15
17
|
services = [], serviceModules = [], context = {}
|
|
16
18
|
} = {}) {
|
|
19
|
+
const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
|
|
17
20
|
const lifetime = new AbortController();
|
|
18
21
|
signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
19
|
-
|
|
22
|
+
// Discovery starts on demand, independently of model runtime preparation.
|
|
23
|
+
const executionDevices = createExecutionDeviceCatalog({signal});
|
|
20
24
|
const localConfig = {
|
|
21
25
|
...selected,
|
|
22
26
|
runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
|
|
@@ -26,7 +30,7 @@ export function createDevelopmentLocalAI({
|
|
|
26
30
|
const service = createLocalAIService(
|
|
27
31
|
localConfig,
|
|
28
32
|
{
|
|
29
|
-
appRoot, signal, onEvent,
|
|
33
|
+
appRoot, signal, onEvent, executionDevices,
|
|
30
34
|
async prepare({signal, onEvent: report}) {
|
|
31
35
|
const discovered = await discoverLocalAIRuntimes({config: localConfig, appRoot, signal});
|
|
32
36
|
const installed = await ensureLocalAIRuntimes(
|
|
@@ -36,7 +40,7 @@ export function createDevelopmentLocalAI({
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
);
|
|
39
|
-
const definitions = [service, ...services];
|
|
43
|
+
const definitions = [createExecutionDeviceService({catalog: executionDevices}), service, ...services];
|
|
40
44
|
let modelAssets;
|
|
41
45
|
let imageService;
|
|
42
46
|
let speechService;
|