arcane-os 0.64.0 → 0.66.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 +50 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +3 -2
- package/browser-runtime/ai/decision-runtime.mjs +14 -24
- package/browser-runtime/core/client.mjs +62 -5
- package/docs/guides/native-whisper-openvino.md +334 -0
- package/docs/publishing.md +39 -0
- package/docs/reference/README.md +11 -1
- package/docs/reference/ai/browser-decisions.md +7 -4
- 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 +232 -4
- package/docs/reference/git-text-snapshot.md +15 -1
- package/docs/reference/inventory/package-api.json +174 -6
- package/docs/reference/inventory/runtime-modules.json +8 -6
- package/docs/reference/local-ai.md +80 -3
- package/docs/reference/local-image-generation.md +5 -0
- package/docs/reference/local-image-sdxl.md +1 -1
- package/docs/reference/local-whisper.md +7 -1
- package/docs/reference/mcp-stdio.md +263 -0
- package/docs/reference/native-speech.md +6 -0
- package/docs/reference/runtime-components.md +18 -1
- package/docs/reference/runtime-modules.md +10 -2
- package/docs/reference/sdk-api.md +346 -4
- 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 +178 -9
- package/src/core/services/local-ai.mjs +80 -10
- package/src/dev-server.mjs +3 -2
- package/src/git-identity.mjs +132 -0
- package/src/git-text-snapshot.mjs +5 -3
- 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
|
@@ -37,6 +37,9 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
37
37
|
requestedBackend: helper?.variant.backend ?? runtime?.backend ?? 'auto',
|
|
38
38
|
observedBackend: helper?.metadata?.observedBackend ?? null,
|
|
39
39
|
backendEvidence: helper?.metadata?.backendEvidence ?? null,
|
|
40
|
+
requestedEncoder: runtime?.encoder === 'openvino-npu' ? 'NPU' : null,
|
|
41
|
+
observedEncoder: helper?.metadata?.observedEncoder ?? null,
|
|
42
|
+
encoderEvidence: helper?.metadata?.encoderEvidence ?? null,
|
|
40
43
|
error: error ? {code: error.code, message: error.message} : null
|
|
41
44
|
};
|
|
42
45
|
}
|
|
@@ -80,6 +83,11 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
80
83
|
const library = variant.libraryDirectory ?? variant.root;
|
|
81
84
|
const args = ['--model-base64', encode(model.path), '--runtime-base64', encode(library),
|
|
82
85
|
'--backend', variant.backend === 'cpu' ? 'cpu' : 'gpu'];
|
|
86
|
+
if (runtime.encoder === 'openvino-npu') {
|
|
87
|
+
args.push('--encoder-model-base64', encode(model.encoderPath),
|
|
88
|
+
'--encoder-cache-base64', encode(path.join(temporaryDirectory, 'encoder-cache')),
|
|
89
|
+
'--encoder-device', 'NPU');
|
|
90
|
+
}
|
|
83
91
|
if (runtime.threads !== undefined) args.push('--threads', String(runtime.threads));
|
|
84
92
|
|
|
85
93
|
async function receive({stream, chunk}) {
|
|
@@ -96,7 +104,7 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
96
104
|
session.metadata = record;
|
|
97
105
|
readiness.resolve(record);
|
|
98
106
|
} else if (record.type === 'error' && !record.requestId) {
|
|
99
|
-
readiness.reject(whisperError('WHISPER_MODEL_FAILED', record.message));
|
|
107
|
+
readiness.reject(whisperError(record.code ?? 'WHISPER_MODEL_FAILED', record.message));
|
|
100
108
|
} else if (pending && record.requestId === pending.id) {
|
|
101
109
|
if (record.type === 'complete') {
|
|
102
110
|
pending.response.resolve({text: record.text, language: record.language,
|
|
@@ -179,7 +187,7 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
179
187
|
session.input.end();
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
|
-
try { await session.completion; }
|
|
190
|
+
try { return await session.completion; }
|
|
183
191
|
catch (failure) {
|
|
184
192
|
if (!session.controller.signal.aborted) throw failure;
|
|
185
193
|
}
|
|
@@ -217,6 +225,13 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
217
225
|
}
|
|
218
226
|
const model = runtime.models?.find(function selectedModel(record) { return record.id === selected; });
|
|
219
227
|
if (!model) throw whisperError('WHISPER_MODEL_UNAVAILABLE', `The selected model ${selected} is not prepared.`);
|
|
228
|
+
if (runtime.encoder === 'openvino-npu') {
|
|
229
|
+
if (!is.string(model.encoderPath) || !is.string(model.encoderDataPath)) {
|
|
230
|
+
throw whisperError('WHISPER_ENCODER_UNAVAILABLE', 'The selected NPU model requires its matching OpenVINO XML and BIN files.');
|
|
231
|
+
}
|
|
232
|
+
await mkdir(path.join(temporaryDirectory, 'encoder-cache'), {recursive: true});
|
|
233
|
+
loadSignal.throwIfAborted();
|
|
234
|
+
}
|
|
220
235
|
const variants = selectedVariants(runtime);
|
|
221
236
|
const failures = [];
|
|
222
237
|
for (const variant of variants) {
|
|
@@ -236,8 +251,13 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
236
251
|
failures.push(failure);
|
|
237
252
|
try { await stopHelper(session, {terminate: true}); }
|
|
238
253
|
catch (exitFailure) { if (exitFailure !== failure) failures.push(exitFailure); }
|
|
254
|
+
if (runtime.encoder === 'openvino-npu') {
|
|
255
|
+
try { await session.completion; }
|
|
256
|
+
catch (exitFailure) { if (exitFailure !== failure) failure.cause = exitFailure; }
|
|
257
|
+
}
|
|
239
258
|
helper = null;
|
|
240
259
|
loadSignal.throwIfAborted();
|
|
260
|
+
if (failure.code === 'WHISPER_ENCODER_UNAVAILABLE') throw failure;
|
|
241
261
|
if (session.observerFailure || failure.code === 'WHISPER_PROTOCOL_INCOMPLETE') throw failure;
|
|
242
262
|
if (variant !== variants[variants.length - 1] && onEvent) {
|
|
243
263
|
await onEvent({type: 'whisper.backend.failed', message: failure.message,
|
|
@@ -247,7 +267,10 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
247
267
|
loadSignal.removeEventListener('abort', cancelLoading);
|
|
248
268
|
}
|
|
249
269
|
}
|
|
250
|
-
|
|
270
|
+
if (runtime.encoder === 'openvino-npu' && failures.length === 1) throw failures[0];
|
|
271
|
+
const failure = new AggregateError(failures, 'The selected Whisper model could not load in the prepared runtimes.');
|
|
272
|
+
if (runtime.encoder === 'openvino-npu') failure.code = failures[0]?.code ?? 'WHISPER_MODEL_FAILED';
|
|
273
|
+
throw failure;
|
|
251
274
|
}).catch(function modelLoadFailed(failure) {
|
|
252
275
|
error = loadSignal.aborted ? null : failure;
|
|
253
276
|
state = unloading || closing ? 'unloading' : loadSignal.aborted ? 'unloaded' : 'error';
|
|
@@ -265,7 +288,7 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
265
288
|
|| session.observerFailure || failure.code === 'WHISPER_PROTOCOL_INCOMPLETE'
|
|
266
289
|
|| (runtime.backend && runtime.backend !== 'auto')) return null;
|
|
267
290
|
if (failure.code !== 'WHISPER_INFERENCE_FAILED' && !session.failed) return null;
|
|
268
|
-
return runtime.
|
|
291
|
+
return selectedVariants(runtime).find(function preparedCPU(variant) { return variant.backend === 'cpu'; }) ?? null;
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
function transcribe(request, {signal, onProgress, requestId: suppliedRequestId} = {}) {
|
|
@@ -384,6 +407,13 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
384
407
|
const response = deferred();
|
|
385
408
|
session.pending = {id, response, signal, onProgress, attempt};
|
|
386
409
|
function cancelInference() {
|
|
410
|
+
if (runtime.encoder === 'openvino-npu') {
|
|
411
|
+
// OpenVINO's synchronous encoder call has no cancellation seam
|
|
412
|
+
// in whisper.cpp. Retire the owned process and join its exit.
|
|
413
|
+
session.stopping = true;
|
|
414
|
+
session.controller.abort(signal.reason);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
387
417
|
writeCommand(session.input, `cancel\t${id}\n`).catch(function cancellationWriteFailed(failure) {
|
|
388
418
|
session.controller.abort(failure);
|
|
389
419
|
});
|
|
@@ -398,6 +428,29 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
398
428
|
signal.throwIfAborted();
|
|
399
429
|
return result;
|
|
400
430
|
} catch (failure) {
|
|
431
|
+
if (runtime.encoder === 'openvino-npu' && failure.code === 'WHISPER_INFERENCE_FAILED') {
|
|
432
|
+
const recovering = cpuRecoveryVariant(session, failure);
|
|
433
|
+
try { failure.data = await stopHelper(session); }
|
|
434
|
+
catch (exitFailure) { if (exitFailure !== failure) failure.cause = exitFailure; }
|
|
435
|
+
if (helper === session) {
|
|
436
|
+
helper = null;
|
|
437
|
+
state = unloading || closing ? 'unloading' : signal.aborted ? 'unloaded' : recovering ? 'recovering' : 'error';
|
|
438
|
+
error = signal.aborted || recovering ? null : failure;
|
|
439
|
+
publish();
|
|
440
|
+
}
|
|
441
|
+
signal.throwIfAborted();
|
|
442
|
+
throw failure;
|
|
443
|
+
}
|
|
444
|
+
if (signal.aborted && runtime.encoder === 'openvino-npu') {
|
|
445
|
+
await stopHelper(session, {terminate: true});
|
|
446
|
+
if (helper === session) {
|
|
447
|
+
helper = null;
|
|
448
|
+
state = unloading || closing ? 'unloading' : 'unloaded';
|
|
449
|
+
error = null;
|
|
450
|
+
publish();
|
|
451
|
+
}
|
|
452
|
+
signal.throwIfAborted();
|
|
453
|
+
}
|
|
401
454
|
if (!response.settled) {
|
|
402
455
|
session.controller.abort(failure);
|
|
403
456
|
await Promise.allSettled([session.completion]);
|
|
@@ -453,7 +506,11 @@ export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirect
|
|
|
453
506
|
}
|
|
454
507
|
|
|
455
508
|
function selectedVariants(runtime) {
|
|
456
|
-
const variants = runtime.variants ?? []
|
|
509
|
+
const variants = (runtime.variants ?? []).filter(function matchingEncoder(variant) {
|
|
510
|
+
return runtime.encoder === 'openvino-npu'
|
|
511
|
+
? variant.encoder === 'openvino-npu'
|
|
512
|
+
: variant.encoder === undefined;
|
|
513
|
+
});
|
|
457
514
|
if (runtime.backend && runtime.backend !== 'auto') {
|
|
458
515
|
const selected = variants.filter(function selectedBackend(variant) { return variant.backend === runtime.backend; });
|
|
459
516
|
if (selected.length) return selected;
|
|
@@ -29,7 +29,10 @@ export async function installedWhisperRuntime(record, signal, includeModels = tr
|
|
|
29
29
|
files.push(variant.executable);
|
|
30
30
|
directories.push(variant.root, variant.libraryDirectory);
|
|
31
31
|
}
|
|
32
|
-
if (includeModels) for (const model of record.models)
|
|
32
|
+
if (includeModels) for (const model of record.models) {
|
|
33
|
+
files.push(model.path);
|
|
34
|
+
if (record.encoder === 'openvino-npu') files.push(model.encoderPath, model.encoderDataPath);
|
|
35
|
+
}
|
|
33
36
|
for (const location of directories) {
|
|
34
37
|
throwIfAborted(signal);
|
|
35
38
|
if (!is.string(location) || !(await stat(location)).isDirectory()) return false;
|
|
@@ -104,6 +107,17 @@ function defaultWhisperVariants(requirement, platform, architecture) {
|
|
|
104
107
|
if (platform !== 'win32' || architecture !== 'x64' || requirement.version !== '1.9.4') {
|
|
105
108
|
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Automatic Whisper distributions are available for 1.9.4 on win32/x64. Supply complete runtime variants for ${requirement.version} on ${platform}/${architecture}.`);
|
|
106
109
|
}
|
|
110
|
+
if (requirement.encoder === 'openvino-npu') {
|
|
111
|
+
if (requirement.backend === 'metal') {
|
|
112
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The selected Windows OpenVINO Whisper distribution has no Metal decoder backend.');
|
|
113
|
+
}
|
|
114
|
+
const version = requirement.helperVersion ?? packageMetadata.version;
|
|
115
|
+
return [{
|
|
116
|
+
backend: requirement.backend === 'cpu' ? 'cpu' : 'cuda',
|
|
117
|
+
encoder: 'openvino-npu',
|
|
118
|
+
url: `https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${version}/arcane-whisper-openvino-windows-x64.tar.gz`
|
|
119
|
+
}];
|
|
120
|
+
}
|
|
107
121
|
if (requirement.backend === 'metal') {
|
|
108
122
|
throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The selected Windows Whisper distribution has no Metal backend.');
|
|
109
123
|
}
|
|
@@ -125,7 +139,10 @@ export async function installWhisperDistribution(requirement, options) {
|
|
|
125
139
|
const controller = new AbortController();
|
|
126
140
|
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
127
141
|
const preparation = {...options, signal};
|
|
128
|
-
const
|
|
142
|
+
const integratedEncoder = requirement.encoder === 'openvino-npu' && !requirement.variants;
|
|
143
|
+
const variants = reusable
|
|
144
|
+
? integratedEncoder ? [reusable.runtime.variants[0]] : reusable.runtime.variants
|
|
145
|
+
: defaultWhisperVariants(requirement, platform, architecture);
|
|
129
146
|
if (!requirement.helperExecutable && !requirement.helperUrl && (platform !== 'win32' || architecture !== 'x64')) {
|
|
130
147
|
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply the Arcane Whisper helperExecutable or helperUrl for ${platform}/${architecture}.`);
|
|
131
148
|
}
|
|
@@ -133,7 +150,7 @@ export async function installWhisperDistribution(requirement, options) {
|
|
|
133
150
|
throw new ArcaneError(ERROR_CODES.targetUnavailable, `Supply a complete decoder tree through decoderExecutable and decoderRoot, or decoderUrl, for ${platform}/${architecture}.`);
|
|
134
151
|
}
|
|
135
152
|
const helperName = `arcane-whisper${platform === 'win32' ? '.exe' : ''}`;
|
|
136
|
-
|
|
153
|
+
let helperRoot = path.join(root, 'helper');
|
|
137
154
|
let helperExecutable = path.join(helperRoot, helperName);
|
|
138
155
|
const decoderRoot = path.join(root, 'decoder');
|
|
139
156
|
const models = [];
|
|
@@ -151,10 +168,18 @@ export async function installWhisperDistribution(requirement, options) {
|
|
|
151
168
|
else await prepareWhisperArchive(variant.url, variantRoot, preparation);
|
|
152
169
|
const libraryName = platform === 'win32' ? 'whisper.dll' : platform === 'darwin' ? 'libwhisper.dylib' : 'libwhisper.so';
|
|
153
170
|
const libraryDirectory = selectedLibraryDirectory ?? path.dirname(await findWhisperFile(variantRoot, libraryName, signal));
|
|
154
|
-
preparedVariants[index] = {backend: variant.backend, root: variantRoot, libraryDirectory
|
|
171
|
+
preparedVariants[index] = {backend: variant.backend, root: variantRoot, libraryDirectory,
|
|
172
|
+
...(variant.encoder ? {encoder: variant.encoder} : {})};
|
|
173
|
+
if (requirement.encoder === 'openvino-npu' && !requirement.helperExecutable && !requirement.helperUrl) {
|
|
174
|
+
preparedVariants[index].executable = await findWhisperFile(variantRoot, helperName, signal);
|
|
175
|
+
}
|
|
155
176
|
};
|
|
156
177
|
});
|
|
157
178
|
jobs.push(async function prepareHelper() {
|
|
179
|
+
// The optional native asset owns its matching helper and runtime as
|
|
180
|
+
// one complete tree. Reuse that tree once and resolve its helper after
|
|
181
|
+
// variant preparation joins, including when models change later.
|
|
182
|
+
if (requirement.encoder === 'openvino-npu' && !requirement.helperExecutable && !requirement.helperUrl) return;
|
|
158
183
|
const existingHelper = reusable?.runtime.helperExecutable ?? requirement.helperExecutable;
|
|
159
184
|
if (existingHelper) {
|
|
160
185
|
const source = path.resolve(existingHelper);
|
|
@@ -190,25 +215,46 @@ export async function installWhisperDistribution(requirement, options) {
|
|
|
190
215
|
jobs.push(async function prepareSelectedModel() {
|
|
191
216
|
const modelRoot = path.join(root, 'models', String(index));
|
|
192
217
|
await mkdir(modelRoot, {recursive: true});
|
|
193
|
-
const filename = model.path ? path.basename(model.path) : model.filename ?? path.posix.basename(new URL(model.url).pathname);
|
|
194
|
-
const destination = path.join(modelRoot, filename);
|
|
195
218
|
const priorIndex = reusable?.requirement.models.findIndex(function sameSelectedModel(candidate) {
|
|
196
219
|
return JSON.stringify(candidate) === JSON.stringify(model);
|
|
197
220
|
}) ?? -1;
|
|
198
|
-
let
|
|
221
|
+
let sourceModel = model;
|
|
199
222
|
if (priorIndex >= 0) {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
223
|
+
const prior = reusable.runtime.models[priorIndex];
|
|
224
|
+
if (prior?.path) {
|
|
202
225
|
try {
|
|
203
|
-
|
|
226
|
+
const paths = [prior.path, ...(requirement.encoder === 'openvino-npu' ? [prior.encoderPath, prior.encoderDataPath] : [])];
|
|
227
|
+
let available = true;
|
|
228
|
+
for (const location of paths) if (!location || !(await stat(location)).isFile()) available = false;
|
|
229
|
+
if (available) sourceModel = prior;
|
|
204
230
|
} catch (error) {
|
|
205
231
|
if (error.code !== 'ENOENT') throw error;
|
|
206
232
|
}
|
|
207
233
|
}
|
|
208
234
|
}
|
|
209
|
-
if (
|
|
235
|
+
if (!sourceModel.path && model.archiveUrl) {
|
|
236
|
+
await prepareWhisperArchive(model.archiveUrl, modelRoot, preparation);
|
|
237
|
+
models[index] = {...model,
|
|
238
|
+
path: await findWhisperFile(modelRoot, model.filename, signal),
|
|
239
|
+
...(requirement.encoder === 'openvino-npu' ? {
|
|
240
|
+
encoderPath: await findWhisperFile(modelRoot, model.encoderFilename, signal),
|
|
241
|
+
encoderDataPath: await findWhisperFile(modelRoot, model.encoderDataFilename, signal)
|
|
242
|
+
} : {})};
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const filename = sourceModel.path ? path.basename(sourceModel.path) : model.filename ?? path.posix.basename(new URL(model.url).pathname);
|
|
246
|
+
const destination = path.join(modelRoot, filename);
|
|
247
|
+
if (sourceModel.path) await copyFile(path.resolve(sourceModel.path), destination);
|
|
210
248
|
else await downloadWhisperResource(model.url, destination, preparation);
|
|
211
|
-
|
|
249
|
+
const preparedModel = {...model, path: destination};
|
|
250
|
+
if (requirement.encoder === 'openvino-npu') {
|
|
251
|
+
for (const field of ['encoderPath', 'encoderDataPath']) {
|
|
252
|
+
throwIfAborted(signal);
|
|
253
|
+
preparedModel[field] = path.join(modelRoot, path.basename(sourceModel[field]));
|
|
254
|
+
await copyFile(path.resolve(sourceModel[field]), preparedModel[field]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
models[index] = preparedModel;
|
|
212
258
|
});
|
|
213
259
|
}
|
|
214
260
|
let nextJob = 0;
|
|
@@ -232,16 +278,26 @@ export async function installWhisperDistribution(requirement, options) {
|
|
|
232
278
|
throwIfAborted(options.signal);
|
|
233
279
|
if (failures.length === 1) throw failures[0];
|
|
234
280
|
if (failures.length > 1) throw new AggregateError(failures, 'Whisper dependency preparation failed.');
|
|
281
|
+
if (requirement.encoder === 'openvino-npu' && !requirement.helperExecutable && !requirement.helperUrl) {
|
|
282
|
+
helperRoot = preparedVariants[0].root;
|
|
283
|
+
helperExecutable = preparedVariants[0].executable;
|
|
284
|
+
}
|
|
285
|
+
// The precompiled optional tree owns both decoder backends. Share its
|
|
286
|
+
// helper and native files while keeping the normal GPU-first CPU recovery.
|
|
287
|
+
if (integratedEncoder && requirement.backend === 'auto') {
|
|
288
|
+
preparedVariants.push({...preparedVariants[0], backend: 'cpu'});
|
|
289
|
+
}
|
|
235
290
|
const locations = {
|
|
236
291
|
version: requirement.version,
|
|
237
292
|
backend: requirement.backend,
|
|
293
|
+
...(requirement.encoder ? {encoder: requirement.encoder} : {}),
|
|
238
294
|
helperRoot,
|
|
239
295
|
helperExecutable,
|
|
240
296
|
helperVersion: requirement.helperVersion ?? packageMetadata.version,
|
|
241
297
|
decoderRoot,
|
|
242
298
|
decoderExecutable,
|
|
243
299
|
variants: preparedVariants.map(function completedWhisperVariant(variant) {
|
|
244
|
-
return {...variant, executable: helperExecutable};
|
|
300
|
+
return {...variant, executable: variant.executable ?? helperExecutable};
|
|
245
301
|
}),
|
|
246
302
|
models,
|
|
247
303
|
...(requirement.modelId ? {modelId: requirement.modelId} : models.length === 1 ? {modelId: models[0].id} : {})
|
|
@@ -268,7 +324,9 @@ export function bundledWhisperRuntime(runtime, relativeRoot) {
|
|
|
268
324
|
return {...variant, root: bundledWhisperPath(variant.root), libraryDirectory: bundledWhisperPath(variant.libraryDirectory), executable: bundledWhisperPath(variant.executable)};
|
|
269
325
|
}),
|
|
270
326
|
models: runtime.models.map(function bundledWhisperModel(model) {
|
|
271
|
-
return {...model, path: bundledWhisperPath(model.path)
|
|
327
|
+
return {...model, path: bundledWhisperPath(model.path),
|
|
328
|
+
...(model.encoderPath ? {encoderPath: bundledWhisperPath(model.encoderPath)} : {}),
|
|
329
|
+
...(model.encoderDataPath ? {encoderDataPath: bundledWhisperPath(model.encoderDataPath)} : {})};
|
|
272
330
|
})
|
|
273
331
|
};
|
|
274
332
|
}
|
|
@@ -30,6 +30,9 @@ struct Configuration {
|
|
|
30
30
|
std::string model;
|
|
31
31
|
std::string runtime;
|
|
32
32
|
std::string backend = "gpu";
|
|
33
|
+
std::string encoder_model;
|
|
34
|
+
std::string encoder_cache;
|
|
35
|
+
std::string encoder_device;
|
|
33
36
|
int threads = 1;
|
|
34
37
|
};
|
|
35
38
|
|
|
@@ -393,6 +396,12 @@ Configuration read_configuration(int argc, char** argv) {
|
|
|
393
396
|
configuration.runtime = decode_base64(value);
|
|
394
397
|
} else if (option == "--backend") {
|
|
395
398
|
configuration.backend = value;
|
|
399
|
+
} else if (option == "--encoder-model-base64") {
|
|
400
|
+
configuration.encoder_model = decode_base64(value);
|
|
401
|
+
} else if (option == "--encoder-cache-base64") {
|
|
402
|
+
configuration.encoder_cache = decode_base64(value);
|
|
403
|
+
} else if (option == "--encoder-device") {
|
|
404
|
+
configuration.encoder_device = value;
|
|
396
405
|
} else if (option == "--threads") {
|
|
397
406
|
configuration.threads = std::stoi(value);
|
|
398
407
|
} else {
|
|
@@ -405,6 +414,12 @@ Configuration read_configuration(int argc, char** argv) {
|
|
|
405
414
|
if (configuration.backend != "gpu" && configuration.backend != "cpu") {
|
|
406
415
|
throw std::runtime_error("The requested backend must be gpu or cpu.");
|
|
407
416
|
}
|
|
417
|
+
if (!configuration.encoder_model.empty() || !configuration.encoder_cache.empty()
|
|
418
|
+
|| !configuration.encoder_device.empty()) {
|
|
419
|
+
if (configuration.encoder_device != "NPU" || configuration.encoder_model.empty()) {
|
|
420
|
+
throw std::runtime_error("The OpenVINO encoder requires its selected model path and NPU device.");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
408
423
|
if (configuration.threads < 1) {
|
|
409
424
|
throw std::runtime_error("Native inference requires a positive thread count.");
|
|
410
425
|
}
|
|
@@ -416,7 +431,9 @@ int serve(const Configuration& configuration) {
|
|
|
416
431
|
whisper_log_set(native_log, &log);
|
|
417
432
|
emit("{\"type\":\"loading\",\"model\":" + json_string(configuration.model)
|
|
418
433
|
+ ",\"requestedBackend\":" + json_string(configuration.backend)
|
|
419
|
-
+ ",\"observedBackend\":null,\"backendEvidence\":null
|
|
434
|
+
+ ",\"observedBackend\":null,\"backendEvidence\":null"
|
|
435
|
+
+ ",\"requestedEncoder\":" + (configuration.encoder_device.empty() ? "null" : json_string(configuration.encoder_device))
|
|
436
|
+
+ ",\"observedEncoder\":null,\"encoderEvidence\":null}");
|
|
420
437
|
ggml_backend_load_all_from_path(configuration.runtime.c_str());
|
|
421
438
|
whisper_context_params parameters = whisper_context_default_params();
|
|
422
439
|
parameters.use_gpu = configuration.backend == "gpu";
|
|
@@ -425,15 +442,29 @@ int serve(const Configuration& configuration) {
|
|
|
425
442
|
if (!context) {
|
|
426
443
|
throw std::runtime_error("Whisper could not load the selected model.");
|
|
427
444
|
}
|
|
445
|
+
if (!configuration.encoder_device.empty()) {
|
|
446
|
+
const int result = whisper_ctx_init_openvino_encoder(context.get(), configuration.encoder_model.c_str(),
|
|
447
|
+
"NPU", configuration.encoder_cache.empty() ? nullptr : configuration.encoder_cache.c_str());
|
|
448
|
+
if (result != 0) {
|
|
449
|
+
emit_error("", "whisper_ctx_init_openvino_encoder for NPU returned " + std::to_string(result) + ".",
|
|
450
|
+
"WHISPER_ENCODER_UNAVAILABLE");
|
|
451
|
+
return 1;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
428
454
|
{
|
|
429
455
|
const std::lock_guard<std::mutex> lock(log.mutex);
|
|
430
456
|
// The public C API exposes available devices, not a context backend
|
|
431
457
|
// getter. This observation is explicitly attributed to completed native
|
|
432
458
|
// initialization logs; it does not claim every operation uses a GPU.
|
|
459
|
+
// A successful OpenVINO call establishes encoder initialization on the
|
|
460
|
+
// explicit NPU device, independently of decoder/backend observation.
|
|
433
461
|
emit("{\"type\":\"ready\",\"model\":" + json_string(configuration.model)
|
|
434
462
|
+ ",\"requestedBackend\":" + json_string(configuration.backend)
|
|
435
463
|
+ ",\"observedBackend\":" + (log.backend.empty() ? "null" : json_string(log.backend))
|
|
436
|
-
+ ",\"backendEvidence\":" + (log.backend.empty() ? "null" : "\"runtime-log\"")
|
|
464
|
+
+ ",\"backendEvidence\":" + (log.backend.empty() ? "null" : "\"runtime-log\"")
|
|
465
|
+
+ ",\"requestedEncoder\":" + (configuration.encoder_device.empty() ? "null" : json_string(configuration.encoder_device))
|
|
466
|
+
+ ",\"observedEncoder\":" + (configuration.encoder_device.empty() ? "null" : json_string(configuration.encoder_device))
|
|
467
|
+
+ ",\"encoderEvidence\":" + (configuration.encoder_device.empty() ? "null" : "\"openvino-initialization\"") + "}");
|
|
437
468
|
}
|
|
438
469
|
Commands commands;
|
|
439
470
|
std::thread reader(read_commands, std::ref(commands));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {cp, mkdir, readdir, readFile, writeFile} from 'node:fs/promises';
|
|
3
|
+
import Is from 'strong-type';
|
|
4
|
+
import {runProcess} from '../../process.mjs';
|
|
5
|
+
|
|
6
|
+
const is = new Is(false);
|
|
7
|
+
|
|
8
|
+
/** Produce the optional Windows Intel NPU encoder runtime from installed upstream sources. */
|
|
9
|
+
export async function buildWhisperOpenVinoRuntime({
|
|
10
|
+
sourceDirectory,
|
|
11
|
+
openvinoDirectory,
|
|
12
|
+
cudaDirectory,
|
|
13
|
+
outputRoot,
|
|
14
|
+
cmake = 'cmake',
|
|
15
|
+
generator,
|
|
16
|
+
cmakeArgs = [],
|
|
17
|
+
env,
|
|
18
|
+
signal,
|
|
19
|
+
onEvent
|
|
20
|
+
} = {}) {
|
|
21
|
+
if (!is.string(sourceDirectory) || !is.string(openvinoDirectory) || !is.string(outputRoot)) {
|
|
22
|
+
throw new TypeError('Whisper sourceDirectory, OpenVINO openvinoDirectory, and outputRoot are required.');
|
|
23
|
+
}
|
|
24
|
+
if (process.platform !== 'win32' || process.arch !== 'x64') {
|
|
25
|
+
throw new Error('This optional OpenVINO producer currently targets Windows x64.');
|
|
26
|
+
}
|
|
27
|
+
const output = path.resolve(outputRoot);
|
|
28
|
+
const source = path.join(output, 'source');
|
|
29
|
+
const build = path.join(output, 'build');
|
|
30
|
+
const root = path.join(output, 'runtime');
|
|
31
|
+
const libraryDirectory = path.join(root, 'bin');
|
|
32
|
+
const toolkit = path.resolve(openvinoDirectory);
|
|
33
|
+
const options = {env, signal, onEvent};
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
await cp(path.resolve(sourceDirectory), source, {recursive: true});
|
|
36
|
+
signal?.throwIfAborted();
|
|
37
|
+
await prepareOpenVinoSource(source);
|
|
38
|
+
const configure = await runProcess(cmake, [
|
|
39
|
+
'-S', source,
|
|
40
|
+
'-B', build,
|
|
41
|
+
...(generator ? ['-G', generator] : []),
|
|
42
|
+
'-DCMAKE_BUILD_TYPE=Release',
|
|
43
|
+
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=${path.join(build, 'bin')}`,
|
|
44
|
+
`-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=${path.join(build, 'bin')}`,
|
|
45
|
+
'-DBUILD_SHARED_LIBS=ON',
|
|
46
|
+
'-DWHISPER_OPENVINO=ON',
|
|
47
|
+
'-DWHISPER_BUILD_IS_DEV=OFF',
|
|
48
|
+
'-DWHISPER_BUILD_TESTS=OFF',
|
|
49
|
+
'-DWHISPER_BUILD_EXAMPLES=OFF',
|
|
50
|
+
'-DWHISPER_BUILD_SERVER=OFF',
|
|
51
|
+
'-DGGML_BACKEND_DL=ON',
|
|
52
|
+
'-DGGML_CPU=ON',
|
|
53
|
+
'-DGGML_NATIVE=OFF',
|
|
54
|
+
'-DGGML_CUDA=OFF',
|
|
55
|
+
'-DGGML_OPENVINO=OFF',
|
|
56
|
+
'-DGGML_OPENMP_FETCH=OFF',
|
|
57
|
+
'-DGGML_CCACHE=OFF',
|
|
58
|
+
`-DOpenVINO_DIR=${path.join(toolkit, 'runtime', 'cmake')}`,
|
|
59
|
+
...cmakeArgs
|
|
60
|
+
], options);
|
|
61
|
+
const compile = await runProcess(cmake, [
|
|
62
|
+
'--build', build, '--config', 'Release', '--target', 'whisper', 'ggml-cpu'
|
|
63
|
+
], options);
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
await mkdir(libraryDirectory, {recursive: true});
|
|
66
|
+
await mkdir(path.join(root, 'lib'), {recursive: true});
|
|
67
|
+
await mkdir(path.join(root, 'notices', 'whisper.cpp'), {recursive: true});
|
|
68
|
+
// Keep all selected upstream runtime DLLs together for Windows and plugin lookup.
|
|
69
|
+
await cp(path.join(build, 'bin'), libraryDirectory, {recursive: true});
|
|
70
|
+
await copyImportLibraries(build, path.join(root, 'lib'));
|
|
71
|
+
await cp(path.join(toolkit, 'runtime', 'bin', 'intel64', 'Release'), libraryDirectory, {recursive: true});
|
|
72
|
+
const tbbDirectory = path.join(toolkit, 'runtime', '3rdparty', 'tbb', 'bin');
|
|
73
|
+
for (const member of await readdir(tbbDirectory, {withFileTypes: true})) {
|
|
74
|
+
if (member.isFile() && member.name.endsWith('.dll') && !member.name.endsWith('_debug.dll')) {
|
|
75
|
+
await cp(path.join(tbbDirectory, member.name), path.join(libraryDirectory, member.name));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (cudaDirectory) {
|
|
79
|
+
// The matching upstream plugin supplies CUDA without compiling a new backend.
|
|
80
|
+
for (const name of [
|
|
81
|
+
'ggml-cuda.dll', 'cublas64_12.dll', 'cublasLt64_12.dll', 'cudart64_12.dll',
|
|
82
|
+
'nvblas64_12.dll', 'nvrtc-builtins64_124.dll', 'nvrtc64_120_0.dll'
|
|
83
|
+
]) {
|
|
84
|
+
await cp(path.join(cudaDirectory, name), path.join(libraryDirectory, name));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await cp(path.join(toolkit, 'docs', 'licensing'), path.join(root, 'notices', 'openvino'), {recursive: true});
|
|
88
|
+
await cp(path.join(source, 'LICENSE'), path.join(root, 'notices', 'whisper.cpp', 'LICENSE'));
|
|
89
|
+
await writeFile(path.join(root, 'notices', 'whisper.cpp', 'CHANGES.txt'),
|
|
90
|
+
'whisper.cpp 1.9.4: propagate OpenVINO encoder inference failure and send both OpenVINO exception diagnostics to stderr.\n');
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
return {
|
|
93
|
+
version: '1.9.4',
|
|
94
|
+
openvinoVersion: '2026.3.1',
|
|
95
|
+
platform: process.platform,
|
|
96
|
+
architecture: process.arch,
|
|
97
|
+
encoder: 'openvino-npu',
|
|
98
|
+
backend: cudaDirectory ? 'cuda' : 'cpu',
|
|
99
|
+
backends: cudaDirectory ? ['cuda', 'cpu'] : ['cpu'],
|
|
100
|
+
root,
|
|
101
|
+
directory: root,
|
|
102
|
+
libraryDirectory,
|
|
103
|
+
sourceDirectory: source,
|
|
104
|
+
diagnostics: {configure, compile}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function prepareOpenVinoSource(source) {
|
|
109
|
+
const whisperPath = path.join(source, 'src', 'whisper.cpp');
|
|
110
|
+
const encoderPath = path.join(source, 'src', 'openvino', 'whisper-openvino-encoder.cpp');
|
|
111
|
+
const whisper = await readFile(whisperPath, 'utf8');
|
|
112
|
+
const call = ' whisper_openvino_encode(wstate.ctx_openvino, mel, wstate.embd_enc);';
|
|
113
|
+
if (!whisper.includes(call)) {
|
|
114
|
+
throw new Error('The selected Whisper source does not contain the expected OpenVINO encoder call.');
|
|
115
|
+
}
|
|
116
|
+
await writeFile(whisperPath, whisper.replace(call,
|
|
117
|
+
' if (!whisper_openvino_encode(wstate.ctx_openvino, mel, wstate.embd_enc)) {\n return false;\n }'));
|
|
118
|
+
const encoder = await readFile(encoderPath, 'utf8');
|
|
119
|
+
const compileDiagnostic = 'std::cout << "in openvino encoder compile routine: exception: "';
|
|
120
|
+
const inferenceDiagnostic = 'std::cout << "in openvino encode inference execution routine: exception: "';
|
|
121
|
+
if (!encoder.includes(compileDiagnostic) || !encoder.includes(inferenceDiagnostic)) {
|
|
122
|
+
throw new Error('The selected Whisper source does not contain both expected OpenVINO diagnostics.');
|
|
123
|
+
}
|
|
124
|
+
await writeFile(encoderPath, encoder
|
|
125
|
+
.replace(compileDiagnostic, compileDiagnostic.replace('std::cout', 'std::cerr'))
|
|
126
|
+
.replace(inferenceDiagnostic, inferenceDiagnostic.replace('std::cout', 'std::cerr')));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function copyImportLibraries(directory, destination) {
|
|
130
|
+
for (const member of await readdir(directory, {withFileTypes: true})) {
|
|
131
|
+
const source = path.join(directory, member.name);
|
|
132
|
+
if (member.isDirectory()) {
|
|
133
|
+
await copyImportLibraries(source, destination);
|
|
134
|
+
} else if (member.name === 'whisper.lib' || /^ggml(?:-.*)?\.lib$/.test(member.name)) {
|
|
135
|
+
await cp(source, path.join(destination, member.name));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|