arcane-os 0.64.0 → 0.65.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 (41) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +62 -5
  4. package/docs/guides/native-whisper-openvino.md +334 -0
  5. package/docs/reference/README.md +4 -0
  6. package/docs/reference/arcane-ollama.md +1 -1
  7. package/docs/reference/codex-app-server.md +269 -0
  8. package/docs/reference/core-client.md +60 -2
  9. package/docs/reference/core-development.md +157 -0
  10. package/docs/reference/core-native-packaging.md +13 -0
  11. package/docs/reference/core-repositories.md +112 -3
  12. package/docs/reference/inventory/package-api.json +143 -3
  13. package/docs/reference/inventory/runtime-modules.json +3 -2
  14. package/docs/reference/local-ai.md +73 -3
  15. package/docs/reference/local-image-generation.md +5 -0
  16. package/docs/reference/local-image-sdxl.md +1 -1
  17. package/docs/reference/mcp-stdio.md +263 -0
  18. package/docs/reference/runtime-components.md +18 -1
  19. package/docs/reference/runtime-modules.md +10 -2
  20. package/docs/reference/sdk-api.md +251 -1
  21. package/package.json +6 -2
  22. package/runtime/arcane/modules/AI.js +156 -13
  23. package/runtime/arcane/modules/Ollama.js +1 -1
  24. package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
  25. package/schemas/arcane-app.schema.json +8 -1
  26. package/schemas/arcane-package.schema.json +8 -1
  27. package/src/codex/app-server.mjs +845 -0
  28. package/src/core/development.mjs +457 -0
  29. package/src/core/repositories.mjs +170 -5
  30. package/src/core/services/local-ai.mjs +80 -10
  31. package/src/dev-server.mjs +3 -2
  32. package/src/index.mjs +1 -0
  33. package/src/local-ai/dev.mjs +80 -426
  34. package/src/local-ai/whisper/config.mjs +31 -3
  35. package/src/local-ai/whisper/index.mjs +62 -5
  36. package/src/local-ai/whisper/install.mjs +72 -14
  37. package/src/local-ai/whisper/native/main.cpp +33 -2
  38. package/src/local-ai/whisper/openvino-build.mjs +138 -0
  39. package/src/mcp/stdio-server.mjs +579 -0
  40. package/src/native/portable-layout.mjs +3 -1
  41. package/src/toolchain.mjs +19 -8
@@ -1,457 +1,111 @@
1
1
  import path from 'node:path';
2
- import Is from 'strong-type';
3
- import {createCoreRuntime} from '../core/runtime.mjs';
2
+ import {createDevelopmentCore} from '../core/development.mjs';
4
3
  import {createLocalAIService} from '../core/services/local-ai.mjs';
5
4
  import {createLocalImageService} from '../core/services/image.mjs';
6
5
  import {createModelAssetService} from '../core/services/model-assets.mjs';
7
6
  import {createSpeechService} from '../core/services/speech.mjs';
8
- import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
9
7
  import {normalizeLocalAIConfig} from './config.mjs';
10
8
  import {discoverLocalAIRuntimes} from './discover.mjs';
11
9
  import {ensureLocalAIRuntimes} from './install.mjs';
12
10
  import {createWhisperRuntime} from './whisper/index.mjs';
13
11
 
14
- const is = new Is(false);
15
-
16
- // The managed application import map owns this public module's actual URL.
17
- const bootstrap = `import {getInstalledCoreClient,installCoreClient} from 'arcane-os/core/client';
18
- let client=getInstalledCoreClient();
19
- const installedRuntime=client?.runtime.current();
20
- if(installedRuntime?.connected===false&&installedRuntime.transport==='standalone'){
21
- client.close();
22
- client=null;
23
- }
24
- if(!client){
25
- globalThis.__ARCANE_DEV_HTTP__=true;
26
- const id=crypto.randomUUID();
27
- let receive;
28
- let events;
29
- let resolveOpen;
30
- let rejectOpen;
31
- const pending=new Set();
32
- const opened=new Promise((resolve,reject)=>{resolveOpen=resolve;rejectOpen=reject;});
33
- opened.catch(()=>{});
34
- const transport={
35
- name:'development-http',
36
- subscribe(listener){
37
- receive=listener;
38
- events=new EventSource('/events?client='+encodeURIComponent(id));
39
- events.onopen=()=>resolveOpen();
40
- events.onmessage=event=>{
41
- const frame=JSON.parse(event.data);
42
- if(frame.type==='response')pending.delete(frame.id);
43
- receive(frame);
44
- };
45
- events.onerror=event=>{
46
- const error=new Error('The local AI development connection closed. Reload to reconnect.');
47
- error.code='LOCAL_AI_DEV_CONNECTION_CLOSED';
48
- error.event=event;
49
- events.close();
50
- rejectOpen(error);
51
- client?.failTransport(error);
52
- };
53
- return ()=>events.close();
54
- },
55
- async send(frame){
56
- if(frame.type==='request')pending.add(frame.id);
57
- if(frame.type==='control'&&frame.control==='requests.cancelAll')frame={...frame,requestIds:[...pending]};
58
- try{
59
- await opened;
60
- const response=await fetch('/rpc?client='+encodeURIComponent(id),{
61
- method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(frame)
62
- });
63
- const result=await response.json();
64
- if(!response.ok)throw Object.assign(new Error(result.error?.message??'The local AI request failed.'),result.error);
65
- // Responses arrive after their chunks on this same SSE connection.
66
- }catch(error){
67
- if(frame.type==='request')pending.delete(frame.id);
68
- throw error;
12
+ /** Local engines share the ordinary app-service development transport. */
13
+ export function createDevelopmentLocalAI({
14
+ config, appRoot, directory, application, version, signal, onEvent,
15
+ services = [], serviceModules = [], context = {}
16
+ } = {}) {
17
+ const lifetime = new AbortController();
18
+ signal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
19
+ const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
20
+ const localConfig = {
21
+ ...selected,
22
+ runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
23
+ return requirement.id !== 'stable-diffusion.cpp' && requirement.id !== 'whisper.cpp';
24
+ })
25
+ };
26
+ const service = createLocalAIService(
27
+ localConfig,
28
+ {
29
+ appRoot, signal, onEvent,
30
+ async prepare({signal, onEvent: report}) {
31
+ const discovered = await discoverLocalAIRuntimes({config: localConfig, appRoot, signal});
32
+ const installed = await ensureLocalAIRuntimes(
33
+ {runtimes: discovered.missing, directory, signal, onEvent: report}
34
+ );
35
+ return [...discovered.available, ...installed];
69
36
  }
70
37
  }
71
- };
72
- client=installCoreClient(globalThis,{transport,replayRuntimeState:true});
73
- }
74
- export {client};
75
- `;
76
-
77
- /** Start local preparation without delaying the application's HTTP listener. */
78
- export function createDevelopmentLocalAI({config, appRoot, directory, application, version, signal, onEvent} = {}) {
79
- const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
80
- const lifetime = new AbortController();
81
- const operationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
82
- const sessions = new Map();
83
- const requestOwners = new Map();
84
- const streamOwners = new Map();
85
- const operations = new Set();
86
- const incoming = new Set();
87
- let runtime;
88
- let service;
38
+ );
39
+ const definitions = [service, ...services];
40
+ let modelAssets;
89
41
  let imageService;
90
42
  let speechService;
91
- let modelAssets;
92
- let stopFrames;
93
- let preparationError = null;
94
- let closing = null;
95
- let closed = false;
96
-
97
- function diagnostic(error) {
98
- if (operationSignal.aborted && (error?.name === 'AbortError' || error?.code === 'ARCANE_CANCELLED')) return;
99
- if (!is.function(onEvent)) {
100
- console.error('Local AI development failed.', error);
101
- return;
102
- }
103
- try {
104
- Promise.resolve(onEvent({type: 'local-ai.development.error', message: error.message, error: serializeCoreError(error)}))
105
- .catch(function observerFailed(cause) { console.error('Local AI development observer failed.', cause, error); });
106
- } catch (cause) { console.error('Local AI development observer failed.', cause, error); }
107
- }
108
-
109
- function sessionFor(id) {
110
- let session = sessions.get(id);
111
- if (!session) {
112
- session = {id, connections: new Set(), requests: new Map(), cancelledRequests: new Set()};
113
- sessions.set(id, session);
114
- }
115
- return session;
116
- }
117
-
118
- function writeFrame(connection, frame) {
119
- const text = `data: ${JSON.stringify(frame)}\n\n`;
120
- connection.writes = connection.writes.then(function writeNextFrame() {
121
- if (connection.closed) return;
122
- return new Promise(function awaitWrite(resolve, reject) {
123
- function finish(error) {
124
- connection.response.removeListener('close', disconnected);
125
- if (error) reject(error);
126
- else resolve();
43
+ if (selected.runtimes.some(function selectedImageRuntime(requirement) {
44
+ return requirement.id === 'stable-diffusion.cpp';
45
+ })) {
46
+ modelAssets = createModelAssetService({appRoot});
47
+ imageService = createLocalImageService(
48
+ selected,
49
+ {
50
+ appRoot, modelAssets, signal, onEvent,
51
+ async prepare({requirement, signal, onEvent: report}) {
52
+ const installed = await ensureLocalAIRuntimes(
53
+ {runtimes: [requirement], directory, signal, onEvent: report}
54
+ );
55
+ return installed[0];
127
56
  }
128
- function disconnected() { finish(); }
129
- connection.response.once('close', disconnected);
130
- connection.response.write(text, finish);
131
- });
132
- }).catch(function streamWriteFailed(error) {
133
- diagnostic(error);
134
- connection.response.destroy(error);
135
- });
136
- }
137
-
138
- function sendSession(session, frame) {
139
- for (const connection of session.connections) writeFrame(connection, frame);
140
- }
141
-
142
- function event(event, data) {
143
- return {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
144
- }
145
-
146
- function routeFrame(frame) {
147
- if (frame.type === 'response') {
148
- const owner = requestOwners.get(frame.id);
149
- if (owner) sendSession(owner, frame);
150
- return;
151
- }
152
- if (frame.type !== 'event') return;
153
- if (frame.data?.streamId !== undefined) {
154
- const owner = streamOwners.get(frame.data.streamId);
155
- if (owner) sendSession(owner, frame);
156
- return;
157
- }
158
- for (const session of sessions.values()) sendSession(session, frame);
159
- }
160
-
161
- function replay(connection) {
162
- if (!runtime || connection.closed) return;
163
- const current = runtime.current();
164
- if (current.state === 'ready') writeFrame(connection, event('core.ready', {version: current.version, app: current.application}));
165
- writeFrame(connection, event('core.state', current));
166
- for (const record of current.services) writeFrame(connection, event('core.service.state', record));
167
- writeFrame(connection, event('localai.state', service.current()));
168
- if (imageService) writeFrame(connection, event('image.state', imageService.current()));
169
- if (speechService) writeFrame(connection, event('speech.state', speechService.current()));
170
- if (modelAssets) writeFrame(connection, event('modelAssets.state', modelAssets.current()));
57
+ }
58
+ );
59
+ definitions.push(modelAssets, imageService);
171
60
  }
172
-
173
- const ready = Promise.resolve().then(function prepareLocalAI() {
174
- operationSignal.throwIfAborted();
175
- const localConfig = {
176
- ...selected,
177
- runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
178
- return requirement.id !== 'stable-diffusion.cpp' && requirement.id !== 'whisper.cpp';
179
- })
180
- };
181
- service = createLocalAIService(
182
- localConfig,
61
+ const whisperRequirement = selected.runtimes.find(function selectedWhisperRuntime(requirement) {
62
+ return requirement.id === 'whisper.cpp';
63
+ });
64
+ if (whisperRequirement) {
65
+ const stt = createWhisperRuntime(
183
66
  {
184
- appRoot, signal: operationSignal, onEvent,
67
+ modelId: whisperRequirement.modelId,
68
+ temporaryDirectory: path.join(appRoot, '.arcane', 'speech', 'whisper'),
69
+ onEvent,
185
70
  async prepare({signal, onEvent: report}) {
186
- const discovered = await discoverLocalAIRuntimes({config: localConfig, appRoot, signal});
187
- const installed = await ensureLocalAIRuntimes({runtimes: discovered.missing, directory, signal, onEvent: report});
188
- return [...discovered.available, ...installed];
71
+ const installed = await ensureLocalAIRuntimes(
72
+ {runtimes: [whisperRequirement], directory, signal, onEvent: report}
73
+ );
74
+ return installed[0];
189
75
  }
190
76
  }
191
77
  );
192
- const services = [service];
193
- if (selected.runtimes.some(function selectedImageRuntime(requirement) {
194
- return requirement.id === 'stable-diffusion.cpp';
195
- })) {
196
- modelAssets = createModelAssetService({appRoot});
197
- imageService = createLocalImageService(
198
- selected,
199
- {
200
- appRoot, modelAssets, signal: operationSignal, onEvent,
201
- async prepare({requirement, signal, onEvent: report}) {
202
- const installed = await ensureLocalAIRuntimes(
203
- {runtimes: [requirement], directory, signal, onEvent: report}
204
- );
205
- return installed[0];
206
- }
207
- }
208
- );
209
- services.push(modelAssets, imageService);
210
- }
211
- const whisperRequirement = selected.runtimes.find(
212
- function selectedWhisperRuntime(requirement) {
213
- return requirement.id === 'whisper.cpp';
78
+ speechService = createSpeechService({stt, signal});
79
+ definitions.push(speechService);
80
+ }
81
+ const core = createDevelopmentCore(
82
+ {
83
+ appRoot, application, version, signal, onEvent, context, serviceModules,
84
+ services: definitions,
85
+ getReplayEvents() {
86
+ const snapshots = [{event: 'localai.state', data: service.current()}];
87
+ if (imageService) snapshots.push({event: 'image.state', data: imageService.current()});
88
+ if (speechService) snapshots.push({event: 'speech.state', data: speechService.current()});
89
+ if (modelAssets) snapshots.push({event: 'modelAssets.state', data: modelAssets.current()});
90
+ return snapshots;
214
91
  }
215
- );
216
- if (whisperRequirement) {
217
- const stt = createWhisperRuntime(
218
- {
219
- modelId: whisperRequirement.modelId,
220
- temporaryDirectory: path.join(appRoot, '.arcane', 'speech', 'whisper'),
221
- onEvent,
222
- async prepare({signal, onEvent: report}) {
223
- const installed = await ensureLocalAIRuntimes(
224
- {runtimes: [whisperRequirement], directory, signal, onEvent: report}
225
- );
226
- return installed[0];
227
- }
228
- }
229
- );
230
- speechService = createSpeechService(
231
- {stt, signal: operationSignal}
232
- );
233
- services.push(speechService);
234
92
  }
235
- runtime = createCoreRuntime({application, version, services});
236
- stopFrames = runtime.onFrame(routeFrame);
237
- runtime.start();
238
- return runtime;
239
- });
240
- ready.catch(function preparationFailed(error) {
241
- preparationError = error;
242
- diagnostic(error);
243
- });
244
-
245
- function waitForReady(signal) {
246
- const waiting = AbortSignal.any([signal, operationSignal]);
247
- if (waiting.aborted) return Promise.reject(waiting.reason);
248
- return new Promise(function awaitPreparedCore(resolve, reject) {
249
- function abort() {
250
- waiting.removeEventListener('abort', abort);
251
- reject(waiting.reason);
252
- }
253
- waiting.addEventListener('abort', abort, {once: true});
254
- ready.then(function prepared(value) {
255
- waiting.removeEventListener('abort', abort);
256
- if (waiting.aborted) reject(waiting.reason);
257
- else resolve(value);
258
- }, function failed(error) {
259
- waiting.removeEventListener('abort', abort);
260
- reject(error);
261
- });
262
- });
263
- }
264
-
265
- function observe(operation) {
266
- operations.add(operation);
267
- operation.then(function operationDone() { operations.delete(operation); }, function operationFailed() { operations.delete(operation); });
268
- return operation;
269
- }
270
-
271
- function cancelRequest(id) {
272
- if (!runtime) return;
273
- const task = runtime.handle({protocol: CORE_PROTOCOL, type: 'control', control: 'request.cancel', requestId: id});
274
- observe(task).catch(diagnostic);
275
- }
276
-
277
- function disconnect(connection) {
278
- if (connection.closed) return;
279
- connection.closed = true;
280
- const session = connection.session;
281
- session.connections.delete(connection);
282
- if (session.connections.size) return;
283
- for (const request of session.requests.values()) {
284
- request.controller.abort(new CoreError({code: 'LOCAL_AI_DEV_DISCONNECTED', message: 'The browser disconnected from local AI.'}));
285
- }
286
- if (!session.requests.size) sessions.delete(session.id);
287
- }
288
-
289
- function json(response, status, value) {
290
- if (response.destroyed || response.writableEnded) return;
291
- response.writeHead(status, {'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store'});
292
- response.end(JSON.stringify(value));
293
- }
294
-
295
- async function dispatch(frame, session, controller) {
296
- if (frame?.protocol !== CORE_PROTOCOL) throw new CoreError({code: 'INVALID_RPC_REQUEST', message: 'Unknown Core protocol.'});
297
- if (frame.type === 'control' && frame.control === 'requests.cancelAll') {
298
- for (const id of frame.requestIds ?? []) session.cancelledRequests.add(id);
299
- for (const request of session.requests.values()) {
300
- request.controller.abort();
301
- }
302
- return;
303
- }
304
- if (frame.type === 'control' && frame.control === 'request.cancel') {
305
- session.cancelledRequests.add(frame.requestId);
306
- const request = session.requests.get(frame.requestId);
307
- if (request) {
308
- request.controller.abort();
309
- }
310
- return;
311
- }
312
- if (frame.type !== 'request') {
313
- await waitForReady(controller.signal);
314
- controller.signal.throwIfAborted();
315
- operationSignal.throwIfAborted();
316
- if (frame.type === 'control' && frame.control === 'runtime.replay') {
317
- for (const connection of session.connections) replay(connection);
318
- return;
319
- }
320
- return runtime.handle(frame);
321
- }
322
- if (requestOwners.has(frame.id)) throw new CoreError({code: 'RPC_REQUEST_ID_ACTIVE', message: 'The request id is already active.'});
323
- const owned = {id: frame.id, controller};
324
- const streamId = frame.parameters?.streamId;
325
- session.requests.set(frame.id, owned);
326
- requestOwners.set(frame.id, session);
327
- if (session.cancelledRequests.delete(frame.id)) controller.abort();
328
- if (streamId !== undefined) streamOwners.set(streamId, session);
329
- function cancelled() { cancelRequest(frame.id); }
330
- controller.signal.addEventListener('abort', cancelled, {once: true});
331
- try {
332
- await waitForReady(controller.signal);
333
- controller.signal.throwIfAborted();
334
- operationSignal.throwIfAborted();
335
- return await runtime.handle(frame);
336
- } finally {
337
- controller.signal.removeEventListener('abort', cancelled);
338
- session.requests.delete(frame.id);
339
- session.cancelledRequests.delete(frame.id);
340
- requestOwners.delete(frame.id);
341
- if (streamId !== undefined) streamOwners.delete(streamId);
342
- if (!session.connections.size && !session.requests.size) sessions.delete(session.id);
343
- }
344
- }
345
-
346
- async function handler(request, response) {
347
- const url = new URL(request.url, 'http://arcane.local');
348
- if (!['/rpc', '/events', '/arcane-local-ai.js'].includes(url.pathname)) return false;
349
- if (closed) {
350
- json(response, 503, {error: {code: 'LOCAL_AI_DEV_CLOSED', message: 'Local AI development is closed.'}});
351
- return true;
352
- }
353
- if (url.pathname === '/arcane-local-ai.js') {
354
- if (request.method !== 'GET' && request.method !== 'HEAD') {
355
- json(response, 405, {error: {message: 'Use GET to load the local AI bootstrap.'}});
356
- return true;
357
- }
358
- response.writeHead(200, {'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store'});
359
- response.end(request.method === 'HEAD' ? undefined : bootstrap);
360
- return true;
361
- }
362
- const clientId = url.searchParams.get('client');
363
- if (!clientId) {
364
- json(response, 400, {error: {code: 'LOCAL_AI_DEV_CLIENT_REQUIRED', message: 'Load the local AI development bootstrap before connecting.'}});
365
- return true;
366
- }
367
- const method = url.pathname === '/events' ? 'GET' : 'POST';
368
- if (request.method !== method) {
369
- json(response, 405, {error: {message: `Use ${method} for ${url.pathname}.`}});
370
- return true;
371
- }
372
- const session = sessionFor(clientId);
373
- if (url.pathname === '/events') {
374
- const connection = {response, session, closed: false, writes: Promise.resolve()};
375
- session.connections.add(connection);
376
- response.on('close', function eventConnectionClosed() { disconnect(connection); });
377
- response.on('error', diagnostic);
378
- response.writeHead(200, {'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store', Connection: 'keep-alive'});
379
- response.flushHeaders();
380
- ready.then(function replayReadyRuntime() { replay(connection); }, function reportPreparationFailure(error) {
381
- if (!connection.closed) writeFrame(connection, event('core.error', serializeCoreError(error)));
382
- }).catch(diagnostic);
383
- return true;
384
- }
385
- const controller = new AbortController();
386
- const rpc = {request, response, controller};
387
- incoming.add(rpc);
388
- const abortOnClose = function rpcConnectionClosed() {
389
- if (!response.writableEnded) controller.abort(new CoreError({code: 'LOCAL_AI_DEV_DISCONNECTED', message: 'The local AI request connection closed.'}));
390
- };
391
- response.once('close', abortOnClose);
392
- let frame;
393
- try {
394
- request.setEncoding('utf8');
395
- let body = '';
396
- for await (const part of request) body += part;
397
- frame = JSON.parse(body);
398
- const result = await observe(dispatch(frame, session, controller));
399
- json(response, 200, result ?? {ok: true});
400
- } catch (error) {
401
- diagnostic(error);
402
- if (frame?.type === 'request' && frame.id) {
403
- const result = {protocol: CORE_PROTOCOL, type: 'response', id: frame.id, ok: false, error: serializeCoreError(error), time: new Date().toISOString()};
404
- sendSession(session, result);
405
- json(response, 200, result);
406
- } else json(response, 400, {error: serializeCoreError(error)});
407
- } finally {
408
- incoming.delete(rpc);
409
- response.removeListener('close', abortOnClose);
410
- if (!session.connections.size && !session.requests.size) sessions.delete(session.id);
411
- }
412
- return true;
413
- }
414
-
93
+ );
415
94
  function current() {
416
- return {state: closed ? 'closed' : preparationError ? 'error' : runtime ? 'ready' : 'preparing',
417
- error: preparationError ? serializeCoreError(preparationError) : null,
418
- core: runtime?.current() ?? null, localAI: service?.current() ?? null,
95
+ return {
96
+ ...core.current(),
97
+ localAI: service.current(),
419
98
  image: imageService?.current() ?? null,
420
- speech: speechService?.current() ?? null};
99
+ speech: speechService?.current() ?? null
100
+ };
421
101
  }
422
-
102
+ let closing;
423
103
  function close() {
424
- if (closing) return closing;
425
- closed = true;
426
- lifetime.abort();
427
- for (const rpc of incoming) {
428
- rpc.controller.abort(lifetime.signal.reason);
429
- rpc.request.destroy();
430
- rpc.response.destroy();
104
+ if (!closing) {
105
+ lifetime.abort();
106
+ closing = core.close().then(current);
431
107
  }
432
- for (const session of sessions.values()) {
433
- for (const connection of [...session.connections]) {
434
- disconnect(connection);
435
- connection.response.destroy();
436
- }
437
- }
438
- closing = (async function closeLocalAI() {
439
- try { await ready; } catch {}
440
- try {
441
- await runtime?.close();
442
- } finally {
443
- await Promise.allSettled([...operations]);
444
- stopFrames?.();
445
- sessions.clear();
446
- signal?.removeEventListener('abort', abortDevelopment);
447
- }
448
- return current();
449
- })();
450
108
  return closing;
451
109
  }
452
-
453
- function abortDevelopment() { close().catch(diagnostic); }
454
- signal?.addEventListener('abort', abortDevelopment, {once: true});
455
- if (signal?.aborted) abortDevelopment();
456
- return {ready, handler, current, close};
110
+ return {...core, current, close};
457
111
  }
@@ -10,6 +10,14 @@ export const WHISPER_SMALL_MODEL = {
10
10
  filename: 'ggml-small.bin'
11
11
  };
12
12
 
13
+ export const WHISPER_SMALL_OPENVINO_MODEL = {
14
+ id: 'whisper-small',
15
+ archiveUrl: 'https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/main/ggml-small-models.zip',
16
+ filename: 'ggml-small.bin',
17
+ encoderFilename: 'ggml-small-encoder-openvino.xml',
18
+ encoderDataFilename: 'ggml-small-encoder-openvino.bin'
19
+ };
20
+
13
21
  /** Describe a selected native runtime and models without acquiring them. */
14
22
  export function normalizeWhisperRuntimeRequirement(record) {
15
23
  if (record.helperRoot !== undefined && (!is.string(record.helperRoot) || !record.helperRoot)) {
@@ -20,16 +28,33 @@ export function normalizeWhisperRuntimeRequirement(record) {
20
28
  if (!['auto', 'cuda', 'metal', 'cpu'].includes(backend)) {
21
29
  throw new ArcaneError(ERROR_CODES.usage, `Unknown whisper.cpp backend: ${String(backend)}.`);
22
30
  }
31
+ if (record.encoder !== undefined && record.encoder !== 'openvino-npu') {
32
+ throw new ArcaneError(ERROR_CODES.usage, `Unknown whisper.cpp encoder: ${String(record.encoder)}.`);
33
+ }
23
34
  if (record.models !== undefined && !is.array(record.models)) {
24
35
  throw new ArcaneError(ERROR_CODES.usage, 'whisper.cpp models must be an array of selected model IDs or descriptors.');
25
36
  }
26
37
  const models = (record.models ?? []).map(function selectedWhisperModel(value) {
27
- const model = value === 'whisper-small' ? WHISPER_SMALL_MODEL : value;
38
+ const model = value === 'whisper-small'
39
+ ? record.encoder === 'openvino-npu' ? WHISPER_SMALL_OPENVINO_MODEL : WHISPER_SMALL_MODEL
40
+ : value;
28
41
  if (!model || !is.object(model) || is.array(model) || !is.string(model.id) || !model.id) {
29
42
  throw new ArcaneError(ERROR_CODES.usage, 'A Whisper model needs a descriptor with an id, or the built-in whisper-small model ID.');
30
43
  }
31
- if ((!is.string(model.path) || !model.path) && (!is.string(model.url) || !model.url)) {
32
- throw new ArcaneError(ERROR_CODES.usage, `Whisper model ${model.id} needs its selected path or URL.`);
44
+ if ((!is.string(model.path) || !model.path) && (!is.string(model.url) || !model.url)
45
+ && (!is.string(model.archiveUrl) || !model.archiveUrl)) {
46
+ throw new ArcaneError(ERROR_CODES.usage, `Whisper model ${model.id} needs its selected path, URL, or model archive URL.`);
47
+ }
48
+ if (!model.path && model.archiveUrl && (!is.string(model.filename) || !model.filename)) {
49
+ throw new ArcaneError(ERROR_CODES.usage, `Whisper model ${model.id} needs its model filename inside the selected archive.`);
50
+ }
51
+ if (record.encoder === 'openvino-npu') {
52
+ const pairedFiles = model.path
53
+ ? [model.path, model.encoderPath, model.encoderDataPath]
54
+ : [model.archiveUrl, model.filename, model.encoderFilename, model.encoderDataFilename];
55
+ if (!pairedFiles.every(function selectedEncoderFile(value) { return is.string(value) && value !== ''; })) {
56
+ throw new ArcaneError(ERROR_CODES.usage, `Whisper model ${model.id} needs its matching GGML, OpenVINO XML, and encoder BIN files.`);
57
+ }
33
58
  }
34
59
  return {...model};
35
60
  });
@@ -43,6 +68,9 @@ export function normalizeWhisperRuntimeRequirement(record) {
43
68
  if ((!is.string(variant.root) || !variant.root) && (!is.string(variant.url) || !variant.url)) {
44
69
  throw new ArcaneError(ERROR_CODES.usage, `Whisper ${variant.backend} needs its complete runtime root or archive URL.`);
45
70
  }
71
+ if (variant.encoder !== undefined && variant.encoder !== 'openvino-npu') {
72
+ throw new ArcaneError(ERROR_CODES.usage, `Unknown Whisper variant encoder: ${String(variant.encoder)}.`);
73
+ }
46
74
  return {...variant};
47
75
  });
48
76
  if (record.modelId !== undefined && (!is.string(record.modelId) || !models.some(function matchingModel(model) {