arcane-os 0.66.0 → 0.67.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 +23 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +84 -34
- package/browser-runtime/ai/core-local.mjs +195 -43
- package/browser-runtime/ai/core-onnx.mjs +71 -12
- package/docs/reference/README.md +1 -0
- package/docs/reference/core-native-packaging.md +11 -4
- package/docs/reference/core-repositories.md +93 -9
- package/docs/reference/core-runtime.md +32 -5
- package/docs/reference/inventory/package-api.json +9 -9
- package/docs/reference/local-ai.md +112 -2
- package/docs/reference/local-image-generation.md +15 -4
- package/docs/reference/model-assets.md +50 -3
- package/docs/reference/native-decisions.md +181 -0
- package/docs/reference/sdk-api.md +55 -12
- package/package.json +5 -2
- package/src/core/repositories.mjs +27 -8
- package/src/core/runtime.mjs +11 -0
- package/src/core/services/decisions.mjs +144 -0
- package/src/core/services/local-ai.mjs +31 -9
- package/src/core/services/model-assets.mjs +98 -13
- package/src/git-identity.mjs +22 -58
- package/src/local-ai/decision-tokenizer-worker.mjs +31 -0
- package/src/local-ai/decision-tokenizer.mjs +109 -0
- package/src/local-ai/decisions.mjs +273 -0
- package/src/local-ai/dev.mjs +8 -2
- package/src/local-ai/onnx-worker.mjs +67 -7
- package/src/local-ai/onnx.mjs +23 -12
- package/src/native/portable-layout.mjs +8 -3
- package/src/repository.mjs +144 -4
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
3
|
+
import {CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
|
|
4
|
+
import {decodeDecisionOutputs} from '../../browser-runtime/ai/decision-runtime.mjs';
|
|
5
|
+
import {createDecisionTokenizer} from './decision-tokenizer.mjs';
|
|
6
|
+
|
|
7
|
+
const STATE_EVENT = 'ai.decisions.state';
|
|
8
|
+
const LAYA_FILES = ['onnx/model.onnx', 'onnx/model.onnx_data', 'tokenizer.json', 'tokenizer_config.json'];
|
|
9
|
+
|
|
10
|
+
function cancellation(message) {
|
|
11
|
+
return new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function reportCleanupFailure(error) { console.error('Native decision cleanup failed.', error); }
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One explicitly activated Laya FP32 model using an existing native ONNX owner.
|
|
18
|
+
* It does not create another inference runtime or load on construction.
|
|
19
|
+
*/
|
|
20
|
+
export function createNativeDecisionModel({
|
|
21
|
+
onnx, modelAssets, workingDirectory, paths,
|
|
22
|
+
model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
|
|
23
|
+
dtype = 'fp32', sessionOptions, executionPreference = 'gpu', signal
|
|
24
|
+
} = {}) {
|
|
25
|
+
if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
|
|
26
|
+
const owner = {};
|
|
27
|
+
const events = createArcaneEventSource(owner, {source: 'native-decisions', eventTypes: [STATE_EVENT]});
|
|
28
|
+
let activation;
|
|
29
|
+
let releaseTask;
|
|
30
|
+
let disposal;
|
|
31
|
+
let disposed = false;
|
|
32
|
+
let state = 'unloaded';
|
|
33
|
+
let progress = null;
|
|
34
|
+
let lastError = null;
|
|
35
|
+
let execution = null;
|
|
36
|
+
|
|
37
|
+
function current() {
|
|
38
|
+
return {
|
|
39
|
+
family: 'laya', model, revision, dtype, state,
|
|
40
|
+
loaded: state === 'ready', busy: state === 'loading' || Boolean(activation?.jobs.size),
|
|
41
|
+
activeRequests: activation?.jobs.size ?? 0, progress,
|
|
42
|
+
error: lastError ? serializeCoreError(lastError) : null,
|
|
43
|
+
execution
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function publish() { events.dispatch(STATE_EVENT, current()); }
|
|
48
|
+
|
|
49
|
+
function subscribe(listener, {emitCurrent = true, signal: subscriptionSignal} = {}) {
|
|
50
|
+
const unsubscribe = events.on(STATE_EVENT, function changed(event) { listener(event.detail); }, {signal: subscriptionSignal});
|
|
51
|
+
try { if (emitCurrent && !subscriptionSignal?.aborted) listener(current()); }
|
|
52
|
+
catch (error) { unsubscribe(); throw error; }
|
|
53
|
+
return unsubscribe;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertOpen(operationSignal) {
|
|
57
|
+
if (disposed) throw new CoreError({code: 'ARCANE_AI_DISPOSED', message: 'The native decision model is disposed.'});
|
|
58
|
+
signal?.throwIfAborted();
|
|
59
|
+
operationSignal?.throwIfAborted();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function updateProgress(entry, value) {
|
|
63
|
+
if (activation !== entry || entry.controller.signal.aborted) return;
|
|
64
|
+
progress = value;
|
|
65
|
+
publish();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stop(entry, reason, terminalState = 'unloaded') {
|
|
69
|
+
if (entry.release) return entry.release;
|
|
70
|
+
if (activation === entry) {
|
|
71
|
+
activation = undefined;
|
|
72
|
+
state = disposed ? 'disposing' : 'unloading';
|
|
73
|
+
progress = null;
|
|
74
|
+
lastError = terminalState === 'error' ? reason : null;
|
|
75
|
+
}
|
|
76
|
+
// Establish cleanup ownership before abort observers or state listeners
|
|
77
|
+
// can request another unload or dispose.
|
|
78
|
+
entry.release = Promise.resolve().then(async function releaseActivation() {
|
|
79
|
+
const failures = [];
|
|
80
|
+
await Promise.allSettled([entry.loading, ...entry.jobs]);
|
|
81
|
+
entry.unsubscribe?.();
|
|
82
|
+
const ownedSession = onnx.current().sessions.some(function owned(value) { return value.id === entry.id; });
|
|
83
|
+
const engineRelease = entry.onnxLoadStarted && ownedSession
|
|
84
|
+
? onnx.unload({id: entry.id})
|
|
85
|
+
: Promise.resolve();
|
|
86
|
+
const releases = await Promise.allSettled([engineRelease, entry.tokenizer?.close(reason)]);
|
|
87
|
+
for (const result of releases) if (result.status === 'rejected') failures.push(result.reason);
|
|
88
|
+
// unload joins actual native worker exit, even when load/run already
|
|
89
|
+
// rejected. Working files cannot be retired at promise rejection.
|
|
90
|
+
const exited = onnx.current().sessions.find(function owned(value) { return value.id === entry.id; })?.exited;
|
|
91
|
+
const engineStopped = releases[0].status === 'fulfilled' || exited === true;
|
|
92
|
+
const tokenizerStopped = !entry.tokenizer || entry.tokenizer.exited;
|
|
93
|
+
if (engineStopped && tokenizerStopped) {
|
|
94
|
+
try { await entry.retained?.release(); } catch (error) { failures.push(error); }
|
|
95
|
+
if (entry.prepared) {
|
|
96
|
+
try { await modelAssets.release(entry.id); } catch (error) { failures.push(error); }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const cleanupError = failures.length ? new AggregateError(failures, 'Releasing native decision resources failed.') : null;
|
|
100
|
+
if (!activation) {
|
|
101
|
+
state = disposed ? 'disposed' : cleanupError ? 'error' : terminalState;
|
|
102
|
+
progress = null;
|
|
103
|
+
execution = null;
|
|
104
|
+
if (cleanupError) lastError = cleanupError;
|
|
105
|
+
publish();
|
|
106
|
+
}
|
|
107
|
+
if (cleanupError) throw cleanupError;
|
|
108
|
+
return current();
|
|
109
|
+
});
|
|
110
|
+
releaseTask = entry.release;
|
|
111
|
+
entry.controller.abort(reason);
|
|
112
|
+
entry.tokenizer?.close(reason).catch(reportCleanupFailure);
|
|
113
|
+
publish();
|
|
114
|
+
entry.release.catch(reportCleanupFailure);
|
|
115
|
+
return entry.release;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function observeCancellation(entry, operationSignal) {
|
|
119
|
+
function abort() { stop(entry, operationSignal.reason).catch(reportCleanupFailure); }
|
|
120
|
+
operationSignal?.addEventListener('abort', abort, {once: true});
|
|
121
|
+
if (operationSignal?.aborted) abort();
|
|
122
|
+
return function detachAbort() { operationSignal?.removeEventListener('abort', abort); };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function prepareFiles(entry) {
|
|
126
|
+
if (paths) return paths;
|
|
127
|
+
if (!modelAssets) throw new CoreError({code: 'ARCANE_DECISION_ASSETS_UNAVAILABLE', message: 'Native decisions need prepared paths or the Core model-assets owner.'});
|
|
128
|
+
const base = `https://huggingface.co/${model}/resolve/${encodeURIComponent(revision)}/`;
|
|
129
|
+
const projection = await modelAssets.prepare({
|
|
130
|
+
id: entry.id, workingDirectory,
|
|
131
|
+
members: LAYA_FILES.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
|
|
132
|
+
signal: entry.controller.signal,
|
|
133
|
+
onProgress(value) { updateProgress(entry, value); }
|
|
134
|
+
});
|
|
135
|
+
entry.prepared = true;
|
|
136
|
+
entry.controller.signal.throwIfAborted();
|
|
137
|
+
entry.retained = modelAssets.retain(entry.id);
|
|
138
|
+
await modelAssets.release(entry.id);
|
|
139
|
+
entry.prepared = false;
|
|
140
|
+
const files = Object.fromEntries(projection.members.map(function memberFile(member) { return [member.path, member.nativePath]; }));
|
|
141
|
+
return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function beginActivation() {
|
|
145
|
+
const entry = {id: randomUUID(), controller: new AbortController(), jobs: new Set(), prepared: false, onnxLoadStarted: false};
|
|
146
|
+
activation = entry;
|
|
147
|
+
state = 'loading';
|
|
148
|
+
progress = {phase: 'preparing-model'};
|
|
149
|
+
lastError = null;
|
|
150
|
+
execution = null;
|
|
151
|
+
entry.loading = Promise.resolve().then(async function activateNativeModel() {
|
|
152
|
+
const files = await prepareFiles(entry);
|
|
153
|
+
entry.controller.signal.throwIfAborted();
|
|
154
|
+
updateProgress(entry, {phase: 'loading-model'});
|
|
155
|
+
entry.tokenizer = createDecisionTokenizer({
|
|
156
|
+
tokenizerPath: files.tokenizer, tokenizerConfigPath: files.tokenizerConfig,
|
|
157
|
+
signal: entry.controller.signal,
|
|
158
|
+
onError(error) {
|
|
159
|
+
if (activation === entry) stop(entry, error, 'error').catch(reportCleanupFailure);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
entry.onnxLoadStarted = true;
|
|
163
|
+
const loaded = onnx.load({
|
|
164
|
+
id: entry.id, model: files.model, sessionOptions, executionPreference,
|
|
165
|
+
signal: entry.controller.signal
|
|
166
|
+
});
|
|
167
|
+
const results = await Promise.allSettled([loaded, entry.tokenizer.ready]);
|
|
168
|
+
const failures = results.filter(function failed(result) { return result.status === 'rejected'; })
|
|
169
|
+
.map(function reason(result) { return result.reason; });
|
|
170
|
+
if (failures.length) throw failures.length === 1 ? failures[0] : new AggregateError(failures, 'Loading the native decision model failed.');
|
|
171
|
+
entry.controller.signal.throwIfAborted();
|
|
172
|
+
if (activation !== entry) throw cancellation('The decision activation was replaced.');
|
|
173
|
+
execution = results[0].value;
|
|
174
|
+
state = 'ready';
|
|
175
|
+
progress = {phase: 'ready'};
|
|
176
|
+
entry.unsubscribe = onnx.subscribe(function nativeStateChanged(snapshot) {
|
|
177
|
+
if (activation !== entry || entry.controller.signal.aborted) return;
|
|
178
|
+
const selected = snapshot.sessions.find(function sameSession(value) { return value.id === entry.id; });
|
|
179
|
+
// A failed run may leave ONNX loaded and its queued runs usable.
|
|
180
|
+
// Stopping revokes readiness before physical residency ends.
|
|
181
|
+
if (snapshot.closed || selected?.stopping || !selected?.loaded || ['unloading', 'unloaded'].includes(selected.state)) {
|
|
182
|
+
const error = selected?.error ? new CoreError(selected.error) : cancellation('The native ONNX session was retired.');
|
|
183
|
+
stop(entry, error, selected?.error ? 'error' : 'unloaded').catch(reportCleanupFailure);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
publish();
|
|
187
|
+
return current();
|
|
188
|
+
});
|
|
189
|
+
entry.loading.catch(function activationFailed(error) {
|
|
190
|
+
if (activation === entry) stop(entry, error, entry.controller.signal.aborted ? 'unloaded' : 'error').catch(reportCleanupFailure);
|
|
191
|
+
});
|
|
192
|
+
publish();
|
|
193
|
+
return entry;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function load({signal: operationSignal} = {}) {
|
|
197
|
+
assertOpen(operationSignal);
|
|
198
|
+
if (releaseTask && !activation) await releaseTask;
|
|
199
|
+
assertOpen(operationSignal);
|
|
200
|
+
const entry = activation ?? beginActivation();
|
|
201
|
+
const detach = observeCancellation(entry, operationSignal);
|
|
202
|
+
try {
|
|
203
|
+
await entry.loading;
|
|
204
|
+
entry.controller.signal.throwIfAborted();
|
|
205
|
+
return current();
|
|
206
|
+
} finally { detach(); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function evaluate(rows, {signal: operationSignal, runOptions} = {}) {
|
|
210
|
+
assertOpen(operationSignal);
|
|
211
|
+
const entry = activation;
|
|
212
|
+
if (!entry) throw new CoreError({code: 'ARCANE_DECISION_NOT_LOADED', message: 'Load the selected native decision model before evaluating.'});
|
|
213
|
+
const detach = observeCancellation(entry, operationSignal);
|
|
214
|
+
const task = Promise.resolve().then(async function evaluateCompleteRows() {
|
|
215
|
+
await entry.loading;
|
|
216
|
+
entry.controller.signal.throwIfAborted();
|
|
217
|
+
if (rows.length === 0) return {decisions: [], outputs: {}};
|
|
218
|
+
updateProgress(entry, {phase: 'tokenizing'});
|
|
219
|
+
const feeds = await entry.tokenizer.encode(rows);
|
|
220
|
+
entry.controller.signal.throwIfAborted();
|
|
221
|
+
updateProgress(entry, {phase: 'evaluating'});
|
|
222
|
+
const outputs = await onnx.run({id: entry.id, feeds, runOptions, signal: entry.controller.signal});
|
|
223
|
+
entry.controller.signal.throwIfAborted();
|
|
224
|
+
return decodeDecisionOutputs(rows, outputs);
|
|
225
|
+
});
|
|
226
|
+
entry.jobs.add(task);
|
|
227
|
+
publish();
|
|
228
|
+
let result;
|
|
229
|
+
try { result = await task; }
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (activation === entry && !entry.controller.signal.aborted) {
|
|
232
|
+
lastError = error;
|
|
233
|
+
publish();
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
} finally {
|
|
237
|
+
detach();
|
|
238
|
+
entry.jobs.delete(task);
|
|
239
|
+
if (activation === entry) {
|
|
240
|
+
progress = {phase: entry.jobs.size ? 'evaluating' : 'ready'};
|
|
241
|
+
publish();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
entry.controller.signal.throwIfAborted();
|
|
245
|
+
if (activation !== entry) throw cancellation('The decision result belongs to a retired activation.');
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function unload() {
|
|
250
|
+
return activation ? stop(activation, cancellation('The native decision model was unloaded.'))
|
|
251
|
+
: releaseTask ?? Promise.resolve(current());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function dispose() {
|
|
255
|
+
if (disposal) return disposal;
|
|
256
|
+
disposed = true;
|
|
257
|
+
signal?.removeEventListener('abort', lifetimeAborted);
|
|
258
|
+
let release;
|
|
259
|
+
disposal = Promise.resolve().then(async function disposeNativeModel() {
|
|
260
|
+
try { await release; }
|
|
261
|
+
finally { state = 'disposed'; publish(); events.dispose(); }
|
|
262
|
+
return current();
|
|
263
|
+
});
|
|
264
|
+
release = activation ? stop(activation, cancellation('The native decision model was disposed.')) : releaseTask;
|
|
265
|
+
if (!activation && !release) { state = 'disposed'; publish(); }
|
|
266
|
+
return disposal;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function lifetimeAborted() { dispose().catch(reportCleanupFailure); }
|
|
270
|
+
signal?.addEventListener('abort', lifetimeAborted, {once: true});
|
|
271
|
+
if (signal?.aborted) lifetimeAborted();
|
|
272
|
+
return {load, current, status: current, subscribe, evaluate, classify: evaluate, unload, dispose};
|
|
273
|
+
}
|
package/src/local-ai/dev.mjs
CHANGED
|
@@ -40,10 +40,16 @@ export function createDevelopmentLocalAI({
|
|
|
40
40
|
let modelAssets;
|
|
41
41
|
let imageService;
|
|
42
42
|
let speechService;
|
|
43
|
-
|
|
43
|
+
const imageSelected = selected.runtimes.some(function selectedImageRuntime(requirement) {
|
|
44
44
|
return requirement.id === 'stable-diffusion.cpp';
|
|
45
|
+
});
|
|
46
|
+
if (imageSelected || selected.runtimes.some(function selectedONNXRuntime(requirement) {
|
|
47
|
+
return requirement.id === 'onnx';
|
|
45
48
|
})) {
|
|
46
49
|
modelAssets = createModelAssetService({appRoot});
|
|
50
|
+
definitions.push(modelAssets);
|
|
51
|
+
}
|
|
52
|
+
if (imageSelected) {
|
|
47
53
|
imageService = createLocalImageService(
|
|
48
54
|
selected,
|
|
49
55
|
{
|
|
@@ -56,7 +62,7 @@ export function createDevelopmentLocalAI({
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
);
|
|
59
|
-
definitions.push(
|
|
65
|
+
definitions.push(imageService);
|
|
60
66
|
}
|
|
61
67
|
const whisperRequirement = selected.runtimes.find(function selectedWhisperRuntime(requirement) {
|
|
62
68
|
return requirement.id === 'whisper.cpp';
|
|
@@ -15,6 +15,71 @@ function describeError(error){
|
|
|
15
15
|
return result;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
async function createSession(request) {
|
|
19
|
+
const supplied = request.sessionOptions;
|
|
20
|
+
const selectProviders = supplied === undefined
|
|
21
|
+
|| (supplied !== null && is.object(supplied) && supplied.executionProviders === undefined);
|
|
22
|
+
const options = selectProviders ? {...supplied, executionProviders: ['cpu']} : supplied;
|
|
23
|
+
if (request.executionPreference !== 'gpu' || !selectProviders) {
|
|
24
|
+
session = await ort.InferenceSession.create(request.model, options);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const execution = {
|
|
29
|
+
preference: 'gpu',
|
|
30
|
+
supportedBackends: [],
|
|
31
|
+
selectedProviders: [],
|
|
32
|
+
attempts: [],
|
|
33
|
+
fallback: false,
|
|
34
|
+
discoveryError: null
|
|
35
|
+
};
|
|
36
|
+
const failures = [];
|
|
37
|
+
try {
|
|
38
|
+
execution.supportedBackends = ort.listSupportedBackends();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
execution.discoveryError = describeError(error);
|
|
41
|
+
failures.push(error);
|
|
42
|
+
}
|
|
43
|
+
// The inventory advertises compiled support. `bundled` describes packaging,
|
|
44
|
+
// not whether an installed provider library or physical device can load.
|
|
45
|
+
const candidates = ['cuda', 'tensorrt', 'dml', 'coreml', 'webgpu'].filter(
|
|
46
|
+
function advertisedProvider(name) {
|
|
47
|
+
return execution.supportedBackends.some(
|
|
48
|
+
function supportedBackend(backend) { return backend.name === name; }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
candidates.push('cpu');
|
|
53
|
+
for (const provider of candidates) {
|
|
54
|
+
const executionProviders = provider === 'cpu' ? ['cpu'] : [provider, 'cpu'];
|
|
55
|
+
const selectedOptions = {...supplied, executionProviders};
|
|
56
|
+
if (provider === 'dml') {
|
|
57
|
+
// DirectML requires these session settings. Explicit caller values
|
|
58
|
+
// remain exact and can produce the native provider's own error.
|
|
59
|
+
if (selectedOptions.enableMemPattern === undefined) selectedOptions.enableMemPattern = false;
|
|
60
|
+
if (selectedOptions.executionMode === undefined) selectedOptions.executionMode = 'sequential';
|
|
61
|
+
}
|
|
62
|
+
const attempt = {executionProviders, status: 'loading', error: null};
|
|
63
|
+
execution.attempts.push(attempt);
|
|
64
|
+
execution.fallback = provider === 'cpu';
|
|
65
|
+
try {
|
|
66
|
+
session = await ort.InferenceSession.create(request.model, selectedOptions);
|
|
67
|
+
attempt.status = 'configured';
|
|
68
|
+
execution.selectedProviders = executionProviders;
|
|
69
|
+
// Successful creation proves this session configuration was accepted.
|
|
70
|
+
// ORT can assign graph nodes to CPU; it does not prove GPU execution.
|
|
71
|
+
return execution;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
attempt.status = 'failed';
|
|
74
|
+
attempt.error = describeError(error);
|
|
75
|
+
failures.push(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const error = new AggregateError(failures, 'ONNX accelerator session creation and CPU fallback failed.');
|
|
79
|
+
error.execution = execution;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
18
83
|
function tensorMap(records,inputs=false){
|
|
19
84
|
return Object.fromEntries(Object.entries(records).map(function createTensor([name,record]){
|
|
20
85
|
// ORT Node 1.30.0 supplies string feeds to FillStringTensor through
|
|
@@ -33,19 +98,14 @@ async function handleRequest(request){
|
|
|
33
98
|
try{
|
|
34
99
|
let result;
|
|
35
100
|
if(request.operation==='load'){
|
|
36
|
-
const
|
|
37
|
-
const options=supplied===undefined
|
|
38
|
-
?{executionProviders:['cpu']}
|
|
39
|
-
:supplied!==null&&is.object(supplied)&&supplied.executionProviders===undefined
|
|
40
|
-
?{...supplied,executionProviders:['cpu']}
|
|
41
|
-
:supplied;
|
|
42
|
-
session=await ort.InferenceSession.create(request.model,options);
|
|
101
|
+
const execution = await createSession(request);
|
|
43
102
|
result={
|
|
44
103
|
inputNames:session.inputNames,
|
|
45
104
|
outputNames:session.outputNames,
|
|
46
105
|
inputMetadata:session.inputMetadata,
|
|
47
106
|
outputMetadata:session.outputMetadata
|
|
48
107
|
};
|
|
108
|
+
if (execution) result.execution = execution;
|
|
49
109
|
}else if(request.operation==='run'){
|
|
50
110
|
const feeds=tensorMap(request.feeds,true);
|
|
51
111
|
const fetches=request.fetches===undefined||request.fetches===null||is.array(request.fetches)
|
package/src/local-ai/onnx.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
95
95
|
|
|
96
96
|
function current(){
|
|
97
97
|
return {sessions:[...sessions.values()].map(function sessionState(entry){
|
|
98
|
-
return structuredClone(entry.state);
|
|
98
|
+
return {...structuredClone(entry.state), stopping:entry.stopping, exited:entry.exited};
|
|
99
99
|
}),closed};
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -157,20 +157,31 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
157
157
|
entry.stopping=true;
|
|
158
158
|
if(failed)entry.terminalError??=reason;
|
|
159
159
|
entry.stopTask=Promise.resolve().then(async function releaseSession(){
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}else{
|
|
164
|
-
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
165
|
-
catch(error){
|
|
166
|
-
entry.releaseError=error;
|
|
160
|
+
let terminationError;
|
|
161
|
+
try{
|
|
162
|
+
if(active||failed||entry.terminalError){
|
|
167
163
|
entry.terminationRequested=true;
|
|
168
164
|
await worker.terminate();
|
|
165
|
+
}else{
|
|
166
|
+
try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
|
|
167
|
+
catch(error){
|
|
168
|
+
entry.releaseError=error;
|
|
169
|
+
entry.terminationRequested=true;
|
|
170
|
+
await worker.terminate();
|
|
171
|
+
}
|
|
169
172
|
}
|
|
173
|
+
}catch(error){
|
|
174
|
+
terminationError=error;
|
|
175
|
+
reportBackgroundError(error);
|
|
170
176
|
}
|
|
177
|
+
// Failed termination is not actual worker exit. Keep native file
|
|
178
|
+
// ownership until exit and complete output delivery have settled.
|
|
171
179
|
await entry.exit;
|
|
172
|
-
await Promise.
|
|
173
|
-
|
|
180
|
+
const outputResults=await Promise.allSettled(entry.outputTasks);
|
|
181
|
+
const failures=[entry.releaseError,terminationError].filter(function present(error){return error!==null&&error!==undefined;});
|
|
182
|
+
for(const result of outputResults)if(result.status==='rejected')failures.push(result.reason);
|
|
183
|
+
if(failures.length>1)throw new AggregateError(failures,'Releasing the ONNX worker failed.');
|
|
184
|
+
if(failures.length)throw failures[0];
|
|
174
185
|
return structuredClone(entry.state);
|
|
175
186
|
});
|
|
176
187
|
// A native call has no AbortSignal. Stop delivery immediately, retain
|
|
@@ -262,7 +273,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
262
273
|
finally{entry.resolveExit(code);}
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
async function load({id,model,sessionOptions,signal:operationSignal}={}){
|
|
276
|
+
async function load({id,model,sessionOptions,executionPreference,signal:operationSignal}={}){
|
|
266
277
|
assertOpen(operationSignal);
|
|
267
278
|
if(!is.string(id)||!is.string(model))throw new TypeError('ONNX load requires a session id and model path.');
|
|
268
279
|
const previous=sessions.get(id);
|
|
@@ -312,7 +323,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
|
|
|
312
323
|
}
|
|
313
324
|
// Queue load before publishing readiness so a synchronous subscriber
|
|
314
325
|
// can enqueue inference or request release without racing ownership.
|
|
315
|
-
const task=enqueue(entry,'load',{model,sessionOptions},operationSignal,true);
|
|
326
|
+
const task=enqueue(entry,'load',{model,sessionOptions,executionPreference},operationSignal,true);
|
|
316
327
|
publish(entry);
|
|
317
328
|
dispatchNext(entry);
|
|
318
329
|
return task;
|
|
@@ -153,6 +153,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
153
153
|
const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
|
|
154
154
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
|
|
155
155
|
}) ?? false;
|
|
156
|
+
const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedONNX(requirement) {
|
|
157
|
+
return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
|
|
158
|
+
}) ?? false);
|
|
156
159
|
const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
|
|
157
160
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
|
|
158
161
|
}) ?? false;
|
|
@@ -175,10 +178,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
175
178
|
imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
|
|
176
179
|
definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
|
|
177
180
|
}
|
|
178
|
-
if (
|
|
179
|
-
imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
|
|
181
|
+
if (modelAssetsSelected) {
|
|
180
182
|
imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
|
|
181
183
|
definitions.push(' modelAssets');
|
|
184
|
+
}
|
|
185
|
+
if (imageSelected) {
|
|
186
|
+
imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
|
|
182
187
|
definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
|
|
183
188
|
}
|
|
184
189
|
if (whisperSelected) {
|
|
@@ -239,7 +244,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
239
244
|
' ...await readCoreLaunchContext()',
|
|
240
245
|
'};',
|
|
241
246
|
'',
|
|
242
|
-
...(
|
|
247
|
+
...(modelAssetsSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
|
|
243
248
|
...(whisperSelected ? [
|
|
244
249
|
'const stt = createWhisperRuntime(',
|
|
245
250
|
' {',
|