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 +16 -0
- package/README.md +33 -8
- package/docs/architecture.md +1 -1
- package/docs/reference/README.md +6 -5
- package/docs/reference/ai/browser-speech.md +45 -1
- package/docs/reference/ai/twin-cloud.md +1 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/inventory/package-api.json +5 -5
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +4 -4
- package/docs/reference/protocols.md +5 -5
- package/docs/reference/runtime-modules.md +52 -15
- package/docs/reference/sdk-api.md +31 -8
- package/package.json +2 -2
- package/runtime/arcane/modules/SpeechPlayback.js +206 -40
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -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.
|
|
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
|
package/docs/reference/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
61
|
-
| Browser runtime | SDK `0.5.
|
|
62
|
-
| Browser SDK runtime | SDK `0.5.
|
|
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.
|
|
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.
|
|
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
|
+
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
|
package/docs/reference/cli.md
CHANGED
|
@@ -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.
|
|
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
|
+
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.
|
|
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
|
|
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
|
|
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",
|
|
8
|
-
"sdkVersion": "0.5.
|
|
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
|
|
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
|
|
1159
|
-
"normalization": "Exact nonblank split/part input strings are preserved in mutable records
|
|
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.
|
|
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.
|
|
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.
|
|
243
|
-
exactly as `arcane-os@0.5.
|
|
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.
|
|
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
|
|
2853
|
-
|
|
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
|
|
2899
|
-
`prepare()` caller.
|
|
2900
|
-
|
|
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
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
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
|
-
|
|
2948
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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){
|
|
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(
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
727
|
-
|
|
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
|
|
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
|
|
776
|
-
|
|
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
|
-
)
|
|
780
|
-
if(
|
|
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
|
|
953
|
+
if(generation===this.generation&&!this.providerSynthesisBatch){
|
|
954
|
+
this.startLookahead(nextIndex+1,generation);
|
|
955
|
+
}
|
|
790
956
|
return played;
|
|
791
957
|
}
|
|
792
958
|
|