arcane-os 0.5.14 → 0.5.15

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
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.15
6
+
7
+ - Let `SpeechPlayback.prepare()` submit every complete segment immediately when
8
+ an `AI.fetchTTS` client advertises positive TTS execution capacity. The AI
9
+ provider queue retains bounded FIFO admission (four synthesis slots by
10
+ default), while indexed Blob URLs keep playback in exact input order even
11
+ when later segments finish first.
12
+ - Preserve the serialized one-segment-lookahead path for native and custom
13
+ speech clients that do not advertise provider execution capacity. Pause and
14
+ Resume keep the same audio element; Stop aborts all owned synthesis; Replay
15
+ retains completed and pending provider segments while retrying only failed
16
+ missing segments, including failures with falsy rejection values.
17
+ - Add basic copyable `SpeechPlayback` examples and synchronize the installed
18
+ reference, current release identities, generated documentation site, and
19
+ focused behavioral contract source for the new admission model.
20
+
5
21
  ## 0.5.14
6
22
 
7
23
  - Add the shared `arcane-os/logging` console owner using the existing
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.5.12` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.15` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.5.12 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.5.15 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -103,6 +103,31 @@ order, but playback waits for earlier segments and plays exact input order.
103
103
  Each slot owns a Worker/model session, so raising capacity trades memory for
104
104
  latency.
105
105
 
106
+ If your app already has complete segments in an array, let the shared
107
+ `SpeechPlayback` owner submit them as soon as they are available:
108
+
109
+ ```javascript
110
+ import SpeechPlayback from 'arcane/SpeechPlayback';
111
+
112
+ const audio = document.body.appendChild(document.createElement('audio'));
113
+ audio.controls = true;
114
+ const narration = new SpeechPlayback({audio, speech: ai});
115
+ const speakAll = document.body.appendChild(document.createElement('button'));
116
+ speakAll.textContent = 'Speak all segments';
117
+ speakAll.addEventListener('click', async function speakAllSegments() {
118
+ await ai.setSpeechMuted(false);
119
+ await narration.prepare({
120
+ parts: ['First segment.', 'Second segment.', 'Third segment.'],
121
+ autoplay: true
122
+ });
123
+ });
124
+ ```
125
+
126
+ With browser speech's default `auto` / 4 configuration, all three complete
127
+ segments enter the provider queue immediately, up to four synthesize at once,
128
+ and the audio still plays first, second, third. Native or custom speech without
129
+ advertised provider execution capacity stays serialized with one lookahead.
130
+
106
131
  Continue with [streaming chunks, device selection, cancellation, status, and cleanup](https://github.com/TheWizardNexus/arcane-os-sdk/blob/main/docs/reference/ai/browser-speech.md),
107
132
  the [tiny TWiN Cloud request and saved-preference migration](https://github.com/TheWizardNexus/arcane-os-sdk/blob/main/docs/reference/ai/twin-cloud.md),
108
133
  or the [maintained WASM voice-chat example](https://github.com/TheWizardNexus/arcane-os-sdk/tree/main/examples/wasm-ai-demo).
@@ -297,7 +322,7 @@ uses the same controller for automatic memory extraction.
297
322
  Create a new repository-shaped Arcane application with the exact stable SDK:
298
323
 
299
324
  ```bash
300
- npx arcane-os@0.5.12 new my-app --path ./my-app --target portable --git
325
+ npx arcane-os@0.5.15 new my-app --path ./my-app --target portable --git
301
326
  cd my-app
302
327
  npm install
303
328
  npm run dev
@@ -307,7 +332,7 @@ To enroll an existing repository, install the exact SDK and initialize only
307
332
  missing Arcane files:
308
333
 
309
334
  ```bash
310
- npm install --save-dev --save-exact arcane-os@0.5.12
335
+ npm install --save-dev --save-exact arcane-os@0.5.15
311
336
  npm exec -- arcane init my-app --target portable
312
337
  ```
313
338
 
@@ -323,7 +348,7 @@ npm exec -- arcane-os targets
323
348
  No global SDK install or standalone Arcane CLI is required. The application
324
349
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
325
350
 
326
- Use `npx arcane-os@0.5.12` for the initial bootstrap because it names this npm
351
+ Use `npx arcane-os@0.5.15` for the initial bootstrap because it names this npm
327
352
  package explicitly; bare `npx arcane` outside an installed project could resolve
328
353
  a different package. Both installed commands invoke the same headless toolchain.
329
354
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -343,7 +368,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
343
368
 
344
369
  # From the generated app repository
345
370
  cd ../local-app
346
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.12.tgz
371
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.15.tgz
347
372
  npm ci
348
373
  ```
349
374
 
@@ -352,7 +377,7 @@ same location. The lockfile retains the selected package dependency while
352
377
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
353
378
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
354
379
  runner also needs that tarball at the locked path. After publication, replace
355
- the local declaration with the exact `arcane-os@0.5.12` registry package and
380
+ the local declaration with the exact `arcane-os@0.5.15` registry package and
356
381
  commit the regenerated lock.
357
382
 
358
383
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -491,7 +516,7 @@ package installation, or assertions.
491
516
 
492
517
  ## Current target support
493
518
 
494
- Version `0.5.12` exposes one browser target and five explicitly paired
519
+ Version `0.5.15` exposes one browser target and five explicitly paired
495
520
  native development targets: a non-runnable portable directory, a
496
521
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
497
522
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -279,7 +279,7 @@ paths are withheld from the native provider. The provider copies the complete
279
279
  selected release rather than accepting an unrelated source path. Verification
280
280
  is a separate explicit operation for a selected release artifact.
281
281
 
282
- The SDK `0.5.12` runtime requires Arcane `0.8.12` or newer. Compatibility
282
+ The SDK `0.5.15` runtime requires Arcane `0.8.12` or newer. Compatibility
283
283
  is contractual rather than exact-version pinning: the prepared Core must meet
284
284
  the highest minimum declared by the runtime, selected app, and bundled app
285
285
  dependencies; keep each app's Arcane protocol generation; and provide every
@@ -17,7 +17,7 @@ high-level page links to the relevant deep section instead of repeating it.
17
17
  Install the SDK in your application:
18
18
 
19
19
  ```sh
20
- npm install --save-exact arcane-os@0.5.14
20
+ npm install --save-exact arcane-os@0.5.15
21
21
  ```
22
22
 
23
23
  For your first AI call, follow the [TWiN Cloud quick start](ai/twin-cloud.md).
@@ -41,6 +41,7 @@ alone does not make bare module names resolve in a browser.
41
41
  | Import a shipped renderer module | [Runtime module catalog](runtime-modules.md) |
42
42
  | Use a shared entity | [Runtime entity modules](runtime-entities.md) and [exact export contracts](core/arcane-entities.md) |
43
43
  | Load a reusable HTML component | [Runtime component catalog](runtime-components.md) |
44
+ | Submit complete speech parts immediately and play them in order | [`SpeechPlayback`](runtime-modules.md#speechplaybackjs) and the [basic browser example](ai/browser-speech.md#play-a-complete-array-with-speechplayback) |
44
45
  | Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
45
46
  | Subscribe to native events | [Arcane event reference](core/arcane-events.md) |
46
47
  | Use provider-neutral AI lifecycle, chat, speech, persistence, or document context | [Normalized AI](#normalized-ai) |
@@ -57,9 +58,9 @@ This repository contains explicitly versioned surfaces with different owners:
57
58
 
58
59
  | Surface | Source identity | Meaning |
59
60
  | --- | --- | --- |
60
- | SDK and CLI | `arcane-os` `0.5.14` | The Node.js toolchain, portable `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`, `arcane-os/preference-store`, and `arcane-os/speech-playback` entrypoints, plus the browser-only `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints in this checkout. |
61
- | Browser runtime | SDK `0.5.14`, protocol `arcane/1`, `runtime/` | The SDK-canonical runtime tree. `listRuntimeFiles()`, `readRuntimeFile()`, and `loadRuntimeRelease()` derive its current inventory directly from the selected directory. |
62
- | Browser SDK runtime | SDK `0.5.14`, `browser-runtime/` | The browser closure for events, shared logging, Wllama, and Browser Speech mechanisms. `listSdkBrowserRuntimeFiles()`, `readSdkBrowserRuntimeFile()`, and `loadSdkBrowserRuntimeRelease()` derive its current inventory directly from the selected directory. |
61
+ | SDK and CLI | `arcane-os` `0.5.15` | The Node.js toolchain, portable `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`, `arcane-os/preference-store`, and `arcane-os/speech-playback` entrypoints, plus the browser-only `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints in this checkout. |
62
+ | Browser runtime | SDK `0.5.15`, protocol `arcane/1`, `runtime/` | The SDK-canonical runtime tree. `listRuntimeFiles()`, `readRuntimeFile()`, and `loadRuntimeRelease()` derive its current inventory directly from the selected directory. |
63
+ | Browser SDK runtime | SDK `0.5.15`, `browser-runtime/` | The browser closure for events, shared logging, Wllama, and Browser Speech mechanisms. `listSdkBrowserRuntimeFiles()`, `readSdkBrowserRuntimeFile()`, and `loadSdkBrowserRuntimeRelease()` derive its current inventory directly from the selected directory. |
63
64
  | Core reference snapshot | Arcane OS commit `567ad110bf57a1c2d4a3daa22ae93716cc5f4d7e`, protocol `arcane/1` | The application-facing Core contract imported into `docs/reference/core/`, with SDK-local links and package-boundary notes added explicitly. |
64
65
 
65
66
  The SDK runtime source and Core reference have different owners. A browser
@@ -74,7 +75,7 @@ and the distinction between a documentation snapshot and the selected runtime.
74
75
 
75
76
  ## Installed documentation and release identity
76
77
 
77
- This reference accompanies `arcane-os@0.5.14`. The installed package includes
78
+ This reference accompanies `arcane-os@0.5.15`. The installed package includes
78
79
  the maintained `docs/` tree and `examples/wasm-ai-demo/` source alongside
79
80
  README and CHANGELOG. Open `node_modules/arcane-os/docs/reference/README.md`
80
81
  for the matching local reference. The generated website and test suites remain
@@ -13,7 +13,7 @@ import map resolves `arcane/AI` and `arcane/DBOPFS`. These browser modules are
13
13
  not Node inference APIs. To create an application:
14
14
 
15
15
  ```bash
16
- npx arcane-os@0.5.14 new hello-speech --path ./hello-speech --target browser
16
+ npx arcane-os@0.5.15 new hello-speech --path ./hello-speech --target browser
17
17
  cd hello-speech
18
18
  npm install
19
19
  npm run dev
@@ -180,6 +180,50 @@ Ready adjacent audio buffers use contiguous AudioContext scheduling. Browser
180
180
  audio scheduling and selected WebGPU status do not prove physical GPU kernel
181
181
  overlap or audio quality. LLM and Whisper/STT capacity remains one.
182
182
 
183
+ ## Play a complete array with `SpeechPlayback`
184
+
185
+ Use this when your application already has complete segments in an array. The
186
+ configured `ai` below is the same instance created in the quick start.
187
+
188
+ ```javascript
189
+ import SpeechPlayback from 'arcane/SpeechPlayback';
190
+
191
+ const audio = document.body.appendChild(document.createElement('audio'));
192
+ audio.controls = true;
193
+ const narration = new SpeechPlayback({audio, speech: ai});
194
+ const speakAll = document.body.appendChild(document.createElement('button'));
195
+ speakAll.textContent = 'Speak all segments';
196
+ speakAll.addEventListener('click', async function speakAllSegments() {
197
+ await ai.setSpeechMuted(false);
198
+ await narration.prepare({
199
+ parts: [
200
+ 'First complete segment.',
201
+ 'Second complete segment.',
202
+ 'Third complete segment.'
203
+ ],
204
+ autoplay: true
205
+ });
206
+ });
207
+ ```
208
+
209
+ `prepare()` submits all three parts immediately because this `ai` exposes
210
+ `fetchTTS` and advertises TTS execution capacity. With the default configuration,
211
+ the provider admits up to four synthesis requests and keeps later requests in
212
+ its FIFO queue. Completed audio remains indexed, so playback is always first,
213
+ second, third even if the third synthesis finishes first.
214
+
215
+ This eager path is capability-driven. A native `Arcane.speech.synthesize`
216
+ client, or a custom client without a positive advertised TTS execution capacity,
217
+ retains serialized synthesis with one lookahead segment. `togglePause()` pauses
218
+ and resumes the same `audio` element. `stop()` cancels all requests owned by the
219
+ playback. `replay()` keeps completed and pending provider segments and retries
220
+ only failed missing segments.
221
+
222
+ The default `{device:'auto',maxConcurrentRequests:4}` attempts the full ONNX
223
+ Worker/session pool on WebGPU, then recreates that pool on WASM only if WebGPU
224
+ loading fails. Use the status example below to read `selectedDevice`; console
225
+ node-assignment warnings alone do not identify the selected execution device.
226
+
183
227
  ## Queue complete passages and wait for playback
184
228
 
185
229
  These options are available in SDK `0.5.12`.
@@ -9,7 +9,7 @@ same managed browser imports as the [browser speech quick start](browser-speech.
9
9
  Create an application and start its source server:
10
10
 
11
11
  ```bash
12
- npx arcane-os@0.5.12 new hello-twin --path ./hello-twin --target browser
12
+ npx arcane-os@0.5.15 new hello-twin --path ./hello-twin --target browser
13
13
  cd hello-twin
14
14
  npm install
15
15
  npm run dev
@@ -214,7 +214,7 @@ portable runtime subpaths such as `arcane-os/preference-store` and
214
214
  modules. The result reports the complete map written to the selected
215
215
  application; no fixed entry count is a release contract.
216
216
 
217
- SDK `0.5.12` preserves the physical workspace route count and ordered include
217
+ SDK `0.5.15` preserves the physical workspace route count and ordered include
218
218
  list. External and modern integrated routes require `components`, `css`,
219
219
  `dependencies`, `entities`, `img`, `modules`, and `sdk`; a physical workspace
220
220
  may omit only an optional trailing `security` include. The external license
@@ -9,7 +9,7 @@ They are not TypeScript declarations.
9
9
 
10
10
  ## Portable SDK AI and Core AI
11
11
 
12
- The SDK `0.5.12` has two related but separate normalized boundaries:
12
+ The SDK `0.5.15` has two related but separate normalized boundaries:
13
13
 
14
14
  | Boundary | Use | Host |
15
15
  |---|---|---|
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.5.14",
3
+ "sdkVersion": "0.5.15",
4
4
  "environment": {
5
5
  "runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
@@ -1392,10 +1392,10 @@
1392
1392
  "entrypoints": ["arcane-os/speech-playback"],
1393
1393
  "primaryImport": "arcane-os/speech-playback",
1394
1394
  "group": "Portable runtime modules",
1395
- "summary": "Default binding for the canonical SpeechPlayback runtime class in Node and managed browsers.",
1395
+ "summary": "Default binding for the canonical SpeechPlayback runtime class, with capability-gated eager provider submission and exact-order playback.",
1396
1396
  "availability": "Node with injected media adapters, or browser/native WebView media",
1397
1397
  "protocol": "SpeechPlayback runtime contract",
1398
- "normalization": "Binding identity and namespace come directly from the canonical runtime module"
1398
+ "normalization": "Binding identity and namespace come directly from the canonical runtime module; a fetchTTS client with positive advertised TTS execution capacity receives all complete segments immediately while its provider owns bounded admission and SpeechPlayback retains indexed order; other clients remain serialized"
1399
1399
  },
1400
1400
  {
1401
1401
  "id": "speech-playback:SPEECH_PLAYBACK_STATE_EVENT",
@@ -1420,10 +1420,10 @@
1420
1420
  "entrypoints": ["arcane-os/speech-playback"],
1421
1421
  "primaryImport": "arcane-os/speech-playback",
1422
1422
  "group": "Portable runtime modules",
1423
- "summary": "Named binding for the same canonical class exposed as the subpath default.",
1423
+ "summary": "Named binding for the same capability-aware, exact-order canonical class exposed as the subpath default.",
1424
1424
  "availability": "Node with injected media adapters, or browser/native WebView media",
1425
1425
  "protocol": "SpeechPlayback runtime contract",
1426
- "normalization": "Binding identity equals the default export; prepare preserves each nonblank part input exactly without trimming, splitting, or freezing it; provider and media availability remain host-owned"
1426
+ "normalization": "Binding identity equals the default export; prepare preserves each nonblank part input exactly without trimming, splitting, or freezing it; provider-advertised capacity enables eager submission while the provider owns its bound, native and custom clients remain serialized, and media availability remains host-owned"
1427
1427
  },
1428
1428
  {
1429
1429
  "id": "speech-playback:splitSpeechText",
@@ -5,7 +5,7 @@
5
5
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
6
6
  "branch": "main",
7
7
  "path": "runtime/arcane/components",
8
- "sdkVersion": "0.5.12"
8
+ "sdkVersion": "0.5.15"
9
9
  },
10
10
  "componentCount": 39,
11
11
  "loader": "/arcane/modules/HTMLImport.js",
@@ -5,7 +5,7 @@
5
5
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
6
6
  "branch": "main",
7
7
  "path": "runtime/arcane",
8
- "sdkVersion": "0.5.12",
8
+ "sdkVersion": "0.5.15",
9
9
  "protocol": "arcane/1"
10
10
  },
11
11
  "artifactCount": 84,
@@ -1153,10 +1153,10 @@
1153
1153
  "default",
1154
1154
  "splitSpeechText"
1155
1155
  ],
1156
- "summary": "Preserves exact nonblank text as one segment without trimming, splitting, or freezing it; queues latest-request synthesis and controls lookahead HTML audio playback.",
1156
+ "summary": "Preserves exact nonblank segment text without trimming, splitting, or freezing it; eagerly submits complete parts to a capacity-advertising fetchTTS provider while keeping playback indexed, and retains serialized one-segment lookahead for other clients.",
1157
1157
  "availability": "Browser + compatible AI/native bridge",
1158
- "protocol": "AI.fetchTTS or compatible Arcane.speech.synthesize, globalThis.arcaneEvents, Blob URLs, audio element",
1159
- "normalization": "Exact nonblank split/part input strings are preserved in mutable records while playback policy, cancellation, Blob results, and lifecycle state are normalized; provider failures remain external.",
1158
+ "protocol": "AI.fetchTTS plus providerRuntime TTS execution capacity, or compatible serialized Arcane.speech.synthesize; globalThis.arcaneEvents, Blob URLs, audio element",
1159
+ "normalization": "Exact nonblank split/part input strings are preserved in mutable records. A capable provider receives complete segments immediately and owns bounded FIFO admission; URLs and playback stay in exact input order. Native/custom clients remain serialized. Cancellation, Replay recovery, Blob results, and lifecycle state are normalized; provider failures remain external.",
1160
1160
  "surface": "SpeechPlayback class/default, `SPEECH_PLAYBACK_STATE_EVENT`, `splitSpeechText()`, canonical playback lifecycle events, cancellation, and destroy APIs."
1161
1161
  },
1162
1162
  {
@@ -159,7 +159,7 @@ imports such as:
159
159
  import ollama from 'arcane/Ollama';
160
160
  ```
161
161
 
162
- The physical-v1 tree lives entirely beneath `arcane/`. SDK `0.5.12` projects the
162
+ The physical-v1 tree lives entirely beneath `arcane/`. SDK `0.5.15` projects the
163
163
  complete canonical runtime and browser runtime selected by the installed SDK
164
164
  package. Runtime dependencies stay under
165
165
  `arcane/dependencies/`; the SDK event and browser-AI closure stays under
@@ -174,7 +174,7 @@ integrated physical route uses the same ordered include list in its one route.
174
174
  Omitting only that final optional entry is compatible. Removing, reordering, or
175
175
  renaming any preceding entry changes the physical contract.
176
176
 
177
- The `0.5.12` map derives its complete entries from the selected runtime graph;
177
+ The `0.5.15` map derives its complete entries from the selected runtime graph;
178
178
  application source imports do not select a fixed entry count. The operation
179
179
  result reports `imports`, `entryCount`, and `excludedModules`; reached-file
180
180
  traversal remains internal. The
@@ -239,8 +239,8 @@ exactly `dependencyName`, `packageSource`,
239
239
  `canonicalPackageRoot`, `packageName`, `packageVersion`, `runtimeRoot`,
240
240
  and `browserRuntimeRoot`. A
241
241
  workspace may use the canonical dependency name or one exact npm alias such as
242
- `npm:arcane-os@0.5.12`; the physical package manifest must still identify
243
- exactly as `arcane-os@0.5.12`. Canonical-plus-alias duplicates, multiple aliases,
242
+ `npm:arcane-os@0.5.15`; the physical package manifest must still identify
243
+ exactly as `arcane-os@0.5.15`. Canonical-plus-alias duplicates, multiple aliases,
244
244
  links/junctions, indirect package roots, or version drift are reported.
245
245
 
246
246
  For external workspaces, `arcane dev` serves the projected `arcane/` root,
@@ -351,7 +351,7 @@ does not silently delete the app-owned cache. A complete whole member supersedes
351
351
  its current resumable fragments. Cleanup failure is warned without hiding the
352
352
  usable model.
353
353
 
354
- SDK `0.5.12` requires WebGPU. Load requests full offload and waits for the runtime
354
+ SDK `0.5.15` requires WebGPU. Load requests full offload and waits for the runtime
355
355
  to report a loaded model. `navigator.gpu` presence alone is
356
356
  not readiness. There is no CPU fallback, partial-offload success mode, or
357
357
  silent switch to native/Core/cloud inference.
@@ -2849,8 +2849,9 @@ console.log(Object.keys(module));
2849
2849
 
2850
2850
  ### Overview
2851
2851
 
2852
- Preserves exact nonblank text as one segment, queues latest-request speech
2853
- synthesis, and controls lookahead HTML audio playback.
2852
+ Preserves exact nonblank text, admits complete speech segments according to the
2853
+ selected client's advertised capacity, and plays indexed HTML audio in exact
2854
+ input order.
2854
2855
 
2855
2856
  ### Public surface
2856
2857
 
@@ -2888,6 +2889,26 @@ freezing it. `prepare()` likewise preserves each nonblank part's exact `input`
2888
2889
  string while normalizing its other playback fields into a new mutable record.
2889
2890
  The class applies no part-count, character-count, pause, or input upper cap.
2890
2891
 
2892
+ ### Admission and playback order
2893
+
2894
+ When `speech` exposes both `fetchTTS(payload, signal)` and
2895
+ `providerRuntime.status('tts', {execution:true}).execution.maxConcurrentRequests`
2896
+ as a positive safe integer, `prepare()` submits every complete `parts` entry
2897
+ immediately. `SpeechPlayback` does not create a second limiter: the provider
2898
+ runtime owns bounded FIFO admission. Browser Kokoro defaults that capacity to
2899
+ four, so up to four segments can synthesize while later submissions wait in the
2900
+ provider queue. A later segment may finish first, but its Blob URL stays at its
2901
+ original index and is never played ahead of an earlier segment.
2902
+
2903
+ If that capacity is absent, invalid, or unavailable, `SpeechPlayback` retains
2904
+ the compatible serialized path and prepares only one lookahead segment. This is
2905
+ the default for `Arcane.speech.synthesize` and custom clients, so native hosts
2906
+ with one synthesis lock are not driven concurrently. Pause and Resume control
2907
+ the same supplied `audio` element. Stop aborts every owned synthesis signal and
2908
+ releases prepared URLs. Replay keeps completed URLs and still-pending provider
2909
+ work, then re-submits only failed missing provider segments before starting
2910
+ again from index zero.
2911
+
2891
2912
  Every preparation owns an operation ID and one AbortController for each active
2892
2913
  synthesis segment or playback delay. Replacement,
2893
2914
  `stop()`, `cancel()`, and `destroy()` abort their owned signals, suppress stale
@@ -2895,9 +2916,10 @@ settlement, release Blob URLs, and publish synchronous
2895
2916
  `speech-playback-state` occurrences through `globalThis.arcaneEvents`.
2896
2917
  Subscribers receive mutable public state detail. The detail contains
2897
2918
  `state`, `message`, `key`, `index`, `total`, `producing`, `buffered`, `hasAudio`,
2898
- `operationId`, `code`, and `reason`; provider rejection remains preserved to the
2899
- `prepare()` caller. `destroy()` also removes every audio listener and disposes
2900
- its per-instance canonical source handle; repeated destroy returns
2919
+ `operationId`, `code`, and `reason`; a first-segment provider rejection remains
2920
+ preserved to the `prepare()` caller. Later failures surface when ordered
2921
+ playback reaches that segment. `destroy()` also removes every audio listener
2922
+ and disposes its per-instance canonical source handle; repeated destroy returns
2901
2923
  `false`. Signal abortion proves delivery suppression; whether provider work
2902
2924
  actually stops remains the selected provider's cancellation boundary.
2903
2925
 
@@ -2934,23 +2956,38 @@ const audio = document.body.appendChild(document.createElement('audio'));
2934
2956
  audio.controls = true;
2935
2957
  const speech = new SpeechPlayback({
2936
2958
  audio,
2937
- speech: globalThis.ai,
2938
- model: 'caller-selected-model',
2939
- voice: 'caller-selected-voice',
2940
- responseFormat: 'wav'
2959
+ speech: globalThis.ai
2941
2960
  });
2942
- const speakButton = document.body.appendChild(document.createElement('button'));
2943
- speakButton.type = 'button';
2944
- speakButton.textContent = 'Speak';
2945
- speakButton.addEventListener('click', async () => {
2961
+ const button = document.body.appendChild(document.createElement('button'));
2962
+ button.textContent = 'Speak';
2963
+ button.addEventListener('click', async function speakCompleteSegments() {
2964
+ await globalThis.ai.setSpeechMuted(false);
2946
2965
  await speech.prepare({
2947
- key: 'ready',
2948
- parts: ['Arcane is ready.'],
2966
+ parts: [
2967
+ 'First complete segment.',
2968
+ 'Second complete segment.'
2969
+ ],
2949
2970
  autoplay: true
2950
2971
  });
2951
2972
  });
2952
2973
  ```
2953
2974
 
2975
+ With the default browser speech configuration, both parts enter its capacity-4
2976
+ queue immediately and still play first, then second. Inspect the selected
2977
+ execution device without guessing from console warnings:
2978
+
2979
+ ```javascript
2980
+ const execution = globalThis.ai.providerRuntime.status(
2981
+ 'tts',
2982
+ { execution: true }
2983
+ ).execution;
2984
+ console.log(execution.selectedDevice, execution.maxConcurrentRequests);
2985
+ ```
2986
+
2987
+ `requestedDevice:'auto'` attempts the complete ONNX Worker/session pool on
2988
+ WebGPU and recreates it on WASM if WebGPU loading fails. The reported selected
2989
+ device proves provider selection, not physical GPU kernel overlap.
2990
+
2954
2991
  ## StaticDocumentCatalog.js
2955
2992
 
2956
2993
  ### Overview
@@ -18,7 +18,7 @@ This table is the Node `package.json#exports` map: it defines package
18
18
  entrypoints for SDK/tooling code. It is distinct from the generated browser
19
19
  import map that resolves application-facing `arcane/*` modules and the focused
20
20
  EventManager entry. See [browser runtime delivery](protocols.md#browser-runtime-delivery)
21
- for the installed-inventory-derived physical-runtime contract in SDK `0.5.14`.
21
+ for the installed-inventory-derived physical-runtime contract in SDK `0.5.15`.
22
22
 
23
23
  | Specifier | Purpose |
24
24
  | --- | --- |
@@ -751,7 +751,7 @@ deterministic map. The package root also contains the public
751
751
  {
752
752
  schemaVersion: 1,
753
753
  kind: 'arcane-app-runtime-projection',
754
- sdkVersion: '0.5.14',
754
+ sdkVersion: '0.5.15',
755
755
  pathPrefix: 'arcane/',
756
756
  files: [{path}]
757
757
  }
@@ -1500,6 +1500,14 @@ specifier `arcane-os/speech-playback` works in Node package consumers and maps
1500
1500
  to the same runtime module in managed browsers. `arcane/SpeechPlayback` is the
1501
1501
  direct browser runtime name generated into the managed import map.
1502
1502
 
1503
+ When the supplied `speech` client has `fetchTTS` and reports a positive
1504
+ `providerRuntime.status('tts', {execution:true}).execution.maxConcurrentRequests`,
1505
+ `prepare()` submits every complete part immediately. The provider owns its
1506
+ bounded FIFO queue (browser Kokoro defaults to four), while Blob URLs and
1507
+ playback remain in exact input order. A native or custom client without that
1508
+ advertised capacity stays serialized with one lookahead. Replay keeps completed
1509
+ URLs and pending requests while retrying failed missing provider segments.
1510
+
1503
1511
  ### Signature and result
1504
1512
 
1505
1513
  ```text
@@ -1516,7 +1524,18 @@ runtime module; provider and media availability remain host-owned.
1516
1524
 
1517
1525
  ```javascript
1518
1526
  import SpeechPlayback from 'arcane-os/speech-playback';
1519
- const playback=new SpeechPlayback({audio,speech});
1527
+
1528
+ const audio=document.body.appendChild(document.createElement('audio'));
1529
+ const playback=new SpeechPlayback({audio,speech:globalThis.ai});
1530
+ const button=document.body.appendChild(document.createElement('button'));
1531
+ button.textContent='Speak';
1532
+ button.addEventListener('click',async function speakCompleteSegments(){
1533
+ await globalThis.ai.setSpeechMuted(false);
1534
+ await playback.prepare({
1535
+ parts:['First complete segment.','Second complete segment.'],
1536
+ autoplay:true
1537
+ });
1538
+ });
1520
1539
  ```
1521
1540
 
1522
1541
  ## SPEECH_PLAYBACK_STATE_EVENT
@@ -1546,7 +1565,9 @@ console.log(SPEECH_PLAYBACK_STATE_EVENT);
1546
1565
 
1547
1566
  ### Overview
1548
1567
 
1549
- Named binding for the same canonical class exposed as the speech-playback default. `prepare()` preserves each nonblank part's exact input string without trimming, splitting, or freezing that content.
1568
+ Named binding for the same capability-aware, exact-order canonical class exposed
1569
+ as the speech-playback default. `prepare()` preserves each nonblank part's exact
1570
+ input string without trimming, splitting, or freezing that content.
1550
1571
 
1551
1572
  ### Signature and result
1552
1573
 
@@ -1557,7 +1578,9 @@ new SpeechPlayback(options={})
1557
1578
  ### Availability and normalization
1558
1579
 
1559
1580
  **Node with injected media adapters, or browser/native WebView media.** Binding
1560
- identity equals the default export; provider and media availability remain host-owned.
1581
+ identity equals the default export. Provider-advertised capacity enables eager
1582
+ submission while that provider owns its bound; native and custom speech stays
1583
+ serialized, and media availability remains host-owned.
1561
1584
 
1562
1585
  ### Example
1563
1586
 
@@ -3440,7 +3463,7 @@ workspace it additionally returns the exact installed package authority:
3440
3463
  packageSource,
3441
3464
  canonicalPackageRoot,
3442
3465
  packageName: 'arcane-os',
3443
- packageVersion: '0.5.14',
3466
+ packageVersion: '0.5.15',
3444
3467
  runtimeRoot,
3445
3468
  browserRuntimeRoot
3446
3469
  }
@@ -3448,9 +3471,9 @@ workspace it additionally returns the exact installed package authority:
3448
3471
  ```
3449
3472
 
3450
3473
  The dependency can be named `arcane-os` or be one exact npm alias for
3451
- `npm:arcane-os@0.5.14`. The selected installation must still be one direct,
3474
+ `npm:arcane-os@0.5.15`. The selected installation must still be one direct,
3452
3475
  physical, non-link package directory whose manifest identifies exactly as
3453
- `arcane-os@0.5.14`; duplicate canonical/alias declarations reject.
3476
+ `arcane-os@0.5.15`; duplicate canonical/alias declarations reject.
3454
3477
  `allowMissingManagedImportMap` is an internal packaging/development seam. An
3455
3478
  ordinary caller should leave it `false`.
3456
3479
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
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",
@@ -64,7 +64,7 @@
64
64
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
65
65
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
66
66
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/contracts.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/targets.test.mjs test/workspace-operation-lock.test.mjs",
67
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/site.test.mjs test/update-check.test.mjs",
67
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.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/update-check.test.mjs",
68
68
  "test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.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/wllama-webgpu-runtime.test.mjs",
69
69
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/logging-regression.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
70
70
  "check": "node tools/check-source.mjs && npm test",
@@ -289,6 +289,90 @@ function queueFor(speech){
289
289
  return queue;
290
290
  }
291
291
 
292
+ function providerSpeechCapacity(speech){
293
+ if(typeof speech?.fetchTTS!=='function')return null;
294
+ try{
295
+ const providerRuntime=speech.providerRuntime;
296
+ if(!providerRuntime||typeof providerRuntime.status!=='function')return null;
297
+ const capacity=providerRuntime.status(
298
+ 'tts',
299
+ {execution:true}
300
+ )?.execution?.maxConcurrentRequests;
301
+ return Number.isSafeInteger(capacity)&&capacity>0?capacity:null;
302
+ }catch{
303
+ // Clients without inspectable provider capacity retain serialized admission.
304
+ return null;
305
+ }
306
+ }
307
+
308
+ function startProviderSegment(playback,index,providerSynthesisBatch){
309
+ if(
310
+ providerSynthesisBatch!==playback.providerSynthesisBatch
311
+ ||index<0
312
+ ||index>=playback.parts.length
313
+ ||playback.urls[index]
314
+ )return null;
315
+ const existing=playback.segmentRequests.get(index);
316
+ if(existing?.providerSynthesisBatch===providerSynthesisBatch){
317
+ return existing;
318
+ }
319
+
320
+ playback.segmentErrors.delete(index);
321
+ const record={providerSynthesisBatch,index,promise:null};
322
+ record.promise=playback.synthesizeSegment(
323
+ index,
324
+ playback.generation,
325
+ index===0,
326
+ providerSynthesisBatch
327
+ ).then(
328
+ function storeProviderSynthesizedSegment(outcome){
329
+ if(outcome===SUPERSEDED)return {ready:false,superseded:true};
330
+ if(providerSynthesisBatch!==playback.providerSynthesisBatch){
331
+ playback.revokeObjectURL(outcome.url);
332
+ return {ready:false,superseded:true};
333
+ }
334
+ playback.urls[index]=outcome.url;
335
+ return {ready:true};
336
+ },
337
+ function handleProviderSegmentFailure(error){
338
+ if(providerSynthesisBatch===playback.providerSynthesisBatch){
339
+ playback.segmentErrors.set(
340
+ index,
341
+ {providerSynthesisBatch,index,error}
342
+ );
343
+ }
344
+ return {ready:false,error};
345
+ }
346
+ ).finally(function clearSettledProviderSegment(){
347
+ if(playback.segmentRequests.get(index)===record){
348
+ playback.segmentRequests.delete(index);
349
+ }
350
+ }).catch(function observeProviderSegmentHandlingFailure(error){
351
+ if(providerSynthesisBatch===playback.providerSynthesisBatch){
352
+ playback.segmentErrors.set(
353
+ index,
354
+ {providerSynthesisBatch,index,error}
355
+ );
356
+ }
357
+ return {ready:false,error};
358
+ });
359
+ playback.segmentRequests.set(index,record);
360
+ return record;
361
+ }
362
+
363
+ function startProviderSynthesis(playback,providerSynthesisBatch){
364
+ let first=null;
365
+ for(let index=0;index<playback.parts.length;index+=1){
366
+ const record=startProviderSegment(
367
+ playback,
368
+ index,
369
+ providerSynthesisBatch
370
+ );
371
+ if(index===0)first=record;
372
+ }
373
+ return first;
374
+ }
375
+
292
376
  const DEFAULT_MESSAGES={
293
377
  idle:'Speech is ready when visual content is available.',
294
378
  queued:'Waiting for the current local speech request. The latest narration will begin next.',
@@ -364,6 +448,9 @@ class SpeechPlayback{
364
448
  this.pendingGenerations=new Set();
365
449
  this.lookahead=null;
366
450
  this.lookaheadError=null;
451
+ this.providerSynthesisBatch=null;
452
+ this.segmentRequests=new Map();
453
+ this.segmentErrors=new Map();
367
454
  this.model=model===null||model===undefined?null:String(model);
368
455
  this.voice=voice===null||voice===undefined?null:String(voice);
369
456
  this.responseFormat=responseFormat===null||responseFormat===undefined
@@ -441,7 +528,12 @@ class SpeechPlayback{
441
528
  );
442
529
  }
443
530
 
444
- hasAudio(key=this.key){return Boolean(this.urls.some(Boolean)&&(!key||key===this.key));}
531
+ hasAudio(key=this.key){
532
+ const playable=this.providerSynthesisBatch
533
+ ?Boolean(this.urls[0])
534
+ :this.urls.some(Boolean);
535
+ return Boolean(playable&&(!key||key===this.key));
536
+ }
445
537
 
446
538
  releaseURLs(){
447
539
  for(const url of this.urls){
@@ -451,6 +543,9 @@ class SpeechPlayback{
451
543
  this.parts=[];
452
544
  this.lookahead=null;
453
545
  this.lookaheadError=null;
546
+ this.providerSynthesisBatch=null;
547
+ this.segmentRequests.clear();
548
+ this.segmentErrors.clear();
454
549
  }
455
550
 
456
551
  releaseUrls(){this.releaseURLs();}
@@ -496,39 +591,55 @@ class SpeechPlayback{
496
591
  throw error;
497
592
  }
498
593
 
499
- async synthesizeSegment(index,generation,announce=false){
594
+ async synthesizeSegment(
595
+ index,
596
+ generation,
597
+ announce=false,
598
+ providerSynthesisBatch=null
599
+ ){
500
600
  const playback=this;
501
601
  const controller=new AbortController();
502
602
  const token={generation,index,controller};
503
- const queue=queueFor(this.speech);
603
+ const queue=providerSynthesisBatch?null:queueFor(this.speech);
504
604
  this.pendingGenerations.add(token);
505
605
  this.abortControllers.add(controller);
606
+ function segmentRequestIsCurrent(){
607
+ return providerSynthesisBatch
608
+ ?providerSynthesisBatch===playback.providerSynthesisBatch
609
+ :generation===playback.generation;
610
+ }
611
+ async function synthesizeQueuedSegment(){
612
+ const part=playback.parts[index];
613
+ if(!segmentRequestIsCurrent()||!part){
614
+ controller.abort();
615
+ return SUPERSEDED;
616
+ }
617
+ if(announce){
618
+ playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
619
+ }
620
+ const audio=await playback.requestSpeech(
621
+ part,
622
+ controller.signal
623
+ );
624
+ if(!segmentRequestIsCurrent()){
625
+ controller.abort();
626
+ return SUPERSEDED;
627
+ }
628
+ const url=playback.createObjectURL(audio);
629
+ if(!segmentRequestIsCurrent()){
630
+ playback.revokeObjectURL(url);
631
+ controller.abort();
632
+ return SUPERSEDED;
633
+ }
634
+ return {url};
635
+ }
506
636
  try{
507
- return await queue.enqueue(async function synthesizeQueuedSegment(){
508
- const part=playback.parts[index];
509
- if(generation!==playback.generation||!part){
510
- controller.abort();
511
- return SUPERSEDED;
512
- }
513
- if(announce){
514
- playback.emit('synthesizing',playback.message('preparing',{count:playback.parts.length}));
515
- }
516
- const audio=await playback.requestSpeech(
517
- part,
518
- controller.signal
637
+ return providerSynthesisBatch
638
+ ?await Promise.resolve().then(synthesizeQueuedSegment)
639
+ :await queue.enqueue(
640
+ synthesizeQueuedSegment,
641
+ {speculative:!announce}
519
642
  );
520
- if(generation!==playback.generation){
521
- controller.abort();
522
- return SUPERSEDED;
523
- }
524
- const url=playback.createObjectURL(audio);
525
- if(generation!==playback.generation){
526
- playback.revokeObjectURL(url);
527
- controller.abort();
528
- return SUPERSEDED;
529
- }
530
- return {url};
531
- },{speculative:!announce});
532
643
  }finally{
533
644
  this.pendingGenerations.delete(token);
534
645
  this.abortControllers.delete(controller);
@@ -576,6 +687,19 @@ class SpeechPlayback{
576
687
  async waitForSegment(index,generation){
577
688
  if(generation!==this.generation)return false;
578
689
  if(this.urls[index])return true;
690
+ if(this.providerSynthesisBatch){
691
+ const segmentError=this.segmentErrors.get(index);
692
+ if(segmentError?.providerSynthesisBatch===this.providerSynthesisBatch){
693
+ return false;
694
+ }
695
+ const record=this.segmentRequests.get(index);
696
+ if(!record)return Boolean(this.urls[index]);
697
+ const result=await record.promise;
698
+ return generation===this.generation
699
+ &&record.providerSynthesisBatch===this.providerSynthesisBatch
700
+ &&result.ready===true
701
+ &&Boolean(this.urls[index]);
702
+ }
579
703
  if(
580
704
  this.lookaheadError
581
705
  &&this.lookaheadError.generation===generation
@@ -666,13 +790,28 @@ class SpeechPlayback{
666
790
  throw error;
667
791
  }
668
792
 
669
- const queue=queueFor(this.speech);
670
- if(queue.busy)this.emit('synthesizing',this.message('queued',{count:normalized.length}));
671
793
  let outcome;
672
794
  try{
673
- outcome=await this.synthesizeSegment(0,generation,true);
795
+ const providerCapacity=providerSpeechCapacity(this.speech);
796
+ if(providerCapacity===null){
797
+ const queue=queueFor(this.speech);
798
+ if(queue.busy)this.emit('synthesizing',this.message('queued',{count:normalized.length}));
799
+ outcome=await this.synthesizeSegment(0,generation,true);
800
+ }else{
801
+ const providerSynthesisBatch={capacity:providerCapacity};
802
+ this.providerSynthesisBatch=providerSynthesisBatch;
803
+ const first=startProviderSynthesis(
804
+ this,
805
+ providerSynthesisBatch
806
+ );
807
+ const result=await first.promise;
808
+ if(Object.hasOwn(result,'error'))throw result.error;
809
+ outcome=result.ready?{url:this.urls[0]}:SUPERSEDED;
810
+ }
674
811
  }catch(error){
675
812
  if(generation!==this.generation)return {ready:false,played:false};
813
+ for(const controller of this.abortControllers)controller.abort();
814
+ this.abortControllers.clear();
676
815
  this.releaseURLs();
677
816
  this.fail(error);
678
817
  throw error;
@@ -693,7 +832,9 @@ class SpeechPlayback{
693
832
  this.loadCurrent();
694
833
  this.emit('ready',this.message('ready'));
695
834
  const played=autoplay?await this.play():false;
696
- if(generation===this.generation)this.startLookahead(1,generation);
835
+ if(generation===this.generation&&!this.providerSynthesisBatch){
836
+ this.startLookahead(1,generation);
837
+ }
697
838
  return {ready:true,played};
698
839
  }
699
840
 
@@ -723,14 +864,34 @@ class SpeechPlayback{
723
864
  if(!this.hasAudio())return false;
724
865
  this.generation+=1;
725
866
  const generation=this.generation;
726
- this.lookahead=null;
727
- this.lookaheadError=null;
867
+ if(this.providerSynthesisBatch){
868
+ const providerSynthesisBatch=this.providerSynthesisBatch;
869
+ for(const [index,segmentError] of this.segmentErrors){
870
+ if(
871
+ segmentError?.providerSynthesisBatch
872
+ ===providerSynthesisBatch
873
+ &&!this.urls[index]
874
+ &&!this.segmentRequests.has(index)
875
+ ){
876
+ startProviderSegment(
877
+ this,
878
+ index,
879
+ providerSynthesisBatch
880
+ );
881
+ }
882
+ }
883
+ }else{
884
+ this.lookahead=null;
885
+ this.lookaheadError=null;
886
+ }
728
887
  this.audio.pause();
729
888
  this.index=0;
730
889
  this.loadCurrent();
731
890
  this.audio.currentTime=0;
732
891
  const played=await this.play();
733
- if(generation===this.generation)this.startLookahead(1,generation);
892
+ if(generation===this.generation&&!this.providerSynthesisBatch){
893
+ this.startLookahead(1,generation);
894
+ }
734
895
  return played;
735
896
  }
736
897
 
@@ -772,12 +933,15 @@ class SpeechPlayback{
772
933
  const ready=await this.waitForSegment(nextIndex,generation);
773
934
  if(!ready||generation!==this.generation){
774
935
  if(generation===this.generation&&this.state==='buffering'){
775
- const bufferedError=(
776
- this.lookaheadError
936
+ const providerError=this.segmentErrors.get(nextIndex);
937
+ const providerFailed=this.providerSynthesisBatch
938
+ &&providerError?.providerSynthesisBatch
939
+ ===this.providerSynthesisBatch;
940
+ const lookaheadFailed=this.lookaheadError
777
941
  &&this.lookaheadError.generation===generation
778
- &&this.lookaheadError.index===nextIndex
779
- )?this.lookaheadError.error:null;
780
- if(bufferedError)this.fail(bufferedError);
942
+ &&this.lookaheadError.index===nextIndex;
943
+ if(providerFailed)this.fail(providerError.error);
944
+ else if(lookaheadFailed)this.fail(this.lookaheadError.error);
781
945
  else this.emit('ready',this.message('ready'));
782
946
  }
783
947
  return false;
@@ -786,7 +950,9 @@ class SpeechPlayback{
786
950
  this.index=nextIndex;
787
951
  this.loadCurrent();
788
952
  const played=await this.play();
789
- if(generation===this.generation)this.startLookahead(nextIndex+1,generation);
953
+ if(generation===this.generation&&!this.providerSynthesisBatch){
954
+ this.startLookahead(nextIndex+1,generation);
955
+ }
790
956
  return played;
791
957
  }
792
958