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.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +62 -5
- package/docs/guides/native-whisper-openvino.md +334 -0
- package/docs/reference/README.md +4 -0
- package/docs/reference/arcane-ollama.md +1 -1
- package/docs/reference/codex-app-server.md +269 -0
- package/docs/reference/core-client.md +60 -2
- package/docs/reference/core-development.md +157 -0
- package/docs/reference/core-native-packaging.md +13 -0
- package/docs/reference/core-repositories.md +112 -3
- package/docs/reference/inventory/package-api.json +143 -3
- package/docs/reference/inventory/runtime-modules.json +3 -2
- package/docs/reference/local-ai.md +73 -3
- package/docs/reference/local-image-generation.md +5 -0
- package/docs/reference/local-image-sdxl.md +1 -1
- package/docs/reference/mcp-stdio.md +263 -0
- package/docs/reference/runtime-components.md +18 -1
- package/docs/reference/runtime-modules.md +10 -2
- package/docs/reference/sdk-api.md +251 -1
- package/package.json +6 -2
- package/runtime/arcane/modules/AI.js +156 -13
- package/runtime/arcane/modules/Ollama.js +1 -1
- package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
- package/schemas/arcane-app.schema.json +8 -1
- package/schemas/arcane-package.schema.json +8 -1
- package/src/codex/app-server.mjs +845 -0
- package/src/core/development.mjs +457 -0
- package/src/core/repositories.mjs +170 -5
- package/src/core/services/local-ai.mjs +80 -10
- package/src/dev-server.mjs +3 -2
- package/src/index.mjs +1 -0
- package/src/local-ai/dev.mjs +80 -426
- package/src/local-ai/whisper/config.mjs +31 -3
- package/src/local-ai/whisper/index.mjs +62 -5
- package/src/local-ai/whisper/install.mjs +72 -14
- package/src/local-ai/whisper/native/main.cpp +33 -2
- package/src/local-ai/whisper/openvino-build.mjs +138 -0
- package/src/mcp/stdio-server.mjs +579 -0
- package/src/native/portable-layout.mjs +3 -1
- package/src/toolchain.mjs +19 -8
package/src/local-ai/dev.mjs
CHANGED
|
@@ -1,457 +1,111 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
67
|
+
modelId: whisperRequirement.modelId,
|
|
68
|
+
temporaryDirectory: path.join(appRoot, '.arcane', 'speech', 'whisper'),
|
|
69
|
+
onEvent,
|
|
185
70
|
async prepare({signal, onEvent: report}) {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
71
|
+
const installed = await ensureLocalAIRuntimes(
|
|
72
|
+
{runtimes: [whisperRequirement], directory, signal, onEvent: report}
|
|
73
|
+
);
|
|
74
|
+
return installed[0];
|
|
189
75
|
}
|
|
190
76
|
}
|
|
191
77
|
);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 {
|
|
417
|
-
|
|
418
|
-
|
|
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)
|
|
425
|
-
|
|
426
|
-
|
|
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'
|
|
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
|
-
|
|
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) {
|