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.
- package/CHANGELOG.md +37 -0
- package/README.md +6 -2
- package/browser-runtime/ai/core-onnx.mjs +96 -0
- package/browser-runtime/ai/onnx-tensors.mjs +85 -0
- package/browser-runtime/core/client.mjs +152 -17
- package/docs/architecture.md +5 -2
- package/docs/platform-targets.md +30 -13
- package/docs/reference/README.md +22 -3
- package/docs/reference/availability-and-normalization.md +14 -7
- package/docs/reference/cli.md +22 -10
- package/docs/reference/core-client.md +87 -0
- package/docs/reference/core-native-packaging.md +129 -9
- package/docs/reference/core-preferences.md +49 -0
- package/docs/reference/diarization.md +206 -0
- package/docs/reference/git-text-snapshot.md +104 -0
- package/docs/reference/inventory/package-api.json +205 -23
- package/docs/reference/local-ai.md +145 -11
- package/docs/reference/protocols.md +28 -19
- package/docs/reference/pwa.md +74 -4
- package/docs/reference/sdk-api.md +589 -39
- package/package.json +11 -3
- package/schemas/arcane-app.schema.json +3 -3
- package/schemas/arcane-package.schema.json +2 -2
- package/schemas/native-build-plan.schema.json +27 -1
- package/schemas/target-adapter.schema.json +2 -0
- package/src/app-descriptor.mjs +1 -1
- package/src/cli/main.mjs +14 -4
- package/src/constants.mjs +2 -0
- package/src/core/host.mjs +14 -5
- package/src/core/hosts/macos/ArcaneHost.h +86 -0
- package/src/core/hosts/macos/ArcaneHost.m +455 -0
- package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
- package/src/core/services/local-ai.mjs +82 -4
- package/src/core/services/packaged-web.mjs +89 -0
- package/src/core/services/preferences.mjs +201 -0
- package/src/core-classic-source.mjs +2 -2
- package/src/dev-server.mjs +2 -5
- package/src/diarization/build.mjs +35 -0
- package/src/diarization/index.mjs +432 -0
- package/src/diarization/native/CMakeLists.txt +9 -0
- package/src/diarization/native/main.cpp +269 -0
- package/src/diarization/service.mjs +129 -0
- package/src/git-text-snapshot.mjs +265 -0
- package/src/import-map.mjs +9 -4
- package/src/index.mjs +1 -0
- package/src/local-ai/config.mjs +1 -1
- package/src/local-ai/install.mjs +134 -19
- package/src/local-ai/onnx-worker.mjs +85 -0
- package/src/local-ai/onnx.mjs +368 -0
- package/src/native/macos-provider.mjs +291 -0
- package/src/native/portable-layout.mjs +10 -2
- package/src/native/portable-provider.mjs +10 -5
- package/src/native-plan.mjs +9 -1
- package/src/native-provider-loader.mjs +3 -1
- package/src/process.mjs +123 -17
- package/src/scaffold.mjs +1 -1
- package/src/targets/index.mjs +11 -1
- package/src/templates/workspace-template.mjs +6 -5
- package/src/toolchain.mjs +42 -13
package/src/import-map.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
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)
|
|
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
package/src/local-ai/config.mjs
CHANGED
|
@@ -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) {
|
package/src/local-ai/install.mjs
CHANGED
|
@@ -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
|
|
20
|
-
|
|
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
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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} ${
|
|
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
|
|
276
|
-
//
|
|
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
|
-
|
|
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
|
+
});
|