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.
@@ -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
+ }
@@ -40,10 +40,16 @@ export function createDevelopmentLocalAI({
40
40
  let modelAssets;
41
41
  let imageService;
42
42
  let speechService;
43
- if (selected.runtimes.some(function selectedImageRuntime(requirement) {
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(modelAssets, imageService);
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 supplied=request.sessionOptions;
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)
@@ -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
- if(active||failed||entry.terminalError){
161
- entry.terminationRequested=true;
162
- await worker.terminate();
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.all(entry.outputTasks);
173
- if(entry.releaseError)throw entry.releaseError;
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 (imageSelected) {
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
- ...(imageSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
247
+ ...(modelAssetsSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
243
248
  ...(whisperSelected ? [
244
249
  'const stt = createWhisperRuntime(',
245
250
  ' {',