arcane-os 0.62.0 → 0.64.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 +40 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +7 -2
- package/docs/reference/README.md +7 -0
- package/docs/reference/cli.md +5 -1
- package/docs/reference/core-client.md +11 -2
- package/docs/reference/core-native-packaging.md +57 -8
- package/docs/reference/core-repositories.md +149 -0
- package/docs/reference/inventory/package-api.json +163 -9
- package/docs/reference/local-ai.md +51 -7
- package/docs/reference/local-image-flux.md +154 -0
- package/docs/reference/local-image-generation.md +12 -0
- package/docs/reference/local-image-sdxl.md +139 -0
- package/docs/reference/local-whisper.md +219 -0
- package/docs/reference/native-speech.md +163 -0
- package/docs/reference/runtime-modules.md +16 -2
- package/docs/reference/sdk-api.md +335 -6
- package/package.json +5 -1
- package/runtime/arcane/components/speech.html +15 -3
- package/runtime/arcane/components/voice-transcription.html +19 -3
- package/runtime/arcane/modules/AI.js +250 -27
- 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/schemas/arcane-app.schema.json +53 -2
- package/schemas/arcane-package.schema.json +53 -2
- package/src/core/repositories.mjs +210 -0
- package/src/core/services/speech.mjs +361 -0
- package/src/local-ai/config.mjs +9 -3
- package/src/local-ai/dev.mjs +32 -2
- package/src/local-ai/image-library.mjs +9 -0
- package/src/local-ai/image-models/flux.mjs +31 -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 +61 -0
- package/src/local-ai/whisper/index.mjs +515 -0
- package/src/local-ai/whisper/install.mjs +274 -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 +489 -0
- package/src/native/portable-layout.mjs +37 -7
- package/src/native/windows-provider.mjs +39 -15
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {PassThrough} from 'node:stream';
|
|
3
|
+
import {mkdir, mkdtemp, writeFile, rm} from 'node:fs/promises';
|
|
4
|
+
import Is from 'strong-type';
|
|
5
|
+
import {runProcess} from '../../process.mjs';
|
|
6
|
+
import {createArcaneEventSource} from '../../event-manager.mjs';
|
|
7
|
+
|
|
8
|
+
const is = new Is(false);
|
|
9
|
+
|
|
10
|
+
/** Retain one explicitly selected native model and transcribe complete recordings. */
|
|
11
|
+
export function createWhisperRuntime({runtime, prepare, modelId, temporaryDirectory, onEvent} = {}) {
|
|
12
|
+
if (!runtime && !is.function(prepare)) {
|
|
13
|
+
throw new TypeError('Whisper requires a prepared runtime or a prepare function.');
|
|
14
|
+
}
|
|
15
|
+
if (!is.string(temporaryDirectory)) {
|
|
16
|
+
throw new TypeError('Whisper requires an application-owned temporaryDirectory.');
|
|
17
|
+
}
|
|
18
|
+
const events = createArcaneEventSource({}, {source: 'whisper', eventTypes: ['whisper.state']});
|
|
19
|
+
let selected = modelId ?? runtime?.modelId ?? null;
|
|
20
|
+
let state = 'unloaded';
|
|
21
|
+
let error = null;
|
|
22
|
+
let helper = null;
|
|
23
|
+
let loading = null;
|
|
24
|
+
let loadController = null;
|
|
25
|
+
let active = null;
|
|
26
|
+
let requestId = null;
|
|
27
|
+
let unloading = null;
|
|
28
|
+
let closing = null;
|
|
29
|
+
let disposed = false;
|
|
30
|
+
let sequence = 0;
|
|
31
|
+
|
|
32
|
+
function current() {
|
|
33
|
+
return {
|
|
34
|
+
providerId: 'whisper.cpp', modelId: selected, state,
|
|
35
|
+
loaded: state === 'ready', busy: active !== null,
|
|
36
|
+
requestId,
|
|
37
|
+
requestedBackend: helper?.variant.backend ?? runtime?.backend ?? 'auto',
|
|
38
|
+
observedBackend: helper?.metadata?.observedBackend ?? null,
|
|
39
|
+
backendEvidence: helper?.metadata?.backendEvidence ?? null,
|
|
40
|
+
error: error ? {code: error.code, message: error.message} : null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function notify(listener, snapshot) {
|
|
45
|
+
const result = listener(snapshot);
|
|
46
|
+
if (result && is.function(result.then)) {
|
|
47
|
+
Promise.resolve(result).catch(function listenerFailed(failure) {
|
|
48
|
+
console.error('Whisper state listener failed.', failure);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function publish() { events.dispatch('whisper.state', current()); }
|
|
54
|
+
|
|
55
|
+
function subscribe(listener, {replay = true, signal} = {}) {
|
|
56
|
+
if (!is.function(listener)) throw new TypeError('A Whisper listener must be a function.');
|
|
57
|
+
if (disposed) {
|
|
58
|
+
if (replay && !signal?.aborted) notify(listener, current());
|
|
59
|
+
return function disposedSubscription() {};
|
|
60
|
+
}
|
|
61
|
+
const unsubscribe = events.on('whisper.state', function stateChanged(event) {
|
|
62
|
+
notify(listener, event.detail);
|
|
63
|
+
}, {signal});
|
|
64
|
+
try {
|
|
65
|
+
if (replay && !signal?.aborted) notify(listener, current());
|
|
66
|
+
} catch (failure) {
|
|
67
|
+
unsubscribe();
|
|
68
|
+
throw failure;
|
|
69
|
+
}
|
|
70
|
+
return unsubscribe;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function startHelper(variant, model) {
|
|
74
|
+
const input = new PassThrough();
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const readiness = deferred();
|
|
77
|
+
const session = {input, controller, variant, model, readiness, metadata: null,
|
|
78
|
+
pending: null, stopping: false, exited: false, fragment: '', completion: null,
|
|
79
|
+
observerFailure: null, failed: false};
|
|
80
|
+
const library = variant.libraryDirectory ?? variant.root;
|
|
81
|
+
const args = ['--model-base64', encode(model.path), '--runtime-base64', encode(library),
|
|
82
|
+
'--backend', variant.backend === 'cpu' ? 'cpu' : 'gpu'];
|
|
83
|
+
if (runtime.threads !== undefined) args.push('--threads', String(runtime.threads));
|
|
84
|
+
|
|
85
|
+
async function receive({stream, chunk}) {
|
|
86
|
+
if (stream !== 'stdout') return;
|
|
87
|
+
session.fragment += chunk;
|
|
88
|
+
let boundary = session.fragment.indexOf('\n');
|
|
89
|
+
while (boundary !== -1) {
|
|
90
|
+
const line = session.fragment.substring(0, boundary);
|
|
91
|
+
session.fragment = session.fragment.substring(boundary + 1);
|
|
92
|
+
if (line !== '') {
|
|
93
|
+
const record = JSON.parse(line);
|
|
94
|
+
const pending = session.pending;
|
|
95
|
+
if (record.type === 'ready') {
|
|
96
|
+
session.metadata = record;
|
|
97
|
+
readiness.resolve(record);
|
|
98
|
+
} else if (record.type === 'error' && !record.requestId) {
|
|
99
|
+
readiness.reject(whisperError('WHISPER_MODEL_FAILED', record.message));
|
|
100
|
+
} else if (pending && record.requestId === pending.id) {
|
|
101
|
+
if (record.type === 'complete') {
|
|
102
|
+
pending.response.resolve({text: record.text, language: record.language,
|
|
103
|
+
duration: record.duration, segments: record.segments});
|
|
104
|
+
} else if (record.type === 'cancelled') {
|
|
105
|
+
if (pending.signal.aborted) pending.response.reject(pending.signal.reason);
|
|
106
|
+
// Unexpected EOF can cancel native work while a failed
|
|
107
|
+
// observer is stopping the process. Preserve that actual
|
|
108
|
+
// failure through completion instead of inventing an abort.
|
|
109
|
+
else if (!session.observerFailure) {
|
|
110
|
+
pending.response.reject(whisperError('WHISPER_NATIVE_CANCELLED', 'Native transcription stopped without a caller cancellation.'));
|
|
111
|
+
}
|
|
112
|
+
} else if (record.type === 'error') {
|
|
113
|
+
pending.response.reject(whisperError(record.code ?? 'WHISPER_TRANSCRIPTION_FAILED', record.message));
|
|
114
|
+
} else if (!pending.signal.aborted && pending.onProgress) {
|
|
115
|
+
await pending.onProgress({...record, backend: variant.backend, attempt: pending.attempt});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
boundary = session.fragment.indexOf('\n');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
session.completion = runProcess(variant.executable ?? runtime.helperExecutable, args, {
|
|
124
|
+
env: runtimeEnvironment(library), signal: controller.signal,
|
|
125
|
+
onEvent: onEvent ? async function nativeDiagnostic(event) {
|
|
126
|
+
try { await onEvent(event); }
|
|
127
|
+
catch (failure) { session.observerFailure = failure; throw failure; }
|
|
128
|
+
} : undefined,
|
|
129
|
+
input: {
|
|
130
|
+
[Symbol.asyncIterator]: function helperInput() {
|
|
131
|
+
const iterator = input[Symbol.asyncIterator]();
|
|
132
|
+
return {
|
|
133
|
+
next: function nextInput() { return iterator.next(); },
|
|
134
|
+
return: function closeInput() { input.end(); return iterator.return(); }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
async onOutput(output) {
|
|
139
|
+
try { await receive(output); }
|
|
140
|
+
catch (failure) { session.observerFailure = failure; throw failure; }
|
|
141
|
+
},
|
|
142
|
+
captureOutput: {stdout: false, stderr: true},
|
|
143
|
+
emitOutputEvents: {stdout: false, stderr: true}
|
|
144
|
+
}).then(function helperExited(result) {
|
|
145
|
+
if (session.fragment !== '') {
|
|
146
|
+
throw whisperError('WHISPER_PROTOCOL_INCOMPLETE', 'Whisper exited during a response.', {fragment: session.fragment, result});
|
|
147
|
+
}
|
|
148
|
+
if (!session.stopping) {
|
|
149
|
+
throw whisperError('WHISPER_HELPER_EXITED', 'The native Whisper process exited.', result);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}).catch(function helperFailed(failure) {
|
|
153
|
+
session.failed = true;
|
|
154
|
+
readiness.reject(failure);
|
|
155
|
+
session.pending?.response.reject(failure);
|
|
156
|
+
if (helper === session && state === 'ready' && !session.stopping) {
|
|
157
|
+
const recovering = cpuRecoveryVariant(session, failure);
|
|
158
|
+
error = recovering ? null : failure;
|
|
159
|
+
state = recovering ? 'recovering' : 'error';
|
|
160
|
+
publish();
|
|
161
|
+
}
|
|
162
|
+
throw failure;
|
|
163
|
+
}).finally(function processDrained() {
|
|
164
|
+
session.exited = true;
|
|
165
|
+
input.end();
|
|
166
|
+
});
|
|
167
|
+
session.completion.catch(function observeHelperExit() {});
|
|
168
|
+
return session;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function stopHelper(session, {terminate = false} = {}) {
|
|
172
|
+
if (!session) return;
|
|
173
|
+
session.stopping = true;
|
|
174
|
+
if (!session.exited) {
|
|
175
|
+
if (terminate || !session.metadata) session.controller.abort();
|
|
176
|
+
else {
|
|
177
|
+
try { await writeCommand(session.input, 'shutdown\n'); }
|
|
178
|
+
catch (failure) { session.controller.abort(failure); }
|
|
179
|
+
session.input.end();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try { await session.completion; }
|
|
183
|
+
catch (failure) {
|
|
184
|
+
if (!session.controller.signal.aborted) throw failure;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function load({modelId: requested = selected, signal} = {}) {
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
if (closing || disposed || unloading) return Promise.reject(whisperError('WHISPER_CLOSING', 'Whisper is closing.'));
|
|
191
|
+
if (loading) {
|
|
192
|
+
if (requested && selected && requested !== selected) {
|
|
193
|
+
return Promise.reject(whisperError('WHISPER_MODEL_BUSY', 'The selected model is already loading.'));
|
|
194
|
+
}
|
|
195
|
+
return waitFor(loading, signal);
|
|
196
|
+
}
|
|
197
|
+
if (state === 'ready') {
|
|
198
|
+
if (!requested || requested === selected) return Promise.resolve(current());
|
|
199
|
+
return Promise.reject(whisperError('WHISPER_MODEL_LOADED', 'Unload the current model before selecting another model.'));
|
|
200
|
+
}
|
|
201
|
+
if (active) return Promise.reject(whisperError('WHISPER_BUSY', 'The selected model belongs to an active transcription.'));
|
|
202
|
+
selected = requested;
|
|
203
|
+
requestId = null;
|
|
204
|
+
error = null;
|
|
205
|
+
state = 'loading';
|
|
206
|
+
loadController = new AbortController();
|
|
207
|
+
const loadSignal = signal ? AbortSignal.any([signal, loadController.signal]) : loadController.signal;
|
|
208
|
+
loading = Promise.resolve().then(async function loadSelectedModel() {
|
|
209
|
+
loadSignal.throwIfAborted();
|
|
210
|
+
if (!runtime) runtime = await prepare({signal: loadSignal, onEvent});
|
|
211
|
+
loadSignal.throwIfAborted();
|
|
212
|
+
selected ??= runtime.modelId ?? (runtime.models?.length === 1 ? runtime.models[0].id : null);
|
|
213
|
+
if (!selected) {
|
|
214
|
+
state = 'unloaded';
|
|
215
|
+
publish();
|
|
216
|
+
return current();
|
|
217
|
+
}
|
|
218
|
+
const model = runtime.models?.find(function selectedModel(record) { return record.id === selected; });
|
|
219
|
+
if (!model) throw whisperError('WHISPER_MODEL_UNAVAILABLE', `The selected model ${selected} is not prepared.`);
|
|
220
|
+
const variants = selectedVariants(runtime);
|
|
221
|
+
const failures = [];
|
|
222
|
+
for (const variant of variants) {
|
|
223
|
+
loadSignal.throwIfAborted();
|
|
224
|
+
const session = startHelper(variant, model);
|
|
225
|
+
helper = session;
|
|
226
|
+
function cancelLoading() { session.controller.abort(loadSignal.reason); }
|
|
227
|
+
loadSignal.addEventListener('abort', cancelLoading, {once: true});
|
|
228
|
+
try {
|
|
229
|
+
if (loadSignal.aborted) cancelLoading();
|
|
230
|
+
await session.readiness.promise;
|
|
231
|
+
loadSignal.throwIfAborted();
|
|
232
|
+
state = 'ready';
|
|
233
|
+
publish();
|
|
234
|
+
return current();
|
|
235
|
+
} catch (failure) {
|
|
236
|
+
failures.push(failure);
|
|
237
|
+
try { await stopHelper(session, {terminate: true}); }
|
|
238
|
+
catch (exitFailure) { if (exitFailure !== failure) failures.push(exitFailure); }
|
|
239
|
+
helper = null;
|
|
240
|
+
loadSignal.throwIfAborted();
|
|
241
|
+
if (session.observerFailure || failure.code === 'WHISPER_PROTOCOL_INCOMPLETE') throw failure;
|
|
242
|
+
if (variant !== variants[variants.length - 1] && onEvent) {
|
|
243
|
+
await onEvent({type: 'whisper.backend.failed', message: failure.message,
|
|
244
|
+
data: {backend: variant.backend, error: failure}});
|
|
245
|
+
}
|
|
246
|
+
} finally {
|
|
247
|
+
loadSignal.removeEventListener('abort', cancelLoading);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw new AggregateError(failures, 'The selected Whisper model could not load in the prepared runtimes.');
|
|
251
|
+
}).catch(function modelLoadFailed(failure) {
|
|
252
|
+
error = loadSignal.aborted ? null : failure;
|
|
253
|
+
state = unloading || closing ? 'unloading' : loadSignal.aborted ? 'unloaded' : 'error';
|
|
254
|
+
publish();
|
|
255
|
+
throw failure;
|
|
256
|
+
}).finally(function modelLoadSettled() { loading = null; loadController = null; });
|
|
257
|
+
loading.catch(function observeModelLoad() {});
|
|
258
|
+
publish();
|
|
259
|
+
return loading;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function cpuRecoveryVariant(session, failure) {
|
|
263
|
+
if (!active?.canRecover || active.session !== session || active.signal.aborted || unloading || closing
|
|
264
|
+
|| session.variant.backend === 'cpu' || session.controller.signal.aborted
|
|
265
|
+
|| session.observerFailure || failure.code === 'WHISPER_PROTOCOL_INCOMPLETE'
|
|
266
|
+
|| (runtime.backend && runtime.backend !== 'auto')) return null;
|
|
267
|
+
if (failure.code !== 'WHISPER_INFERENCE_FAILED' && !session.failed) return null;
|
|
268
|
+
return runtime.variants.find(function preparedCPU(variant) { return variant.backend === 'cpu'; }) ?? null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function transcribe(request, {signal, onProgress, requestId: suppliedRequestId} = {}) {
|
|
272
|
+
signal?.throwIfAborted();
|
|
273
|
+
if (closing || disposed || unloading) return Promise.reject(whisperError('WHISPER_CLOSING', 'Whisper is closing.'));
|
|
274
|
+
if (active) return Promise.reject(whisperError('WHISPER_BUSY', 'The selected Whisper model is transcribing another recording.'));
|
|
275
|
+
if (!is.string(request?.audioBase64)) throw new TypeError('Transcription requires the complete audioBase64 recording.');
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
278
|
+
const operation = {controller, signal: operationSignal, session: null, canRecover: false, task: null};
|
|
279
|
+
requestId = suppliedRequestId ?? null;
|
|
280
|
+
active = operation;
|
|
281
|
+
operation.task = Promise.resolve().then(async function transcribeRecording() {
|
|
282
|
+
operationSignal.throwIfAborted();
|
|
283
|
+
await onProgress?.({phase: 'accepted'});
|
|
284
|
+
if (loading) await waitFor(loading, operationSignal);
|
|
285
|
+
operationSignal.throwIfAborted();
|
|
286
|
+
if (state !== 'ready' || !helper) throw whisperError('WHISPER_MODEL_NOT_READY', 'Load the selected transcription model before transcribing.');
|
|
287
|
+
if (request.model && request.model !== selected) {
|
|
288
|
+
throw whisperError('WHISPER_MODEL_MISMATCH', `The loaded model is ${selected}; the request selected ${request.model}.`);
|
|
289
|
+
}
|
|
290
|
+
const initialSession = helper;
|
|
291
|
+
operation.session = initialSession;
|
|
292
|
+
operation.canRecover = true;
|
|
293
|
+
await mkdir(temporaryDirectory, {recursive: true});
|
|
294
|
+
const directory = await mkdtemp(path.join(temporaryDirectory, 'recording-'));
|
|
295
|
+
try {
|
|
296
|
+
const original = path.join(directory, 'recording');
|
|
297
|
+
const pcm = path.join(directory, 'audio.f32le');
|
|
298
|
+
await writeFile(original, Buffer.from(request.audioBase64, 'base64'), {signal: operationSignal});
|
|
299
|
+
await onProgress?.({phase: 'decoding'});
|
|
300
|
+
await runProcess(runtime.decoderExecutable, ['-nostdin', '-v', 'error', '-i', original,
|
|
301
|
+
'-vn', '-ac', '1', '-ar', '16000', '-c:a', 'pcm_f32le', '-f', 'f32le', pcm],
|
|
302
|
+
{signal: operationSignal, onEvent});
|
|
303
|
+
operationSignal.throwIfAborted();
|
|
304
|
+
if (initialSession !== helper) throw whisperError('WHISPER_MODEL_CHANGED', 'The selected transcription model changed.');
|
|
305
|
+
try {
|
|
306
|
+
const result = await infer(initialSession, pcm, request, operationSignal, onProgress, 1);
|
|
307
|
+
operation.canRecover = false;
|
|
308
|
+
return result;
|
|
309
|
+
} catch (failure) {
|
|
310
|
+
operationSignal.throwIfAborted();
|
|
311
|
+
const cpu = cpuRecoveryVariant(initialSession, failure);
|
|
312
|
+
operation.canRecover = false;
|
|
313
|
+
if (!cpu) throw failure;
|
|
314
|
+
// A failed GPU context is fully drained before CPU owns the
|
|
315
|
+
// same complete decoded recording and exact selected model.
|
|
316
|
+
state = 'recovering';
|
|
317
|
+
error = null;
|
|
318
|
+
publish();
|
|
319
|
+
try { await stopHelper(initialSession); }
|
|
320
|
+
catch (exitFailure) { if (exitFailure !== failure) failure = new AggregateError([failure, exitFailure], failure.message); }
|
|
321
|
+
operationSignal.throwIfAborted();
|
|
322
|
+
await onEvent?.({type: 'whisper.backend.failed', message: failure.message,
|
|
323
|
+
data: {backend: initialSession.variant.backend, error: failure}});
|
|
324
|
+
operationSignal.throwIfAborted();
|
|
325
|
+
const replacement = startHelper(cpu, initialSession.model);
|
|
326
|
+
helper = replacement;
|
|
327
|
+
operation.session = replacement;
|
|
328
|
+
publish();
|
|
329
|
+
function cancelFallbackLoad() { replacement.controller.abort(operationSignal.reason); }
|
|
330
|
+
operationSignal.addEventListener('abort', cancelFallbackLoad, {once: true});
|
|
331
|
+
try {
|
|
332
|
+
if (operationSignal.aborted) cancelFallbackLoad();
|
|
333
|
+
await replacement.readiness.promise;
|
|
334
|
+
operationSignal.throwIfAborted();
|
|
335
|
+
state = 'ready';
|
|
336
|
+
publish();
|
|
337
|
+
} catch (loadFailure) {
|
|
338
|
+
await Promise.allSettled([stopHelper(replacement, {terminate: true})]);
|
|
339
|
+
if (operationSignal.aborted) {
|
|
340
|
+
helper = null;
|
|
341
|
+
state = unloading || closing ? 'unloading' : 'unloaded';
|
|
342
|
+
operationSignal.throwIfAborted();
|
|
343
|
+
}
|
|
344
|
+
state = 'error';
|
|
345
|
+
error = loadFailure;
|
|
346
|
+
throw new AggregateError([failure, loadFailure], 'GPU inference and CPU model loading failed.');
|
|
347
|
+
} finally {
|
|
348
|
+
operationSignal.removeEventListener('abort', cancelFallbackLoad);
|
|
349
|
+
}
|
|
350
|
+
try { return await infer(replacement, pcm, request, operationSignal, onProgress, 2); }
|
|
351
|
+
catch (cpuFailure) {
|
|
352
|
+
operationSignal.throwIfAborted();
|
|
353
|
+
throw new AggregateError([failure, cpuFailure], 'GPU and CPU transcription failed.');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} finally {
|
|
357
|
+
operation.canRecover = false;
|
|
358
|
+
// Decoder completion and the helper's joined terminal record both
|
|
359
|
+
// precede removal of this operation's complete recording files.
|
|
360
|
+
await rm(directory, {recursive: true, force: true});
|
|
361
|
+
}
|
|
362
|
+
}).catch(function transcriptionFailed(failure) {
|
|
363
|
+
if (state === 'recovering' && (!helper || helper.stopping || helper.exited)) {
|
|
364
|
+
helper = null;
|
|
365
|
+
state = unloading || closing ? 'unloading' : operationSignal.aborted ? 'unloaded' : 'error';
|
|
366
|
+
error = operationSignal.aborted ? null : failure;
|
|
367
|
+
}
|
|
368
|
+
operationSignal.throwIfAborted();
|
|
369
|
+
throw failure;
|
|
370
|
+
}).finally(function transcriptionSettled() {
|
|
371
|
+
active = null;
|
|
372
|
+
if (state !== 'error') requestId = null;
|
|
373
|
+
publish();
|
|
374
|
+
});
|
|
375
|
+
operation.task.catch(function observeTranscription() {});
|
|
376
|
+
publish();
|
|
377
|
+
return operation.task;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function infer(session, pcm, request, signal, onProgress, attempt) {
|
|
381
|
+
signal.throwIfAborted();
|
|
382
|
+
if (session.failed) await session.completion;
|
|
383
|
+
const id = String(++sequence);
|
|
384
|
+
const response = deferred();
|
|
385
|
+
session.pending = {id, response, signal, onProgress, attempt};
|
|
386
|
+
function cancelInference() {
|
|
387
|
+
writeCommand(session.input, `cancel\t${id}\n`).catch(function cancellationWriteFailed(failure) {
|
|
388
|
+
session.controller.abort(failure);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
// Keep each request ahead of its matching cancel on this input pipe.
|
|
393
|
+
await writeCommand(session.input,
|
|
394
|
+
`transcribe\t${id}\t${encode(pcm)}\t${encode(request.language ?? 'auto')}\t${request.translate === true ? 1 : 0}\n`);
|
|
395
|
+
signal.addEventListener('abort', cancelInference, {once: true});
|
|
396
|
+
if (signal.aborted) cancelInference();
|
|
397
|
+
const result = await response.promise;
|
|
398
|
+
signal.throwIfAborted();
|
|
399
|
+
return result;
|
|
400
|
+
} catch (failure) {
|
|
401
|
+
if (!response.settled) {
|
|
402
|
+
session.controller.abort(failure);
|
|
403
|
+
await Promise.allSettled([session.completion]);
|
|
404
|
+
}
|
|
405
|
+
throw failure;
|
|
406
|
+
} finally {
|
|
407
|
+
signal.removeEventListener('abort', cancelInference);
|
|
408
|
+
session.pending = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function unload() {
|
|
413
|
+
if (unloading) return unloading;
|
|
414
|
+
if (disposed) return Promise.resolve(current());
|
|
415
|
+
state = 'unloading';
|
|
416
|
+
requestId = null;
|
|
417
|
+
unloading = Promise.resolve().then(async function unloadModel() {
|
|
418
|
+
await Promise.allSettled([loading, active?.task].filter(Boolean));
|
|
419
|
+
const session = helper;
|
|
420
|
+
try { await stopHelper(session); }
|
|
421
|
+
finally { helper = null; }
|
|
422
|
+
state = 'unloaded';
|
|
423
|
+
error = null;
|
|
424
|
+
return current();
|
|
425
|
+
}).catch(function unloadFailed(failure) {
|
|
426
|
+
state = 'error';
|
|
427
|
+
error = failure;
|
|
428
|
+
throw failure;
|
|
429
|
+
}).finally(function unloadSettled() { unloading = null; publish(); });
|
|
430
|
+
loadController?.abort();
|
|
431
|
+
active?.controller.abort();
|
|
432
|
+
publish();
|
|
433
|
+
return unloading;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function close() {
|
|
437
|
+
if (closing) return closing;
|
|
438
|
+
requestId = null;
|
|
439
|
+
closing = Promise.resolve().then(async function closeWhisper() {
|
|
440
|
+
try { await unload(); }
|
|
441
|
+
finally {
|
|
442
|
+
disposed = true;
|
|
443
|
+
state = 'disposed';
|
|
444
|
+
publish();
|
|
445
|
+
events.dispose();
|
|
446
|
+
}
|
|
447
|
+
return current();
|
|
448
|
+
});
|
|
449
|
+
return closing;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {current, subscribe, load, transcribe, unload, close};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function selectedVariants(runtime) {
|
|
456
|
+
const variants = runtime.variants ?? [];
|
|
457
|
+
if (runtime.backend && runtime.backend !== 'auto') {
|
|
458
|
+
const selected = variants.filter(function selectedBackend(variant) { return variant.backend === runtime.backend; });
|
|
459
|
+
if (selected.length) return selected;
|
|
460
|
+
} else if (variants.length) {
|
|
461
|
+
return [...variants.filter(function accelerated(variant) { return variant.backend !== 'cpu'; }),
|
|
462
|
+
...variants.filter(function cpu(variant) { return variant.backend === 'cpu'; })];
|
|
463
|
+
}
|
|
464
|
+
throw whisperError('WHISPER_RUNTIME_UNAVAILABLE', 'The selected Whisper runtime has no prepared backend.');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function runtimeEnvironment(directory) {
|
|
468
|
+
const env = {...process.env};
|
|
469
|
+
const key = process.platform === 'win32'
|
|
470
|
+
? Object.keys(env).find(function pathKey(name) { return name.toLowerCase() === 'path'; }) ?? 'PATH'
|
|
471
|
+
: process.platform === 'darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH';
|
|
472
|
+
env[key] = [directory, env[key]].filter(Boolean).join(path.delimiter);
|
|
473
|
+
return env;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function encode(value) { return Buffer.from(value, 'utf8').toString('base64'); }
|
|
477
|
+
|
|
478
|
+
function whisperError(code, message, data) {
|
|
479
|
+
const error = new Error(message);
|
|
480
|
+
error.code = code;
|
|
481
|
+
if (data !== undefined) error.data = data;
|
|
482
|
+
return error;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function deferred() {
|
|
486
|
+
const response = {promise: null, resolve: null, reject: null, settled: false};
|
|
487
|
+
response.promise = new Promise(function pending(resolve, reject) {
|
|
488
|
+
response.resolve = function completed(value) { response.settled = true; resolve(value); };
|
|
489
|
+
response.reject = function failed(error) { response.settled = true; reject(error); };
|
|
490
|
+
});
|
|
491
|
+
response.promise.catch(function observePending() {});
|
|
492
|
+
return response;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function writeCommand(input, command) {
|
|
496
|
+
return new Promise(function commandWritten(resolve, reject) {
|
|
497
|
+
input.write(command, function written(error) { if (error) reject(error); else resolve(); });
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function waitFor(task, signal) {
|
|
502
|
+
if (!signal) return task;
|
|
503
|
+
signal.throwIfAborted();
|
|
504
|
+
return new Promise(function waitForOwnedTask(resolve, reject) {
|
|
505
|
+
function cancelled() { signal.removeEventListener('abort', cancelled); reject(signal.reason); }
|
|
506
|
+
signal.addEventListener('abort', cancelled, {once: true});
|
|
507
|
+
task.then(function completed(value) {
|
|
508
|
+
signal.removeEventListener('abort', cancelled);
|
|
509
|
+
if (signal.aborted) reject(signal.reason);
|
|
510
|
+
else resolve(value);
|
|
511
|
+
}, function failed(error) { signal.removeEventListener('abort', cancelled); reject(error); });
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export default createWhisperRuntime;
|