arcane-os 0.57.0 → 0.59.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-onnx.mjs +96 -0
  4. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  5. package/browser-runtime/core/client.mjs +152 -17
  6. package/docs/architecture.md +5 -2
  7. package/docs/platform-targets.md +30 -13
  8. package/docs/reference/README.md +22 -3
  9. package/docs/reference/availability-and-normalization.md +14 -7
  10. package/docs/reference/cli.md +22 -10
  11. package/docs/reference/core-client.md +87 -0
  12. package/docs/reference/core-native-packaging.md +129 -9
  13. package/docs/reference/core-preferences.md +49 -0
  14. package/docs/reference/diarization.md +206 -0
  15. package/docs/reference/git-text-snapshot.md +104 -0
  16. package/docs/reference/inventory/package-api.json +205 -23
  17. package/docs/reference/local-ai.md +145 -11
  18. package/docs/reference/protocols.md +28 -19
  19. package/docs/reference/pwa.md +74 -4
  20. package/docs/reference/sdk-api.md +589 -39
  21. package/package.json +11 -3
  22. package/schemas/arcane-app.schema.json +3 -3
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/schemas/native-build-plan.schema.json +27 -1
  25. package/schemas/target-adapter.schema.json +2 -0
  26. package/src/app-descriptor.mjs +1 -1
  27. package/src/cli/main.mjs +14 -4
  28. package/src/constants.mjs +2 -0
  29. package/src/core/host.mjs +14 -5
  30. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  31. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  32. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  33. package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
  34. package/src/core/services/local-ai.mjs +82 -4
  35. package/src/core/services/packaged-web.mjs +89 -0
  36. package/src/core/services/preferences.mjs +201 -0
  37. package/src/core-classic-source.mjs +2 -2
  38. package/src/dev-server.mjs +2 -5
  39. package/src/diarization/build.mjs +35 -0
  40. package/src/diarization/index.mjs +432 -0
  41. package/src/diarization/native/CMakeLists.txt +9 -0
  42. package/src/diarization/native/main.cpp +269 -0
  43. package/src/diarization/service.mjs +129 -0
  44. package/src/git-text-snapshot.mjs +265 -0
  45. package/src/import-map.mjs +9 -4
  46. package/src/index.mjs +1 -0
  47. package/src/local-ai/config.mjs +1 -1
  48. package/src/local-ai/install.mjs +134 -19
  49. package/src/local-ai/onnx-worker.mjs +85 -0
  50. package/src/local-ai/onnx.mjs +368 -0
  51. package/src/native/macos-provider.mjs +291 -0
  52. package/src/native/portable-layout.mjs +10 -2
  53. package/src/native/portable-provider.mjs +10 -5
  54. package/src/native-plan.mjs +9 -1
  55. package/src/native-provider-loader.mjs +3 -1
  56. package/src/process.mjs +123 -17
  57. package/src/scaffold.mjs +1 -1
  58. package/src/targets/index.mjs +11 -1
  59. package/src/templates/workspace-template.mjs +6 -5
  60. package/src/toolchain.mjs +42 -13
@@ -50,6 +50,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
50
50
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
51
51
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
52
52
  ['arcane-os/ai/core-local','sdk/ai/core-local.mjs'],
53
+ ['arcane-os/ai/core-onnx','sdk/ai/core-onnx.mjs'],
53
54
  ['arcane-os/ai/browser-decisions','sdk/ai/browser-decisions.mjs'],
54
55
  ['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
55
56
  ['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
@@ -2090,8 +2091,8 @@ function scanHtmlStructure(html){
2090
2091
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
2091
2092
  }
2092
2093
 
2093
- /** Remove generated PWA references from a selected output document without changing authored content. */
2094
- export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {}) {
2094
+ /** Retire active PWA references; source refresh may restrict removal to marked ownership. */
2095
+ export function removePwaEntryReferences(html, {documentUrl, manifestUrl, markedManifestOnly = false} = {}) {
2095
2096
  if (!is.string(html)) throw new TypeError('PWA entry HTML must be a string.');
2096
2097
  const structure = scanHtmlStructure(html);
2097
2098
  const manifest = new URL(manifestUrl, documentUrl);
@@ -2105,6 +2106,7 @@ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {})
2105
2106
  const edits = [];
2106
2107
  for (const link of structure.links) {
2107
2108
  const attributes = parseTagAttributes(link.open);
2109
+ if (markedManifestOnly && !attributes.has('data-arcane-manifest')) continue;
2108
2110
  const href = htmlAttributeView(attributes.get('href') ?? '');
2109
2111
  const rel = htmlAttributeView(attributes.get('rel') ?? '');
2110
2112
  if (!href?.decoded || !rel) continue;
@@ -2151,7 +2153,7 @@ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {})
2151
2153
  }
2152
2154
 
2153
2155
  /** Update only active PWA entry references; registration never orders application startup. */
2154
- export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2156
+ export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl,markGeneratedManifest=false}={}){
2155
2157
  if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');
2156
2158
  if(!is.string(manifestUrl)||!manifestUrl||!is.string(bootstrapUrl)||!bootstrapUrl){
2157
2159
  throw new TypeError('PWA manifestUrl and bootstrapUrl must be nonempty strings.');
@@ -2222,7 +2224,10 @@ export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2222
2224
  });
2223
2225
  }
2224
2226
  }
2225
- if(!manifestFound)additions.push(`<link rel="manifest" href="${attributeText(manifestUrl)}">`);
2227
+ if(!manifestFound){
2228
+ const marker=markGeneratedManifest?' data-arcane-manifest':'';
2229
+ additions.push(`<link rel="manifest" href="${attributeText(manifestUrl)}"${marker}>`);
2230
+ }
2226
2231
  let bootstrapFound=false;
2227
2232
  for(const script of structure.scripts){
2228
2233
  if(!parseTagAttributes(script.open).has('data-arcane-pwa'))continue;
package/src/index.mjs CHANGED
@@ -65,6 +65,7 @@ export {
65
65
  TIME_TRAVEL_SEEK_EVENT
66
66
  } from './event-manager.mjs';
67
67
  export {runProcess} from './process.mjs';
68
+ export {createGitTextSnapshot} from './git-text-snapshot.mjs';
68
69
  export {
69
70
  DEFAULT_TEST_TIMEOUT_MS,
70
71
  registeredTestCount,
@@ -4,7 +4,7 @@ import Is from 'strong-type';
4
4
  import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
5
5
 
6
6
  const is = new Is(false);
7
- export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama'];
7
+ export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama', 'onnx'];
8
8
 
9
9
  /** Runtime requirements are application configuration, separate from models. */
10
10
  export function normalizeLocalAIConfig(value) {
@@ -1,11 +1,13 @@
1
1
  import Is from 'strong-type';
2
2
  import {createWriteStream} from 'node:fs';
3
3
  import {chmod,copyFile,lstat,mkdir,mkdtemp,readFile,readdir,readlink,rm,stat,symlink,unlink,writeFile} from 'node:fs/promises';
4
+ import {createRequire} from 'node:module';
4
5
  import path from 'node:path';
5
6
  import {Readable} from 'node:stream';
6
7
  import {pipeline} from 'node:stream/promises';
7
8
  import {ArcaneError,ERROR_CODES,normalizeError,throwIfAborted} from '../errors.mjs';
8
9
  import {createEventQueue} from '../event-queue.mjs';
10
+ import {runProcess} from '../process.mjs';
9
11
  import {extractLocalAIArchive} from './archive.mjs';
10
12
 
11
13
  const is=new Is(false);
@@ -16,8 +18,9 @@ function selectRuntimes(runtimes){
16
18
  throw new ArcaneError(ERROR_CODES.usage,'Local AI runtimes must be an array.');
17
19
  }
18
20
  const selected=new Map();
19
- for(const runtime of runtimes){
20
- if(!runtime||!['llama.cpp','ollama'].includes(runtime.id)){
21
+ for(const item of runtimes){
22
+ const runtime=is.string(item)?{id:item}:item;
23
+ if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx'].includes(runtime.id)){
21
24
  throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
22
25
  }
23
26
  const prior=selected.get(runtime.id);
@@ -43,6 +46,15 @@ function selectedDirectory(directory){
43
46
  }
44
47
 
45
48
  function releaseAssetNames(id,version,platform,architecture){
49
+ if(id==='nemo-speech'){
50
+ if(!['x64','arm64'].includes(architecture))return [];
51
+ const arch=architecture==='x64'?'x86_64':'aarch64';
52
+ const name=`nemo-speech-${version.replace(/^v/u,'')}`;
53
+ if(platform==='win32'&&architecture==='x64')return [`${name}-windows-${arch}-cpu.zip`];
54
+ if(platform==='linux')return [`${name}-linux-${arch}-cpu.tar.gz`];
55
+ if(platform==='darwin')return [`${name}-macos-${arch}-cpu.tar.gz`];
56
+ return [];
57
+ }
46
58
  if(id==='ollama'){
47
59
  if(platform==='darwin'&&['x64','arm64'].includes(architecture))return ['ollama-darwin.tgz'];
48
60
  const arch=architecture==='x64'?'amd64':architecture;
@@ -74,9 +86,10 @@ async function selectRelease(runtime,platform,architecture,signal){
74
86
  const name=path.posix.basename(new URL(runtime.url).pathname);
75
87
  return {version:runtime.version??'custom',url:runtime.url,name};
76
88
  }
77
- const repository=runtime.id==='llama.cpp'?'ggml-org/llama.cpp':'ollama/ollama';
78
- const selected=runtime.version&&runtime.version!=='latest'
79
- ?`tags/${encodeURIComponent(runtime.id==='ollama'&&!runtime.version.startsWith('v')?`v${runtime.version}`:runtime.version)}`
89
+ const repository=runtime.id==='nemo-speech'?'NVIDIA/NeMo-Speech.cpp':runtime.id==='llama.cpp'?'ggml-org/llama.cpp':'ollama/ollama';
90
+ const version=runtime.version??(runtime.id==='nemo-speech'?'0.2.0':undefined);
91
+ const selected=version&&version!=='latest'
92
+ ?`tags/${encodeURIComponent(runtime.id!=='llama.cpp'&&!version.startsWith('v')?`v${version}`:version)}`
80
93
  :'latest';
81
94
  let release=await (await upstreamResponse(`https://api.github.com/repos/${repository}/releases/${selected}`,signal)).json();
82
95
  if(runtime.id==='llama.cpp'){
@@ -118,8 +131,24 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
118
131
  const record=installation.runtime;
119
132
  if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
120
133
  try{
134
+ if(runtime.id==='onnx'){
135
+ if(!is.string(record.root)||!is.string(record.modulePath))continue;
136
+ const locations=await onnxRuntimeModule(record.root,platform,architecture,signal);
137
+ return {...record,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
138
+ }
139
+ if(runtime.id==='nemo-speech'){
140
+ const directories=[record.root,record.includeDirectory,record.libraryDirectory,record.binaryDirectory,record.cmakeDirectory];
141
+ if(directories.some(function missingDirectory(directory){return !is.string(directory);}))continue;
142
+ const files=[path.join(record.includeDirectory,'nemo_speech','diar.h'),path.join(record.includeDirectory,'nemo_speech','asr.h'),path.join(record.cmakeDirectory,'NeMoSpeechConfig.cmake')];
143
+ const entries=await Promise.all([...directories,...files].map(function inspectRuntimePath(location){return stat(location);}));
144
+ if(entries.every(function runtimePathPresent(info,index){return index<directories.length?info.isDirectory():info.isFile();})){
145
+ return {...record,...(runtime.version?{requestedVersion:runtime.version}:{})};
146
+ }
147
+ continue;
148
+ }
121
149
  if((await stat(record.executable)).isFile())return {...record,...(runtime.version?{requestedVersion:runtime.version}:{})};
122
150
  }catch(error){
151
+ if(runtime.id==='onnx'&&(error.code==='MODULE_NOT_FOUND'||error instanceof SyntaxError))continue;
123
152
  if(error.code!=='ENOENT')throw error;
124
153
  }
125
154
  }
@@ -141,6 +170,77 @@ async function runtimeExecutable(root,id,platform,signal){
141
170
  throw new ArcaneError(ERROR_CODES.operationFailed,`The ${id} archive does not contain ${filename}.`);
142
171
  }
143
172
 
173
+ async function nemoRuntimeDirectories(root,signal){
174
+ const directories=[root];
175
+ for(const directory of directories){
176
+ throwIfAborted(signal);
177
+ const entries=await readdir(directory,{withFileTypes:true});
178
+ for(const entry of entries){
179
+ if(entry.isDirectory())directories.push(path.join(directory,entry.name));
180
+ }
181
+ if(path.basename(directory)!=='nemo_speech'||path.basename(path.dirname(directory))!=='include')continue;
182
+ const headers=['diar.h','asr.h'];
183
+ if(!headers.every(function headerPresent(name){
184
+ return entries.some(function matchingHeader(entry){return entry.name===name&&(entry.isFile()||entry.isSymbolicLink());});
185
+ }))continue;
186
+ const includeDirectory=path.dirname(directory);
187
+ const prefix=path.dirname(includeDirectory);
188
+ const children=await readdir(prefix,{withFileTypes:true});
189
+ const binaryDirectory=path.join(prefix,'bin');
190
+ if(!(await stat(binaryDirectory)).isDirectory()){
191
+ throw new ArcaneError(ERROR_CODES.operationFailed,'The NeMo Speech archive does not contain its installed binary directory.');
192
+ }
193
+ const candidates=children.filter(function libraryDirectory(entry){
194
+ return ['lib','lib64'].includes(entry.name)&&(entry.isDirectory()||entry.isSymbolicLink());
195
+ }).map(function librarySearch(entry){
196
+ const libraryDirectory=path.join(prefix,entry.name);
197
+ return {directory:libraryDirectory,libraryDirectory};
198
+ });
199
+ for(const candidate of candidates){
200
+ throwIfAborted(signal);
201
+ const entries=await readdir(candidate.directory,{withFileTypes:true});
202
+ if(entries.some(function packageConfiguration(entry){
203
+ return entry.name==='NeMoSpeechConfig.cmake'&&(entry.isFile()||entry.isSymbolicLink());
204
+ })){
205
+ return {includeDirectory,libraryDirectory:candidate.libraryDirectory,binaryDirectory,cmakeDirectory:candidate.directory};
206
+ }
207
+ for(const entry of entries){
208
+ if(entry.isDirectory())candidates.push({directory:path.join(candidate.directory,entry.name),libraryDirectory:candidate.libraryDirectory});
209
+ }
210
+ }
211
+ throw new ArcaneError(ERROR_CODES.operationFailed,'The NeMo Speech archive does not contain its installed NeMoSpeech CMake package.');
212
+ }
213
+ throw new ArcaneError(ERROR_CODES.operationFailed,'The NeMo Speech archive does not contain include/nemo_speech/diar.h and asr.h.');
214
+ }
215
+
216
+ async function onnxRuntimeModule(root,platform,architecture,signal){
217
+ throwIfAborted(signal);
218
+ const packageDirectory=path.join(root,'node_modules','onnxruntime-node');
219
+ const metadata=JSON.parse(await readFile(path.join(packageDirectory,'package.json'),{encoding:'utf8',signal}));
220
+ // Node resolves the installed public entry without loading its native code.
221
+ const modulePath=createRequire(path.join(root,'package.json')).resolve('onnxruntime-node');
222
+ const binding=path.join(packageDirectory,'bin','napi-v6',platform,architecture,'onnxruntime_binding.node');
223
+ const entries=await Promise.all([modulePath,binding].map(function inspectONNXFile(filename){return stat(filename);}));
224
+ if(!entries.every(function onnxFilePresent(info){return info.isFile();})){
225
+ throw new ArcaneError(ERROR_CODES.operationFailed,`The ONNX package does not contain its public module and ${platform}/${architecture} native binding.`);
226
+ }
227
+ throwIfAborted(signal);
228
+ return {version:metadata.version,modulePath};
229
+ }
230
+
231
+ async function installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent}){
232
+ const version=runtime.version??'1.30.0';
233
+ await onEvent({type:'local-ai.install.installing',message:`Installing onnxruntime-node ${version} for ${platform}/${architecture}.`,data:{id:runtime.id,version,platform,architecture}});
234
+ await mkdir(root,{recursive:true});
235
+ await writeFile(path.join(root,'package.json'),'{"private":true}\n',{flag:'wx',signal});
236
+ await runProcess('npm',[
237
+ 'install','--prefix',root,'--global=false','--save-exact','--omit=dev',
238
+ '--no-audit','--no-fund','--foreground-scripts','--registry=https://registry.npmjs.org/',
239
+ '--cache',path.join(directory,'npm-cache'),`onnxruntime-node@${runtime.url??version}`
240
+ ],{cwd:root,env:{ONNXRUNTIME_NODE_INSTALL:'skip'},signal,onEvent});
241
+ return onnxRuntimeModule(root,platform,architecture,signal);
242
+ }
243
+
144
244
  async function installRuntime(runtime,{directory,platform,architecture,signal,onEvent}){
145
245
  const base=path.join(directory,runtime.id,`${platform}-${architecture}`,encodeURIComponent(runtime.version??'default'));
146
246
  await onEvent({type:'local-ai.install.starting',message:`Preparing ${runtime.id} for ${platform}/${architecture}.`,data:{id:runtime.id,platform,architecture}});
@@ -149,26 +249,36 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
149
249
  await onEvent({type:'local-ai.install.available',message:`${runtime.id} ${installed.version} is already installed.`,data:installed});
150
250
  return installed;
151
251
  }
152
- const release=await selectRelease(runtime,platform,architecture,signal);
252
+ if(runtime.id==='onnx'&&(!['win32','linux','darwin'].includes(platform)||!['x64','arm64'].includes(architecture))){
253
+ throw new ArcaneError(ERROR_CODES.targetUnavailable,`onnxruntime-node has no supported ${platform}/${architecture} CPU runtime.`);
254
+ }
255
+ const release=runtime.id==='onnx'?null:await selectRelease(runtime,platform,architecture,signal);
153
256
  throwIfAborted(signal);
154
257
  await mkdir(base,{recursive:true});
155
258
  const attempt=await mkdtemp(path.join(base,'install-'));
156
259
  const root=path.join(attempt,'runtime');
157
- const archive=path.join(attempt,release.name);
260
+ const archive=release?path.join(attempt,release.name):null;
158
261
  let completed=false;
159
262
  try{
160
- await onEvent({type:'local-ai.install.downloading',message:`Downloading ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version,platform,architecture}});
161
- const response=await upstreamResponse(release.url,signal);
162
- await pipeline(Readable.fromWeb(response.body),createWriteStream(archive,{flags:'wx'}),{signal});
163
- await onEvent({type:'local-ai.install.extracting',message:`Extracting ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version}});
164
- await extractLocalAIArchive({archive,directory:root,signal,onEvent});
165
- const executable=await runtimeExecutable(root,runtime.id,platform,signal);
166
- const record={id:runtime.id,version:release.version,platform,architecture,root,executable,...(runtime.version?{requestedVersion:runtime.version}:{})};
263
+ let locations;
264
+ if(runtime.id==='onnx'){
265
+ locations=await installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent});
266
+ }else{
267
+ await onEvent({type:'local-ai.install.downloading',message:`Downloading ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version,platform,architecture}});
268
+ const response=await upstreamResponse(release.url,signal);
269
+ await pipeline(Readable.fromWeb(response.body),createWriteStream(archive,{flags:'wx'}),{signal});
270
+ await onEvent({type:'local-ai.install.extracting',message:`Extracting ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version}});
271
+ await extractLocalAIArchive({archive,directory:root,signal,onEvent});
272
+ locations={version:release.version,...(runtime.id==='nemo-speech'
273
+ ?await nemoRuntimeDirectories(root,signal)
274
+ :{executable:await runtimeExecutable(root,runtime.id,platform,signal)})};
275
+ }
276
+ const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
167
277
  throwIfAborted(signal);
168
- await unlink(archive);
278
+ if(archive)await unlink(archive);
169
279
  await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,runtime:record},null,2)}\n`,{flag:'wx',signal});
170
280
  completed=true;
171
- await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${release.version} is available.`,data:record});
281
+ await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
172
282
  return record;
173
283
  }catch(error){
174
284
  // Only this invocation's newly created incomplete attempt is removed.
@@ -272,8 +382,8 @@ export async function ensureLocalAIRuntimes({runtimes=[],directory,platform=proc
272
382
  },5000);
273
383
  heartbeat.unref?.();
274
384
  try{
275
- // There is one job per selected runtime, at most one llama.cpp and one
276
- // Ollama job. Independent runtimes download and extract concurrently.
385
+ // There is one job per selected runtime. Independent runtime archives
386
+ // download and extract concurrently; matching requests share that job.
277
387
  const outcomes=await Promise.allSettled(selected.map(function prepareRuntime(runtime){
278
388
  return shareInstallation(runtime,{directory:root,platform,architecture,signal:operationSignal,onEvent:events.send});
279
389
  }));
@@ -332,7 +442,12 @@ export async function bundleLocalAIRuntimes({runtimes=[],directory,outputRoot,pl
332
442
  const relative=`runtime/local-ai/${runtime.id}`;
333
443
  const destination=path.join(root,relative);
334
444
  await copyRuntimeTree(runtime.root,destination,root,files,signal);
335
- return {runtime:{...runtime,root:relative,executable:`${relative}/${path.relative(runtime.root,runtime.executable).split(path.sep).join('/')}`},files};
445
+ const bundled={...runtime,root:relative};
446
+ for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory']){
447
+ if(runtime[field]===undefined)continue;
448
+ bundled[field]=path.posix.join(relative,path.relative(runtime.root,runtime[field]).split(path.sep).join('/'));
449
+ }
450
+ return {runtime:bundled,files};
336
451
  }));
337
452
  const failures=outcomes.filter(function failedBundle(outcome){return outcome.status==='rejected';});
338
453
  if(failures.length===1)throw failures[0].reason;
@@ -0,0 +1,85 @@
1
+ import {parentPort,workerData} from 'node:worker_threads';
2
+ import {pathToFileURL} from 'node:url';
3
+ import Is from 'strong-type';
4
+
5
+ const is=new Is(false);
6
+ const imported=await import(pathToFileURL(workerData.modulePath).href);
7
+ const ort=imported.default??imported;
8
+ let session;
9
+
10
+ function describeError(error){
11
+ if(!(error instanceof Error))return {name:'Error',message:String(error),cause:error};
12
+ const result={...error,name:error.name,message:error.message,stack:error.stack};
13
+ if(error.cause!==undefined)result.cause=error.cause instanceof Error?describeError(error.cause):error.cause;
14
+ if(error instanceof AggregateError)result.errors=error.errors.map(describeError);
15
+ return result;
16
+ }
17
+
18
+ function tensorMap(records,inputs=false){
19
+ return Object.fromEntries(Object.entries(records).map(function createTensor([name,record]){
20
+ // ORT Node 1.30.0 supplies string feeds to FillStringTensor through
21
+ // null-terminated strings, so this exact content cannot be preserved.
22
+ if(inputs&&record?.type==='string'&&is.array(record.data)
23
+ &&record.data.some(function embeddedNull(value){return is.string(value)&&value.includes('\0');})){
24
+ const error=new Error(`ONNX Runtime Node cannot preserve embedded U+0000 in string tensor input "${name}".`);
25
+ error.code='LOCAL_AI_ONNX_STRING_INPUT_UNSUPPORTED';
26
+ throw error;
27
+ }
28
+ return [name,record===null?null:new ort.Tensor(record.type,record.data,record.dims)];
29
+ }));
30
+ }
31
+
32
+ async function handleRequest(request){
33
+ try{
34
+ let result;
35
+ 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);
43
+ result={
44
+ inputNames:session.inputNames,
45
+ outputNames:session.outputNames,
46
+ inputMetadata:session.inputMetadata,
47
+ outputMetadata:session.outputMetadata
48
+ };
49
+ }else if(request.operation==='run'){
50
+ const feeds=tensorMap(request.feeds,true);
51
+ const fetches=request.fetches===undefined||request.fetches===null||is.array(request.fetches)
52
+ ?request.fetches:tensorMap(request.fetches);
53
+ // ORT treats an empty fetch map as its options overload and would
54
+ // ignore the third argument. Both forms select every output.
55
+ const allOutputs=fetches===undefined
56
+ ||(fetches!==null&&!is.array(fetches)&&Object.keys(fetches).length===0);
57
+ const outputs=allOutputs
58
+ ?await session.run(feeds,request.runOptions)
59
+ :await session.run(feeds,fetches,request.runOptions);
60
+ result=Object.fromEntries(Object.entries(outputs).map(function tensorRecord([name,tensor]){
61
+ return [name,{type:tensor.type,data:tensor.data,dims:tensor.dims}];
62
+ }));
63
+ }else if(request.operation==='unload'){
64
+ await session?.release();
65
+ session=undefined;
66
+ }else{
67
+ throw new Error(`Unknown ONNX worker operation: ${String(request.operation)}.`);
68
+ }
69
+ // Structured cloning retains typed tensors and BigInt without taking
70
+ // ownership of the caller's buffers or converting their contents.
71
+ parentPort.postMessage({requestId:request.requestId,operation:request.operation,result});
72
+ }catch(error){
73
+ parentPort.postMessage({requestId:request.requestId,operation:request.operation,error:describeError(error)});
74
+ }finally{
75
+ if(request.operation==='unload')parentPort.close();
76
+ }
77
+ }
78
+
79
+ parentPort.on('message',function receiveRequest(request){
80
+ // The session owner dispatches one operation at a time. Native ORT work
81
+ // runs synchronously inside this worker, never on the caller's thread.
82
+ void handleRequest(request).catch(function reportWorkerFailure(error){
83
+ setImmediate(function throwWorkerFailure(){throw error;});
84
+ });
85
+ });