arcane-os 0.63.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 +48 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +69 -7
- package/docs/guides/native-whisper-openvino.md +334 -0
- package/docs/reference/README.md +6 -0
- package/docs/reference/arcane-ollama.md +1 -1
- package/docs/reference/codex-app-server.md +269 -0
- package/docs/reference/core-client.md +71 -4
- package/docs/reference/core-development.md +157 -0
- package/docs/reference/core-native-packaging.md +48 -6
- package/docs/reference/core-repositories.md +258 -0
- package/docs/reference/inventory/package-api.json +199 -3
- package/docs/reference/inventory/runtime-modules.json +3 -2
- package/docs/reference/local-ai.md +77 -4
- package/docs/reference/local-image-flux.md +1 -1
- package/docs/reference/local-image-generation.md +11 -0
- package/docs/reference/local-image-sdxl.md +139 -0
- package/docs/reference/local-whisper.md +219 -0
- package/docs/reference/mcp-stdio.md +263 -0
- package/docs/reference/native-speech.md +51 -2
- package/docs/reference/runtime-components.md +18 -1
- package/docs/reference/runtime-modules.md +26 -4
- package/docs/reference/sdk-api.md +412 -2
- package/package.json +9 -2
- package/runtime/arcane/components/speech.html +15 -3
- package/runtime/arcane/components/voice-transcription.html +19 -3
- package/runtime/arcane/modules/AI.js +406 -40
- package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
- package/runtime/arcane/modules/AIRuntimeState.js +15 -2
- package/runtime/arcane/modules/ComponentContracts.js +3 -0
- package/runtime/arcane/modules/Ollama.js +1 -1
- package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
- package/schemas/arcane-app.schema.json +60 -2
- package/schemas/arcane-package.schema.json +60 -2
- package/src/codex/app-server.mjs +845 -0
- package/src/core/development.mjs +457 -0
- package/src/core/repositories.mjs +375 -0
- package/src/core/services/local-ai.mjs +80 -10
- package/src/core/services/speech.mjs +1 -0
- package/src/dev-server.mjs +3 -2
- package/src/index.mjs +1 -0
- package/src/local-ai/config.mjs +7 -3
- package/src/local-ai/dev.mjs +85 -401
- package/src/local-ai/image-library.mjs +9 -0
- package/src/local-ai/image-models/sdxl.mjs +18 -0
- package/src/local-ai/install.mjs +26 -7
- package/src/local-ai/whisper/build.mjs +48 -0
- package/src/local-ai/whisper/config.mjs +89 -0
- package/src/local-ai/whisper/index.mjs +572 -0
- package/src/local-ai/whisper/install.mjs +332 -0
- package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
- package/src/local-ai/whisper/native/NOTICE +18 -0
- package/src/local-ai/whisper/native/README.md +104 -0
- package/src/local-ai/whisper/native/main.cpp +520 -0
- package/src/local-ai/whisper/openvino-build.mjs +138 -0
- package/src/mcp/stdio-server.mjs +579 -0
- package/src/native/portable-layout.mjs +39 -7
- package/src/toolchain.mjs +19 -8
package/src/local-ai/dev.mjs
CHANGED
|
@@ -1,427 +1,111 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {createDevelopmentCore} from '../core/development.mjs';
|
|
3
3
|
import {createLocalAIService} from '../core/services/local-ai.mjs';
|
|
4
4
|
import {createLocalImageService} from '../core/services/image.mjs';
|
|
5
5
|
import {createModelAssetService} from '../core/services/model-assets.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import {createSpeechService} from '../core/services/speech.mjs';
|
|
7
7
|
import {normalizeLocalAIConfig} from './config.mjs';
|
|
8
8
|
import {discoverLocalAIRuntimes} from './discover.mjs';
|
|
9
9
|
import {ensureLocalAIRuntimes} from './install.mjs';
|
|
10
|
+
import {createWhisperRuntime} from './whisper/index.mjs';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
events=new EventSource('/events?client='+encodeURIComponent(id));
|
|
36
|
-
events.onopen=()=>resolveOpen();
|
|
37
|
-
events.onmessage=event=>{
|
|
38
|
-
const frame=JSON.parse(event.data);
|
|
39
|
-
if(frame.type==='response')pending.delete(frame.id);
|
|
40
|
-
receive(frame);
|
|
41
|
-
};
|
|
42
|
-
events.onerror=event=>{
|
|
43
|
-
const error=new Error('The local AI development connection closed. Reload to reconnect.');
|
|
44
|
-
error.code='LOCAL_AI_DEV_CONNECTION_CLOSED';
|
|
45
|
-
error.event=event;
|
|
46
|
-
events.close();
|
|
47
|
-
rejectOpen(error);
|
|
48
|
-
client?.failTransport(error);
|
|
49
|
-
};
|
|
50
|
-
return ()=>events.close();
|
|
51
|
-
},
|
|
52
|
-
async send(frame){
|
|
53
|
-
if(frame.type==='request')pending.add(frame.id);
|
|
54
|
-
if(frame.type==='control'&&frame.control==='requests.cancelAll')frame={...frame,requestIds:[...pending]};
|
|
55
|
-
try{
|
|
56
|
-
await opened;
|
|
57
|
-
const response=await fetch('/rpc?client='+encodeURIComponent(id),{
|
|
58
|
-
method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(frame)
|
|
59
|
-
});
|
|
60
|
-
const result=await response.json();
|
|
61
|
-
if(!response.ok)throw Object.assign(new Error(result.error?.message??'The local AI request failed.'),result.error);
|
|
62
|
-
// Responses arrive after their chunks on this same SSE connection.
|
|
63
|
-
}catch(error){
|
|
64
|
-
if(frame.type==='request')pending.delete(frame.id);
|
|
65
|
-
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];
|
|
66
36
|
}
|
|
67
37
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
export {client};
|
|
72
|
-
`;
|
|
73
|
-
|
|
74
|
-
/** Start local preparation without delaying the application's HTTP listener. */
|
|
75
|
-
export function createDevelopmentLocalAI({config, appRoot, directory, application, version, signal, onEvent} = {}) {
|
|
76
|
-
const selected = normalizeLocalAIConfig(config) ?? {runtimes: []};
|
|
77
|
-
const lifetime = new AbortController();
|
|
78
|
-
const operationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
79
|
-
const sessions = new Map();
|
|
80
|
-
const requestOwners = new Map();
|
|
81
|
-
const streamOwners = new Map();
|
|
82
|
-
const operations = new Set();
|
|
83
|
-
const incoming = new Set();
|
|
84
|
-
let runtime;
|
|
85
|
-
let service;
|
|
86
|
-
let imageService;
|
|
38
|
+
);
|
|
39
|
+
const definitions = [service, ...services];
|
|
87
40
|
let modelAssets;
|
|
88
|
-
let
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function sessionFor(id) {
|
|
106
|
-
let session = sessions.get(id);
|
|
107
|
-
if (!session) {
|
|
108
|
-
session = {id, connections: new Set(), requests: new Map(), cancelledRequests: new Set()};
|
|
109
|
-
sessions.set(id, session);
|
|
110
|
-
}
|
|
111
|
-
return session;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function writeFrame(connection, frame) {
|
|
115
|
-
const text = `data: ${JSON.stringify(frame)}\n\n`;
|
|
116
|
-
connection.writes = connection.writes.then(function writeNextFrame() {
|
|
117
|
-
if (connection.closed) return;
|
|
118
|
-
return new Promise(function awaitWrite(resolve, reject) {
|
|
119
|
-
function finish(error) {
|
|
120
|
-
connection.response.removeListener('close', disconnected);
|
|
121
|
-
if (error) reject(error);
|
|
122
|
-
else resolve();
|
|
41
|
+
let imageService;
|
|
42
|
+
let speechService;
|
|
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];
|
|
123
56
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
}).catch(function streamWriteFailed(error) {
|
|
129
|
-
diagnostic(error);
|
|
130
|
-
connection.response.destroy(error);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function sendSession(session, frame) {
|
|
135
|
-
for (const connection of session.connections) writeFrame(connection, frame);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function event(event, data) {
|
|
139
|
-
return {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function routeFrame(frame) {
|
|
143
|
-
if (frame.type === 'response') {
|
|
144
|
-
const owner = requestOwners.get(frame.id);
|
|
145
|
-
if (owner) sendSession(owner, frame);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (frame.type !== 'event') return;
|
|
149
|
-
if (frame.data?.streamId !== undefined) {
|
|
150
|
-
const owner = streamOwners.get(frame.data.streamId);
|
|
151
|
-
if (owner) sendSession(owner, frame);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
for (const session of sessions.values()) sendSession(session, frame);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function replay(connection) {
|
|
158
|
-
if (!runtime || connection.closed) return;
|
|
159
|
-
const current = runtime.current();
|
|
160
|
-
if (current.state === 'ready') writeFrame(connection, event('core.ready', {version: current.version, app: current.application}));
|
|
161
|
-
writeFrame(connection, event('core.state', current));
|
|
162
|
-
for (const record of current.services) writeFrame(connection, event('core.service.state', record));
|
|
163
|
-
writeFrame(connection, event('localai.state', service.current()));
|
|
164
|
-
if (imageService) writeFrame(connection, event('image.state', imageService.current()));
|
|
165
|
-
if (modelAssets) writeFrame(connection, event('modelAssets.state', modelAssets.current()));
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
definitions.push(modelAssets, imageService);
|
|
166
60
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
runtimes: selected.runtimes.filter(function selectedChatRuntime(requirement) {
|
|
173
|
-
return requirement.id !== 'stable-diffusion.cpp';
|
|
174
|
-
})
|
|
175
|
-
};
|
|
176
|
-
service = createLocalAIService(
|
|
177
|
-
localConfig,
|
|
61
|
+
const whisperRequirement = selected.runtimes.find(function selectedWhisperRuntime(requirement) {
|
|
62
|
+
return requirement.id === 'whisper.cpp';
|
|
63
|
+
});
|
|
64
|
+
if (whisperRequirement) {
|
|
65
|
+
const stt = createWhisperRuntime(
|
|
178
66
|
{
|
|
179
|
-
|
|
67
|
+
modelId: whisperRequirement.modelId,
|
|
68
|
+
temporaryDirectory: path.join(appRoot, '.arcane', 'speech', 'whisper'),
|
|
69
|
+
onEvent,
|
|
180
70
|
async prepare({signal, onEvent: report}) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
71
|
+
const installed = await ensureLocalAIRuntimes(
|
|
72
|
+
{runtimes: [whisperRequirement], directory, signal, onEvent: report}
|
|
73
|
+
);
|
|
74
|
+
return installed[0];
|
|
184
75
|
}
|
|
185
76
|
}
|
|
186
77
|
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return installed[0];
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
);
|
|
204
|
-
services.push(modelAssets, imageService);
|
|
205
|
-
}
|
|
206
|
-
runtime = createCoreRuntime({application, version, services});
|
|
207
|
-
stopFrames = runtime.onFrame(routeFrame);
|
|
208
|
-
runtime.start();
|
|
209
|
-
return runtime;
|
|
210
|
-
});
|
|
211
|
-
ready.catch(function preparationFailed(error) {
|
|
212
|
-
preparationError = error;
|
|
213
|
-
diagnostic(error);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
function waitForReady(signal) {
|
|
217
|
-
const waiting = AbortSignal.any([signal, operationSignal]);
|
|
218
|
-
if (waiting.aborted) return Promise.reject(waiting.reason);
|
|
219
|
-
return new Promise(function awaitPreparedCore(resolve, reject) {
|
|
220
|
-
function abort() {
|
|
221
|
-
waiting.removeEventListener('abort', abort);
|
|
222
|
-
reject(waiting.reason);
|
|
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;
|
|
223
91
|
}
|
|
224
|
-
waiting.addEventListener('abort', abort, {once: true});
|
|
225
|
-
ready.then(function prepared(value) {
|
|
226
|
-
waiting.removeEventListener('abort', abort);
|
|
227
|
-
if (waiting.aborted) reject(waiting.reason);
|
|
228
|
-
else resolve(value);
|
|
229
|
-
}, function failed(error) {
|
|
230
|
-
waiting.removeEventListener('abort', abort);
|
|
231
|
-
reject(error);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function observe(operation) {
|
|
237
|
-
operations.add(operation);
|
|
238
|
-
operation.then(function operationDone() { operations.delete(operation); }, function operationFailed() { operations.delete(operation); });
|
|
239
|
-
return operation;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function cancelRequest(id) {
|
|
243
|
-
if (!runtime) return;
|
|
244
|
-
const task = runtime.handle({protocol: CORE_PROTOCOL, type: 'control', control: 'request.cancel', requestId: id});
|
|
245
|
-
observe(task).catch(diagnostic);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function disconnect(connection) {
|
|
249
|
-
if (connection.closed) return;
|
|
250
|
-
connection.closed = true;
|
|
251
|
-
const session = connection.session;
|
|
252
|
-
session.connections.delete(connection);
|
|
253
|
-
if (session.connections.size) return;
|
|
254
|
-
for (const request of session.requests.values()) {
|
|
255
|
-
request.controller.abort(new CoreError({code: 'LOCAL_AI_DEV_DISCONNECTED', message: 'The browser disconnected from local AI.'}));
|
|
256
|
-
}
|
|
257
|
-
if (!session.requests.size) sessions.delete(session.id);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function json(response, status, value) {
|
|
261
|
-
if (response.destroyed || response.writableEnded) return;
|
|
262
|
-
response.writeHead(status, {'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store'});
|
|
263
|
-
response.end(JSON.stringify(value));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function dispatch(frame, session, controller) {
|
|
267
|
-
if (frame?.protocol !== CORE_PROTOCOL) throw new CoreError({code: 'INVALID_RPC_REQUEST', message: 'Unknown Core protocol.'});
|
|
268
|
-
if (frame.type === 'control' && frame.control === 'requests.cancelAll') {
|
|
269
|
-
for (const id of frame.requestIds ?? []) session.cancelledRequests.add(id);
|
|
270
|
-
for (const request of session.requests.values()) {
|
|
271
|
-
request.controller.abort();
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (frame.type === 'control' && frame.control === 'request.cancel') {
|
|
276
|
-
session.cancelledRequests.add(frame.requestId);
|
|
277
|
-
const request = session.requests.get(frame.requestId);
|
|
278
|
-
if (request) {
|
|
279
|
-
request.controller.abort();
|
|
280
|
-
}
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (frame.type !== 'request') {
|
|
284
|
-
await waitForReady(controller.signal);
|
|
285
|
-
controller.signal.throwIfAborted();
|
|
286
|
-
operationSignal.throwIfAborted();
|
|
287
|
-
if (frame.type === 'control' && frame.control === 'runtime.replay') {
|
|
288
|
-
for (const connection of session.connections) replay(connection);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
return runtime.handle(frame);
|
|
292
|
-
}
|
|
293
|
-
if (requestOwners.has(frame.id)) throw new CoreError({code: 'RPC_REQUEST_ID_ACTIVE', message: 'The request id is already active.'});
|
|
294
|
-
const owned = {id: frame.id, controller};
|
|
295
|
-
const streamId = frame.parameters?.streamId;
|
|
296
|
-
session.requests.set(frame.id, owned);
|
|
297
|
-
requestOwners.set(frame.id, session);
|
|
298
|
-
if (session.cancelledRequests.delete(frame.id)) controller.abort();
|
|
299
|
-
if (streamId !== undefined) streamOwners.set(streamId, session);
|
|
300
|
-
function cancelled() { cancelRequest(frame.id); }
|
|
301
|
-
controller.signal.addEventListener('abort', cancelled, {once: true});
|
|
302
|
-
try {
|
|
303
|
-
await waitForReady(controller.signal);
|
|
304
|
-
controller.signal.throwIfAborted();
|
|
305
|
-
operationSignal.throwIfAborted();
|
|
306
|
-
return await runtime.handle(frame);
|
|
307
|
-
} finally {
|
|
308
|
-
controller.signal.removeEventListener('abort', cancelled);
|
|
309
|
-
session.requests.delete(frame.id);
|
|
310
|
-
session.cancelledRequests.delete(frame.id);
|
|
311
|
-
requestOwners.delete(frame.id);
|
|
312
|
-
if (streamId !== undefined) streamOwners.delete(streamId);
|
|
313
|
-
if (!session.connections.size && !session.requests.size) sessions.delete(session.id);
|
|
314
92
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
async function handler(request, response) {
|
|
318
|
-
const url = new URL(request.url, 'http://arcane.local');
|
|
319
|
-
if (!['/rpc', '/events', '/arcane-local-ai.js'].includes(url.pathname)) return false;
|
|
320
|
-
if (closed) {
|
|
321
|
-
json(response, 503, {error: {code: 'LOCAL_AI_DEV_CLOSED', message: 'Local AI development is closed.'}});
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
if (url.pathname === '/arcane-local-ai.js') {
|
|
325
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
326
|
-
json(response, 405, {error: {message: 'Use GET to load the local AI bootstrap.'}});
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
response.writeHead(200, {'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store'});
|
|
330
|
-
response.end(request.method === 'HEAD' ? undefined : bootstrap);
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
const clientId = url.searchParams.get('client');
|
|
334
|
-
if (!clientId) {
|
|
335
|
-
json(response, 400, {error: {code: 'LOCAL_AI_DEV_CLIENT_REQUIRED', message: 'Load the local AI development bootstrap before connecting.'}});
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
const method = url.pathname === '/events' ? 'GET' : 'POST';
|
|
339
|
-
if (request.method !== method) {
|
|
340
|
-
json(response, 405, {error: {message: `Use ${method} for ${url.pathname}.`}});
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
const session = sessionFor(clientId);
|
|
344
|
-
if (url.pathname === '/events') {
|
|
345
|
-
const connection = {response, session, closed: false, writes: Promise.resolve()};
|
|
346
|
-
session.connections.add(connection);
|
|
347
|
-
response.on('close', function eventConnectionClosed() { disconnect(connection); });
|
|
348
|
-
response.on('error', diagnostic);
|
|
349
|
-
response.writeHead(200, {'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store', Connection: 'keep-alive'});
|
|
350
|
-
response.flushHeaders();
|
|
351
|
-
ready.then(function replayReadyRuntime() { replay(connection); }, function reportPreparationFailure(error) {
|
|
352
|
-
if (!connection.closed) writeFrame(connection, event('core.error', serializeCoreError(error)));
|
|
353
|
-
}).catch(diagnostic);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
const controller = new AbortController();
|
|
357
|
-
const rpc = {request, response, controller};
|
|
358
|
-
incoming.add(rpc);
|
|
359
|
-
const abortOnClose = function rpcConnectionClosed() {
|
|
360
|
-
if (!response.writableEnded) controller.abort(new CoreError({code: 'LOCAL_AI_DEV_DISCONNECTED', message: 'The local AI request connection closed.'}));
|
|
361
|
-
};
|
|
362
|
-
response.once('close', abortOnClose);
|
|
363
|
-
let frame;
|
|
364
|
-
try {
|
|
365
|
-
request.setEncoding('utf8');
|
|
366
|
-
let body = '';
|
|
367
|
-
for await (const part of request) body += part;
|
|
368
|
-
frame = JSON.parse(body);
|
|
369
|
-
const result = await observe(dispatch(frame, session, controller));
|
|
370
|
-
json(response, 200, result ?? {ok: true});
|
|
371
|
-
} catch (error) {
|
|
372
|
-
diagnostic(error);
|
|
373
|
-
if (frame?.type === 'request' && frame.id) {
|
|
374
|
-
const result = {protocol: CORE_PROTOCOL, type: 'response', id: frame.id, ok: false, error: serializeCoreError(error), time: new Date().toISOString()};
|
|
375
|
-
sendSession(session, result);
|
|
376
|
-
json(response, 200, result);
|
|
377
|
-
} else json(response, 400, {error: serializeCoreError(error)});
|
|
378
|
-
} finally {
|
|
379
|
-
incoming.delete(rpc);
|
|
380
|
-
response.removeListener('close', abortOnClose);
|
|
381
|
-
if (!session.connections.size && !session.requests.size) sessions.delete(session.id);
|
|
382
|
-
}
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
|
|
93
|
+
);
|
|
386
94
|
function current() {
|
|
387
|
-
return {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
image: imageService?.current() ?? null
|
|
95
|
+
return {
|
|
96
|
+
...core.current(),
|
|
97
|
+
localAI: service.current(),
|
|
98
|
+
image: imageService?.current() ?? null,
|
|
99
|
+
speech: speechService?.current() ?? null
|
|
100
|
+
};
|
|
391
101
|
}
|
|
392
|
-
|
|
102
|
+
let closing;
|
|
393
103
|
function close() {
|
|
394
|
-
if (closing)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
for (const rpc of incoming) {
|
|
398
|
-
rpc.controller.abort(lifetime.signal.reason);
|
|
399
|
-
rpc.request.destroy();
|
|
400
|
-
rpc.response.destroy();
|
|
104
|
+
if (!closing) {
|
|
105
|
+
lifetime.abort();
|
|
106
|
+
closing = core.close().then(current);
|
|
401
107
|
}
|
|
402
|
-
for (const session of sessions.values()) {
|
|
403
|
-
for (const connection of [...session.connections]) {
|
|
404
|
-
disconnect(connection);
|
|
405
|
-
connection.response.destroy();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
closing = (async function closeLocalAI() {
|
|
409
|
-
try { await ready; } catch {}
|
|
410
|
-
try {
|
|
411
|
-
await runtime?.close();
|
|
412
|
-
} finally {
|
|
413
|
-
await Promise.allSettled([...operations]);
|
|
414
|
-
stopFrames?.();
|
|
415
|
-
sessions.clear();
|
|
416
|
-
signal?.removeEventListener('abort', abortDevelopment);
|
|
417
|
-
}
|
|
418
|
-
return current();
|
|
419
|
-
})();
|
|
420
108
|
return closing;
|
|
421
109
|
}
|
|
422
|
-
|
|
423
|
-
function abortDevelopment() { close().catch(diagnostic); }
|
|
424
|
-
signal?.addEventListener('abort', abortDevelopment, {once: true});
|
|
425
|
-
if (signal?.aborted) abortDevelopment();
|
|
426
|
-
return {ready, handler, current, close};
|
|
110
|
+
return {...core, current, close};
|
|
427
111
|
}
|
|
@@ -18,6 +18,15 @@ export function createImageLibrary(
|
|
|
18
18
|
) {
|
|
19
19
|
const requireBinding = createRequire(bindingModulePath);
|
|
20
20
|
const koffi = requireBinding(bindingModulePath);
|
|
21
|
+
const configuration = koffi.config();
|
|
22
|
+
// Async inference needs the normal native stack before Koffi creates its pools.
|
|
23
|
+
if (configuration.async_stack_size < configuration.sync_stack_size) {
|
|
24
|
+
koffi.config(
|
|
25
|
+
{
|
|
26
|
+
async_stack_size: configuration.sync_stack_size
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
21
30
|
const library = loadLibrary(libraryPath);
|
|
22
31
|
const backendDirectory = dirname(libraryPath);
|
|
23
32
|
let backendLibrary;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Official complete SDXL checkpoint; acquisition belongs to the model asset owner. */
|
|
2
|
+
export const SDXL_BASE_1_0_MODEL = {
|
|
3
|
+
id: 'sdxl-base-1.0',
|
|
4
|
+
name: 'SDXL Base 1.0',
|
|
5
|
+
family: 'sdxl',
|
|
6
|
+
resources: {
|
|
7
|
+
model: {
|
|
8
|
+
url: 'https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/462165984030d82259a11f4367a4eed129e94a7b/sd_xl_base_1.0.safetensors',
|
|
9
|
+
filename: 'sd_xl_base_1.0.safetensors'
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
context: {},
|
|
13
|
+
defaults: {
|
|
14
|
+
width: 1024,
|
|
15
|
+
height: 1024
|
|
16
|
+
},
|
|
17
|
+
operations: ['txt2img']
|
|
18
|
+
};
|
package/src/local-ai/install.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import {createEventQueue} from '../event-queue.mjs';
|
|
|
10
10
|
import {runProcess} from '../process.mjs';
|
|
11
11
|
import {extractLocalAIArchive} from './archive.mjs';
|
|
12
12
|
import {normalizeImageRuntimeRequirement} from './config.mjs';
|
|
13
|
+
import {normalizeWhisperRuntimeRequirement} from './whisper/config.mjs';
|
|
14
|
+
import {bundledWhisperRuntime, installedWhisperRuntime, installWhisperDistribution, whisperRuntimeSelection} from './whisper/install.mjs';
|
|
13
15
|
|
|
14
16
|
const is=new Is(false);
|
|
15
17
|
const installations=new Map();
|
|
@@ -21,11 +23,15 @@ function selectRuntimes(runtimes){
|
|
|
21
23
|
const selected=new Map();
|
|
22
24
|
for(const item of runtimes){
|
|
23
25
|
const requirement=is.string(item)?{id:item}:item;
|
|
24
|
-
const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement)
|
|
25
|
-
|
|
26
|
+
const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement)
|
|
27
|
+
:requirement?.id==='whisper.cpp'?normalizeWhisperRuntimeRequirement(requirement):requirement;
|
|
28
|
+
if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp','whisper.cpp'].includes(runtime.id)){
|
|
26
29
|
throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
|
|
27
30
|
}
|
|
28
31
|
const prior=selected.get(runtime.id);
|
|
32
|
+
if(prior&&runtime.id==='whisper.cpp'&&whisperRuntimeSelection(prior)!==whisperRuntimeSelection(runtime)){
|
|
33
|
+
throw new ArcaneError(ERROR_CODES.usage,'Select one complete whisper.cpp runtime and model requirement for this operation.');
|
|
34
|
+
}
|
|
29
35
|
if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url||(runtime.id==='stable-diffusion.cpp'&&prior.backend!==runtime.backend))){
|
|
30
36
|
throw new ArcaneError(ERROR_CODES.usage,`Select one ${runtime.id} runtime version and URL for this operation.`);
|
|
31
37
|
}
|
|
@@ -351,7 +357,7 @@ function bundledImageModelResource(resource, descriptor, model, role, runtimeRoo
|
|
|
351
357
|
return bundled;
|
|
352
358
|
}
|
|
353
359
|
|
|
354
|
-
async function installedRuntime(base,runtime,platform,architecture,signal){
|
|
360
|
+
async function installedRuntime(base,runtime,platform,architecture,signal,reuseWhisper=false){
|
|
355
361
|
let entries;
|
|
356
362
|
try{
|
|
357
363
|
entries=await readdir(base,{withFileTypes:true});
|
|
@@ -374,9 +380,19 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
|
|
|
374
380
|
}
|
|
375
381
|
if(installation.requestVersion!==(runtime.version??null)||installation.requestUrl!==(runtime.url??null))continue;
|
|
376
382
|
if (runtime.id === 'stable-diffusion.cpp' && installation.requestBackend !== runtime.backend) continue;
|
|
383
|
+
let whisperRequirement;
|
|
384
|
+
if (runtime.id === 'whisper.cpp') {
|
|
385
|
+
if (!installation.requestWhisper) continue;
|
|
386
|
+
whisperRequirement = JSON.parse(installation.requestWhisper);
|
|
387
|
+
if (whisperRuntimeSelection(whisperRequirement, !reuseWhisper) !== whisperRuntimeSelection(runtime, !reuseWhisper)) continue;
|
|
388
|
+
}
|
|
377
389
|
const record=installation.runtime;
|
|
378
390
|
if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
|
|
379
391
|
try{
|
|
392
|
+
if(runtime.id==='whisper.cpp'){
|
|
393
|
+
if(await installedWhisperRuntime(record,signal,!reuseWhisper))return reuseWhisper?{runtime:record,requirement:whisperRequirement}:record;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
380
396
|
if (runtime.id === 'stable-diffusion.cpp') {
|
|
381
397
|
const locations = [record.libraryPath, record.bindingModulePath];
|
|
382
398
|
if (!Array.isArray(record.variants) || record.variants.length === 0) continue;
|
|
@@ -524,10 +540,11 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
524
540
|
await onEvent({type:'local-ai.install.available',message:`${runtime.id} ${installed.version} is already installed.`,data:installed});
|
|
525
541
|
return installed;
|
|
526
542
|
}
|
|
543
|
+
const reusableWhisper=runtime.id==='whisper.cpp'?await installedRuntime(base,runtime,platform,architecture,signal,true):null;
|
|
527
544
|
if(runtime.id==='onnx'&&(!['win32','linux','darwin'].includes(platform)||!['x64','arm64'].includes(architecture))){
|
|
528
545
|
throw new ArcaneError(ERROR_CODES.targetUnavailable,`onnxruntime-node has no supported ${platform}/${architecture} CPU runtime.`);
|
|
529
546
|
}
|
|
530
|
-
const release=runtime.id
|
|
547
|
+
const release=['onnx','whisper.cpp'].includes(runtime.id)?null:await selectRelease(runtime,platform,architecture,signal);
|
|
531
548
|
throwIfAborted(signal);
|
|
532
549
|
await mkdir(base,{recursive:true});
|
|
533
550
|
const attempt=await mkdtemp(path.join(base,'install-'));
|
|
@@ -538,6 +555,8 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
538
555
|
let locations;
|
|
539
556
|
if(runtime.id==='onnx'){
|
|
540
557
|
locations=await installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent});
|
|
558
|
+
} else if (runtime.id === 'whisper.cpp') {
|
|
559
|
+
locations = await installWhisperDistribution(runtime, {root, directory, platform, architecture, signal, onEvent, upstreamResponse, reusable: reusableWhisper});
|
|
541
560
|
} else if (runtime.id === 'stable-diffusion.cpp') {
|
|
542
561
|
locations = await installImageDistribution(
|
|
543
562
|
release,
|
|
@@ -556,7 +575,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
556
575
|
const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
|
|
557
576
|
throwIfAborted(signal);
|
|
558
577
|
if(archive)await unlink(archive);
|
|
559
|
-
await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
|
|
578
|
+
await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),...(runtime.id==='whisper.cpp'?{requestWhisper:whisperRuntimeSelection(runtime)}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
|
|
560
579
|
completed=true;
|
|
561
580
|
await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
|
|
562
581
|
return record;
|
|
@@ -576,7 +595,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
576
595
|
|
|
577
596
|
function shareInstallation(runtime,options){
|
|
578
597
|
throwIfAborted(options.signal);
|
|
579
|
-
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null]);
|
|
598
|
+
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null,runtime.id==='whisper.cpp'?whisperRuntimeSelection(runtime):null]);
|
|
580
599
|
let entry=installations.get(key);
|
|
581
600
|
if(entry?.controller.signal.aborted){
|
|
582
601
|
// The prior cancelled attempt owns its cleanup. A later request waits
|
|
@@ -725,7 +744,7 @@ export async function bundleLocalAIRuntimes({runtimes=[],directory,outputRoot,pl
|
|
|
725
744
|
const files=[];
|
|
726
745
|
const relative=`runtime/local-ai/${runtime.id}`;
|
|
727
746
|
const destination=path.join(root,relative);
|
|
728
|
-
const bundled={...runtime,root:relative};
|
|
747
|
+
const bundled=runtime.id==='whisper.cpp'?bundledWhisperRuntime(runtime,relative):{...runtime,root:relative};
|
|
729
748
|
for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory','libraryPath','bindingModulePath']){
|
|
730
749
|
if(runtime[field]===undefined)continue;
|
|
731
750
|
bundled[field]=path.posix.join(relative,path.relative(runtime.root,runtime[field]).split(path.sep).join('/'));
|