arcane-os 0.71.1 → 0.71.2

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.71.2
4
+
5
+ - Preserve complete browser Kokoro synthesis input through the selected runtime's
6
+ phonemization and tokenizer. Process every token in ordered model-capacity
7
+ segments and retain all resulting audio, with one model owner and request-local
8
+ cancellation. Keep short-input behavior, voice, speed and the public API intact.
9
+ - Include the browser decision runtime entry in an explicitly configured DBOPFS
10
+ model store. Retain the original resource URL across activations, import the
11
+ complete stored self-contained module, and join cancellation cleanup before
12
+ settling callers. Preserve direct loading when no store is configured.
13
+
3
14
  ## 0.71.1
4
15
 
5
16
  - Correct explicit disabled PWA lifecycle in standalone source development by
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.71.1` SDK contract. Development applications declare
23
+ This checkout defines the `0.71.2` SDK contract. Development applications declare
24
24
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
25
25
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
26
26
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -204,6 +204,9 @@ class BrowserDecisionModel {
204
204
  pending: new Map(),
205
205
  evaluations: new Set(),
206
206
  resources: null,
207
+ runtimeController: null,
208
+ runtimePreparation: null,
209
+ runtimeUrl: null,
207
210
  cleanup: null,
208
211
  reason: undefined
209
212
  };
@@ -263,9 +266,58 @@ class BrowserDecisionModel {
263
266
  }
264
267
  const id = this.#nextId++;
265
268
  activation.pending.set(id, activation.load);
266
- worker.postMessage(
267
- {id, op: 'load', payload: this.#configuration, storedResources: this.#store !== null}
268
- );
269
+ if (this.#store) {
270
+ activation.runtimeController = new AbortController();
271
+ // Retain the operation before a store progress callback can
272
+ // synchronously unload this activation.
273
+ activation.runtimePreparation = Promise.resolve().then(
274
+ async function prepareDecisionRuntimeEntry() {
275
+ if (!model.#isCurrent(activation)) return;
276
+ const configuration = model.#configuration;
277
+ const sourceUrl = new URL(configuration.runtime.moduleUrl, import.meta.url).href;
278
+ const response = await model.#store.fetchResource(sourceUrl, {
279
+ signal: activation.runtimeController.signal,
280
+ onProgress: function reportRuntimeEntryProgress(progress) {
281
+ if (!model.#isCurrent(activation)) return;
282
+ model.#progress = progress;
283
+ model.#publish();
284
+ }
285
+ });
286
+ if (!model.#isCurrent(activation)) return;
287
+ if (response.status < 200 || response.status >= 300) {
288
+ const error = new Error(
289
+ `The decision runtime returned HTTP ${response.status} ${response.statusText}.`
290
+ );
291
+ error.code = 'ARCANE_DECISION_RUNTIME_DOWNLOAD_FAILED';
292
+ error.response = response;
293
+ throw error;
294
+ }
295
+ // Import the complete stored module without source
296
+ // rewriting. This activation owns its URL until stop.
297
+ activation.runtimeUrl = URL.createObjectURL(
298
+ new Blob([response.file], {type: 'text/javascript'})
299
+ );
300
+ worker.postMessage({
301
+ id,
302
+ op: 'load',
303
+ payload: {
304
+ ...configuration,
305
+ runtime: {...configuration.runtime, moduleUrl: activation.runtimeUrl}
306
+ },
307
+ storedResources: true
308
+ });
309
+ }
310
+ );
311
+ activation.runtimePreparation.catch(
312
+ function stopFailedRuntimePreparation(error) {
313
+ if (model.#isCurrent(activation)) model.#stop(activation, error, 'error');
314
+ }
315
+ );
316
+ } else {
317
+ worker.postMessage(
318
+ {id, op: 'load', payload: this.#configuration, storedResources: false}
319
+ );
320
+ }
269
321
  } catch (error) {
270
322
  this.#stop(activation, error, 'error');
271
323
  }
@@ -333,11 +385,42 @@ class BrowserDecisionModel {
333
385
  this.#state = state;
334
386
  this.#progress = null;
335
387
  this.#error = reason;
336
- if (activation.resources) {
388
+ const cleanup = [];
389
+ activation.runtimeController?.abort(reason);
390
+ if (activation.runtimePreparation) {
391
+ cleanup.push(activation.runtimePreparation.catch(
392
+ function preserveRuntimePreparationFailure(error) {
393
+ if (error === reason || error?.name === 'AbortError') return;
394
+ throw error;
395
+ }
396
+ ));
397
+ }
398
+ if (activation.resources) cleanup.push(activation.resources.close(reason));
399
+ if (activation.worker) {
400
+ for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
401
+ activation.worker.removeEventListener(type, listener);
402
+ }
403
+ activation.worker.terminate();
404
+ }
405
+ if (activation.runtimeUrl !== null) {
406
+ try {
407
+ URL.revokeObjectURL(activation.runtimeUrl);
408
+ } catch (error) {
409
+ cleanup.push(Promise.reject(error));
410
+ }
411
+ activation.runtimeUrl = null;
412
+ }
413
+ if (cleanup.length > 0) {
337
414
  const model = this;
338
- activation.cleanup = activation.resources.close(reason).catch(
339
- function preserveResourceCleanupFailure(error) {
340
- activation.reason = new AggregateError([reason, error], 'Unable to settle decision model resources.');
415
+ activation.cleanup = Promise.allSettled(cleanup).then(
416
+ function settleDecisionResourceCleanup(results) {
417
+ const failures = results.filter(function failedCleanup(result) {
418
+ return result.status === 'rejected';
419
+ }).map(function cleanupReason(result) {
420
+ return result.reason;
421
+ });
422
+ if (failures.length === 0) return;
423
+ activation.reason = new AggregateError([reason, ...failures], 'Unable to settle decision model resources.');
341
424
  throw activation.reason;
342
425
  }
343
426
  );
@@ -350,12 +433,6 @@ class BrowserDecisionModel {
350
433
  }
351
434
  );
352
435
  }
353
- if (activation.worker) {
354
- for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
355
- activation.worker.removeEventListener(type, listener);
356
- }
357
- activation.worker.terminate();
358
- }
359
436
  activation.load.reject(reason);
360
437
  for (const operation of activation.pending.values()) operation.reject(reason);
361
438
  activation.pending.clear();
@@ -0,0 +1,116 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+
3
+ const is = new Is(false);
4
+
5
+ function throwIfCancelled(signal) {
6
+ if (signal?.aborted) {
7
+ throw signal.reason ?? new DOMException('Speech synthesis was cancelled.', 'AbortError');
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Compose the selected Kokoro public classes without changing loaded instances.
13
+ * The original synthesizer remains the sole owner of its model and disposal.
14
+ */
15
+ export async function createCompleteKokoroSynthesis({
16
+ KokoroTTS,
17
+ synthesizer,
18
+ repository,
19
+ signal,
20
+ onProgress,
21
+ }) {
22
+ throwIfCancelled(signal);
23
+ const Tokenizer = synthesizer.tokenizer.constructor;
24
+ class CompleteKokoroTokenizer extends Tokenizer {
25
+ _call(text, options = {}) {
26
+ return super._call(text, { ...options, truncation: false });
27
+ }
28
+ }
29
+ // Kokoro 1.2.1's factory uses the tokenizer's default revision. Reuse that
30
+ // same public loading contract through the existing Worker resource router.
31
+ const tokenizer = await CompleteKokoroTokenizer.from_pretrained(repository, {
32
+ progress_callback: onProgress,
33
+ });
34
+ throwIfCancelled(signal);
35
+ const contextLength = tokenizer.model_max_length;
36
+ if (!is.safeInteger(contextLength) || contextLength < 3) {
37
+ throw new Error('The selected Kokoro tokenizer does not expose its model context length.');
38
+ }
39
+ const contentCapacity = contextLength - 2;
40
+ function boundaryIds(text) {
41
+ const encoded = tokenizer(text, {
42
+ add_special_tokens: false,
43
+ return_tensor: false,
44
+ truncation: false,
45
+ });
46
+ return new Set(encoded.input_ids.map(function tokenId(id) { return BigInt(id); }));
47
+ }
48
+ const punctuation = boundaryIds(';:,.!?—…');
49
+ const spaces = boundaryIds(' ');
50
+
51
+ function segmentEnd(tokens, start, limit, contentEnd) {
52
+ if (limit === contentEnd) return limit;
53
+ for (const boundaries of [punctuation, spaces]) {
54
+ for (let index = limit - 1; index >= start; index -= 1) {
55
+ if (boundaries.has(tokens[index])) return index + 1;
56
+ }
57
+ }
58
+ return limit;
59
+ }
60
+
61
+ class CompleteKokoroTTS extends KokoroTTS {
62
+ #signal;
63
+
64
+ constructor(requestSignal) {
65
+ super(synthesizer.model, tokenizer);
66
+ this.#signal = requestSignal;
67
+ }
68
+
69
+ async generate_from_ids(inputIds, options = {}) {
70
+ throwIfCancelled(this.#signal);
71
+ if (inputIds.dims.at(-1) <= contextLength) {
72
+ const output = await super.generate_from_ids(inputIds, options);
73
+ throwIfCancelled(this.#signal);
74
+ return output;
75
+ }
76
+ const Tensor = inputIds.constructor;
77
+ const tokens = inputIds.data;
78
+ const contentEnd = tokens.length - 1;
79
+ const outputs = [];
80
+ let sampleCount = 0;
81
+ for (let start = 1; start < contentEnd;) {
82
+ throwIfCancelled(this.#signal);
83
+ const end = segmentEnd(tokens, start, Math.min(start + contentCapacity, contentEnd), contentEnd);
84
+ // Only the original outer padding positions are replaced. All
85
+ // content tokens, including boundary punctuation/space, survive.
86
+ const data = new BigInt64Array(end - start + 2);
87
+ data[0] = tokens[0];
88
+ data.set(tokens.subarray(start, end), 1);
89
+ data[data.length - 1] = tokens[tokens.length - 1];
90
+ const ids = new Tensor('int64', data, [1, data.length]);
91
+ const output = await super.generate_from_ids(ids, options);
92
+ throwIfCancelled(this.#signal);
93
+ outputs.push(output);
94
+ sampleCount += output.audio.length;
95
+ start = end;
96
+ }
97
+ const audio = new Float32Array(sampleCount);
98
+ let offset = 0;
99
+ for (const output of outputs) {
100
+ audio.set(output.audio, offset);
101
+ offset += output.audio.length;
102
+ }
103
+ throwIfCancelled(this.#signal);
104
+ const RawAudio = outputs[0].constructor;
105
+ return new RawAudio(audio, outputs[0].sampling_rate);
106
+ }
107
+ }
108
+
109
+ return async function synthesizeCompleteKokoro(text, { voice, speed, signal: requestSignal } = {}) {
110
+ throwIfCancelled(requestSignal);
111
+ // Each invocation retains its own signal while sharing the one loaded
112
+ // model. Inherited generate() still owns all upstream phonemization.
113
+ const request = new CompleteKokoroTTS(requestSignal);
114
+ return request.generate(text, { voice, speed });
115
+ };
116
+ }
@@ -1,5 +1,6 @@
1
1
  import Is from "../dependencies/strong-type/index.js";
2
2
  import { createModelResourceClient, createModelResourceHost } from './model-resource-bridge.mjs';
3
+ import { createCompleteKokoroSynthesis } from './kokoro-complete-input.mjs';
3
4
 
4
5
  const is = new Is(false);
5
6
 
@@ -1493,54 +1494,70 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
1493
1494
  }
1494
1495
 
1495
1496
  async function createKokoroEngine(namespace, configuration, signal, report) {
1496
- if (!is.function(namespace?.KokoroTTS?.from_pretrained)) {
1497
- throw workerError(
1498
- "ARCANE_AI_PROVIDER_UNAVAILABLE",
1499
- "The Kokoro runtime does not export KokoroTTS.",
1500
- undefined,
1501
- "kokoro-tts-constructor-export-missing",
1497
+ if (!is.function(namespace?.KokoroTTS?.from_pretrained)) {
1498
+ throw workerError(
1499
+ 'ARCANE_AI_PROVIDER_UNAVAILABLE',
1500
+ 'The Kokoro runtime does not export KokoroTTS.',
1501
+ undefined,
1502
+ 'kokoro-tts-constructor-export-missing',
1503
+ );
1504
+ }
1505
+ throwIfAborted(signal, 'tts-load-cancelled');
1506
+ const synthesizer = await namespace.KokoroTTS.from_pretrained(
1507
+ configuration.model.repository,
1508
+ {
1509
+ device: configuration.execution?.device ?? 'wasm',
1510
+ dtype: configuration.model.dtype ?? 'q8',
1511
+ revision: configuration.model.revision,
1512
+ progress_callback: report,
1513
+ },
1502
1514
  );
1503
- }
1504
- throwIfAborted(signal, "tts-load-cancelled");
1505
- const synthesizer = await namespace.KokoroTTS.from_pretrained(
1506
- configuration.model.repository,
1507
- {
1508
- device: configuration.execution?.device ?? "wasm",
1509
- dtype: configuration.model.dtype ?? "q8",
1510
- revision: configuration.model.revision,
1511
- progress_callback: report,
1512
- },
1513
- );
1514
- try {
1515
- throwIfAborted(signal, "tts-load-cancelled");
1516
- } catch (error) {
1515
+ let synthesize;
1517
1516
  try {
1518
- await disposeEngine(synthesizer);
1519
- } catch {
1520
- // Preserve the cancellation boundary while the owning Worker terminates.
1517
+ throwIfAborted(signal, 'tts-load-cancelled');
1518
+ synthesize = await createCompleteKokoroSynthesis({
1519
+ KokoroTTS: namespace.KokoroTTS,
1520
+ synthesizer,
1521
+ repository: configuration.model.repository,
1522
+ signal,
1523
+ onProgress: report,
1524
+ });
1525
+ throwIfAborted(signal, 'tts-load-cancelled');
1526
+ } catch (error) {
1527
+ try {
1528
+ await disposeEngine(synthesizer);
1529
+ } catch (cleanupError) {
1530
+ throw new AggregateError([error, cleanupError], 'Kokoro preparation and model cleanup failed.', { cause: error });
1531
+ }
1532
+ throwIfAborted(signal, 'tts-load-cancelled');
1533
+ throw error;
1521
1534
  }
1522
- throw error;
1523
- }
1524
- return completeValue({
1525
- async synthesize(input, { signal: requestSignal } = {}) {
1526
- throwIfAborted(requestSignal, "tts-synthesis-cancelled");
1527
- const output = await synthesizer.generate(input.text, {
1528
- voice: input.voice,
1529
- speed: input.speed,
1530
- signal: requestSignal,
1531
- });
1532
- throwIfAborted(requestSignal, "tts-synthesis-cancelled");
1533
- const audio = is.instanceCheck(output?.audio, Float32Array)
1534
- ? output.audio
1535
- : new Float32Array(output?.audio ?? []);
1536
- return completeValue({
1537
- audio,
1538
- sampleRate: output?.sampling_rate,
1539
- voice: input.voice,
1540
- });
1541
- },
1542
- dispose: () => disposeEngine(synthesizer),
1543
- });
1535
+ return completeValue({
1536
+ async synthesize(input, { signal: requestSignal } = {}) {
1537
+ throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
1538
+ let output;
1539
+ try {
1540
+ output = await synthesize(input.text, {
1541
+ voice: input.voice,
1542
+ speed: input.speed,
1543
+ signal: requestSignal,
1544
+ });
1545
+ } catch (error) {
1546
+ throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
1547
+ throw error;
1548
+ }
1549
+ throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
1550
+ const audio = is.instanceCheck(output?.audio, Float32Array)
1551
+ ? output.audio
1552
+ : new Float32Array(output?.audio ?? []);
1553
+ return completeValue({
1554
+ audio,
1555
+ sampleRate: output?.sampling_rate,
1556
+ voice: input.voice,
1557
+ });
1558
+ },
1559
+ dispose: function disposeKokoroEngine() { return disposeEngine(synthesizer); },
1560
+ });
1544
1561
  }
1545
1562
 
1546
1563
  function callerVoiceIds(configuration) {
@@ -68,9 +68,9 @@ it does not substitute a quantized graph or another model.
68
68
  | `revision` | Upstream revision, default `main`; select a stable revision when repeatable model selection is needed. |
69
69
  | `device` | Transformers.js backend, default `webgpu`; an explicit supported alternative is caller-owned. No automatic backend fallback. |
70
70
  | `dtype` | Exact upstream precision selection; defaults to `fp16` for Laya and `fp32` for Julia. For Laya FP32, supply `dtype: 'fp32'`. Unsupported selections fail at the selected upstream loader rather than being replaced. |
71
- | `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. |
71
+ | `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. With `store`, the SDK stores and materializes the complete selected entry for that activation. |
72
72
  | `runtime.wasmPaths` | Optional upstream ONNX WASM location passed to the selected runtime's public environment API. |
73
- | `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK bridges the loader's actual resource requests to it. |
73
+ | `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK uses it for the runtime entry and bridges the loader's actual resource requests to it. |
74
74
 
75
75
  Without `store`, the runtime, tokenizer and weights use normal upstream loading
76
76
  and caching. With `store`, the selected loader still owns filenames and model
@@ -110,17 +110,27 @@ await decisions.load();
110
110
  ```
111
111
 
112
112
  The same `store` may serve other SDK model clients. The application supplies no
113
- tokenizer/configuration/graph manifest and performs no file routing. Inside the
114
- dedicated decision Worker, Transformers.js's public `env.fetch` hook routes the
113
+ tokenizer/configuration/graph manifest and performs no file routing. Explicit
114
+ activation first opens `runtime.moduleUrl` through `store.fetchResource()`.
115
+ The SDK materializes the complete stored entry as a JavaScript object URL for
116
+ the dedicated Worker, without rewriting its content. The configured source URL
117
+ is retained for later activations, so they reuse the same stored resource rather
118
+ than saving a temporary object URL. Construction still starts no download.
119
+
120
+ Inside the dedicated decision Worker, Transformers.js's public `env.fetch` hook routes the
115
121
  actual selected tokenizer, configuration, ONNX graph and external-data requests
116
122
  to `store.fetchResource(input, options)`. Its competing browser, custom and
117
123
  filesystem model caches are disabled only for this explicit stored mode. The
118
124
  default upstream ONNX WASM binary and factory preloads use this same hook.
119
125
  Native support-file fetches in the dedicated Worker also use the store. The
120
- runtime module itself still loads from `runtime.moduleUrl` through normal module
121
- loading. Native ESM imports remain browser-owned: an upstream custom factory
122
- import outside its preload path is not claimed as a DBOPFS resource. No second
123
- application cache or model substitution is introduced.
126
+ default runtime entry is self-contained. Entry materialization does not rewrite
127
+ dependency imports in custom modules or preserve their original `import.meta.url`
128
+ base. Custom stored entries must work from an object URL; relative imports or
129
+ support-file URLs resolved against that module's original location require a
130
+ compatible self-contained entry. Native absolute ESM dependencies and an upstream
131
+ custom factory import outside its preload path remain browser-owned. No second
132
+ application cache or model substitution is introduced. Without `store`, the
133
+ configured runtime URL keeps its direct native module-loading behavior.
124
134
 
125
135
  The store returns `{file, status, statusText, headers, url, redirected}`. The
126
136
  Worker reconstructs the complete response with its actual HTTP status and
@@ -138,9 +148,14 @@ url}`. `completed` counts closed persistent shards; `total` is `null` until the
138
148
  resource finishes. This progress carries no network-size, rate or ETA fields.
139
149
  Cancellation aborts the owned fetch and joins reader/writer cleanup before the
140
150
  resource owner becomes available to a later request. Interrupted closed shards
141
- are retained. `unload()` and `dispose()` still acknowledge their lifecycle
142
- transition synchronously; outstanding `load()`/`evaluate()` calls settle after
143
- their resource cleanup.
151
+ are retained. Each activation owns its runtime object URL and revokes it after
152
+ terminating the Worker on cancellation, failure, unload or disposal. A cancelled
153
+ runtime-entry fetch is joined before its waiting calls settle; a late response
154
+ cannot activate a replacement Worker. Runtime-entry HTTP failures retain the
155
+ complete stored response in `error.response` for developer diagnostics.
156
+ `unload()` and `dispose()` still acknowledge their lifecycle transition
157
+ synchronously; outstanding `load()`/`evaluate()` calls settle after their
158
+ resource cleanup.
144
159
 
145
160
  ## Complete rows and results
146
161
 
@@ -1065,11 +1065,32 @@ owners. Only `'plain'` selects exact input; omission and other values retain
1065
1065
  the existing narration cleanup. Applications never supply the internal
1066
1066
  `speechInputPrepared` argument.
1067
1067
 
1068
- This selection governs SDK text treatment, not the selected engine's input
1069
- capacity or pronunciation. [Kokoro.js 1.2.1](https://github.com/hexgrad/kokoro/blob/664c76a704021239ba59c84dcbaa4d3dece01fe9/kokoro.js/src/kokoro.js)
1070
- still tokenizes with truncation in both `generate()` and `stream()`; plain input
1071
- does not establish complete audio for an oversized engine input. The SDK's existing complete-text forwarding
1072
- fixture does not prove upstream synthesis completeness.
1068
+ This selection governs SDK text treatment, not pronunciation. The browser Kokoro
1069
+ adapter preserves the selected runtime's own text normalization and phonemization,
1070
+ then uses a genuine tokenizer subclass with truncation disabled. Its Kokoro
1071
+ subclass sends every resulting content token through the public
1072
+ `generate_from_ids()` method. Short inputs retain one inference; longer inputs
1073
+ use ordered segments within the selected engine's inference capacity and return
1074
+ all audio in order. This is token-based engine transport, not a character limit,
1075
+ text estimate, summary, or input discard.
1076
+
1077
+ The [selected Kokoro model](https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/blob/1939ad2a8e416c0acfeecc08a694d14ef25f2231/README.md#python)
1078
+ accepts 510 content tokens plus its two outer padding positions per inference.
1079
+ Each segment receives that padding independently. Upstream voice-style selection,
1080
+ speed, waveform generation and sample rate remain upstream-owned. The SDK reuses
1081
+ one loaded model; it does not copy the private phonemizer, replace instance methods,
1082
+ load a second model, or import another Transformers runtime. Its tokenizer
1083
+ reconstruction uses the same effective selection as the
1084
+ [Kokoro 1.2.1 factory](https://github.com/hexgrad/kokoro/blob/664c76a704021239ba59c84dcbaa4d3dece01fe9/kokoro.js/src/kokoro.js).
1085
+ That factory ignores a `revision` option; this change does not establish revision
1086
+ pinning that the upstream factory does not implement.
1087
+
1088
+ Cancellation is request-local and checked between inference segments and before
1089
+ returning audio. As with the existing Worker contract, cancellation prevents later
1090
+ work and output while an already-running upstream inference may finish. Unload
1091
+ and disposal retain their existing Worker-termination boundary. Source and
1092
+ controlled-fixture coverage of token/audio preservation are distinct from speech
1093
+ quality or execution evidence for a particular model, browser and device.
1073
1094
 
1074
1095
  ## Choose a device or reduce memory use
1075
1096
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.71.1",
3
+ "version": "0.71.2",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -123,7 +123,7 @@
123
123
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
124
124
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
125
125
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
126
- "test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
126
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
127
127
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
128
128
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
129
129
  "check": "node tools/check-source.mjs && npm test",