arcane-os 0.5.16 → 0.5.18

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.
Files changed (190) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +42 -19
  3. package/bin/arcane-test.mjs +16 -13
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +41 -38
  5. package/browser-runtime/ai/browser-speech-providers.mjs +53 -46
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +127 -124
  7. package/browser-runtime/ai/browser-wasm.mjs +6 -3
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +26 -23
  9. package/browser-runtime/ai/model-controller.mjs +94 -91
  10. package/browser-runtime/ai/speech-worker-client.mjs +12 -9
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +56 -53
  12. package/browser-runtime/dependencies/strong-type/index.js +207 -82
  13. package/browser-runtime/dependencies/strong-type/package.json +15 -6
  14. package/browser-runtime/dom-event-instrumentation.mjs +24 -20
  15. package/browser-runtime/event-manager.mjs +111 -108
  16. package/browser-runtime/speech-text.mjs +35 -0
  17. package/docs/architecture.md +1 -1
  18. package/docs/reference/README.md +8 -8
  19. package/docs/reference/ai/browser-speech.md +230 -16
  20. package/docs/reference/ai/twin-cloud.md +1 -1
  21. package/docs/reference/availability-and-normalization.md +42 -1
  22. package/docs/reference/cli.md +1 -1
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/inventory/package-api.json +60 -4
  25. package/docs/reference/inventory/runtime-components.json +2 -2
  26. package/docs/reference/inventory/runtime-modules.json +29 -31
  27. package/docs/reference/protocols.md +17 -13
  28. package/docs/reference/runtime-components.md +6 -5
  29. package/docs/reference/runtime-modules.md +212 -70
  30. package/docs/reference/sdk-api.md +197 -11
  31. package/examples/wasm-ai-demo/index.html +1 -0
  32. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  33. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  34. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  35. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  36. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  37. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  38. package/node_modules/strong-type/README.md +42 -24
  39. package/node_modules/strong-type/benchmark/core.js +324 -0
  40. package/node_modules/strong-type/index.js +207 -82
  41. package/node_modules/strong-type/package.json +14 -6
  42. package/package.json +4 -3
  43. package/runtime/arcane/components/app-bar.html +5 -2
  44. package/runtime/arcane/components/assistant-panel.html +5 -2
  45. package/runtime/arcane/components/chart.html +15 -12
  46. package/runtime/arcane/components/chat.html +118 -141
  47. package/runtime/arcane/components/dashboard-config.html +6 -3
  48. package/runtime/arcane/components/data-view.html +4 -1
  49. package/runtime/arcane/components/directory-picker.html +5 -2
  50. package/runtime/arcane/components/document-inspector.html +6 -3
  51. package/runtime/arcane/components/file-drop.html +5 -2
  52. package/runtime/arcane/components/file-inspector.html +9 -6
  53. package/runtime/arcane/components/file-manager.html +16 -13
  54. package/runtime/arcane/components/local-ai-status.html +5 -2
  55. package/runtime/arcane/components/markdown-document.html +14 -11
  56. package/runtime/arcane/components/markdown-editor.html +4 -1
  57. package/runtime/arcane/components/modal.html +4 -1
  58. package/runtime/arcane/components/output-panel.html +6 -3
  59. package/runtime/arcane/components/preferences-form.html +4 -1
  60. package/runtime/arcane/components/record-timeline.html +8 -5
  61. package/runtime/arcane/components/relationship-board.html +9 -6
  62. package/runtime/arcane/components/source-code-viewer.html +12 -9
  63. package/runtime/arcane/components/source-explanation.html +8 -5
  64. package/runtime/arcane/components/speech.html +24 -21
  65. package/runtime/arcane/components/summary-strip.html +4 -1
  66. package/runtime/arcane/components/task-progress.html +6 -3
  67. package/runtime/arcane/components/terminal-workspace.html +4 -1
  68. package/runtime/arcane/components/voice-transcription.html +11 -8
  69. package/runtime/arcane/components/web-navigator.html +5 -2
  70. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  71. package/runtime/arcane/entities/Calculation.js +5 -2
  72. package/runtime/arcane/entities/Chat.js +33 -33
  73. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  74. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  75. package/runtime/arcane/entities/File.js +1 -1
  76. package/runtime/arcane/entities/Image.js +8 -5
  77. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  78. package/runtime/arcane/entities/Preference.js +9 -6
  79. package/runtime/arcane/entities/Theme.js +5 -2
  80. package/runtime/arcane/entities/User.js +11 -11
  81. package/runtime/arcane/entities/Weather.js +5 -2
  82. package/runtime/arcane/modules/AI.js +503 -187
  83. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  84. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  85. package/runtime/arcane/modules/AIProviderRuntime.js +117 -92
  86. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  87. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  88. package/runtime/arcane/modules/AnsiText.js +4 -1
  89. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  90. package/runtime/arcane/modules/AppDataScope.js +8 -5
  91. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  92. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  93. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  94. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  95. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  96. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  97. package/runtime/arcane/modules/ChatRecords.js +18 -15
  98. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  99. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  100. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  101. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  102. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  103. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  104. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  105. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  106. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  107. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  108. package/runtime/arcane/modules/DBLS.js +7 -4
  109. package/runtime/arcane/modules/DBOPFS.js +7 -7
  110. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  111. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  112. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  113. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  114. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  115. package/runtime/arcane/modules/Errors.js +28 -25
  116. package/runtime/arcane/modules/HTMLImport.js +7 -4
  117. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  118. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  119. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  120. package/runtime/arcane/modules/MD.js +1 -1
  121. package/runtime/arcane/modules/Mail.js +46 -43
  122. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  123. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  124. package/runtime/arcane/modules/MarkdownSpeech.js +1 -50
  125. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  126. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  127. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  128. package/runtime/arcane/modules/Ollama.js +6 -3
  129. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  130. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  131. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  132. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  133. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  134. package/runtime/arcane/modules/Questionnaire.js +6 -3
  135. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  136. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  137. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  138. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  139. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  140. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  141. package/runtime/arcane/modules/SpeechPlayback.js +61 -28
  142. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  143. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  144. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  145. package/runtime/arcane/modules/TerminalClient.js +14 -11
  146. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  147. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  148. package/runtime/arcane/modules/ThemeManager.js +5 -2
  149. package/runtime/arcane/modules/TimeGuard.js +1 -1
  150. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  151. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  152. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  153. package/runtime/strong-type/index.js +1276 -352
  154. package/runtime/strong-type/package.json +69 -45
  155. package/src/app-descriptor.mjs +17 -14
  156. package/src/application-tests.mjs +4 -1
  157. package/src/cli/main.mjs +8 -5
  158. package/src/constants.mjs +4 -1
  159. package/src/dev-server.mjs +8 -5
  160. package/src/doctor.mjs +5 -2
  161. package/src/dom-event-instrumentation.mjs +24 -20
  162. package/src/errors.mjs +7 -3
  163. package/src/event-manager.mjs +111 -108
  164. package/src/event-queue.mjs +8 -5
  165. package/src/events.mjs +12 -9
  166. package/src/import-map.mjs +36 -27
  167. package/src/installed-sdk-runtime.mjs +4 -1
  168. package/src/integrated-provider-loader.mjs +9 -6
  169. package/src/mail-credentials.mjs +16 -13
  170. package/src/mail-server.mjs +53 -50
  171. package/src/mail.mjs +18 -15
  172. package/src/native-plan.mjs +12 -9
  173. package/src/native-provider-loader.mjs +7 -4
  174. package/src/packager/core.mjs +18 -15
  175. package/src/process.mjs +6 -3
  176. package/src/release-bundle.mjs +14 -11
  177. package/src/runtime.mjs +4 -1
  178. package/src/scaffold.mjs +9 -6
  179. package/src/sdk-browser-runtime.mjs +4 -1
  180. package/src/source-server.mjs +17 -14
  181. package/src/targets/index.mjs +5 -2
  182. package/src/templates/workspace-template.mjs +13 -6
  183. package/src/testing-loader.mjs +7 -4
  184. package/src/testing.mjs +10 -7
  185. package/src/toolchain.mjs +13 -10
  186. package/src/update-check.mjs +9 -6
  187. package/src/workspace-operation-lock.mjs +14 -11
  188. package/src/workspace-runtime.mjs +4 -1
  189. package/src/workspace.mjs +17 -14
  190. package/runtime/arcane/modules/AIResponseLength.js +0 -32
@@ -4,6 +4,13 @@ Every file shipped under `runtime/arcane/modules/` appears here. Start with the
4
4
 
5
5
  Apps import renderer ESM from `/arcane/modules/<file>`. Classic scripts, the OPFS worker, uPlot stylesheet, and vendor license are called out explicitly. Importing a module does not grant a native capability.
6
6
 
7
+ Applications own response-detail preferences, their saved values, and any
8
+ verbosity instruction appended to the system prompt. The former
9
+ `AIResponseLength.js` module and its exports have been removed. Before adopting
10
+ this source change, consumers must normalize preferences in their application
11
+ and pass their complete system prompt directly instead of calling the former
12
+ no-op `applyAIResponseLength()` helper. Existing saved preferences are unchanged.
13
+
7
14
  ## Availability shorthand
8
15
 
9
16
  - **Cross-host** means in-process logic built from standard JavaScript/Web APIs.
@@ -44,8 +51,7 @@ own asynchronous work, cancellation, and backpressure.
44
51
  | [`AIPreferenceRuntime.js`](#aipreferenceruntimejs) | esm | Applies and reads non-persistent per-user AI preference overrides. | Cross-host | Normalized six-slot preference state. |
45
52
  | [`AIPreferenceTuple.js`](#aipreferencetuplejs) | esm | Normalizes and compares the six provider/model preference slots. | Cross-host | Fully normalized frozen tuple. |
46
53
  | [`AIProviderRuntime.js`](#aiproviderruntimejs) | esm | Owns provider-neutral selection, lifecycle, routing, startup, requests, streaming, cancellation, and independent LLM/STT/TTS state. | Cross-host runtime; provider-specific availability | Normalized required provider members plus route/status contracts, with explicit local-only selection and no implicit fallback. |
47
- | [`AIResponseLength.js`](#airesponselengthjs) | esm | Normalizes current low/medium/high response-preference selectors while preserving complete prompts. | Cross-host | Current selector normalization; prompt content remains unchanged. |
48
- | [`AIResponseURLPolicy.js`](#airesponseurlpolicyjs) | esm | Extracts and audits links from AI Markdown, rendered HTML, CSS, srcset, bare URLs, and email text. | Cross-host | Normalized frozen allowlist audit. |
54
+ | [`AIResponseURLPolicy.js`](#airesponseurlpolicyjs) | esm | Extracts and audits links from AI Markdown, rendered HTML, CSS, srcset, bare URLs, and email text. | Cross-host | Mutable audit with exact link comparison after renderer-level decoding. |
49
55
  | [`AIRuntimeState.js`](#airuntimestatejs) | esm | Publishes sticky mutable role snapshots, lifecycle intents, and startup-settlement barriers. | Cross-host state contract | Closed monotonic state records; events report state but grant no authority. |
50
56
  | [`AnsiText.js`](#ansitextjs) | esm | Parses terminal ANSI sequences into display spans or strips them to plain text. | Cross-host | Normalized text/span output. |
51
57
  | [`ApiModelDatabase.js`](#apimodeldatabasejs) | esm | Fetches an injectable HTTP JSON model with parser, cache, redacted public endpoint records, and request lifecycle events. | Browser / native WebView / server with fetch | Request records are normalized; fetch/provider failures remain mixed. |
@@ -100,6 +106,7 @@ own asynchronous work, cancellation, and backpressure.
100
106
  | [`OpenMeteoWeatherProvider.js`](#openmeteoweatherproviderjs) | esm | Searches and loads Open-Meteo data into complete mutable Arcane weather entities. | Browser / native WebView / server with fetch + cloud | Provider data normalized to mutable entities; transport errors mixed. |
101
107
  | [`PersistentAIChatSession.js`](#persistentaichatsessionjs) | esm | Adds explicit retained-history/memory policy to complete configured chat without changing DBOPFS or ChatEntity semantics. | Browser / native WebView with DBOPFS and configured chat | Retained context commits atomically; `persist:false` turns are one-operation-only. |
102
108
  | [`PreferenceStore.js`](#preferencestorejs) | esm | Loads and updates schema-defined app preferences through native storage with a narrow browser fallback. | Browser/native hybrid | Complete ordinary values remain mutable; setAll uses one optional atomic adapter batch for every selected value when advertised, otherwise performs complete ordered serial writes, and only exact unsupported native capability changes future operations to the browser fallback. |
109
+ | [`PreparedSpeech.js`](#preparedspeechjs) | esm | Owns detached ordered preparation, semantic audio reuse, and per-caller cancellation behind AI.prepareTTS. | Browser / native WebView with injected synthesis and optional DBOPFS | Complete original inputs, ordered audio metadata, durable reuse, and observable preparation results. |
103
110
  | [`QRCode.min.js`](#qrcodeminjs) | classic-script | Vendored QRCode generator for DOM, canvas, SVG, and image output. | Browser vendor script | Vendor-native. |
104
111
  | [`Questionnaire.js`](#questionnairejs) | esm | Evaluates whether a one-time questionnaire prompt is due without performing the prompt. | Cross-host | Normalized conservative boolean. |
105
112
  | [`RecordLinkIndex.js`](#recordlinkindexjs) | esm | Parses record links and builds their normalized index. | Cross-host | Fully normalized. |
@@ -109,7 +116,7 @@ own asynchronous work, cancellation, and backpressure.
109
116
  | [`ScamRiskPolicy.js`](#scamriskpolicyjs) | esm | Combines deterministic scam signals with optional Arcane blocked-domain evidence and safety guidance. | Cross-host | Complete mutable results; blocked-domain policy requires `secure:true`. |
110
117
  | [`ScopedOPFSCache.js`](#scopedopfscachejs) | esm | Provides a narrow exact-key JSON cache inside one app-owned OPFS namespace. | Browser / native WebView | Filename-safe keys, complete JSON values, and malformed-cache cleanup normalized; storage errors mixed. |
111
118
  | [`ScreenCapture.js`](#screencapturejs) | esm | Captures a display surface as image, video, or GIF with explicit lifecycle events. | Browser / native WebView | State/events normalized; permission and codec errors mixed. |
112
- | [`SpeechPlayback.js`](#speechplaybackjs) | esm | Preserves exact nonblank text as one speech segment, queues latest-request synthesis, and controls lookahead HTML audio playback. | Browser + native bridge | Exact input text and state normalized; provider/media failures mixed. |
119
+ | [`SpeechPlayback.js`](#speechplaybackjs) | esm | Admits complete speech segments to a capacity-advertising provider immediately, retains serialized native/custom lookahead, and plays every result in exact indexed order. | Browser + native bridge | Stored part text stays exact; the outbound speech-input copy receives automatic formatting-mark cleanup; provider/media failures remain mixed. |
113
120
  | [`StaticDocumentCatalog.js`](#staticdocumentcatalogjs) | esm | Loads a positive static document inventory with cache, search, and complete context. | Browser / native WebView / server with fetch | Mutable complete catalog/content normalization; malformed data and transport failures remain visible. |
114
121
  | [`SystemAppearance.js`](#systemappearancejs) | esm | Reads or applies native appearance, returning an explicit unsupported browser state when no bridge exists. | Browser/native hybrid | Absent bridge normalized; native result/error preserved. |
115
122
  | [`SystemPlatformPresentation.js`](#systemplatformpresentationjs) | classic-script | Maps kernel names to presentation labels/classes without granting platform authority. | Browser / native WebView classic script | Fully normalized presentation only. |
@@ -144,6 +151,8 @@ default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and
144
151
  `streamRequest()`, `streamMessage()`, `fetchRequest()`, `fetch()`,
145
152
  read-only `ttsSegmentation`, `configureTTSSegmentation()`,
146
153
  `streamTTS(text='',end=false,options={})`,
154
+ `prepareTTS({parts,storage,identity,signal,onState})`,
155
+ `playPreparedTTS(prepared,{signal,onState})`,
147
156
  `finishTTS()`, `fetchTTS()`, `fetchSTT()`, `stopAudio()`, `resumeAudio()`,
148
157
  `playAudio()`; consumes `user-entity-loaded` and `arcane-ollama-ready`,
149
158
  installs `window.ai`, and emits `ai-ready` and `ai-tts-failure`.
@@ -263,7 +272,7 @@ reports `requestedDevice`, `selectedDevice`, `maxConcurrentRequests`, and
263
272
  WASM fallback after a successful load. Calling `status()` without options keeps
264
273
  the existing sticky lifecycle snapshot and does not inspect provider execution.
265
274
  Provider inspection failures are surfaced to the caller.
266
- `fetchTTS({model,voice,input,responseFormat,speed},signal)` accepts the public
275
+ `fetchTTS({model,voice,input,responseFormat,speed},signal,preparation={})` accepts the public
267
276
  provider-neutral synthesis shape, requires any explicit model to match the
268
277
  selected route, and fills an omitted voice only from the selected model
269
278
  catalog's `defaultVoice`. An omitted response format preserves the instance's
@@ -273,9 +282,14 @@ the setting is the instance's `opus` default and the model rejects it, the catal
273
282
  `speech.defaultResponseFormat` is used, while any other unsupported setting is
274
283
  rejected. It propagates the caller-owned signal and returns a playable `Blob`;
275
284
  it does not independently choose a provider, cloud fallback, model, runtime, or
276
- voice policy for the application. `streamTTS(text='',end=false,options={})` and
285
+ voice policy for the application. Every call removes repeated same formatting
286
+ marks from a cloned outbound input before delegation; the caller's payload stays
287
+ unchanged. The third `preparation` argument is reserved for SDK-internal
288
+ delegation, where `{speechInputPrepared:true}` prevents a second cleanup pass;
289
+ applications omit it. `streamTTS(text='',end=false,options={})` and
277
290
  `finishTTS()` use this same request boundary. The third-argument options below
278
- are available in SDK `0.5.12`, with `textFormat` added in SDK `0.5.16`:
291
+ are available in SDK `0.5.12`. The `textFormat` compatibility extra added in
292
+ SDK `0.5.16` is ignored beginning in `0.5.17`:
279
293
 
280
294
  | Field | Default | Meaning |
281
295
  | --- | --- | --- |
@@ -283,24 +297,23 @@ are available in SDK `0.5.12`, with `textFormat` added in SDK `0.5.16`:
283
297
  | `speed` | Current `ai.voiceSpeed` | A supplied positive speed is captured for those segments and forwarded to `fetchTTS()`. It does not change `ai.voiceSpeed`. |
284
298
  | `pauseAfterMs` | `0` | Finite, nonnegative milliseconds placed after the final extracted segment on the existing audio clock. Invalid values throw `RangeError`; no pause is inserted between this call's other segments. |
285
299
  | `waitForPlayback` | `false` | Omission retains the preparation promise. With `true`, the promise resolves after every extracted segment reaches a terminal playback state: `true` when all naturally end, or `false` after terminal cancellation or failure. |
286
- | `textFormat` | `plain` for a new stream | `markdown` removes repeated same formatting marks (`*`, `#`, `_`, backtick, `~`) before segmentation. Single marks and ordinary punctuation remain literal. The selected format lasts through this producer's terminal flush. |
300
+ | `textFormat` | Ignored compatibility extra | Repeated same formatting marks are removed automatically from every TTS call. This value no longer selects or disables cleanup. |
287
301
 
288
302
  The voice and speed use the existing `fetchTTS()` validation and error path.
289
- Plain mode preserves the submitted text. Markdown mode changes narration only;
290
- it leaves displayed, stored, and model content untouched. It is a narrow
303
+ The automatic cleanup changes only the outbound speech-input copy; displayed,
304
+ stored, model, and caller-owned content remains exact. It is a narrow
291
305
  formatting-mark filter, not a full Markdown parser: links, code contents, list
292
- text, and other characters remain literal. A trailing candidate mark waits for
293
- the next character so a repeated run split across chunks is still omitted.
294
- Ordinary prose streams immediately. `end:true`, `finishTTS()`, muted terminal
295
- calls, and `stopAudio()` clear the formatting state. An explicit format change
296
- flushes pending formatting marks under the preceding mode before accepting
297
- the next text. `fetchTTS()` retains its exact-text contract.
306
+ text, single marks, ordinary punctuation, and other characters remain literal.
307
+ A trailing candidate mark waits for the next character so a repeated run split
308
+ across chunks is still omitted. Ordinary prose streams immediately.
309
+ `end:true`, `finishTTS()`, muted terminal calls, and `stopAudio()` clear pending
310
+ formatting state. `textFormat` is not an opt-out.
298
311
 
299
312
  Voice, speed, pause, and playback overrides belong to the segments
300
313
  extracted in that invocation, including any text buffered by an earlier call.
301
314
  Those overrides are not retained with an unfinished `end:false` remainder; a
302
315
  later call supplies its own options, and `finishTTS()` uses their defaults
303
- while retaining the active text format. Use `end:true`
316
+ while flushing any pending formatting mark. Use `end:true`
304
317
  for a complete passage. A call extracting no segments resolves
305
318
  `true` without waiting for earlier jobs; `finishTTS()` remains a preparation
306
319
  flush, not a queue-wide playback barrier. A muted call resolves `false`.
@@ -308,13 +321,63 @@ flush, not a queue-wide playback barrier. A muted call resolves `false`.
308
321
  Playback completion stays pending while the browser waits for an audio-unlock
309
322
  gesture or a recoverable resume attempt. If resuming a closed `AudioContext`
310
323
  fails, the affected jobs terminate and their playback results settle `false`.
311
- `stopAudio()` cancels all speech owned
312
- by this AI instance and settles pending playback promises `false`. A trailing
324
+ `stopAudio()` cancels streamed speech and prepared playback owned
325
+ by this AI instance and settles pending playback promises `false`; detached
326
+ preparation retains its own cancellation lifetime. A trailing
313
327
  pause delays the next queued audio; the preceding promise resolves when its
314
328
  last audio buffer ends, without waiting out that pause. Completion describes
315
329
  the playback lifecycle, not proof that a listener heard the sound. See the
316
330
  [complete-passage example](ai/browser-speech.md#queue-complete-passages-and-wait-for-playback).
317
331
 
332
+ `prepareTTS({parts,storage,identity,signal,onState})` returns an immediate
333
+ `{segments,state,ready,getAudio(index),cancel()}` handle for detached complete
334
+ speech preparation. Parts are strings or `{input,voice?,speed?,pauseAfterMs?}`
335
+ records. It retains the full source for semantic matching, snapshots the
336
+ selected speech configuration and segmentation, and applies automatic
337
+ formatting cleanup once to the speech copy. Generation uses the existing
338
+ bounded provider queue without adding playback. Optional
339
+ `storage:{db,table,key}` saves complete audio files and their MIME metadata in
340
+ the caller's ready DBOPFS instance; the separate JSON-compatible `identity`
341
+ adds application-owned semantic context. Reuse compares complete inputs rather
342
+ than an SDK version alone.
343
+
344
+ `state` is `queued`, `preparing`, `ready`, `error`, or `cancelled`.
345
+ `onState({state,completed,total,segments,error})` synchronously observes
346
+ preparation progress. `ready` resolves the complete record after every segment
347
+ is ready and, when storage is selected, durably saved. `getAudio(index)` waits
348
+ for the corresponding ordered segment and returns its complete `Blob` with
349
+ the retained MIME type. Preparation failure rejects; cancellation rejects as
350
+ `AbortError` and preserves successfully stored segments. Matching pending
351
+ requests share synthesis only on the same AI instance and storage group;
352
+ cancelling one handle does not cancel another active matching caller.
353
+ Preparation cancellation prevents later synthesis, but an already-started
354
+ shared provider load/unmute has no per-preparation signal and may finish.
355
+
356
+ `playPreparedTTS(prepared,{signal,onState})` can attach immediately. Its returned
357
+ `{state,error,finished,pause(),resume(),stop()}` handle schedules segments in
358
+ their original order using the existing AI audio clock. `finished` resolves `true` after
359
+ natural completion and `false` after stop, cancellation, or failure; genuine
360
+ failures also use the existing complete diagnostics and `ai-tts-failure` event.
361
+ Part pauses separate adjacent audio; the final trailing pause does not delay
362
+ `finished` after the final audio buffer ends. State/error getters and the
363
+ optional synchronous `onState({state,error})` callback expose `waiting`,
364
+ `waiting-for-gesture`, `scheduled`, `paused`, `complete`, `stopped`, or `error`.
365
+ Use `waiting-for-gesture` for audio-unlock UI; a `false` result from `resume()`
366
+ alone is not a first-segment-ready signal.
367
+ Pause and resume are asynchronous boolean controls scoped to that handle's
368
+ audio context; stop is synchronous. Playback completion and these controls do
369
+ not cancel independent preparation. One AI has one playback lane: attaching
370
+ prepared playback replaces its preceding streamed or prepared audio, and
371
+ `streamTTS()` interrupts active prepared playback. `stopAudio()` stops all
372
+ this AI's playback but keeps detached preparation running; `setSpeechMuted(true)` also cancels
373
+ provider TTS work and unloads it. Replacing the selected speech configuration
374
+ cancels missing generation for that earlier selection. Completed stored audio
375
+ remains in application storage.
376
+ Fully stored replay enables playback without loading the selected speech
377
+ model even when the AI starts muted. Missing audio alone requests the shared
378
+ TTS readiness path. See [prepared narration](ai/browser-speech.md#prepare-narration-once-and-replay-stored-audio)
379
+ for full record shapes, storage ownership, and a complete example.
380
+
318
381
  Streaming speech retains sentence
319
382
  segmentation by default. `configureTTSSegmentation({punctuation,wordCadence})`
320
383
  accepts `punctuation:'sentence'|'any'|'none'` and a `wordCadence` that is either
@@ -324,8 +387,9 @@ commas, and hyphens remain inside a segment when they join Unicode letters or
324
387
  numbers. A potentially joining mark at the current end of an incremental stream
325
388
  waits for the next character or terminal flush before the boundary is decided;
326
389
  `wordCadence` completes one after that many whole words. The earliest available
327
- boundary wins. Segmentation preserves every character, including punctuation
328
- and whitespace. Every completed segment enters synthesis immediately; provider
390
+ boundary wins. Segmentation preserves every character of the already prepared
391
+ speech text, including punctuation and whitespace. Every completed segment
392
+ enters synthesis immediately; provider
329
393
  capacity supplies FIFO backpressure while allowing bounded TTS work to overlap.
330
394
  A later segment may finish synthesis first, but playback schedules only the
331
395
  contiguous ready prefix in original order. Decoded buffers with known duration
@@ -662,9 +726,11 @@ The singleton exposes read-only `protocol`, `configured`, and `speechMuted`;
662
726
  `status(role=null,options={})`; `catalog(role)`;
663
727
  `inspect(role,options={})`; `start(options)`; `load(role,options={})`;
664
728
  `unload(role,options={})`; `dispose(role,options={})`;
665
- `disposeAll(options={})`; `cancel(role)`; `request(role,options={})`;
729
+ `disposeAll(options={})`; `cancel(role)`;
730
+ `request(role,options={},preparation={})`;
666
731
  `chat(payload,options={})`; `stream(payload,options={})`;
667
- `transcribe(payload,options={})`; `synthesize(payload,options={})`; and
732
+ `transcribe(payload,options={})`;
733
+ `synthesize(payload,options={},preparation={})`; and
668
734
  `setSpeechMuted(muted)`. Provider payloads must be data-only; callbacks,
669
735
  accessors, symbols, and cycles are rejected at the provider boundary.
670
736
 
@@ -680,6 +746,14 @@ records are the closed `{llm,stt,tts}`, `{stt,tts}`, or
680
746
  `{providers,routes,expectedProviders}` shapes described below.
681
747
  `configureFromTuple()` accepts exactly six provider/model preference entries.
682
748
 
749
+ Every direct TTS `request()` or `synthesize()` call removes repeated same
750
+ formatting marks from a cloned outbound payload's `input` or `text` field. The
751
+ caller's payload and request records remain unchanged. The optional
752
+ `preparation` argument is reserved for SDK-owned delegation;
753
+ `{speechInputPrepared:true}` prevents a second pass after another SDK speech
754
+ boundary has already cleaned the copy. Applications omit that argument. LLM
755
+ and STT payloads are unaffected.
756
+
683
757
  `register()` returns the provider's single unregister closure; caller-
684
758
  registered providers remain caller-owned. The high-level
685
759
  `AI.configureBrowserSpeech()` boundary is different: AI constructs, registers,
@@ -794,35 +868,6 @@ const runtime = getAIProviderRuntime();
794
868
  console.log(runtime.protocol, runtime.status());
795
869
  ```
796
870
 
797
- ## AIResponseLength.js
798
-
799
- ### Overview
800
-
801
- Normalizes current low/medium/high response-preference selectors while
802
- preserving complete prompts unchanged.
803
-
804
- ### Public surface
805
-
806
- Response-preference constants plus `normalizeAIResponseLength()`,
807
- `aiResponseLengthInstruction()`, and `applyAIResponseLength()`. Every option is
808
- labeled `Complete`, the instruction helper returns an empty string, and the
809
- application helper returns its complete `systemPrompt` unchanged.
810
-
811
- Exact exports: `AI_RESPONSE_LENGTH_DEFAULT`, `AI_RESPONSE_LENGTH_OPTIONS`, `aiResponseLengthInstruction`, `applyAIResponseLength`, `normalizeAIResponseLength`.
812
-
813
- ### Availability and normalization
814
-
815
- **Cross-host.** Current selector normalization with no prompt transformation.
816
- Transport: In-process only. [Deep protocol details](protocols.md).
817
-
818
- ### Example
819
-
820
- ```javascript
821
- import * as module from '/arcane/modules/AIResponseLength.js';
822
-
823
- console.log(Object.keys(module));
824
- ```
825
-
826
871
  ## AIResponseURLPolicy.js
827
872
 
828
873
  ### Overview
@@ -837,7 +882,19 @@ Exact exports: `auditAIResponseLinks`, `decodeHTMLCharacterReferences`, `extract
837
882
 
838
883
  ### Availability and normalization
839
884
 
840
- **Cross-host.** Normalized frozen allowlist audit. Transport: In-process; bundled Marked parser. [Deep protocol details](protocols.md).
885
+ **Cross-host.** Returns a mutable `{ok, links, unsupportedLinks, allowedLinks}`
886
+ audit. Browsers use detached native HTML elements to parse rendered markup and
887
+ decode character references. Hosts without a document retain lexical extraction
888
+ and the existing limited entity decoder. Rendered values are decoded only once.
889
+ Markdown destinations, CSS URLs, srcset candidates, and authored source positions
890
+ remain part of the audit; DOM-only attribute links use document order after
891
+ authored links because the DOM does not expose source offsets.
892
+
893
+ Comparison uses exact values after entity and Markdown escape decoding. URI
894
+ encoding, decoding, or URL canonicalization would change those values and is
895
+ not applied. The audit neither changes the response content nor fetches or
896
+ navigates to links. Transport: In-process; bundled Marked parser.
897
+ [Deep protocol details](protocols.md).
841
898
 
842
899
  ### Example
843
900
 
@@ -2261,11 +2318,12 @@ console.log(Object.keys(module));
2261
2318
 
2262
2319
  ### Overview
2263
2320
 
2264
- Filters repeated same Markdown formatting marks from narration before speech
2265
- segmentation. The filter recognizes `*`, `#`, `_`, backtick, and `~` runs of two
2266
- or more, including runs arriving across separate chunks. Single marks,
2267
- ellipses, quoted sentence endings, whitespace, and all other text remain
2268
- literal. It neither interprets links nor changes language or voice.
2321
+ Re-exports the streaming `MarkdownSpeech` filter from the shared
2322
+ `arcane-os/speech-text` package entrypoint. The filter removes repeated runs of
2323
+ `*`, `#`, `_`, backtick, and `~` before speech segmentation, including runs
2324
+ arriving across separate chunks. Single marks, ellipses, quoted sentence
2325
+ endings, whitespace, and all other text remain literal. It neither interprets
2326
+ links nor changes language or voice.
2269
2327
 
2270
2328
  ### Public surface
2271
2329
 
@@ -2275,7 +2333,8 @@ Exact exports: `MarkdownSpeech`.
2275
2333
 
2276
2334
  ### Availability and normalization
2277
2335
 
2278
- **Cross-host.** Plain JavaScript with no dependencies. `append()` returns only
2336
+ **Cross-host.** The runtime projection and public package entrypoint share the
2337
+ same implementation. `append()` returns only
2279
2338
  the newly available narration. Only a trailing candidate marker and whether
2280
2339
  it repeats are retained; ordinary text is emitted immediately. Terminal
2281
2340
  `append(text,true)` flushes a single pending mark and resets state. `reset()`
@@ -2673,6 +2732,54 @@ import * as module from '/arcane/modules/PreferenceStore.js';
2673
2732
  console.log(Object.keys(module));
2674
2733
  ```
2675
2734
 
2735
+ ## PreparedSpeech.js
2736
+
2737
+ ### Overview
2738
+
2739
+ Shared preparation mechanism used by `AI.prepareTTS()`. It owns ordered
2740
+ generation admission, same-owner request sharing, complete audio storage and
2741
+ semantic reuse, and each caller's preparation lifetime. It does not construct
2742
+ an audio context, play speech, or select application content.
2743
+
2744
+ ### Public surface
2745
+
2746
+ Named `prepareSpeech({owner,parts,originalParts=parts,selection=null,
2747
+ segmentation=null,storage=null,identity=null,signal=null,onState,synthesize})`.
2748
+ The owning AI supplies already segmented speech parts, complete original parts,
2749
+ its selection snapshot, and its synthesis callback. The returned handle is
2750
+ `{segments,state,ready,getAudio(index),cancel()}`. Applications use
2751
+ `AI.prepareTTS()` and `AI.playPreparedTTS()` so the existing AI owner retains
2752
+ automatic formatting cleanup, segmentation, provider readiness/capacity, and
2753
+ ordered playback. See the [complete preparation contract](ai/browser-speech.md#prepare-narration-once-and-replay-stored-audio).
2754
+
2755
+ `storage:{db,table,key}` is optional; the ready DBOPFS instance supplies
2756
+ `get`, `set`, `readFile`, and `writeFile`. JSON-compatible semantic inputs stay
2757
+ complete in the version-1 manifest. Raw audio is persisted separately and its
2758
+ MIME type is retained in metadata. Storage mutation serializes by database,
2759
+ table, and key within the realm. Synthesis sharing is scoped to the same owner
2760
+ and matching semantic inputs/storage; playback remains outside this module.
2761
+
2762
+ Exact exports: `prepareSpeech`.
2763
+
2764
+ ### Availability and normalization
2765
+
2766
+ **Browser or native WebView with Blob, AbortController, an injected synthesis
2767
+ callback, and optional ready DBOPFS.** Import creates no provider or
2768
+ playback. Calling the preparation function starts owned asynchronous work.
2769
+ `ready` rejects complete synthesis/storage failures or `AbortError` after
2770
+ cancellation; successful audio remains available for reuse. Malformed part,
2771
+ storage, or semantic metadata inputs throw `TypeError`; an invalid segment
2772
+ index rejects with `RangeError`. No Core capability is selected here.
2773
+
2774
+ ### Example
2775
+
2776
+ ```javascript
2777
+ import {prepareSpeech} from '/arcane/modules/PreparedSpeech.js';
2778
+
2779
+ // Applications use AI.prepareTTS; this import only exposes the SDK mechanism.
2780
+ console.log(typeof prepareSpeech); // function
2781
+ ```
2782
+
2676
2783
  ## QRCode.min.js
2677
2784
 
2678
2785
  ### Overview
@@ -2899,15 +3006,17 @@ console.log(Object.keys(module));
2899
3006
 
2900
3007
  Preserves exact nonblank text, admits complete speech segments according to the
2901
3008
  selected client's advertised capacity, and plays indexed HTML audio in exact
2902
- input order.
3009
+ input order. Stored parts remain exact; only each outbound synthesis payload
3010
+ copy receives automatic formatting-mark cleanup.
2903
3011
 
2904
3012
  ### Public surface
2905
3013
 
2906
- `SpeechPlayback` class/default, `SPEECH_PLAYBACK_STATE_EVENT`,
2907
- `splitSpeechText()`, and playback lifecycle APIs.
3014
+ `SpeechPlayback` class/default, the shared voice compatibility catalogs,
3015
+ `SPEECH_PLAYBACK_STATE_EVENT`, `splitSpeechText()`, the optional constructor
3016
+ state callback, and playback lifecycle APIs.
2908
3017
 
2909
- Exact exports: `SPEECH_PLAYBACK_STATE_EVENT`, `SpeechPlayback`, `default`, and
2910
- `splitSpeechText`.
3018
+ Exact exports: `SPEECH_PLAYBACK_STATE_EVENT`, `SPEECH_VOICE_ALIASES`,
3019
+ `SPEECH_VOICE_OPTIONS`, `SpeechPlayback`, `default`, and `splitSpeechText`.
2911
3020
 
2912
3021
  ```text
2913
3022
  new SpeechPlayback({
@@ -2917,6 +3026,7 @@ new SpeechPlayback({
2917
3026
  voice=null,
2918
3027
  responseFormat=null,
2919
3028
  speed=1,
3029
+ onState=()=>{},
2920
3030
  createObjectURL,
2921
3031
  revokeObjectURL,
2922
3032
  delay,
@@ -2925,17 +3035,28 @@ new SpeechPlayback({
2925
3035
  ```
2926
3036
 
2927
3037
  `speech` must expose either `fetchTTS(payload, signal)` or
2928
- `synthesize(payload, {signal})`. `prepare({key,parts,model,voice,responseFormat,
3038
+ `synthesize(payload, {signal})`. `SpeechPlayback` also supplies a third
3039
+ SDK-internal preparation object; existing two-argument clients may ignore it.
3040
+ `prepare({key,parts,model,voice,responseFormat,
2929
3041
  speed,autoplay=true})` uses only caller-supplied model, voice, and response-format
2930
3042
  values; those three omitted values remain omitted so the selected AI/model
2931
3043
  catalog may provide its documented defaults. Speed defaults to `1`, is normalized
2932
- as a positive number, and is always sent. There is no
2933
- hard-coded model, response format, voice, or cloud/browser fallback.
3044
+ as a positive number, and is always sent. `SPEECH_VOICE_OPTIONS` is the ordered
3045
+ mutable compatibility array `{value,label}` for `alloy`, `ash`, `ballad`,
3046
+ `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, and `shimmer`;
3047
+ `SPEECH_VOICE_ALIASES` is the mutable `Set` of those values. The class does not
3048
+ select either catalog or promise that a selected provider supports its values.
3049
+ There is no hard-coded model, response format, voice, or cloud/browser fallback.
2934
3050
  `splitSpeechText(value)` uses trimming only to detect blank input, then returns
2935
3051
  the caller's exact string in one mutable array without trimming, splitting, or
2936
3052
  freezing it. `prepare()` likewise preserves each nonblank part's exact `input`
2937
3053
  string while normalizing its other playback fields into a new mutable record.
2938
- The class applies no part-count, character-count, pause, or input upper cap.
3054
+ At synthesis time, `requestSpeech()` copies that record, removes repeated same
3055
+ formatting marks from only the outbound `input`, and delegates with the
3056
+ SDK-internal `{speechInputPrepared:true}` argument so downstream SDK boundaries
3057
+ do not apply the non-idempotent filter again. Original part objects, stored
3058
+ parts, displayed text, and all non-input payload fields remain unchanged. The
3059
+ class applies no part-count, character-count, pause, or input upper cap.
2939
3060
 
2940
3061
  ### Admission and playback order
2941
3062
 
@@ -2961,8 +3082,13 @@ Every preparation owns an operation ID and one AbortController for each active
2961
3082
  synthesis segment or playback delay. Replacement,
2962
3083
  `stop()`, `cancel()`, and `destroy()` abort their owned signals, suppress stale
2963
3084
  settlement, release Blob URLs, and publish synchronous
2964
- `speech-playback-state` occurrences through `globalThis.arcaneEvents`.
2965
- Subscribers receive mutable public state detail. The detail contains
3085
+ `speech-playback-state` occurrences through `globalThis.arcaneEvents` before
3086
+ calling the optional `onState(detail)` function synchronously. Canonical
3087
+ subscribers and the callback observe the same public field values at dispatch
3088
+ time, but object identity is not promised. Both surfaces expose mutable public
3089
+ state detail. A callback failure is reported through `globalThis.reportError`
3090
+ when available, otherwise `console.error`, and does not replace playback
3091
+ settlement. The detail contains
2966
3092
  `state`, `message`, `key`, `index`, `total`, `producing`, `buffered`, `hasAudio`,
2967
3093
  `operationId`, `code`, and `reason`; a first-segment provider rejection remains
2968
3094
  preserved to the `prepare()` caller. Later failures surface when ordered
@@ -3004,7 +3130,10 @@ const audio = document.body.appendChild(document.createElement('audio'));
3004
3130
  audio.controls = true;
3005
3131
  const speech = new SpeechPlayback({
3006
3132
  audio,
3007
- speech: globalThis.ai
3133
+ speech: globalThis.ai,
3134
+ onState(detail) {
3135
+ console.log('Speech state:', detail.state);
3136
+ }
3008
3137
  });
3009
3138
  const button = document.body.appendChild(document.createElement('button'));
3010
3139
  button.textContent = 'Speak';
@@ -3020,6 +3149,19 @@ button.addEventListener('click', async function speakCompleteSegments() {
3020
3149
  });
3021
3150
  ```
3022
3151
 
3152
+ The shared compatibility catalogs are also available directly. They do not
3153
+ select a voice for `SpeechPlayback`:
3154
+
3155
+ ```javascript
3156
+ import {
3157
+ SPEECH_VOICE_ALIASES,
3158
+ SPEECH_VOICE_OPTIONS
3159
+ } from '/arcane/modules/SpeechPlayback.js';
3160
+
3161
+ console.log(SPEECH_VOICE_OPTIONS[0]); // {value: 'alloy', label: 'Alloy'}
3162
+ console.log(SPEECH_VOICE_ALIASES.has('alloy')); // true
3163
+ ```
3164
+
3023
3165
  With the default browser speech configuration, both parts enter its capacity-4
3024
3166
  queue immediately and still play first, then second. Inspect the selected
3025
3167
  execution device without guessing from console warnings: