arcane-os 0.93.0 → 0.94.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.
@@ -0,0 +1,645 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import Is from 'strong-type';
3
+ import {createArcaneEventSource} from '../../event-manager.mjs';
4
+ import {serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
5
+ import {createKokoroHelper} from './helper.mjs';
6
+ import {
7
+ KOKORO_MODEL, KOKORO_REVISION, KOKORO_SAMPLE_RATE, KOKORO_VOICES,
8
+ loadKokoroFrontend, voiceLanguage, encodeKokoroWav
9
+ } from './frontend.mjs';
10
+
11
+ export {KOKORO_MODEL, KOKORO_REVISION, KOKORO_SAMPLE_RATE, KOKORO_VOICES};
12
+
13
+ const is = new Is(false);
14
+
15
+ /** Retain one Kokoro activation using the host's existing ONNX and model-resource owners. */
16
+ export function createNativeKokoroRuntime({
17
+ onnx, modelAssets, runtime, prepare, modelId = 'kokoro', model = KOKORO_MODEL,
18
+ revision = KOKORO_REVISION, dtype = 'fp32', paths, assetProjectionId, resourcePaths,
19
+ sessionOptions, executionPreference = 'gpu', executionTarget, signal, onEvent
20
+ } = {}) {
21
+ if (!onnx && !is.function(prepare)) throw new TypeError('Kokoro requires the existing native ONNX owner or its preparation callback.');
22
+ if (model !== KOKORO_MODEL || revision !== KOKORO_REVISION || dtype !== 'fp32') {
23
+ throw new TypeError('This native frontend implements the selected Kokoro-82M v1.0 FP32 model and revision.');
24
+ }
25
+ const events = createArcaneEventSource({}, {source: 'kokoro', eventTypes: ['kokoro.state']});
26
+ const loads = new Set();
27
+ let selection = copySelection({assetProjectionId, resourcePaths, executionTarget});
28
+ let activation = null;
29
+ let pending = null;
30
+ let releaseTask = null;
31
+ let closing = null;
32
+ let disposed = false;
33
+ let state = 'unloaded';
34
+ let error = null;
35
+ let progress = null;
36
+ let execution = null;
37
+
38
+ function current() {
39
+ return {
40
+ providerId: 'kokoro-onnx', modelId, model, revision, dtype, state,
41
+ loaded: Boolean(activation?.ready && !activation.controller.signal.aborted),
42
+ busy: Boolean(pending || activation?.active || activation?.restarting),
43
+ requestId: activation?.active?.requestId ?? null,
44
+ queuedRequests: activation?.queue.length ?? 0,
45
+ execution, pendingActivation: pending ? copySelection(pending.selection) : null,
46
+ progress, error: error ? serializeCoreError(error) : null,
47
+ defaultVoice: 'af_heart', voices: [...KOKORO_VOICES]
48
+ };
49
+ }
50
+
51
+ function publish() { events.dispatch('kokoro.state', current()); }
52
+
53
+ function notify(listener, snapshot) {
54
+ const result = listener(snapshot);
55
+ if (result && is.function(result.then)) Promise.resolve(result).catch(reportObserverFailure);
56
+ }
57
+
58
+ function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
59
+ if (disposed) {
60
+ if (replay && !subscriptionSignal?.aborted) notify(listener, current());
61
+ return function disposedSubscription() {};
62
+ }
63
+ const unsubscribe = events.on('kokoro.state', function changed(event) { notify(listener, event.detail); }, {signal: subscriptionSignal});
64
+ try { if (replay && !subscriptionSignal?.aborted) notify(listener, current()); }
65
+ catch (failure) { unsubscribe(); throw failure; }
66
+ return unsubscribe;
67
+ }
68
+
69
+ function assertOpen(operationSignal) {
70
+ signal?.throwIfAborted();
71
+ operationSignal?.throwIfAborted();
72
+ if (closing || disposed) throw failure('KOKORO_CLOSED', 'The Kokoro engine is closing.');
73
+ }
74
+
75
+ function stop(entry, reason, terminalState = 'unloaded') {
76
+ if (entry.release) return entry.release;
77
+ if (activation === entry) {
78
+ activation = null;
79
+ state = 'unloading';
80
+ progress = null;
81
+ error = terminalState === 'error' ? reason : null;
82
+ }
83
+ // Establish ownership before abort listeners and state subscribers can reenter unload/load.
84
+ entry.release = Promise.resolve().then(async function releaseActivation() {
85
+ await Promise.allSettled([entry.loading, entry.restarting, entry.active?.task].filter(Boolean));
86
+ const failures = [];
87
+ try { await retireNative(entry, reason); }
88
+ catch (releaseFailure) { failures.push(releaseFailure); }
89
+ const session = onnx?.current().sessions.find(function owned(record) { return record.id === entry.id; });
90
+ if ((!session || session.exited) && (!entry.helper || entry.helper.exited)) {
91
+ try { await entry.retained?.release(); }
92
+ catch (releaseFailure) { failures.push(releaseFailure); }
93
+ }
94
+ const cleanupError = failures.length ? joinedFailure(failures, 'Releasing Kokoro resources failed.') : null;
95
+ if (!activation) {
96
+ state = cleanupError ? 'error' : terminalState;
97
+ execution = null;
98
+ if (cleanupError) error = terminalState === 'error'
99
+ ? joinedFailure([reason, cleanupError], 'Kokoro activation and cleanup failed.') : cleanupError;
100
+ try { publish(); }
101
+ catch (observerFailure) { failures.push(observerFailure); }
102
+ }
103
+ if (failures.length) throw joinedFailure(failures, 'Releasing Kokoro resources failed.');
104
+ return current();
105
+ });
106
+ releaseTask = entry.release;
107
+ entry.controller.abort(reason);
108
+ retireNative(entry, reason).catch(reportCleanupFailure);
109
+ entry.release.catch(reportCleanupFailure);
110
+ publish();
111
+ return entry.release;
112
+ }
113
+
114
+ function retireNative(entry, reason) {
115
+ if (entry.nativeRelease) return entry.nativeRelease;
116
+ entry.ready = false;
117
+ entry.nativeRelease = Promise.resolve().then(async function releaseNativeGeneration() {
118
+ entry.unsubscribe?.();
119
+ entry.unsubscribe = null;
120
+ const session = onnx?.current().sessions.find(function owned(record) { return record.id === entry.id; });
121
+ const released = await Promise.allSettled([
122
+ session ? onnx.unload({id: entry.id}) : Promise.resolve(),
123
+ entry.helper?.close(reason)
124
+ ]);
125
+ const failures = released.filter(function rejected(result) { return result.status === 'rejected'; })
126
+ .map(function reasonOf(result) { return result.reason; });
127
+ if (failures.length) throw joinedFailure(failures, 'Releasing the Kokoro native generation failed.');
128
+ });
129
+ return entry.nativeRelease;
130
+ }
131
+
132
+ async function startNative(entry, continuation = false) {
133
+ entry.controller.signal.throwIfAborted();
134
+ const previousExecution = continuation ? entry.session?.execution : null;
135
+ const providers = previousExecution?.configuredTarget?.executionProviders;
136
+ let target = entry.selection.executionTarget;
137
+ let options = sessionOptions;
138
+ if (continuation) {
139
+ entry.id = randomUUID();
140
+ if (previousExecution?.resolvedDevice?.deviceId) {
141
+ target = {deviceId: previousExecution.resolvedDevice.deviceId};
142
+ } else if (providers) {
143
+ // Preserve the accepted provider configuration without another automatic provider search.
144
+ target = entry.selection.executionTarget ?? undefined;
145
+ }
146
+ if (providers) {
147
+ options = {...sessionOptions, executionProviders: providers};
148
+ const primary = providers[0];
149
+ if ((is.string(primary) ? primary : primary?.name) === 'dml') {
150
+ options.enableMemPattern ??= false;
151
+ options.executionMode ??= 'sequential';
152
+ }
153
+ }
154
+ }
155
+ entry.nativeRelease = null;
156
+ const helper = createKokoroHelper(runtime, {onEvent});
157
+ entry.helper = helper;
158
+ helper.completion.catch(function unexpectedHelperExit(cause) {
159
+ if (entry.helper === helper && !entry.nativeRelease && !entry.controller.signal.aborted && activation === entry) {
160
+ stop(entry, cause, 'error').catch(reportCleanupFailure);
161
+ }
162
+ });
163
+ entry.unsubscribe = onnx.subscribe(function observeSession(snapshot) {
164
+ const session = snapshot.sessions.find(function owned(record) { return record.id === entry.id; });
165
+ if (entry.nativeRelease || entry.controller.signal.aborted || !session) return;
166
+ if (session.exited || session.stopping || (!session.loaded && (entry.ready || session.state === 'error' || session.state === 'unloaded'))) {
167
+ stop(entry, failure('KOKORO_SESSION_UNAVAILABLE', 'The selected Kokoro ONNX session stopped.', session.error), 'error').catch(reportCleanupFailure);
168
+ }
169
+ });
170
+ const tasks = [
171
+ onnx.load({id: entry.id, model: entry.files.model, sessionOptions: options, executionPreference,
172
+ executionTarget: target, signal: entry.controller.signal}),
173
+ helper.ready
174
+ ];
175
+ try {
176
+ const [session] = await Promise.all(tasks);
177
+ entry.controller.signal.throwIfAborted();
178
+ return session;
179
+ } catch (cause) {
180
+ const results = await Promise.allSettled([...tasks, retireNative(entry, cause)]);
181
+ const failures = [operationFailure(cause, entry.controller.signal), ...rejectedFailures(results, entry.controller.signal)];
182
+ throw joinedFailure(failures, 'Loading Kokoro native resources and cleanup failed.');
183
+ }
184
+ }
185
+
186
+ async function requireNativeReady(entry) {
187
+ entry.controller.signal.throwIfAborted();
188
+ if (entry.helper.exited) {
189
+ // Preserve the helper's original completion failure, including captured process diagnostics.
190
+ await entry.helper.completion;
191
+ throw failure('KOKORO_HELPER_UNAVAILABLE', 'The selected Kokoro helper stopped before activation completed.');
192
+ }
193
+ const session = onnx.current().sessions.find(function owned(record) { return record.id === entry.id; });
194
+ if (!session?.loaded || session.stopping || session.exited) {
195
+ throw failure('KOKORO_SESSION_UNAVAILABLE', 'The selected Kokoro ONNX session stopped before activation completed.', session?.error);
196
+ }
197
+ }
198
+
199
+ function resume(entry) {
200
+ if (entry.restarting) return entry.restarting;
201
+ entry.restarting = Promise.resolve().then(async function resumeNativeGeneration() {
202
+ await entry.nativeRelease;
203
+ entry.controller.signal.throwIfAborted();
204
+ assertOpen();
205
+ if (activation !== entry) throw cancellation('The Kokoro activation changed before its next request.');
206
+ const session = await startNative(entry, true);
207
+ await requireNativeReady(entry);
208
+ entry.controller.signal.throwIfAborted();
209
+ entry.session = session;
210
+ execution = session.execution ?? null;
211
+ entry.ready = true;
212
+ state = 'ready';
213
+ progress = null;
214
+ publish();
215
+ entry.controller.signal.throwIfAborted();
216
+ assertOpen();
217
+ return current();
218
+ });
219
+ entry.restarting.then(function resumed() {
220
+ entry.restarting = null;
221
+ advance(entry);
222
+ }, function resumeFailed(cause) {
223
+ entry.restarting = null;
224
+ if (!entry.release) stop(entry, cause, 'error').catch(reportCleanupFailure);
225
+ }).catch(reportCleanupFailure);
226
+ state = 'loading';
227
+ progress = {phase: 'loading-model'};
228
+ publish();
229
+ return entry.restarting;
230
+ }
231
+
232
+ function preparedPaths(request) {
233
+ if (!request.retained) return paths;
234
+ const mapping = request.selection.resourcePaths;
235
+ if (!mapping) throw new TypeError('A Kokoro projection requires its complete resourcePaths mapping.');
236
+ function member(role) {
237
+ const record = request.retained.members.find(function selected(value) { return value.path === role; });
238
+ if (!record) throw failure('KOKORO_RESOURCE_UNAVAILABLE', `The retained Kokoro projection has no member ${String(role)}.`);
239
+ return record.nativePath;
240
+ }
241
+ const files = {model: member(mapping.model), tokenizer: member(mapping.tokenizer), tokenizerConfig: member(mapping.tokenizerConfig), voices: {}};
242
+ for (const voice of KOKORO_VOICES) files.voices[voice] = member(mapping.voices?.[voice]);
243
+ return files;
244
+ }
245
+
246
+ function begin(request) {
247
+ const files = preparedPaths(request);
248
+ const entry = {
249
+ id: randomUUID(), controller: new AbortController(), selection: request.selection,
250
+ retained: request.retained, files, ready: false, helper: null, frontend: null,
251
+ loading: null, restarting: null, nativeRelease: null, active: null, queue: [], release: null, unsubscribe: null
252
+ };
253
+ request.retained = null;
254
+ request.entry = entry;
255
+ activation = entry;
256
+ state = 'loading';
257
+ error = null;
258
+ execution = null;
259
+ progress = {phase: 'loading-model'};
260
+ entry.loading = Promise.resolve().then(async function activate() {
261
+ entry.controller.signal.throwIfAborted();
262
+ // Explicit host recovery replaces a closed ONNX owner. Reacquire it
263
+ // only after the previous activation's native retirement has joined.
264
+ const refreshONNX = !onnx || onnx.current().closed;
265
+ if ((refreshONNX || !runtime) && prepare) {
266
+ const prepared = await prepare({signal: entry.controller.signal});
267
+ entry.controller.signal.throwIfAborted();
268
+ if (refreshONNX) onnx = prepared.onnx;
269
+ runtime ??= prepared.runtime;
270
+ }
271
+ if (!onnx) throw failure('KOKORO_RUNTIME_UNAVAILABLE', 'The host has not supplied its existing ONNX runtime.');
272
+ if (!runtime?.helperExecutable || !runtime?.espeakDataDirectory) {
273
+ throw failure('KOKORO_RUNTIME_UNAVAILABLE', 'Prepare the native Kokoro helper and matching eSpeak data before loading.');
274
+ }
275
+ const tasks = [
276
+ loadKokoroFrontend(files, {signal: entry.controller.signal}),
277
+ startNative(entry)
278
+ ];
279
+ let results;
280
+ try { results = await Promise.all(tasks); }
281
+ catch (cause) {
282
+ entry.controller.abort(cause);
283
+ const results = await Promise.allSettled([...tasks, retireNative(entry, cause)]);
284
+ const failures = [operationFailure(cause, entry.controller.signal), ...rejectedFailures(results, entry.controller.signal)];
285
+ throw joinedFailure(failures, 'Loading the Kokoro activation and cleanup failed.');
286
+ }
287
+ await requireNativeReady(entry);
288
+ entry.controller.signal.throwIfAborted();
289
+ entry.frontend = results[0];
290
+ entry.session = results[1];
291
+ if (activation !== entry) throw cancellation('The Kokoro activation changed while loading.');
292
+ execution = results[1].execution ?? null;
293
+ entry.ready = true;
294
+ state = 'ready';
295
+ progress = null;
296
+ if (pending === request) pending = null;
297
+ publish();
298
+ entry.controller.signal.throwIfAborted();
299
+ assertOpen();
300
+ return current();
301
+ });
302
+ entry.loading.catch(function activationFailed(cause) {
303
+ if (!entry.release) stop(entry, cause, cause?.name === 'AbortError' ? 'unloaded' : 'error').catch(reportCleanupFailure);
304
+ });
305
+ publish();
306
+ return entry.loading;
307
+ }
308
+
309
+ function load(options = {}) {
310
+ assertOpen(options.signal);
311
+ if (options.modelId !== undefined && options.modelId !== modelId) {
312
+ throw failure('KOKORO_MODEL_MISMATCH', 'This engine owns the selected Kokoro model.');
313
+ }
314
+ const next = copySelection({
315
+ assetProjectionId: Object.hasOwn(options, 'assetProjectionId') ? options.assetProjectionId : selection.assetProjectionId,
316
+ resourcePaths: Object.hasOwn(options, 'resourcePaths') ? options.resourcePaths : selection.resourcePaths,
317
+ executionTarget: options.executionTarget === undefined ? selection.executionTarget : options.executionTarget
318
+ });
319
+ if (pending && sameSelection(pending.selection, next)) return waitFor(pending.task, options.signal);
320
+ // A closed shared owner requires the ordinary retained-resource
321
+ // replacement path, including after a cancelled native generation.
322
+ if (activation && sameSelection(activation.selection, next) && !pending && !onnx?.current().closed) {
323
+ if (activation.ready) return Promise.resolve(current());
324
+ if (activation.nativeRelease) {
325
+ const entry = activation;
326
+ const resumed = entry.active ? entry.active.settled.promise.then(function awaitActiveRetirement() {
327
+ assertOpen(options.signal);
328
+ if (activation !== entry) throw cancellation('The Kokoro selection changed while its active request stopped.');
329
+ return entry.ready ? current() : resume(entry);
330
+ }) : resume(entry);
331
+ return waitFor(resumed, options.signal);
332
+ }
333
+ if (activation.restarting) return waitFor(activation.restarting, options.signal);
334
+ }
335
+ if (!next.assetProjectionId && !paths) {
336
+ selection = next;
337
+ return activation || pending ? unload() : Promise.resolve(current());
338
+ }
339
+ // Acquire the incoming retain before retiring an activation of the same projection.
340
+ const retained = next.assetProjectionId ? modelAssets.retain(next.assetProjectionId) : null;
341
+ const previous = pending;
342
+ const predecessor = activation;
343
+ const request = {selection: next, retained, controller: new AbortController(), task: null, entry: null};
344
+ selection = next;
345
+ pending = request;
346
+ request.task = Promise.resolve().then(async function selectActivation() {
347
+ const failures = [];
348
+ let value;
349
+ try {
350
+ if (previous) await Promise.allSettled([previous.task]);
351
+ if (predecessor?.release) await predecessor.release;
352
+ else if (releaseTask) await releaseTask;
353
+ request.controller.signal.throwIfAborted();
354
+ assertOpen();
355
+ value = await begin(request);
356
+ } catch (cause) {
357
+ failures.push(cause);
358
+ if (request.entry) {
359
+ try { await stop(request.entry, cause, cause?.name === 'AbortError' ? 'unloaded' : 'error'); }
360
+ catch (cleanupFailure) { failures.push(cleanupFailure); }
361
+ } else if (pending === request && !request.controller.signal.aborted) {
362
+ state = 'error';
363
+ error = cause;
364
+ }
365
+ } finally {
366
+ try { await request.retained?.release(); }
367
+ catch (cleanupFailure) { failures.push(cleanupFailure); }
368
+ finally {
369
+ if (pending === request && failures.length && !activation) {
370
+ error = joinedFailure(failures, 'Selecting the Kokoro activation and cleanup failed.');
371
+ state = error?.name === 'AbortError' ? 'unloaded' : 'error';
372
+ }
373
+ if (pending === request) pending = null;
374
+ loads.delete(request.task);
375
+ try { if (!disposed) publish(); }
376
+ catch (observerFailure) { failures.push(observerFailure); }
377
+ }
378
+ }
379
+ if (failures.length) throw joinedFailure(failures, 'Selecting the Kokoro activation and cleanup failed.');
380
+ return value;
381
+ });
382
+ loads.add(request.task);
383
+ request.task.catch(reportLoadFailure);
384
+ previous?.controller.abort(cancellation('A newer Kokoro activation was selected.'));
385
+ if (predecessor) stop(predecessor, cancellation('The Kokoro activation is being replaced.')).catch(reportCleanupFailure);
386
+ publish();
387
+ return waitFor(request.task, options.signal);
388
+ }
389
+
390
+ function synthesize(request, {signal: operationSignal, onProgress, requestId = null} = {}) {
391
+ assertOpen(operationSignal);
392
+ if (!is.string(request?.input)) throw new TypeError('Kokoro synthesis requires the complete input string.');
393
+ if (request.model !== undefined && request.model !== modelId) throw failure('KOKORO_MODEL_MISMATCH', 'The request selected a different speech model.');
394
+ const voice = request.voice ?? 'af_heart';
395
+ const language = voiceLanguage(voice);
396
+ const speed = request.speed ?? 1;
397
+ if (!is.finite(speed) || speed <= 0 || !is.finite(Math.fround(speed)) || Math.fround(speed) <= 0) {
398
+ throw new RangeError('Kokoro speed must be a positive finite float32 value.');
399
+ }
400
+ const format = request.responseFormat ?? 'wav';
401
+ if (!['wav', 'opus', 'ogg'].includes(format)) throw failure('KOKORO_FORMAT_UNSUPPORTED', `Kokoro supports WAV and Ogg Opus; ${String(format)} is unavailable.`);
402
+ const entry = activation;
403
+ if (!entry?.ready || entry.controller.signal.aborted) throw failure('KOKORO_MODEL_NOT_READY', 'Load the selected Kokoro model before synthesis.');
404
+ const response = deferred();
405
+ const requestSignal = operationSignal ? AbortSignal.any([operationSignal, entry.controller.signal]) : entry.controller.signal;
406
+ const operation = {request, requestId, voice, language, speed, format, signal: requestSignal,
407
+ onProgress, response, settled: deferred(), task: null, cancel: null};
408
+ operation.cancel = function cancelRequest() {
409
+ if (entry.active === operation) {
410
+ // Retire the cancelled caller's native work, preserving the activation and surviving FIFO.
411
+ retireNative(entry, requestSignal.reason).catch(reportCleanupFailure);
412
+ if (activation === entry) {
413
+ state = 'unloading';
414
+ progress = null;
415
+ publish();
416
+ }
417
+ } else {
418
+ const position = entry.queue.indexOf(operation);
419
+ if (position >= 0) entry.queue.splice(position, 1);
420
+ requestSignal.removeEventListener('abort', operation.cancel);
421
+ response.reject(requestSignal.reason);
422
+ operation.settled.resolve();
423
+ if (activation === entry) publish();
424
+ }
425
+ };
426
+ entry.queue.push(operation);
427
+ requestSignal.addEventListener('abort', operation.cancel, {once: true});
428
+ if (requestSignal.aborted) operation.cancel();
429
+ advance(entry);
430
+ return response.promise;
431
+ }
432
+
433
+ function advance(entry) {
434
+ if (entry.active || entry.controller.signal.aborted || activation !== entry) return;
435
+ if (!entry.ready) {
436
+ if (entry.queue.length && !entry.restarting) resume(entry);
437
+ return;
438
+ }
439
+ const operation = entry.queue.shift();
440
+ if (!operation) return;
441
+ entry.active = operation;
442
+ state = 'running';
443
+ error = null;
444
+ operation.task = Promise.resolve().then(async function generateSpeech() {
445
+ const requestSignal = operation.signal;
446
+ requestSignal.throwIfAborted();
447
+ await operation.onProgress?.({phase: 'phonemizing'});
448
+ requestSignal.throwIfAborted();
449
+ const clauses = await entry.helper.phonemize(operation.request.input, operation.language, {signal: requestSignal});
450
+ const chunks = [];
451
+ let completed = 0;
452
+ for (const segment of entry.frontend.segments(clauses, operation.voice)) {
453
+ requestSignal.throwIfAborted();
454
+ await operation.onProgress?.({phase: 'synthesizing', completed, unit: 'segments'});
455
+ requestSignal.throwIfAborted();
456
+ const outputs = await onnx.run({id: entry.id, signal: requestSignal, feeds: {
457
+ input_ids: {type: 'int64', data: segment.input, dims: [1, segment.input.length]},
458
+ style: {type: 'float32', data: segment.style, dims: [1, 256]},
459
+ speed: {type: 'float32', data: Float32Array.of(operation.speed), dims: [1]}
460
+ }});
461
+ requestSignal.throwIfAborted();
462
+ const waveform = outputs[entry.session.outputNames[0]];
463
+ if (waveform?.type !== 'float32' || !(waveform.data instanceof Float32Array)) {
464
+ throw failure('KOKORO_OUTPUT_UNSUPPORTED', 'The selected Kokoro graph did not return its float32 waveform.', outputs);
465
+ }
466
+ chunks.push(waveform.data);
467
+ completed += 1;
468
+ }
469
+ await operation.onProgress?.({phase: 'encoding', completed, unit: 'segments'});
470
+ requestSignal.throwIfAborted();
471
+ const audio = operation.format === 'wav' ? encodeKokoroWav(chunks)
472
+ : await entry.helper.encodeOpus(chunks, {signal: requestSignal});
473
+ requestSignal.throwIfAborted();
474
+ return {
475
+ audioBase64: audio.toString('base64'),
476
+ contentType: operation.format === 'wav' ? 'audio/wav' : 'audio/ogg; codecs=opus',
477
+ sampleRate: KOKORO_SAMPLE_RATE, channels: 1, model: modelId, voice: operation.voice,
478
+ speed: operation.speed
479
+ };
480
+ });
481
+ operation.task.then(function generated(value) { return finish(null, value); }, function generationFailed(cause) { return finish(cause); })
482
+ .catch(reportCleanupFailure);
483
+ async function finish(cause, value) {
484
+ if (operation.signal.aborted) {
485
+ cause = operationFailure(cause, operation.signal);
486
+ retireNative(entry, operation.signal.reason).catch(reportCleanupFailure);
487
+ } else if (cause && !entry.release && entry.helper.exited) {
488
+ stop(entry, cause, 'error').catch(reportCleanupFailure);
489
+ }
490
+ // Active cancellation resolves only after native ownership has actually drained.
491
+ try {
492
+ await (entry.release ?? entry.nativeRelease);
493
+ if (operation.signal.aborted) {
494
+ cause = operationFailure(cause, operation.signal);
495
+ await (entry.release ?? retireNative(entry, operation.signal.reason));
496
+ }
497
+ } catch (cleanupFailure) {
498
+ cause = cause ? joinedFailure([cause, cleanupFailure], 'Synthesis and cleanup failed.') : cleanupFailure;
499
+ }
500
+ complete(cause);
501
+ function complete(cause) {
502
+ operation.signal.removeEventListener('abort', operation.cancel);
503
+ if (entry.active === operation) entry.active = null;
504
+ if (cause) operation.response.reject(cause);
505
+ else operation.response.resolve(value);
506
+ operation.settled.resolve();
507
+ if (activation === entry) {
508
+ state = entry.ready ? 'ready' : 'unloaded';
509
+ if (cause && !operation.signal.aborted) error = cause;
510
+ publish();
511
+ }
512
+ advance(entry);
513
+ }
514
+ }
515
+ publish();
516
+ }
517
+
518
+ function unload() {
519
+ const reason = cancellation('The Kokoro engine is unloading.');
520
+ pending?.controller.abort(reason);
521
+ const entry = activation;
522
+ if (entry) stop(entry, reason).catch(reportCleanupFailure);
523
+ const tasks = [...loads, releaseTask].filter(Boolean);
524
+ return Promise.allSettled(tasks).then(function unloaded(results) {
525
+ const failures = results.filter(function failed(result) {
526
+ return result.status === 'rejected' && result.reason?.name !== 'AbortError';
527
+ }).map(function cause(result) { return result.reason; });
528
+ if (failures.length) throw new AggregateError(failures, 'Unloading Kokoro failed.');
529
+ return current();
530
+ });
531
+ }
532
+
533
+ function close() {
534
+ if (closing) return closing;
535
+ closing = Promise.resolve().then(async function closeEngine() {
536
+ const failures = [];
537
+ try { await unload(); }
538
+ catch (cleanupFailure) { failures.push(cleanupFailure); }
539
+ finally {
540
+ signal?.removeEventListener('abort', lifetimeEnded);
541
+ disposed = true;
542
+ state = 'disposed';
543
+ try { publish(); }
544
+ catch (observerFailure) { failures.push(observerFailure); }
545
+ finally {
546
+ try { events.dispose(); }
547
+ catch (disposeFailure) { failures.push(disposeFailure); }
548
+ }
549
+ }
550
+ if (failures.length) throw joinedFailure(failures, 'Closing Kokoro failed.');
551
+ return current();
552
+ });
553
+ return closing;
554
+ }
555
+
556
+ function lifetimeEnded() { close().catch(reportCleanupFailure); }
557
+ signal?.addEventListener('abort', lifetimeEnded, {once: true});
558
+ if (signal?.aborted) lifetimeEnded();
559
+ return {current, subscribe, load, synthesize, unload, close};
560
+ }
561
+
562
+ function sameSelection(left, right) {
563
+ return left.assetProjectionId === right.assetProjectionId && sameResources(left.resourcePaths, right.resourcePaths)
564
+ && (left.executionTarget === right.executionTarget
565
+ || (left.executionTarget != null && right.executionTarget != null
566
+ && left.executionTarget.deviceId === right.executionTarget.deviceId));
567
+ }
568
+
569
+ function copySelection(selection) {
570
+ return {
571
+ assetProjectionId: selection.assetProjectionId,
572
+ resourcePaths: selection.resourcePaths ? {...selection.resourcePaths, voices: {...selection.resourcePaths.voices}} : selection.resourcePaths,
573
+ executionTarget: selection.executionTarget == null ? selection.executionTarget : {...selection.executionTarget}
574
+ };
575
+ }
576
+
577
+ function sameResources(left, right) {
578
+ if (left === right) return true;
579
+ if (!left || !right) return false;
580
+ return left.model === right.model && left.tokenizer === right.tokenizer && left.tokenizerConfig === right.tokenizerConfig
581
+ && KOKORO_VOICES.every(function sameVoice(voice) { return left.voices?.[voice] === right.voices?.[voice]; });
582
+ }
583
+
584
+ function waitFor(task, signal) {
585
+ if (!signal) return task;
586
+ signal.throwIfAborted();
587
+ return new Promise(function waitForActivation(resolve, reject) {
588
+ function cancel() { signal.removeEventListener('abort', cancel); reject(signal.reason); }
589
+ signal.addEventListener('abort', cancel, {once: true});
590
+ task.then(function loaded(value) {
591
+ signal.removeEventListener('abort', cancel);
592
+ if (signal.aborted) reject(signal.reason); else resolve(value);
593
+ }, function failed(cause) { signal.removeEventListener('abort', cancel); reject(cause); });
594
+ });
595
+ }
596
+
597
+ function deferred() {
598
+ const response = {};
599
+ response.promise = new Promise(function pending(resolve, reject) { response.resolve = resolve; response.reject = reject; });
600
+ response.promise.catch(function observeResponseFailure() {});
601
+ return response;
602
+ }
603
+
604
+ function failure(code, message, data) {
605
+ const error = new Error(message);
606
+ error.code = code;
607
+ if (data !== undefined) error.data = data;
608
+ return error;
609
+ }
610
+
611
+ function cancellation(message) {
612
+ const error = failure('ARCANE_AI_REQUEST_ABORTED', message);
613
+ error.name = 'AbortError';
614
+ return error;
615
+ }
616
+
617
+ function joinedFailure(failures, message) {
618
+ const distinct = [...new Set(failures)];
619
+ return distinct.length === 1 ? distinct[0] : new AggregateError(distinct, message, {cause: distinct[0]});
620
+ }
621
+
622
+ function expectedCancellationOf(error, reason) {
623
+ if (error === reason) return true;
624
+ if (error?.name !== 'AbortError' && error?.code !== 'ARCANE_CANCELLED' && error?.code !== 'ABORT_ERR') return false;
625
+ if (is.array(error.errors) && !error.errors.every(function expected(cause) { return expectedCancellationOf(cause, reason); })) return false;
626
+ return error.cause !== undefined && expectedCancellationOf(error.cause, reason);
627
+ }
628
+
629
+ function operationFailure(error, signal) {
630
+ if (!signal.aborted) return error;
631
+ if (error === null || expectedCancellationOf(error, signal.reason)) return signal.reason;
632
+ return joinedFailure([signal.reason, error], 'Kokoro cancellation and operation failed.');
633
+ }
634
+
635
+ function rejectedFailures(results, signal) {
636
+ return results.filter(function rejected(result) {
637
+ return result.status === 'rejected' && !(signal.aborted && expectedCancellationOf(result.reason, signal.reason));
638
+ }).map(function reasonOf(result) { return result.reason; });
639
+ }
640
+
641
+ function reportCleanupFailure(error) { console.error('Kokoro cleanup failed.', error); }
642
+ function reportObserverFailure(error) { console.error('Kokoro state observer failed.', error); }
643
+ function reportLoadFailure(error) { if (error?.name !== 'AbortError') console.error('Kokoro model load failed.', error); }
644
+
645
+ export default createNativeKokoroRuntime;