arcane-os 0.5.15 → 0.5.17

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,36 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.17
6
+
7
+ - Restore the public `SPEECH_VOICE_OPTIONS` ordered records and
8
+ `SPEECH_VOICE_ALIASES` membership set used by existing speech controls. Both
9
+ remain ordinary mutable compatibility values, and `SpeechPlayback` does not
10
+ select a voice from them automatically.
11
+ - Restore the optional `SpeechPlayback` constructor `onState(detail)` callback.
12
+ Canonical state dispatch remains first; callback failures are reported
13
+ without replacing playback settlement. Preserve capability-gated eager
14
+ submission, the provider-owned default capacity of four, indexed playback
15
+ order, native/custom serialization, cancellation, and Replay behavior.
16
+ - Apply the shared repeated-formatting-mark cleanup automatically to every TTS
17
+ entry path, including direct fetch, provider-runtime synthesis, browser
18
+ Kokoro requests, streaming chunks, and `SpeechPlayback`. Export the shared
19
+ `MarkdownSpeech` and `stripSpeechFormatting()` owner from
20
+ `arcane-os/speech-text`; keep original caller records unchanged and use
21
+ operation-local SDK metadata to prevent a second cleanup pass. No application
22
+ option is required, and the prior `textFormat` extra no longer disables or
23
+ selects cleanup.
24
+
25
+ ## 0.5.16
26
+
27
+ - Add optional Markdown narration filtering before `AI.streamTTS()`
28
+ segmentation. Omit repeated same formatting marks across streamed chunks,
29
+ preserve single marks and ordinary punctuation, and clear formatting state
30
+ on terminal flush or cancellation. Keep plain speech, displayed and stored
31
+ text, language, voice, synthesis capacity, and audio scheduling unchanged.
32
+ - Select Markdown narration in shared chat and include focused filter test
33
+ source without adding a parser dependency.
34
+
5
35
  ## 0.5.15
6
36
 
7
37
  - Let `SpeechPlayback.prepare()` submit every complete segment immediately when
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.15` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.17` 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.15 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.5.17 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -51,6 +51,10 @@ For a first spoken sentence, copy the application-owned
51
51
  beside `App.js`, then use this module. That one configuration file defines the
52
52
  upstream runtime, model, dtype, and voice. This module creates the application's
53
53
  DBOPFS store; the SDK creates and manages its speech providers and Workers.
54
+ The linked automatic/WebGPU-first selection uses `fp32` because
55
+ [Kokoro.js recommends `fp32` when using WebGPU](https://github.com/hexgrad/kokoro/tree/main/kokoro.js#usage).
56
+ Its `selectedDevice` status reports the loaded route, not speech correctness or
57
+ audio quality.
54
58
 
55
59
  ```javascript
56
60
  import arcaneThemeReady from 'arcane/ThemeBootstrap';
@@ -111,7 +115,13 @@ import SpeechPlayback from 'arcane/SpeechPlayback';
111
115
 
112
116
  const audio = document.body.appendChild(document.createElement('audio'));
113
117
  audio.controls = true;
114
- const narration = new SpeechPlayback({audio, speech: ai});
118
+ const narration = new SpeechPlayback({
119
+ audio,
120
+ speech: ai,
121
+ onState(detail) {
122
+ console.log('Speech state:', detail.state);
123
+ }
124
+ });
115
125
  const speakAll = document.body.appendChild(document.createElement('button'));
116
126
  speakAll.textContent = 'Speak all segments';
117
127
  speakAll.addEventListener('click', async function speakAllSegments() {
@@ -128,6 +138,20 @@ segments enter the provider queue immediately, up to four synthesize at once,
128
138
  and the audio still plays first, second, third. Native or custom speech without
129
139
  advertised provider execution capacity stays serialized with one lookahead.
130
140
 
141
+ Existing speech controls can also import the shared compatibility catalogs.
142
+ They list the ten existing value/label choices but do not choose a voice for
143
+ the application or prove that a selected provider supports one:
144
+
145
+ ```javascript
146
+ import {
147
+ SPEECH_VOICE_ALIASES,
148
+ SPEECH_VOICE_OPTIONS
149
+ } from 'arcane/SpeechPlayback';
150
+
151
+ console.log(SPEECH_VOICE_OPTIONS[0]); // {value: 'alloy', label: 'Alloy'}
152
+ console.log(SPEECH_VOICE_ALIASES.has('alloy')); // true
153
+ ```
154
+
131
155
  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),
132
156
  the [tiny TWiN Cloud request and saved-preference migration](https://github.com/TheWizardNexus/arcane-os-sdk/blob/main/docs/reference/ai/twin-cloud.md),
133
157
  or the [maintained WASM voice-chat example](https://github.com/TheWizardNexus/arcane-os-sdk/tree/main/examples/wasm-ai-demo).
@@ -286,13 +310,14 @@ TTS stream without taking over synthesis or playback:
286
310
  ```js
287
311
  ai.configureTTSSegmentation({
288
312
  punctuation:'any',
289
- wordCadence:4
313
+ wordCadence:null
290
314
  });
291
315
  ```
292
316
 
293
317
  The compatibility default remains sentence punctuation with no word cadence.
294
- The configured stream preserves every character and punctuation mark, chooses
295
- the earliest complete boundary, begins synthesis as each segment becomes
318
+ Within the already prepared speech text, the configured segmentation preserves
319
+ every character and punctuation mark, chooses the earliest complete boundary,
320
+ begins synthesis as each segment becomes
296
321
  available, and plays the completed audio in exact segment order. Ready adjacent
297
322
  buffers are scheduled consecutively on the browser audio clock rather than
298
323
  waiting for an `ended` callback before the next start.
@@ -301,6 +326,16 @@ keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
301
326
  inside the same segment.
302
327
  Mute, stop, provider transition, and cancellation still govern the whole queue.
303
328
 
329
+ Every TTS call automatically removes repeated same formatting marks (`*`, `#`,
330
+ `_`, backtick, `~`) from its outbound speech-input copy before synthesis.
331
+ Streaming calls also recognize a run split across chunks. Single marks and
332
+ ordinary repeated punctuation remain literal. No application option is needed;
333
+ an existing `textFormat` extra is ignored and cannot disable cleanup. Original
334
+ messages and caller payloads remain unchanged for display, storage, and model
335
+ input. The dependency-free `arcane-os/speech-text` entrypoint exports the same
336
+ `MarkdownSpeech` streaming class and `stripSpeechFormatting()` one-pass helper
337
+ for code that needs the speech-only transformation directly.
338
+
304
339
  The SDK runtime also owns `DBOPFSDocumentLibrary`,
305
340
  `DocumentLexicalSearch`, and `PersistentAIChatSession`. Document bootstrap is
306
341
  schema-driven and explicit; chat never searches a corpus unless the app wires
@@ -322,7 +357,7 @@ uses the same controller for automatic memory extraction.
322
357
  Create a new repository-shaped Arcane application with the exact stable SDK:
323
358
 
324
359
  ```bash
325
- npx arcane-os@0.5.15 new my-app --path ./my-app --target portable --git
360
+ npx arcane-os@0.5.17 new my-app --path ./my-app --target portable --git
326
361
  cd my-app
327
362
  npm install
328
363
  npm run dev
@@ -332,7 +367,7 @@ To enroll an existing repository, install the exact SDK and initialize only
332
367
  missing Arcane files:
333
368
 
334
369
  ```bash
335
- npm install --save-dev --save-exact arcane-os@0.5.15
370
+ npm install --save-dev --save-exact arcane-os@0.5.17
336
371
  npm exec -- arcane init my-app --target portable
337
372
  ```
338
373
 
@@ -348,7 +383,7 @@ npm exec -- arcane-os targets
348
383
  No global SDK install or standalone Arcane CLI is required. The application
349
384
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
350
385
 
351
- Use `npx arcane-os@0.5.15` for the initial bootstrap because it names this npm
386
+ Use `npx arcane-os@0.5.17` for the initial bootstrap because it names this npm
352
387
  package explicitly; bare `npx arcane` outside an installed project could resolve
353
388
  a different package. Both installed commands invoke the same headless toolchain.
354
389
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -368,7 +403,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
368
403
 
369
404
  # From the generated app repository
370
405
  cd ../local-app
371
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.15.tgz
406
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.17.tgz
372
407
  npm ci
373
408
  ```
374
409
 
@@ -377,7 +412,7 @@ same location. The lockfile retains the selected package dependency while
377
412
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
378
413
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
379
414
  runner also needs that tarball at the locked path. After publication, replace
380
- the local declaration with the exact `arcane-os@0.5.15` registry package and
415
+ the local declaration with the exact `arcane-os@0.5.17` registry package and
381
416
  commit the regenerated lock.
382
417
 
383
418
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -516,7 +551,7 @@ package installation, or assertions.
516
551
 
517
552
  ## Current target support
518
553
 
519
- Version `0.5.15` exposes one browser target and five explicitly paired
554
+ Version `0.5.17` exposes one browser target and five explicitly paired
520
555
  native development targets: a non-runnable portable directory, a
521
556
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
522
557
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -5,6 +5,7 @@ import {
5
5
  isBrowserSpeechAuthority,
6
6
  isDbopfsSpeechArtifactStore,
7
7
  } from "./browser-speech-artifacts.mjs";
8
+ import { stripSpeechFormatting } from "../speech-text.mjs";
8
9
 
9
10
  const completeValue = (value) => value;
10
11
  import {
@@ -896,7 +897,7 @@ function cloneNativeTranscriptionPayload(payload, authority) {
896
897
  });
897
898
  }
898
899
 
899
- function normalizeSynthesisPayload(payload, authority) {
900
+ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
900
901
  const shared = Object.hasOwn(payload, "input");
901
902
  let descriptors;
902
903
  let textValue;
@@ -932,7 +933,8 @@ function normalizeSynthesisPayload(payload, authority) {
932
933
  assertPayloadModel(payload, authority, { operationSubject: "tts-synthesis" });
933
934
  textValue = descriptors.text.value;
934
935
  }
935
- const text = typeof textValue === "string" ? textValue : "";
936
+ const suppliedText = typeof textValue === "string" ? textValue : "";
937
+ const text = speechInputPrepared === true ? suppliedText : stripSpeechFormatting(suppliedText);
936
938
  const voiceValue = Object.hasOwn(descriptors, "voice")
937
939
  ? descriptors.voice.value
938
940
  : authority.defaultVoice;
@@ -983,6 +985,7 @@ async function normalizeRequestPayload(
983
985
  authority,
984
986
  signal,
985
987
  cancellationReason,
988
+ speechInputPrepared,
986
989
  ) {
987
990
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
988
991
  throw providerError(
@@ -1009,7 +1012,7 @@ async function normalizeRequestPayload(
1009
1012
  }
1010
1013
  return cloneNativeTranscriptionPayload(payload, authority);
1011
1014
  }
1012
- return normalizeSynthesisPayload(payload, authority);
1015
+ return normalizeSynthesisPayload(payload, authority, speechInputPrepared);
1013
1016
  }
1014
1017
 
1015
1018
  function encodeSharedSynthesisResult(result, authority) {
@@ -1619,7 +1622,7 @@ function createBrowserSpeechProvider({
1619
1622
  }, role);
1620
1623
  },
1621
1624
 
1622
- async request(context = {}) {
1625
+ async request(context = {}, { speechInputPrepared = false } = {}) {
1623
1626
  let requestContext;
1624
1627
  try {
1625
1628
  requestContext = providerContext(context, role, "request");
@@ -1713,6 +1716,7 @@ function createBrowserSpeechProvider({
1713
1716
  authority,
1714
1717
  linked.controller.signal,
1715
1718
  browserSpeechRequestAbortReason,
1719
+ speechInputPrepared,
1716
1720
  );
1717
1721
  throwIfAborted(
1718
1722
  linked.controller.signal,
@@ -0,0 +1,31 @@
1
+ function stripSpeechFormatting(text=''){
2
+ return text.replace(/([*#_`~])\1+/g,'');
3
+ }
4
+
5
+ // Speech-only filtering; single markers and all other text remain literal.
6
+ class MarkdownSpeech {
7
+ #pending='';
8
+
9
+ append(text='',end=false){
10
+ if(typeof text!=='string'){
11
+ throw new TypeError('Markdown speech input must be text.');
12
+ }
13
+
14
+ const source=this.#pending+text;
15
+ const trailing=end?null:/([*#_`~])\1*$/u.exec(source);
16
+ // Two copies retain the fact that a run repeats without retaining it all.
17
+ this.#pending=trailing
18
+ ?trailing[1].repeat(trailing[0].length===1?1:2)
19
+ :'';
20
+
21
+ return stripSpeechFormatting(
22
+ trailing?source.slice(0,trailing.index):source
23
+ );
24
+ }
25
+
26
+ reset(){
27
+ this.#pending='';
28
+ }
29
+ }
30
+
31
+ export {MarkdownSpeech,stripSpeechFormatting};
@@ -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.15` runtime requires Arcane `0.8.12` or newer. Compatibility
282
+ The SDK `0.5.17` 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.15
20
+ npm install --save-exact arcane-os@0.5.17
21
21
  ```
22
22
 
23
23
  For your first AI call, follow the [TWiN Cloud quick start](ai/twin-cloud.md).
@@ -58,9 +58,9 @@ This repository contains explicitly versioned surfaces with different owners:
58
58
 
59
59
  | Surface | Source identity | Meaning |
60
60
  | --- | --- | --- |
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. |
61
+ | SDK and CLI | `arcane-os` `0.5.17` | The Node.js toolchain, portable `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`, `arcane-os/preference-store`, `arcane-os/speech-playback`, and `arcane-os/speech-text` 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.17`, 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.17`, `browser-runtime/` | The browser closure for events, shared logging, speech-text cleanup, Wllama, and Browser Speech mechanisms. `listSdkBrowserRuntimeFiles()`, `readSdkBrowserRuntimeFile()`, and `loadSdkBrowserRuntimeRelease()` derive its current inventory directly from the selected directory. |
64
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. |
65
65
 
66
66
  The SDK runtime source and Core reference have different owners. A browser
@@ -75,7 +75,7 @@ and the distinction between a documentation snapshot and the selected runtime.
75
75
 
76
76
  ## Installed documentation and release identity
77
77
 
78
- This reference accompanies `arcane-os@0.5.15`. The installed package includes
78
+ This reference accompanies `arcane-os@0.5.17`. The installed package includes
79
79
  the maintained `docs/` tree and `examples/wasm-ai-demo/` source alongside
80
80
  README and CHANGELOG. Open `node_modules/arcane-os/docs/reference/README.md`
81
81
  for the matching local reference. The generated website and test suites remain
@@ -124,11 +124,11 @@ Public reference entries follow the established Arcane documentation model:
124
124
 
125
125
  ## Public runtime inventory
126
126
 
127
- The package exposes 198 semantic JavaScript records across 17 JavaScript
127
+ The package exposes 202 semantic JavaScript records across 18 JavaScript
128
128
  entrypoints, plus eight JSON Schemas and package metadata. Ten entrypoints are
129
129
  Node.js control-plane surfaces,
130
130
  `arcane-os/event-manager`, `arcane-os/logging`, `arcane-os/mail`, `arcane-os/preference-store`, and
131
- `arcane-os/speech-playback` run in Node and browsers, and
131
+ `arcane-os/speech-playback` plus `arcane-os/speech-text` run in Node and browsers, and
132
132
  `arcane-os/ai/browser-wasm` plus `arcane-os/ai/browser-speech` are browser-only.
133
133
  The [machine-readable package
134
134
  inventory](inventory/package-api.json) and [SDK member reference](sdk-api.md)
@@ -139,7 +139,7 @@ download, install, or self-update.
139
139
 
140
140
  The synchronized browser payload exposes:
141
141
 
142
- - 82 JavaScript module artifacts under `runtime/arcane/modules/`, including
142
+ - 83 JavaScript module artifacts under `runtime/arcane/modules/`, including
143
143
  ESM modules, classic vendor globals, one worker protocol, and one Node-oriented
144
144
  mail transport;
145
145
  - 14 shared entity modules under `runtime/arcane/entities/`;
@@ -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.15 new hello-speech --path ./hello-speech --target browser
16
+ npx arcane-os@0.5.17 new hello-speech --path ./hello-speech --target browser
17
17
  cd hello-speech
18
18
  npm install
19
19
  npm run dev
@@ -37,7 +37,7 @@ export const speechSelection = {
37
37
  id: 'onnx-community/Kokoro-82M-v1.0-ONNX',
38
38
  repository: 'onnx-community/Kokoro-82M-v1.0-ONNX',
39
39
  revision: '1939ad2a8e416c0acfeecc08a694d14ef25f2231',
40
- dtype: 'q8',
40
+ dtype: 'fp32',
41
41
  defaultVoice: 'af_heart'
42
42
  },
43
43
  runtime: {
@@ -55,6 +55,15 @@ export const speechSelection = {
55
55
  };
56
56
  ```
57
57
 
58
+ The omitted execution record below uses the SDK's WebGPU-first automatic
59
+ selection, so this basic configuration uses `fp32` because
60
+ [Kokoro.js recommends `fp32` when using WebGPU](https://github.com/hexgrad/kokoro/tree/main/kokoro.js#usage).
61
+ Automatic fallback carries this same selected model and dtype to WASM; the SDK
62
+ does not rewrite the application selection. If you intentionally choose
63
+ another dtype, evaluate that exact model, browser, and execution route.
64
+ `selectedDevice` reports routing after load, not pronunciation, text fidelity,
65
+ or audio quality.
66
+
58
67
  Then use this **`App.js`**. The application creates and owns the DBOPFS
59
68
  instance. Configuration selects the provider without loading it; the button
60
69
  explicitly loads and unmutes TTS before requesting speech.
@@ -221,8 +230,10 @@ only failed missing segments.
221
230
 
222
231
  The default `{device:'auto',maxConcurrentRequests:4}` attempts the full ONNX
223
232
  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.
233
+ loading fails. The basic configuration above keeps `fp32` for this WebGPU-first
234
+ path and any automatic WASM fallback. Use the status example below to read
235
+ `selectedDevice`; console node-assignment warnings alone do not identify the
236
+ selected execution device or assess the generated audio.
226
237
 
227
238
  ## Queue complete passages and wait for playback
228
239
 
@@ -275,8 +286,10 @@ all speech owned by that AI instance and settles pending playback results
275
286
  The selected voice and speed are captured for segments extracted by that call.
276
287
  That includes any text left in the same AI instance's partial-stream buffer;
277
288
  finish the previous producer before starting a separate complete passage.
278
- Options are not retained with an unfinished `end:false` remainder. A later
279
- call supplies its own options, and `finishTTS()` uses defaults.
289
+ Voice, speed, pause, and playback options are not retained with an unfinished
290
+ `end:false` remainder. A later call supplies its own options, and `finishTTS()`
291
+ uses their defaults while flushing any pending single formatting mark through
292
+ the same automatic speech-input cleanup.
280
293
  A call extracting no segments returns `true` without waiting for earlier jobs.
281
294
  An already muted call returns `false`.
282
295
 
@@ -331,12 +344,55 @@ if (finalPrepared === false) {
331
344
  ```
332
345
 
333
346
  The segmentation default uses sentence punctuation. To submit smaller complete
334
- segments, call `ai.configureTTSSegmentation({punctuation:'any',wordCadence:4})`
347
+ segments, call `ai.configureTTSSegmentation({punctuation:'any',wordCadence:null})`
335
348
  before feeding the stream. Chunk boundaries themselves do not force a sentence
336
349
  boundary; `finishTTS()` flushes any remaining text. It is not a playback-ended
337
350
  notification. Do not mute or dispose immediately after it if playback should
338
351
  continue.
339
352
 
353
+ ## Automatic speech-input formatting cleanup
354
+
355
+ Every TTS entrypoint removes repeated same formatting marks from the outbound
356
+ speech-input copy automatically. No application option is required:
357
+
358
+ ```javascript
359
+ ai.streamTTS('## Heading\n**Hello');
360
+ ai.streamTTS('**. Next sentence.');
361
+ await ai.finishTTS();
362
+ ```
363
+
364
+ The SDK omits runs of two or more of the same `*`, `#`, `_`, backtick, or `~`
365
+ before speech segmentation. A candidate run split across chunks is still
366
+ recognized. Single marks, ellipses, quoted endings, and all other text are
367
+ preserved. This is a small narration filter, not a full Markdown parser.
368
+ Ordinary prose is forwarded immediately; only a trailing formatting candidate
369
+ waits for its next character or the final flush.
370
+
371
+ `end:true`, `finishTTS()`, or cancellation clears pending streaming formatting
372
+ state. `fetchTTS()`, provider-runtime TTS requests, direct Kokoro provider
373
+ requests, and `SpeechPlayback` apply the same cleanup to complete input. An
374
+ existing `textFormat` extra is ignored and cannot disable or select cleanup.
375
+ SDK-internal delegation carries `{speechInputPrepared:true}` outside the speech
376
+ payload only after one cleanup pass, preventing a second non-idempotent pass.
377
+ Applications omit that internal metadata. Displayed messages, saved history,
378
+ model input, caller payload objects, language, voice, synthesis capacity, and
379
+ playback timing are not changed by the filter.
380
+
381
+ The dependency-free helper is public for code that needs the speech-only
382
+ transformation directly:
383
+
384
+ ```javascript
385
+ import {
386
+ MarkdownSpeech,
387
+ stripSpeechFormatting
388
+ } from 'arcane-os/speech-text';
389
+
390
+ console.log(stripSpeechFormatting('**Hello**')); // Hello
391
+ const speechText = new MarkdownSpeech();
392
+ console.log(speechText.append('## Head', false)); // ' Head'
393
+ console.log(speechText.append('ing', true)); // ing
394
+ ```
395
+
340
396
  ## Choose a device or reduce memory use
341
397
 
342
398
  Omitting `tts.execution` selects `{device:'auto',maxConcurrentRequests:4}`.
@@ -401,6 +457,8 @@ until a pool is selected and returns to `null` on unload. Providers without an
401
457
  execution report omit `execution`; do not infer a device from `navigator.gpu`
402
458
  or a configured preference alone. An explicit inspection can throw a provider
403
459
  status error; handle it with the same `error.code` / `error.message` pattern.
460
+ Treat `selectedDevice` as route status only; evaluate actual speech output for
461
+ the model, dtype, browser, and device combinations your application supports.
404
462
 
405
463
  ## Stop, mute, cancel, and release
406
464
 
@@ -438,7 +496,8 @@ in place; it does not erase the application's data.
438
496
  For a cancellable individual synthesis, unmute first. A fresh browser speech
439
497
  configuration is muted, so calling `providerRuntime.load('tts')` directly at
440
498
  that point rejects with `ARCANE_AI_TTS_MUTED`. `fetchTTS()` accepts an
441
- `AbortSignal` as its second argument and returns a WAV `Blob` without playing it:
499
+ `AbortSignal` as its second argument, cleans repeated formatting marks from the
500
+ outbound input copy, and returns a WAV `Blob` without playing it:
442
501
 
443
502
  ```javascript
444
503
  const synthesisController = new AbortController();
@@ -875,7 +934,8 @@ cache state, and warnings. Kokoro status also includes an `execution` record
875
934
  with requested and selected device, request limit, and active request count. A
876
935
  successful `selectedDevice:'webgpu'` reports the execution provider selected by
877
936
  the upstream model load; it does not claim that browser, driver, or GPU kernels
878
- overlap physically. A security field is absent in ordinary mode.
937
+ overlap physically or that generated audio has been quality-validated. A
938
+ security field is absent in ordinary mode.
879
939
 
880
940
  The provider/2 load context accepts an optional progress callback for interface
881
941
  compatibility, but the current browser-speech artifact and Worker transport
@@ -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.15 new hello-twin --path ./hello-twin --target browser
12
+ npx arcane-os@0.5.17 new hello-twin --path ./hello-twin --target browser
13
13
  cd hello-twin
14
14
  npm install
15
15
  npm run dev
@@ -32,12 +32,13 @@ version; WebKitGTK availability must not be generalized to macOS.
32
32
  | Select and observe independent LLM/STT/TTS roles | `/arcane/modules/AIProviderRuntime.js` and `AIRuntimeState.js` | **Cross-host** controller/state; registered providers retain their own host requirements | Required/projected provider members, route/configuration records, and status fields; per-role lifecycle, cancellation, stream cleanup, sticky state, and startup barriers are normalized. `localOnly` creates no fallback. |
33
33
  | Run a caller-selected local LLM entirely in a browser renderer | `arcane-os/ai/browser-wasm` through `createArcaneAI()` | **Browser** only; secure context, WebAssembly, OPFS/DBOPFS, WebGPU, and requested full offload are required; no CPU fallback | The public AI API module normalizes multi-model lifecycle, status, complete all-choice streaming, cancellation, exact ordered structural tool-call visibility, and session persistence. Model sources are canonical ordered file descriptors; licenses and model choice remain application policy. |
34
34
  | Run caller-selected Whisper or Kokoro in a browser renderer | `arcane-os/ai/browser-speech` registered with `AIProviderRuntime` | **Browser** only; DBOPFS, Web Locks, Workers, Fetch/object URLs, and a caller-supplied self-contained runtime/model closure are required | STT/TTS use independent provider/2 lifecycle and status. Kokoro adds bounded Worker/session concurrency and explicit `auto`, `webgpu`, or `wasm` execution. Complete model/runtime selection, offline behavior, cancellation, Worker teardown, and request/result shapes are normalized. No runtime/model content or cloud fallback is supplied. |
35
+ | Prepare ordered speech playback or reuse the speech-input formatting filter | `arcane-os/speech-playback` and `arcane-os/speech-text` | **Node** with injected media adapters, or **Browser / Native WebView** media; the text filter itself is **Cross-host** | Stored and caller-owned text stays exact. Only the outbound synthesis copy automatically loses repeated same formatting marks. A capacity-advertising provider receives complete segments immediately while retaining indexed playback; native/custom synthesis stays serialized. |
35
36
  | Preserve complete chat history and memory | `/arcane/modules/PersistentAIChatSession.js` | **Browser / native WebView** with ChatEntity/DBOPFS and a configured chat function | Existing DBOPFS names and memory semantics are preserved. Live-context commit is atomic; durable persistence is explicit and coherent across user/assistant turns and atomic all-ID tool-result batches. |
36
37
  | Search an app-owned document corpus for explicit chat context | `/arcane/modules/DBOPFSDocumentLibrary.js` | **Browser** or compatible injected DBOPFS adapter | Generation/manifest completion, complete lexical search, partial read failures, and untrusted context labels are normalized. Construction does not search; an explicitly wired context builder performs retrieval for each prepared chat send. |
37
38
  | Read host identity, capabilities, storage, preferences, appearance, or platform state | `globalThis.Arcane` | **Cross-host** where the method is implemented and admitted | Promise behavior and `Arcane.Error` are normalized. Result fields are normalized unless the method explicitly documents a platform-dependent snapshot. |
38
39
  | Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
39
40
  | Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
40
- | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
41
+ | Use speech through one application helper | `/arcane/modules/AI.js` and `Arcane.speech` | **Browser** or **Native** | The helper keeps audio on device: Whisper owns STT and Kokoro owns TTS. It automatically cleans only the outbound speech-input copy and normalizes application-facing audio/text behavior while browser and native request/response plumbing differs below that boundary. |
41
42
  | Inspect or manage raw Ollama models | `Arcane.ollama` or `/arcane/modules/Ollama.js` | **Native** desktop Core for management; narrower Android inference only | Wrapper method names, errors, streaming correlation, and admission are Arcane-controlled. Direct Ollama success envelopes are intentionally provider-native. |
42
43
  | Use native terminal, installation, user, provisioning, or machine controls | matching `Arcane.*` namespace | **Native** and app/capability restricted | Calls and errors use the common bridge contract. Platform results can be host-specific and are marked in the method guide. |
43
44
 
@@ -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.15` preserves the physical workspace route count and ordered include
217
+ SDK `0.5.17` 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.15` has two related but separate normalized boundaries:
12
+ The SDK `0.5.17` has two related but separate normalized boundaries:
13
13
 
14
14
  | Boundary | Use | Host |
15
15
  |---|---|---|
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.5.15",
3
+ "sdkVersion": "0.5.17",
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",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 198,
9
+ "memberCount": 202,
10
10
  "members": [
11
11
  {
12
12
  "id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
@@ -1395,7 +1395,7 @@
1395
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; 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"
1398
+ "normalization": "Binding identity and namespace come directly from the canonical runtime module; stored part input remains exact while only each outbound synthesis copy receives automatic repeated-formatting-mark cleanup and SDK-internal preparation metadata; 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; canonical state dispatch precedes the optional synchronous onState callback"
1399
1399
  },
1400
1400
  {
1401
1401
  "id": "speech-playback:SPEECH_PLAYBACK_STATE_EVENT",
@@ -1411,6 +1411,34 @@
1411
1411
  "protocol": "SpeechPlayback runtime contract",
1412
1412
  "normalization": "Exact immutable speech-playback-state event name"
1413
1413
  },
1414
+ {
1415
+ "id": "speech-playback:SPEECH_VOICE_ALIASES",
1416
+ "name": "SPEECH_VOICE_ALIASES",
1417
+ "displayName": "SPEECH_VOICE_ALIASES",
1418
+ "kind": "constant",
1419
+ "signature": "const SPEECH_VOICE_ALIASES",
1420
+ "entrypoints": ["arcane-os/speech-playback"],
1421
+ "primaryImport": "arcane-os/speech-playback",
1422
+ "group": "Portable runtime modules",
1423
+ "summary": "Mutable compatibility membership set for the ten shared speech voice identifiers.",
1424
+ "availability": "Node and browser",
1425
+ "protocol": "SpeechPlayback runtime contract",
1426
+ "normalization": "Derived once from SPEECH_VOICE_OPTIONS in published order; ordinary mutable Set containing alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, and shimmer; SpeechPlayback does not select it automatically"
1427
+ },
1428
+ {
1429
+ "id": "speech-playback:SPEECH_VOICE_OPTIONS",
1430
+ "name": "SPEECH_VOICE_OPTIONS",
1431
+ "displayName": "SPEECH_VOICE_OPTIONS",
1432
+ "kind": "constant",
1433
+ "signature": "const SPEECH_VOICE_OPTIONS",
1434
+ "entrypoints": ["arcane-os/speech-playback"],
1435
+ "primaryImport": "arcane-os/speech-playback",
1436
+ "group": "Portable runtime modules",
1437
+ "summary": "Mutable ordered compatibility value/label records for existing speech controls.",
1438
+ "availability": "Node and browser",
1439
+ "protocol": "SpeechPlayback runtime contract",
1440
+ "normalization": "Ordinary mutable array and mutable records ordered as Alloy, Ash, Ballad, Coral, Echo, Fable, Nova, Onyx, Sage, and Shimmer; provider support and selection remain caller-owned"
1441
+ },
1414
1442
  {
1415
1443
  "id": "speech-playback:SpeechPlayback",
1416
1444
  "name": "SpeechPlayback",
@@ -1423,7 +1451,7 @@
1423
1451
  "summary": "Named binding for the same capability-aware, exact-order canonical class exposed as the subpath default.",
1424
1452
  "availability": "Node with injected media adapters, or browser/native WebView media",
1425
1453
  "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-advertised capacity enables eager submission while the provider owns its bound, native and custom clients remain serialized, and media availability remains host-owned"
1454
+ "normalization": "Binding identity equals the default export; prepare preserves each nonblank part input exactly without trimming, splitting, or freezing it; requestSpeech changes only the cloned outbound input through automatic repeated-formatting-mark cleanup and carries SDK-internal preparation metadata; provider-advertised capacity enables eager submission while the provider owns its bound, native and custom clients remain serialized, canonical state dispatch precedes optional synchronous onState delivery, and media availability remains host-owned"
1427
1455
  },
1428
1456
  {
1429
1457
  "id": "speech-playback:splitSpeechText",
@@ -1439,6 +1467,34 @@
1439
1467
  "protocol": "SpeechPlayback runtime contract",
1440
1468
  "normalization": "Uses trimming only to detect blank input; nonblank text is returned unchanged in one mutable array without trimming, splitting, or freezing"
1441
1469
  },
1470
+ {
1471
+ "id": "speech-text:MarkdownSpeech",
1472
+ "name": "MarkdownSpeech",
1473
+ "displayName": "MarkdownSpeech",
1474
+ "kind": "class",
1475
+ "signature": "new MarkdownSpeech()",
1476
+ "entrypoints": ["arcane-os/speech-text"],
1477
+ "primaryImport": "arcane-os/speech-text",
1478
+ "group": "Portable runtime modules",
1479
+ "summary": "Streams speech-only repeated-formatting-mark removal across input chunk boundaries.",
1480
+ "availability": "Node and browser",
1481
+ "protocol": "Speech text normalization",
1482
+ "normalization": "append(text='',end=false) emits newly available narration, retains only a trailing candidate formatting mark across chunks, and resets after a terminal append; reset() discards that pending state; caller text remains unchanged"
1483
+ },
1484
+ {
1485
+ "id": "speech-text:stripSpeechFormatting",
1486
+ "name": "stripSpeechFormatting",
1487
+ "displayName": "stripSpeechFormatting()",
1488
+ "kind": "function",
1489
+ "signature": "stripSpeechFormatting(text='')",
1490
+ "entrypoints": ["arcane-os/speech-text"],
1491
+ "primaryImport": "arcane-os/speech-text",
1492
+ "group": "Portable runtime modules",
1493
+ "summary": "Removes repeated runs of speech formatting marks from one complete text value.",
1494
+ "availability": "Node and browser",
1495
+ "protocol": "Speech text normalization",
1496
+ "normalization": "Calls the supplied value's replace method and returns its result with runs of two or more identical *, #, _, backtick, or ~ marks removed; single marks and every other character remain literal; it does not parse Markdown or modify the supplied value; values without callable replace fail with the native TypeError"
1497
+ },
1442
1498
  {
1443
1499
  "id": "root:readRuntimeFile",
1444
1500
  "name": "readRuntimeFile",