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
@@ -1,3 +1,6 @@
1
+ import Is from 'strong-type';
2
+ const is=new Is(false);
3
+
1
4
  import './DBOPFS.js';
2
5
  import UserEntity from '../entities/User.js';
3
6
  import {
@@ -13,7 +16,8 @@ import {
13
16
  } from './AIProviderRuntime.js';
14
17
  import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
15
18
  import {arcaneLogging} from 'arcane-os/logging';
16
- import {MarkdownSpeech} from './MarkdownSpeech.js';
19
+ import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
20
+ import {prepareSpeech} from './PreparedSpeech.js';
17
21
 
18
22
  const completeValue=(value)=>value;
19
23
 
@@ -141,7 +145,7 @@ function normalizeAIReasoningEffort(value){
141
145
  if(value===undefined||value===null||value===''){
142
146
  return '';
143
147
  }
144
- if(typeof value!=='string'||!AI_REASONING_EFFORTS.has(value)){
148
+ if(!is.string(value)||!AI_REASONING_EFFORTS.has(value)){
145
149
  const error=new TypeError(
146
150
  'AI reasoningEffort must be none, low, medium, high, or max.'
147
151
  );
@@ -168,7 +172,7 @@ function aiStructuralError(code,message,cause){
168
172
  }
169
173
 
170
174
  function isPlainAIRecord(value){
171
- if(!value||typeof value!=='object'||Array.isArray(value)){
175
+ if(!value||!is.object(value)||is.array(value)){
172
176
  return false;
173
177
  }
174
178
  const prototype=Object.getPrototypeOf(value);
@@ -197,7 +201,7 @@ function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
197
201
  :value.wordCadence;
198
202
  if(
199
203
  wordCadence!==null
200
- &&(!Number.isSafeInteger(wordCadence)||wordCadence<1)
204
+ &&(!is.safeInteger(wordCadence)||wordCadence<1)
201
205
  ){
202
206
  throw new RangeError(
203
207
  'TTS segmentation wordCadence must be null or a positive integer.'
@@ -208,7 +212,7 @@ function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
208
212
  }
209
213
 
210
214
  function requireAIToolMessageSchemas(value,label='AI tools'){
211
- if(!Array.isArray(value)){
215
+ if(!is.array(value)){
212
216
  throw aiStructuralError(
213
217
  'AI_CHAT_INVALID_TOOL_CALL',
214
218
  `${label} must be an array.`
@@ -227,9 +231,9 @@ function requireAIToolMessageSchemas(value,label='AI tools'){
227
231
  ||!isPlainAIRecord(parameters.properties)
228
232
  ||!isPlainAIRecord(messageSchema)
229
233
  ||messageSchema.type!=='string'
230
- ||!Number.isInteger(messageSchema.minLength)
234
+ ||!is.integer(messageSchema.minLength)
231
235
  ||messageSchema.minLength<1
232
- ||!Array.isArray(parameters.required)
236
+ ||!is.array(parameters.required)
233
237
  ||!parameters.required.includes('message')
234
238
  ){
235
239
  throw aiStructuralError(
@@ -243,13 +247,13 @@ function requireAIToolMessageSchemas(value,label='AI tools'){
243
247
  function normalizeAIStructuralToolCall(call,label='Structural tool call'){
244
248
  if(
245
249
  !isPlainAIRecord(call)
246
- ||typeof call.id!=='string'
250
+ ||!is.string(call.id)
247
251
  ||!call.id.trim()
248
252
  ||call.type!=='function'
249
253
  ||!isPlainAIRecord(call.function)
250
- ||typeof call.function.name!=='string'
254
+ ||!is.string(call.function.name)
251
255
  ||!call.function.name.trim()
252
- ||typeof call.function.arguments!=='string'
256
+ ||!is.string(call.function.arguments)
253
257
  ){
254
258
  throw aiStructuralError(
255
259
  'AI_CHAT_INVALID_TOOL_CALL',
@@ -272,7 +276,7 @@ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
272
276
  `${label} arguments must encode a JSON object.`
273
277
  );
274
278
  }
275
- if(typeof argumentsRecord.message!=='string'||!argumentsRecord.message.trim()){
279
+ if(!is.string(argumentsRecord.message)||!argumentsRecord.message.trim()){
276
280
  throw aiStructuralError(
277
281
  'AI_CHAT_TOOL_MESSAGE_REQUIRED',
278
282
  `${label} arguments must include a nonempty user-facing message.`
@@ -291,7 +295,7 @@ function normalizeAIStructuralToolCall(call,label='Structural tool call'){
291
295
  }
292
296
 
293
297
  function validateAIRequestMessages(messages=[]){
294
- if(!Array.isArray(messages)){
298
+ if(!is.array(messages)){
295
299
  throw new TypeError('AI messages must be an array.');
296
300
  }
297
301
  const pendingToolCallIds=new Set();
@@ -300,7 +304,7 @@ function validateAIRequestMessages(messages=[]){
300
304
  const calls=message?.tool_calls;
301
305
  let openedToolCall=false;
302
306
  if(calls!==undefined){
303
- if(message?.role!=='assistant'||!Array.isArray(calls)){
307
+ if(message?.role!=='assistant'||!is.array(calls)){
304
308
  throw aiStructuralError(
305
309
  'AI_CHAT_INVALID_TOOL_CALL',
306
310
  `AI messages[${messageIndex}].tool_calls is invalid.`
@@ -330,7 +334,7 @@ function validateAIRequestMessages(messages=[]){
330
334
  }
331
335
  }
332
336
  if(message?.role==='tool'){
333
- if(typeof message.content!=='string'||!message.content.trim()){
337
+ if(!is.string(message.content)||!message.content.trim()){
334
338
  throw aiStructuralError(
335
339
  'AI_CHAT_INVALID_TOOL_MESSAGE',
336
340
  `AI messages[${messageIndex}] must contain a nonblank user-facing tool result.`
@@ -338,7 +342,7 @@ function validateAIRequestMessages(messages=[]){
338
342
  }
339
343
  if(
340
344
  !pendingToolCallIds.size
341
- ||typeof message.tool_call_id!=='string'
345
+ ||!is.string(message.tool_call_id)
342
346
  ||!pendingToolCallIds.has(message.tool_call_id)
343
347
  ){
344
348
  throw aiStructuralError(
@@ -365,7 +369,7 @@ function validateAIRequestMessages(messages=[]){
365
369
  function validateAIStructuralRequest(messages,tools,parallelToolCalls){
366
370
  validateAIRequestMessages(messages);
367
371
  requireAIToolMessageSchemas(tools);
368
- if(parallelToolCalls!==undefined&&typeof parallelToolCalls!=='boolean'){
372
+ if(parallelToolCalls!==undefined&&!is.boolean(parallelToolCalls)){
369
373
  throw new TypeError('AI parallelToolCalls must be a boolean when provided.');
370
374
  }
371
375
  }
@@ -374,12 +378,12 @@ function normalizeAICompletionToolCalls(completion){
374
378
  const calls=[];
375
379
  const hasMessage=Boolean(
376
380
  completion
377
- &&typeof completion==='object'
381
+ &&is.object(completion)
378
382
  &&Object.hasOwn(completion,'message')
379
383
  );
380
384
  const hasChoices=Boolean(
381
385
  completion
382
- &&typeof completion==='object'
386
+ &&is.object(completion)
383
387
  &&Object.hasOwn(completion,'choices')
384
388
  );
385
389
  if(hasMessage&&hasChoices){
@@ -388,7 +392,7 @@ function normalizeAICompletionToolCalls(completion){
388
392
  'The AI completion must not mix message and choices envelopes.'
389
393
  );
390
394
  }
391
- if(hasChoices&&!Array.isArray(completion.choices)){
395
+ if(hasChoices&&!is.array(completion.choices)){
392
396
  throw aiStructuralError(
393
397
  'AI_CHAT_INVALID_RESPONSE',
394
398
  'The AI completion choices envelope must be an array.'
@@ -401,7 +405,7 @@ function normalizeAICompletionToolCalls(completion){
401
405
  :[];
402
406
  for(let messageIndex=0;messageIndex<messages.length;messageIndex+=1){
403
407
  const toolCalls=messages[messageIndex]?.tool_calls;
404
- if(toolCalls!==undefined&&!Array.isArray(toolCalls)){
408
+ if(toolCalls!==undefined&&!is.array(toolCalls)){
405
409
  throw aiStructuralError(
406
410
  'AI_CHAT_INVALID_TOOL_CALL',
407
411
  `AI response message ${messageIndex+1} contains invalid structural tool calls.`
@@ -428,12 +432,12 @@ function normalizeAICompletionToolCalls(completion){
428
432
 
429
433
  function sameAIDataValue(left,right,seen=new Map()){
430
434
  if(Object.is(left,right))return true;
431
- if(!left||!right||typeof left!=='object'||typeof right!=='object')return false;
432
- if(Array.isArray(left)!==Array.isArray(right))return false;
435
+ if(!left||!right||!is.object(left)||!is.object(right))return false;
436
+ if(is.array(left)!==is.array(right))return false;
433
437
  const matched=seen.get(left);
434
438
  if(matched!==undefined)return matched===right;
435
439
  seen.set(left,right);
436
- if(Array.isArray(left)){
440
+ if(is.array(left)){
437
441
  return left.length===right.length
438
442
  &&left.every((value,index)=>sameAIDataValue(value,right[index],seen));
439
443
  }
@@ -507,11 +511,11 @@ function isAIStreamStructuralKey(key){
507
511
  const OMITTED_AI_STREAM_DATA=Symbol('omitted-ai-stream-data');
508
512
 
509
513
  function projectAIStreamData(value,seen=new Map()){
510
- if(value===null||value===undefined||typeof value!=='object'){
514
+ if(value===null||value===undefined||!is.object(value)){
511
515
  return value;
512
516
  }
513
517
  if(seen.has(value)) return seen.get(value);
514
- if(Array.isArray(value)){
518
+ if(is.array(value)){
515
519
  const result=[];
516
520
  seen.set(value,result);
517
521
  for(const item of value){
@@ -688,7 +692,7 @@ function normalizeAIStartupOptions(options){
688
692
  signal:null
689
693
  });
690
694
  }
691
- if(!options||typeof options!=='object'||Array.isArray(options)){
695
+ if(!options||!is.object(options)||is.array(options)){
692
696
  throw new TypeError('AI startup options must be a plain object.');
693
697
  }
694
698
  const prototype=Object.getPrototypeOf(options);
@@ -697,7 +701,7 @@ function normalizeAIStartupOptions(options){
697
701
  }
698
702
  const descriptors=Object.getOwnPropertyDescriptors(options);
699
703
  for(const key of Reflect.ownKeys(descriptors)){
700
- if(typeof key==='symbol'||(
704
+ if(is.symbol(key)||(
701
705
  key!=='startLanguageModel'
702
706
  &&key!=='startMuted'
703
707
  &&key!=='startTranscription'
@@ -721,20 +725,20 @@ function normalizeAIStartupOptions(options){
721
725
  const signal=Object.hasOwn(descriptors,'signal')
722
726
  ?descriptors.signal.value
723
727
  :null;
724
- if(typeof startLanguageModel!=='boolean'){
728
+ if(!is.boolean(startLanguageModel)){
725
729
  throw new TypeError('AI startup startLanguageModel must be a boolean.');
726
730
  }
727
- if(typeof startMuted!=='boolean'){
731
+ if(!is.boolean(startMuted)){
728
732
  throw new TypeError('AI startup startMuted must be a boolean.');
729
733
  }
730
- if(typeof startTranscription!=='boolean'){
734
+ if(!is.boolean(startTranscription)){
731
735
  throw new TypeError('AI startup startTranscription must be a boolean.');
732
736
  }
733
737
  if(signal!==null&&signal!==undefined&&(
734
- typeof signal!=='object'
735
- ||typeof signal.aborted!=='boolean'
736
- ||typeof signal.addEventListener!=='function'
737
- ||typeof signal.removeEventListener!=='function'
738
+ !is.object(signal)
739
+ ||!is.boolean(signal.aborted)
740
+ ||!is.function(signal.addEventListener)
741
+ ||!is.function(signal.removeEventListener)
738
742
  )){
739
743
  throw new TypeError('AI startup signal must be an AbortSignal.');
740
744
  }
@@ -759,16 +763,16 @@ function aiBrowserSpeechError(code,reason,message,cause,{committed=false,name='E
759
763
 
760
764
  function isAbortSignal(value){
761
765
  return Boolean(value)
762
- &&typeof value==='object'
763
- &&typeof value.aborted==='boolean'
764
- &&typeof value.addEventListener==='function'
765
- &&typeof value.removeEventListener==='function';
766
+ &&is.object(value)
767
+ &&is.boolean(value.aborted)
768
+ &&is.function(value.addEventListener)
769
+ &&is.function(value.removeEventListener);
766
770
  }
767
771
 
768
772
  function closedRecord(value,keys,required,label){
769
773
  if(!value
770
- ||typeof value!=='object'
771
- ||Array.isArray(value)
774
+ ||!is.object(value)
775
+ ||is.array(value)
772
776
  ||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
773
777
  throw aiBrowserSpeechError(
774
778
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
@@ -778,7 +782,7 @@ function closedRecord(value,keys,required,label){
778
782
  }
779
783
  const descriptors=Object.getOwnPropertyDescriptors(value);
780
784
  for(const key of Reflect.ownKeys(descriptors)){
781
- if(typeof key!=='string'
785
+ if(!is.string(key)
782
786
  ||!keys.includes(key)
783
787
  ||!Object.hasOwn(descriptors[key],'value')){
784
788
  throw aiBrowserSpeechError(
@@ -801,7 +805,7 @@ function closedRecord(value,keys,required,label){
801
805
  }
802
806
 
803
807
  function browserSpeechIdentifier(value,label){
804
- if(typeof value!=='string'
808
+ if(!is.string(value)
805
809
  ||value.trim()!==value
806
810
  ||value.length<1){
807
811
  throw aiBrowserSpeechError(
@@ -829,14 +833,14 @@ function normalizeBrowserTTSExecution(value){
829
833
  const maxConcurrentRequests=descriptors.maxConcurrentRequests
830
834
  ?descriptors.maxConcurrentRequests.value
831
835
  :DEFAULT_BROWSER_TTS_EXECUTION.maxConcurrentRequests;
832
- if(typeof device!=='string'||!BROWSER_TTS_EXECUTION_DEVICES.has(device)){
836
+ if(!is.string(device)||!BROWSER_TTS_EXECUTION_DEVICES.has(device)){
833
837
  throw aiBrowserSpeechError(
834
838
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
835
839
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
836
840
  'AI browser speech tts.execution.device must be auto, webgpu, or wasm.'
837
841
  );
838
842
  }
839
- if(!Number.isSafeInteger(maxConcurrentRequests)
843
+ if(!is.safeInteger(maxConcurrentRequests)
840
844
  ||maxConcurrentRequests<1
841
845
  ||maxConcurrentRequests>MAX_BROWSER_TTS_CONCURRENT_REQUESTS){
842
846
  throw aiBrowserSpeechError(
@@ -881,7 +885,7 @@ function normalizeBrowserSpeechRole(value,role){
881
885
  }
882
886
  if(hasGraph){
883
887
  const graph=descriptors.graph.value;
884
- if(!graph||typeof graph!=='object'){
888
+ if(!graph||!is.object(graph)){
885
889
  throw aiBrowserSpeechError(
886
890
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
887
891
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
@@ -891,7 +895,7 @@ function normalizeBrowserSpeechRole(value,role){
891
895
  }else{
892
896
  for(const key of ['model','runtime']){
893
897
  const descriptor=descriptors[key].value;
894
- if(!descriptor||typeof descriptor!=='object'||Array.isArray(descriptor)){
898
+ if(!descriptor||!is.object(descriptor)||is.array(descriptor)){
895
899
  throw aiBrowserSpeechError(
896
900
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
897
901
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
@@ -906,7 +910,7 @@ function normalizeBrowserSpeechRole(value,role){
906
910
  // an implementation may be enabled.
907
911
  secure=descriptors.security.value?.secure===true;
908
912
  }
909
- if(typeof descriptors.offline.value!=='boolean'){
913
+ if(!is.boolean(descriptors.offline.value)){
910
914
  throw aiBrowserSpeechError(
911
915
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
912
916
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
@@ -960,7 +964,7 @@ function normalizeBrowserSpeechConfiguration(value){
960
964
  'AI browser speech configuration.id'
961
965
  );
962
966
  const dbopfs=descriptors.dbopfs.value;
963
- if(!dbopfs||typeof dbopfs!=='object'){
967
+ if(!dbopfs||!is.object(dbopfs)){
964
968
  throw aiBrowserSpeechError(
965
969
  AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
966
970
  AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
@@ -997,8 +1001,8 @@ function normalizeBrowserSpeechConfiguration(value){
997
1001
  function normalizeBrowserSpeechOperationOptions(value,label){
998
1002
  if(value===undefined)return completeValue({signal:null});
999
1003
  if(!value
1000
- ||typeof value!=='object'
1001
- ||Array.isArray(value)
1004
+ ||!is.object(value)
1005
+ ||is.array(value)
1002
1006
  ||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
1003
1007
  throw aiBrowserSpeechError(
1004
1008
  AI_BROWSER_SPEECH_ERROR_CODES.operationOptionsContractMismatch,
@@ -1185,6 +1189,9 @@ class AI {
1185
1189
  #speechJobSequence=0;
1186
1190
  #stopOllamaReady=null;
1187
1191
  #ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
1192
+ #preparedSpeechReadiness = null;
1193
+ #preparedSpeechPlaybacks = new Set();
1194
+ #speechUnlockContext = null;
1188
1195
 
1189
1196
  #traceSpeech(phase,detail={}){
1190
1197
  if(!arcaneLogging.enabled)return null;
@@ -1294,7 +1301,7 @@ class AI {
1294
1301
  }
1295
1302
 
1296
1303
  set twinKey(value){
1297
- this.#license=typeof value==='string' ? value.trim():'';
1304
+ this.#license=is.string(value) ? value.trim():'';
1298
1305
  this.#retainBuiltInLLMReadiness(
1299
1306
  this.#reconcileBuiltInLLMReadiness()
1300
1307
  );
@@ -1317,7 +1324,7 @@ class AI {
1317
1324
  return this.llmService==='TWIN'
1318
1325
  &&Boolean(this.model)
1319
1326
  &&Boolean(this.license)
1320
- &&typeof globalThis.fetch==='function';
1327
+ &&is.function(globalThis.fetch);
1321
1328
  }
1322
1329
  if(providerId==='OLLAMA'){
1323
1330
  return this.llmService==='OLLAMA'
@@ -1434,7 +1441,7 @@ class AI {
1434
1441
  'ARCANE_AI_ROLE_BUSY'
1435
1442
  );
1436
1443
  }
1437
- if(typeof context.progress!=='function'){
1444
+ if(!is.function(context.progress)){
1438
1445
  throw new TypeError(
1439
1446
  'Built-in LLM provider load progress must be a function.'
1440
1447
  );
@@ -1684,7 +1691,7 @@ class AI {
1684
1691
  'ARCANE_AI_ROLE_BUSY'
1685
1692
  );
1686
1693
  }
1687
- if(typeof context.progress!=='function'){
1694
+ if(!is.function(context.progress)){
1688
1695
  throw new TypeError(
1689
1696
  `Built-in ${role.toUpperCase()} provider load progress must be a function.`
1690
1697
  );
@@ -2030,7 +2037,6 @@ class AI {
2030
2037
  }
2031
2038
 
2032
2039
  audioMessageChunks='';
2033
- #speechTextFormat='plain';
2034
2040
  #markdownSpeech=new MarkdownSpeech();
2035
2041
  sourceNodes=[];
2036
2042
  isSpeaking=false;
@@ -2053,7 +2059,7 @@ class AI {
2053
2059
  if(value===undefined||value===null||value===''){
2054
2060
  return current[index];
2055
2061
  }
2056
- if(typeof value!=='string'||value.trim()!==value||!value){
2062
+ if(!is.string(value)||value.trim()!==value||!value){
2057
2063
  throw new TypeError('AI preferences must be nonempty trimmed strings.');
2058
2064
  }
2059
2065
  return value;
@@ -2488,7 +2494,7 @@ class AI {
2488
2494
 
2489
2495
  #browserSpeechOperationId(action){
2490
2496
  this.#browserSpeechOperationSequence=
2491
- typeof this.#browserSpeechOperationSequence==='bigint'
2497
+ is.bigint(this.#browserSpeechOperationSequence)
2492
2498
  ? this.#browserSpeechOperationSequence+1n
2493
2499
  : this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER
2494
2500
  ? BigInt(this.#browserSpeechOperationSequence)+1n
@@ -2710,7 +2716,7 @@ class AI {
2710
2716
  publicDetail:completeValue({
2711
2717
  configurationId:normalized.id,
2712
2718
  ...(descriptor?{descriptor}:{}),
2713
- ...(typeof error?.code==='string'?{code:error.code}:{}),
2719
+ ...(is.string(error?.code)?{code:error.code}:{}),
2714
2720
  reason
2715
2721
  })
2716
2722
  }
@@ -2738,7 +2744,7 @@ class AI {
2738
2744
  continue;
2739
2745
  }
2740
2746
  const catalog=providers[role].catalog();
2741
- if(catalog.length!==1||typeof catalog[0]?.id!=='string'){
2747
+ if(catalog.length!==1||!is.string(catalog[0]?.id)){
2742
2748
  throw aiBrowserSpeechError(
2743
2749
  AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
2744
2750
  AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
@@ -2952,7 +2958,7 @@ class AI {
2952
2958
  for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
2953
2959
  if(record.registrationState?.[role]===false)continue;
2954
2960
  const unregister=record.unregisters?.[role];
2955
- if(typeof unregister!=='function'){
2961
+ if(!is.function(unregister)){
2956
2962
  failures.push(aiBrowserSpeechError(
2957
2963
  AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
2958
2964
  AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
@@ -3746,7 +3752,7 @@ class AI {
3746
3752
 
3747
3753
  async setSpeechMuted(muted){
3748
3754
  const callId=this.#traceSpeech('setSpeechMuted.call',{requestedMuted:muted});
3749
- if(typeof muted!=='boolean'){
3755
+ if(!is.boolean(muted)){
3750
3756
  throw new TypeError('AI speech muted state must be a boolean.');
3751
3757
  }
3752
3758
  const generation=++this.#speechControlGeneration;
@@ -3793,12 +3799,12 @@ class AI {
3793
3799
  if(signal?.aborted){
3794
3800
  throw normalizeAIRequestAbort(signal.reason);
3795
3801
  }
3796
- const message=typeof error==='string'
3802
+ const message=is.string(error)
3797
3803
  ?error
3798
3804
  :error?.error?.message??error?.message;
3799
3805
  if(
3800
3806
  response.status!==429
3801
- ||typeof message!=='string'
3807
+ ||!is.string(message)
3802
3808
  ||!message.toLowerCase().includes('overload')
3803
3809
  ){
3804
3810
  throw error;
@@ -3837,7 +3843,7 @@ class AI {
3837
3843
  const client=globalThis.Arcane?.ollama;
3838
3844
 
3839
3845
  return this.llmService==='OLLAMA'
3840
- &&typeof client?.chat==='function'
3846
+ &&is.function(client?.chat)
3841
3847
  ?client
3842
3848
  :null;
3843
3849
  }
@@ -3848,10 +3854,10 @@ class AI {
3848
3854
  if(service!=='LOCAL_SPEACH'){
3849
3855
  return null;
3850
3856
  }
3851
- if(role==='tts'&&typeof client?.synthesize==='function'){
3857
+ if(role==='tts'&&is.function(client?.synthesize)){
3852
3858
  return client;
3853
3859
  }
3854
- if(role==='stt'&&typeof client?.transcribe==='function'){
3860
+ if(role==='stt'&&is.function(client?.transcribe)){
3855
3861
  return client;
3856
3862
  }
3857
3863
  return null;
@@ -3859,7 +3865,7 @@ class AI {
3859
3865
 
3860
3866
  async #requestBuiltInSpeechTranscription(payload={},signal=null){
3861
3867
  const audio=payload?.audio;
3862
- if(!audio||typeof audio.arrayBuffer!=='function'){
3868
+ if(!audio||!is.function(audio.arrayBuffer)){
3863
3869
  throw new TypeError('Speech transcription requires an audio Blob or File.');
3864
3870
  }
3865
3871
  if(signal?.aborted){
@@ -3881,7 +3887,7 @@ class AI {
3881
3887
  if(signal?.aborted){
3882
3888
  throw normalizeAIRequestAbort(signal.reason);
3883
3889
  }
3884
- if(!response||typeof response.text!=='string'){
3890
+ if(!response||!is.string(response.text)){
3885
3891
  throw new TypeError('Arcane returned an invalid local speech transcription.');
3886
3892
  }
3887
3893
  return response.text;
@@ -3909,7 +3915,7 @@ class AI {
3909
3915
  }
3910
3916
 
3911
3917
  async #requestBuiltInSpeechSynthesis(payload={},signal=null){
3912
- const input=typeof payload?.input==='string'?payload.input:'';
3918
+ const input=is.string(payload?.input)?payload.input:'';
3913
3919
  if(!input){
3914
3920
  throw new TypeError('Speech synthesis requires nonempty input.');
3915
3921
  }
@@ -3917,14 +3923,14 @@ class AI {
3917
3923
  throw normalizeAIRequestAbort(signal.reason);
3918
3924
  }
3919
3925
  const model=String(payload.model||this.modelTTS);
3920
- const voice=typeof payload.voice==='string'&&payload.voice.trim()
3926
+ const voice=is.string(payload.voice)&&payload.voice.trim()
3921
3927
  ?payload.voice.trim()
3922
3928
  :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
3923
3929
  if(!voice){
3924
3930
  throw new TypeError('The selected speech provider requires a voice.');
3925
3931
  }
3926
3932
  const responseFormat=String(payload.responseFormat||this.audioFormat);
3927
- const speed=Number.isFinite(payload.speed)?payload.speed:this.voiceSpeed;
3933
+ const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
3928
3934
  const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
3929
3935
  if(nativeSpeech){
3930
3936
  const response=await nativeSpeech.synthesize({
@@ -3937,7 +3943,7 @@ class AI {
3937
3943
  if(signal?.aborted){
3938
3944
  throw normalizeAIRequestAbort(signal.reason);
3939
3945
  }
3940
- if(!response||typeof response.audioBase64!=='string'){
3946
+ if(!response||!is.string(response.audioBase64)){
3941
3947
  const error=new TypeError(
3942
3948
  'The Arcane speech bridge returned invalid TTS audio.'
3943
3949
  );
@@ -3947,7 +3953,7 @@ class AI {
3947
3953
  return new Blob(
3948
3954
  [this.#base64ToBytes(response.audioBase64)],
3949
3955
  {
3950
- type:typeof response.contentType==='string'
3956
+ type:is.string(response.contentType)
3951
3957
  ?response.contentType
3952
3958
  :this.audioType
3953
3959
  }
@@ -3996,7 +4002,7 @@ class AI {
3996
4002
  }
3997
4003
 
3998
4004
  async #androidNativeHost(){
3999
- if(typeof globalThis.arcaneAndroid?.postMessage==='function'){
4005
+ if(is.function(globalThis.arcaneAndroid?.postMessage)){
4000
4006
  return true;
4001
4007
  }
4002
4008
 
@@ -4038,7 +4044,7 @@ class AI {
4038
4044
  }
4039
4045
 
4040
4046
  #base64ToBytes(value){
4041
- if(typeof value!=='string'||!value){
4047
+ if(!is.string(value)||!value){
4042
4048
  throw new TypeError('Arcane returned invalid local speech audio.');
4043
4049
  }
4044
4050
 
@@ -4053,7 +4059,7 @@ class AI {
4053
4059
  }
4054
4060
 
4055
4061
  #ollamaTools(tools=[],toolChoice='auto'){
4056
- if(!Array.isArray(tools)){
4062
+ if(!is.array(tools)){
4057
4063
  return [];
4058
4064
  }
4059
4065
 
@@ -4077,12 +4083,12 @@ class AI {
4077
4083
  const role=String(message?.role??'');
4078
4084
  const content=String(message?.content??'');
4079
4085
 
4080
- if(role==='assistant'&&Array.isArray(message?.tool_calls)){
4086
+ if(role==='assistant'&&is.array(message?.tool_calls)){
4081
4087
  const toolCalls=message.tool_calls.map(function sanitizeOllamaToolCall(call){
4082
4088
  const name=String(call?.function?.name??'').trim();
4083
4089
  const callId=String(call?.id??'').trim();
4084
4090
  let argumentValue=call?.function?.arguments;
4085
- if(typeof argumentValue==='string'){
4091
+ if(is.string(argumentValue)){
4086
4092
  try{
4087
4093
  argumentValue=JSON.parse(argumentValue);
4088
4094
  }catch(cause){
@@ -4096,8 +4102,8 @@ class AI {
4096
4102
  }
4097
4103
  if(
4098
4104
  !argumentValue
4099
- ||typeof argumentValue!=='object'
4100
- ||Array.isArray(argumentValue)
4105
+ ||!is.object(argumentValue)
4106
+ ||is.array(argumentValue)
4101
4107
  ){
4102
4108
  const error=new TypeError(
4103
4109
  'Ollama tool call arguments must contain a JSON object.'
@@ -4173,8 +4179,8 @@ class AI {
4173
4179
  return 'json';
4174
4180
  }
4175
4181
  if(
4176
- typeof value==='object'
4177
- &&!Array.isArray(value)
4182
+ is.object(value)
4183
+ &&!is.array(value)
4178
4184
  &&(
4179
4185
  Object.getPrototypeOf(value)===Object.prototype
4180
4186
  ||Object.getPrototypeOf(value)===null
@@ -4208,7 +4214,7 @@ class AI {
4208
4214
  }
4209
4215
 
4210
4216
  async #reportRequest(requestHandler,request,id,metadata){
4211
- if(typeof requestHandler!=='function'){
4217
+ if(!is.function(requestHandler)){
4212
4218
  throw new TypeError('AI onRequest callback must be a function.');
4213
4219
  }
4214
4220
  await requestHandler(request,id,metadata);
@@ -4216,31 +4222,31 @@ class AI {
4216
4222
 
4217
4223
  #providerStreamEmissions(chunk,seeThinking){
4218
4224
  const chunks=[];
4219
- if(typeof chunk==='string'){
4225
+ if(is.string(chunk)){
4220
4226
  if(chunk) chunks.push({text:chunk,thinking:false});
4221
4227
  return {chunks};
4222
4228
  }
4223
- const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
4229
+ const choices=is.array(chunk?.choices)?chunk.choices:[];
4224
4230
  for(const choice of choices){
4225
4231
  const delta=choice?.delta||{};
4226
4232
  if(
4227
4233
  seeThinking
4228
- &&typeof delta.reasoning_content==='string'
4234
+ &&is.string(delta.reasoning_content)
4229
4235
  &&delta.reasoning_content
4230
4236
  ){
4231
4237
  chunks.push({text:delta.reasoning_content,thinking:true});
4232
4238
  }
4233
- if(typeof delta.content==='string'&&delta.content){
4239
+ if(is.string(delta.content)&&delta.content){
4234
4240
  chunks.push({text:delta.content,thinking:false});
4235
4241
  }
4236
4242
  }
4237
4243
  if(!choices.length){
4238
- if(seeThinking&&typeof chunk?.thinking==='string'){
4244
+ if(seeThinking&&is.string(chunk?.thinking)){
4239
4245
  chunks.push({text:chunk.thinking,thinking:true});
4240
4246
  }
4241
- const text=typeof chunk?.text==='string'
4247
+ const text=is.string(chunk?.text)
4242
4248
  ?chunk.text
4243
- :typeof chunk?.content==='string'
4249
+ :is.string(chunk?.content)
4244
4250
  ?chunk.content
4245
4251
  :'';
4246
4252
  if(text){
@@ -4251,16 +4257,16 @@ class AI {
4251
4257
  }
4252
4258
 
4253
4259
  #providerCompletionOutput(completion){
4254
- if(typeof completion==='string'){
4260
+ if(is.string(completion)){
4255
4261
  return completion;
4256
4262
  }
4257
- if(Array.isArray(completion?.choices)&&completion.choices.length>1){
4263
+ if(is.array(completion?.choices)&&completion.choices.length>1){
4258
4264
  return completion;
4259
4265
  }
4260
4266
  const structuralToolCalls=normalizeAICompletionToolCalls(completion);
4261
4267
  if(structuralToolCalls.length)return structuralToolCalls;
4262
4268
  const content=completion?.choices?.[0]?.message?.content;
4263
- return typeof content==='string'?content:completion;
4269
+ return is.string(content)?content:completion;
4264
4270
  }
4265
4271
 
4266
4272
  #requestBuiltInLLMChat(payload={},signal=null){
@@ -4332,14 +4338,14 @@ class AI {
4332
4338
  if(value===undefined){
4333
4339
  return [];
4334
4340
  }
4335
- if(!Array.isArray(value)){
4341
+ if(!is.array(value)){
4336
4342
  throw aiStructuralError(
4337
4343
  'AI_CHAT_INVALID_TOOL_CALL',
4338
4344
  'The native Ollama response contains invalid structural tool calls.'
4339
4345
  );
4340
4346
  }
4341
4347
  return value.map(function adaptNativeOllamaToolCall(call,index){
4342
- if(!call||typeof call!=='object'||Array.isArray(call)){
4348
+ if(!call||!is.object(call)||is.array(call)){
4343
4349
  throw aiStructuralError(
4344
4350
  'AI_CHAT_INVALID_TOOL_CALL',
4345
4351
  `The native Ollama structural tool call ${index+1} is invalid.`
@@ -4348,8 +4354,8 @@ class AI {
4348
4354
  const nativeFunction=call.function;
4349
4355
  if(
4350
4356
  !nativeFunction
4351
- ||typeof nativeFunction!=='object'
4352
- ||Array.isArray(nativeFunction)
4357
+ ||!is.object(nativeFunction)
4358
+ ||is.array(nativeFunction)
4353
4359
  ){
4354
4360
  throw aiStructuralError(
4355
4361
  'AI_CHAT_INVALID_TOOL_CALL',
@@ -4359,8 +4365,8 @@ class AI {
4359
4365
  let encodedArguments=nativeFunction.arguments;
4360
4366
  if(
4361
4367
  encodedArguments
4362
- &&typeof encodedArguments==='object'
4363
- &&!Array.isArray(encodedArguments)
4368
+ &&is.object(encodedArguments)
4369
+ &&!is.array(encodedArguments)
4364
4370
  ){
4365
4371
  try{
4366
4372
  encodedArguments=JSON.stringify(encodedArguments);
@@ -4394,7 +4400,7 @@ class AI {
4394
4400
  :{};
4395
4401
  const responseFields={...responseRecord};
4396
4402
  delete responseFields.message;
4397
- const responseId=typeof response?.id==='string'&&response.id
4403
+ const responseId=is.string(response?.id)&&response.id
4398
4404
  ?response.id
4399
4405
  :`ollama-${id}`;
4400
4406
  const adaptedToolCalls=this.#openAICompatibleOllamaToolCalls(
@@ -4406,7 +4412,7 @@ class AI {
4406
4412
  });
4407
4413
  const messageRecord={
4408
4414
  ...message,
4409
- role:typeof message.role==='string'&&message.role
4415
+ role:is.string(message.role)&&message.role
4410
4416
  ?message.role
4411
4417
  :'assistant',
4412
4418
  content:Object.hasOwn(message,'content')?message.content:'',
@@ -4733,8 +4739,8 @@ class AI {
4733
4739
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4734
4740
  this.#assertServiceConfigured(this.llmService);
4735
4741
  if(signal&&(
4736
- typeof signal.aborted!=='boolean'
4737
- ||typeof signal.addEventListener!=='function'
4742
+ !is.boolean(signal.aborted)
4743
+ ||!is.function(signal.addEventListener)
4738
4744
  )){
4739
4745
  throw new TypeError('AI request signal must be an AbortSignal.');
4740
4746
  }
@@ -4972,7 +4978,7 @@ class AI {
4972
4978
  const toolFunction=toolCall.function||{};
4973
4979
  if(
4974
4980
  toolCall.index!==undefined
4975
- &&(!Number.isSafeInteger(toolCall.index)||toolCall.index<0)
4981
+ &&(!is.safeInteger(toolCall.index)||toolCall.index<0)
4976
4982
  ){
4977
4983
  throw aiStructuralError(
4978
4984
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
@@ -4992,7 +4998,7 @@ class AI {
4992
4998
  };
4993
4999
 
4994
5000
  if(toolCall.id!==undefined){
4995
- if(typeof toolCall.id!=='string'||!toolCall.id){
5001
+ if(!is.string(toolCall.id)||!toolCall.id){
4996
5002
  record.invalidIdentity=true;
4997
5003
  }else if(record.id&&record.id!==toolCall.id){
4998
5004
  record.invalidIdentity=true;
@@ -5002,7 +5008,7 @@ class AI {
5002
5008
  }
5003
5009
 
5004
5010
  if(toolCall.type!==undefined){
5005
- if(typeof toolCall.type!=='string'||!toolCall.type){
5011
+ if(!is.string(toolCall.type)||!toolCall.type){
5006
5012
  record.invalidIdentity=true;
5007
5013
  }else if(record.type&&record.type!==toolCall.type){
5008
5014
  record.invalidIdentity=true;
@@ -5012,14 +5018,14 @@ class AI {
5012
5018
  }
5013
5019
 
5014
5020
  if(toolFunction.name!==undefined){
5015
- if(typeof toolFunction.name!=='string'){
5021
+ if(!is.string(toolFunction.name)){
5016
5022
  record.invalidName=true;
5017
5023
  }else{
5018
5024
  record.name+=toolFunction.name;
5019
5025
  }
5020
5026
  }
5021
5027
 
5022
- if(typeof toolFunction.arguments==='string'){
5028
+ if(is.string(toolFunction.arguments)){
5023
5029
  record.arguments+=toolFunction.arguments;
5024
5030
  }else if(toolFunction.arguments!==undefined){
5025
5031
  record.invalidArguments=true;
@@ -5033,7 +5039,7 @@ class AI {
5033
5039
  function receiveStreamedChoice(choice={},choicePosition=0){
5034
5040
  if(
5035
5041
  choice.index!==undefined
5036
- &&(!Number.isSafeInteger(choice.index)||choice.index<0)
5042
+ &&(!is.safeInteger(choice.index)||choice.index<0)
5037
5043
  ){
5038
5044
  throw aiStructuralError(
5039
5045
  'AI_CHAT_INVALID_RESPONSE',
@@ -5065,8 +5071,8 @@ class AI {
5065
5071
  if(key==='content'||key==='reasoning'||key==='reasoning_content'){
5066
5072
  if(
5067
5073
  !replaceText
5068
- &&typeof value==='string'
5069
- &&typeof record.message[key]==='string'
5074
+ &&is.string(value)
5075
+ &&is.string(record.message[key])
5070
5076
  ){
5071
5077
  record.message[key]+=value;
5072
5078
  }else{
@@ -5077,7 +5083,7 @@ class AI {
5077
5083
  record.message[key]=value;
5078
5084
  }
5079
5085
  }
5080
- if(typeof record.message.role!=='string'||!record.message.role){
5086
+ if(!is.string(record.message.role)||!record.message.role){
5081
5087
  record.message.role='assistant';
5082
5088
  }
5083
5089
  if(!Object.hasOwn(record.message,'content')) record.message.content='';
@@ -5099,7 +5105,7 @@ class AI {
5099
5105
  for(const [key,value] of Object.entries(streamedResponse)){
5100
5106
  if(key!=='choices') streamMetadata[key]=value;
5101
5107
  }
5102
- const choices=Array.isArray(streamedResponse.choices)
5108
+ const choices=is.array(streamedResponse.choices)
5103
5109
  ?streamedResponse.choices
5104
5110
  :[];
5105
5111
  for(let choicePosition=0;choicePosition<choices.length;choicePosition+=1){
@@ -5126,7 +5132,7 @@ class AI {
5126
5132
  const choiceDelta=choice.delta??{};
5127
5133
  const choiceMessage=choice.message??null;
5128
5134
  if(Object.hasOwn(choiceDelta,'tool_calls')){
5129
- if(!Array.isArray(choiceDelta.tool_calls)){
5135
+ if(!is.array(choiceDelta.tool_calls)){
5130
5136
  throw aiStructuralError(
5131
5137
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5132
5138
  `AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
@@ -5135,7 +5141,7 @@ class AI {
5135
5141
  receiveStreamedToolCalls(choiceDelta.tool_calls,choiceIndex);
5136
5142
  }
5137
5143
  if(choiceMessage&&Object.hasOwn(choiceMessage,'tool_calls')){
5138
- if(!Array.isArray(choiceMessage.tool_calls)){
5144
+ if(!is.array(choiceMessage.tool_calls)){
5139
5145
  throw aiStructuralError(
5140
5146
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5141
5147
  `AI stream choice ${choicePosition+1} contains invalid terminal structural tool-call data.`
@@ -5149,14 +5155,14 @@ class AI {
5149
5155
  )
5150
5156
  );
5151
5157
  }
5152
- const content=typeof choiceDelta.content==='string'
5158
+ const content=is.string(choiceDelta.content)
5153
5159
  ?choiceDelta.content
5154
5160
  :'';
5155
5161
  let reasoning='';
5156
5162
  if(seeThinking){
5157
- reasoning=typeof choiceDelta.reasoning_content==='string'
5163
+ reasoning=is.string(choiceDelta.reasoning_content)
5158
5164
  ?choiceDelta.reasoning_content
5159
- :typeof choiceDelta.reasoning==='string'
5165
+ :is.string(choiceDelta.reasoning)
5160
5166
  ?choiceDelta.reasoning
5161
5167
  :'';
5162
5168
  }
@@ -5344,7 +5350,7 @@ class AI {
5344
5350
  const hasToolCalls=structuralToolCallsByChoice.has(choicePosition);
5345
5351
  return {
5346
5352
  ...retained.choice,
5347
- index:Number.isSafeInteger(retained.choice.index)
5353
+ index:is.safeInteger(retained.choice.index)
5348
5354
  &&retained.choice.index>=0
5349
5355
  ?retained.choice.index
5350
5356
  :choicePosition,
@@ -5562,8 +5568,8 @@ class AI {
5562
5568
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
5563
5569
  this.#assertServiceConfigured(this.llmService);
5564
5570
  if(signal&&(
5565
- typeof signal.aborted!=='boolean'
5566
- ||typeof signal.addEventListener!=='function'
5571
+ !is.boolean(signal.aborted)
5572
+ ||!is.function(signal.addEventListener)
5567
5573
  )){
5568
5574
  throw new TypeError('AI request signal must be an AbortSignal.');
5569
5575
  }
@@ -5725,16 +5731,291 @@ class AI {
5725
5731
  return result;
5726
5732
  }
5727
5733
 
5734
+ prepareTTS({parts, storage = null, identity = null, signal = null, onState = null} = {}) {
5735
+ if(!is.array(parts)) {
5736
+ throw new TypeError('AI.prepareTTS parts must be an array.');
5737
+ }
5738
+ const runtime = this;
5739
+ const selected = this.#providerRuntime.selection('tts');
5740
+ const configuredSpeech = this.browserSpeechConfiguration?.tts;
5741
+ const configuration = configuredSpeech
5742
+ ? {
5743
+ model: configuredSpeech.model ?? null,
5744
+ runtime: configuredSpeech.runtime ?? null,
5745
+ graph: configuredSpeech.graph ?? null,
5746
+ device: configuredSpeech.execution?.device ?? 'auto'
5747
+ }
5748
+ : null;
5749
+ const selection = {
5750
+ providerId: selected?.providerId ?? this.ttsService,
5751
+ modelId: selected?.modelId ?? this.modelTTS,
5752
+ responseFormat: selected ? this.#providerSpeechResponseFormat() : this.audioFormat,
5753
+ configuration
5754
+ };
5755
+ const browserGeneration = this.#browserSpeechGeneration;
5756
+ const controlGeneration = this.#speechControlGeneration;
5757
+ const defaultVoice = selected
5758
+ ? this.#providerSpeechVoice()
5759
+ : this.#builtInSpeechDefaultVoice('tts', this.ttsService);
5760
+ const responseFormat = selection.responseFormat;
5761
+ const segmentation = this.ttsSegmentation;
5762
+ const segments = [];
5763
+ for(const part of parts) {
5764
+ const source = is.string(part) ? {input: part} : part;
5765
+ if(!is.string(source?.input)) {
5766
+ throw new TypeError('Each prepared speech part requires input text.');
5767
+ }
5768
+ const voice = source.voice ?? defaultVoice;
5769
+ const speed = Number(source.speed ?? this.voiceSpeed);
5770
+ const pauseAfterMs = Number(source.pauseAfterMs ?? 0);
5771
+ if(!is.finite(speed) || speed <= 0) {
5772
+ throw new RangeError('Prepared speech speed must be positive.');
5773
+ }
5774
+ if(!is.finite(pauseAfterMs) || pauseAfterMs < 0) {
5775
+ throw new RangeError('Prepared speech pauses must be nonnegative milliseconds.');
5776
+ }
5777
+ const cleaned = stripSpeechFormatting(source.input);
5778
+ const {segments: outputs} = this.#segmentSpeechText(cleaned, true, segmentation);
5779
+ for(const [index, input] of outputs.entries()) {
5780
+ if(!input.trim()) continue;
5781
+ segments.push(
5782
+ {input, voice, speed, pauseAfterMs: index === outputs.length - 1 ? pauseAfterMs : 0}
5783
+ );
5784
+ }
5785
+ }
5786
+ this.#traceSpeech('prepareTTS.call', {parts, segments, selection, segmentation, identity, storage});
5787
+ async function synthesizePreparedSpeech(part, requestSignal) {
5788
+ function assertPreparedSpeechSelection() {
5789
+ const current = runtime.#providerRuntime.selection('tts');
5790
+ if(requestSignal?.aborted) throw normalizeAIRequestAbort(requestSignal.reason);
5791
+ if(browserGeneration !== runtime.#browserSpeechGeneration
5792
+ || controlGeneration !== runtime.#speechControlGeneration
5793
+ || selection.providerId !== (current?.providerId ?? runtime.ttsService)
5794
+ || selection.modelId !== (current?.modelId ?? runtime.modelTTS)) {
5795
+ throw normalizeAIRequestAbort('The prepared speech selection changed.');
5796
+ }
5797
+ }
5798
+ assertPreparedSpeechSelection();
5799
+ if(selected) {
5800
+ const status = runtime.#providerRuntime.status('tts');
5801
+ if(status.state !== 'ready' || status.loaded !== true) {
5802
+ if(!runtime.#preparedSpeechReadiness) {
5803
+ const readiness = runtime.#providerRuntime.setSpeechMuted(false);
5804
+ runtime.#preparedSpeechReadiness = readiness;
5805
+ readiness.finally(
5806
+ function releasePreparedSpeechReadiness() {
5807
+ if(runtime.#preparedSpeechReadiness === readiness) {
5808
+ runtime.#preparedSpeechReadiness = null;
5809
+ }
5810
+ }
5811
+ ).catch(
5812
+ function reportPreparedSpeechReadinessFailure(error) {
5813
+ runtime.#traceSpeech('prepareTTS.readiness.error', {error});
5814
+ }
5815
+ );
5816
+ }
5817
+ await runtime.#preparedSpeechReadiness;
5818
+ assertPreparedSpeechSelection();
5819
+ }
5820
+ }
5821
+ return runtime.fetchTTS(
5822
+ {model: selection.modelId, input: part.input, voice: part.voice, speed: part.speed, responseFormat},
5823
+ requestSignal,
5824
+ {speechInputPrepared: true}
5825
+ );
5826
+ }
5827
+ return prepareSpeech(
5828
+ {
5829
+ owner: this,
5830
+ parts: segments,
5831
+ originalParts: parts,
5832
+ selection,
5833
+ segmentation,
5834
+ storage,
5835
+ identity,
5836
+ signal,
5837
+ onState,
5838
+ synthesize: synthesizePreparedSpeech
5839
+ }
5840
+ );
5841
+ }
5842
+
5843
+ playPreparedTTS(prepared, {signal = null, onState = null} = {}) {
5844
+ if(!is.array(prepared?.segments) || !is.function(prepared?.getAudio)) {
5845
+ throw new TypeError('AI.playPreparedTTS requires a speech preparation handle.');
5846
+ }
5847
+ const runtime = this;
5848
+ if(onState !== null && !is.function(onState)) {
5849
+ throw new TypeError('Prepared playback onState must be a function when provided.');
5850
+ }
5851
+ // One playback lane per AI; detached preparation has its own lifetime.
5852
+ this.stopAudio();
5853
+ const group = {jobs: [], context: null, paused: false, stopped: false, state: 'waiting', error: null};
5854
+ const completions = [];
5855
+ const generation = this.speechGeneration;
5856
+ this.#traceSpeech('playPreparedTTS.call', {segments: prepared.segments});
5857
+ function publishPreparedPlaybackState(state, error = group.error) {
5858
+ group.state = state;
5859
+ group.error = error;
5860
+ try {
5861
+ onState?.({state, error});
5862
+ } catch(observerError) {
5863
+ arcaneLogging.error('Prepared playback state observer failed.', observerError);
5864
+ }
5865
+ }
5866
+ group.publish = publishPreparedPlaybackState;
5867
+ group.fail = function reportPreparedPlaybackFailure(error) {
5868
+ publishPreparedPlaybackState('error', error);
5869
+ };
5870
+ function stopPreparedPlayback() {
5871
+ if(group.stopped) return false;
5872
+ group.stopped = true;
5873
+ runtime.speechResumeAttempt += 1;
5874
+ runtime.speechResumePending = false;
5875
+ publishPreparedPlaybackState('stopped');
5876
+ for(const job of group.jobs) {
5877
+ if(job.state === 'complete') continue;
5878
+ if(job.sourceNode) {
5879
+ job.sourceNode.onended = null;
5880
+ if(job.sourceNode.__arcaneStarted) {
5881
+ try {
5882
+ job.sourceNode.stop();
5883
+ } catch(error) {
5884
+ arcaneLogging.warn('Prepared speech could not be stopped.', error);
5885
+ }
5886
+ }
5887
+ }
5888
+ runtime.#cancelSpeechJob(job);
5889
+ }
5890
+ if(runtime.speechScheduleContext === group.context) {
5891
+ runtime.speechScheduleContext = null;
5892
+ runtime.speechScheduleTime = 0;
5893
+ }
5894
+ runtime.#requestSpeechPlayback();
5895
+ return true;
5896
+ }
5897
+ const control = {
5898
+ finished: null,
5899
+ get state() { return group.state; },
5900
+ get error() { return group.error; },
5901
+ async pause() {
5902
+ if(group.stopped || !group.context || group.context.state === 'closed') return false;
5903
+ group.paused = true;
5904
+ if(runtime.#speechUnlockContext === group.context) runtime.#clearSpeechUnlock();
5905
+ await group.context.suspend();
5906
+ if(!group.stopped) publishPreparedPlaybackState('paused');
5907
+ return true;
5908
+ },
5909
+ async resume() {
5910
+ if(group.stopped || !group.context || group.context.state === 'closed') return false;
5911
+ group.paused = false;
5912
+ const resumed = await runtime.resumeAudio(group.context, true);
5913
+ if(!group.stopped && !group.paused && !group.error) {
5914
+ publishPreparedPlaybackState(resumed
5915
+ ? group.jobs.some(function hasScheduledPreparedAudio(job) { return job.state === 'scheduled'; })
5916
+ ? 'scheduled' : 'waiting'
5917
+ : 'waiting-for-gesture');
5918
+ }
5919
+ return resumed;
5920
+ },
5921
+ stop: stopPreparedPlayback
5922
+ };
5923
+ this.#preparedSpeechPlaybacks.add(control);
5924
+ for(const [index, segment] of prepared.segments.entries()) {
5925
+ const job = {
5926
+ diagnosticId: ++this.#speechJobSequence,
5927
+ abortController: null,
5928
+ audioBuffer: null,
5929
+ audioContext: null,
5930
+ generation,
5931
+ scheduledEnd: null,
5932
+ scheduledStart: null,
5933
+ sourceNode: null,
5934
+ state: 'preparing',
5935
+ text: segment.input,
5936
+ pauseAfterMs: segment.pauseAfterMs ?? 0,
5937
+ preparedPlayback: group,
5938
+ resolvePlayback: null
5939
+ };
5940
+ completions.push(
5941
+ new Promise(
5942
+ function capturePreparedPlaybackCompletion(resolve) {
5943
+ job.resolvePlayback = resolve;
5944
+ }
5945
+ )
5946
+ );
5947
+ group.jobs.push(job);
5948
+ this.speechJobs.push(job);
5949
+ Promise.resolve().then(
5950
+ async function decodePreparedSpeechSegment() {
5951
+ if(group.stopped) return false;
5952
+ const audio = await prepared.getAudio(index);
5953
+ if(group.stopped || generation !== runtime.speechGeneration) return false;
5954
+ return runtime.playAudio([audio], group.context, null, audio.type, job);
5955
+ }
5956
+ ).catch(
5957
+ function failPreparedSpeechSegment(error) {
5958
+ if(group.stopped) return false;
5959
+ return runtime.#failSpeechJob(job, error, 'prepared-audio');
5960
+ }
5961
+ );
5962
+ }
5963
+ control.finished = Promise.all(completions).then(
5964
+ async function finishPreparedPlayback(results) {
5965
+ const completed = !group.stopped && results.every(Boolean);
5966
+ group.stopped = true;
5967
+ publishPreparedPlaybackState(completed ? 'complete' : group.error ? 'error' : 'stopped');
5968
+ signal?.removeEventListener('abort', stopPreparedPlayback);
5969
+ runtime.#preparedSpeechPlaybacks.delete(control);
5970
+ if(runtime.#speechUnlockContext === group.context) runtime.#clearSpeechUnlock();
5971
+ if(runtime.speechScheduleContext === group.context) {
5972
+ runtime.speechScheduleContext = null;
5973
+ runtime.speechScheduleTime = 0;
5974
+ }
5975
+ if(group.context && group.context.state !== 'closed') {
5976
+ try {
5977
+ await group.context.close();
5978
+ } catch(error) {
5979
+ arcaneLogging.warn('Prepared speech audio context could not be closed.', error);
5980
+ }
5981
+ }
5982
+ runtime.#traceSpeech('playPreparedTTS.result', {completed});
5983
+ return completed;
5984
+ }
5985
+ );
5986
+ signal?.addEventListener('abort', stopPreparedPlayback, {once: true});
5987
+ publishPreparedPlaybackState('waiting');
5988
+ if(signal?.aborted) {
5989
+ stopPreparedPlayback();
5990
+ } else if(group.jobs.length) {
5991
+ // Playing saved audio does not require activating a synthesis provider.
5992
+ this.muted = false;
5993
+ try {
5994
+ const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
5995
+ if(!is.function(AudioContext)) throw new TypeError('Audio playback is unavailable in this browser.');
5996
+ group.context = new AudioContext();
5997
+ this.resumeAudio(group.context, true).catch(
5998
+ function failPreparedSpeechUnlock(error) {
5999
+ for(const job of group.jobs) runtime.#failSpeechJob(job, error, 'playback-resume');
6000
+ }
6001
+ );
6002
+ } catch(error) {
6003
+ for(const job of group.jobs) this.#failSpeechJob(job, error, 'playback-start');
6004
+ }
6005
+ }
6006
+ return control;
6007
+ }
6008
+
5728
6009
  streamTTS(
5729
6010
  text='',
5730
6011
  end=false,
5731
6012
  options={}
5732
6013
  ){
5733
6014
  const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
6015
+ for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
5734
6016
  if(this.muted){
5735
6017
  if(end){
5736
6018
  this.audioMessageChunks='';
5737
- this.#speechTextFormat='plain';
5738
6019
  this.#markdownSpeech.reset();
5739
6020
  }
5740
6021
  this.#traceSpeech('streamTTS.result',{callId,result:false,reason:'muted'});
@@ -5743,28 +6024,16 @@ class AI {
5743
6024
 
5744
6025
  const {voice,speed,waitForPlayback=false}=options;
5745
6026
  const pauseAfterMs=Number(options.pauseAfterMs??0);
5746
- if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0){
6027
+ if(!is.finite(pauseAfterMs)||pauseAfterMs<0){
5747
6028
  throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
5748
6029
  }
5749
6030
 
5750
- const textFormat=options.textFormat??this.#speechTextFormat;
5751
- if(textFormat!=='plain'&&textFormat!=='markdown'){
5752
- throw new TypeError('Speech textFormat must be plain or markdown.');
5753
- }
5754
- let speechText='';
5755
- if(textFormat!==this.#speechTextFormat){
5756
- speechText=this.#markdownSpeech.append('',true);
5757
- }
5758
- const incomingText=String(text||'');
5759
- speechText+=textFormat==='markdown'
5760
- ?this.#markdownSpeech.append(incomingText,end)
5761
- :incomingText;
5762
- this.#speechTextFormat=end?'plain':textFormat;
6031
+ const speechText=this.#markdownSpeech.append(String(text||''),end);
5763
6032
  this.audioMessageChunks+=speechText;
5764
6033
  const outputs=this.#extractSpeechSegments(end);
5765
6034
  this.#traceSpeech('streamTTS.segments',{
5766
6035
  callId,segments:outputs,remainder:this.audioMessageChunks,
5767
- segmentation:this.ttsSegmentation,textFormat,speechText
6036
+ segmentation:this.ttsSegmentation,speechText
5768
6037
  });
5769
6038
 
5770
6039
  if(!outputs.length){
@@ -5814,15 +6083,22 @@ class AI {
5814
6083
  }
5815
6084
 
5816
6085
  #extractSpeechSegments(end=false){
6086
+ const {segments, remainder} = this.#segmentSpeechText(this.audioMessageChunks, end, this.#ttsSegmentation);
6087
+ this.audioMessageChunks = remainder;
6088
+ return segments;
6089
+ }
6090
+
6091
+ #segmentSpeechText(text, end, segmentation) {
5817
6092
  const segments=[];
5818
- let remainder=this.audioMessageChunks;
6093
+ let remainder=text;
5819
6094
 
5820
6095
  while(remainder.length>0){
5821
6096
  const punctuationBoundary=this.#findSpeechPunctuationBoundary(
5822
6097
  remainder,
5823
- end
6098
+ end,
6099
+ segmentation
5824
6100
  );
5825
- const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
6101
+ const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end,segmentation);
5826
6102
  let boundary=punctuationBoundary;
5827
6103
 
5828
6104
  if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
@@ -5845,15 +6121,14 @@ class AI {
5845
6121
  }
5846
6122
  }
5847
6123
 
5848
- this.audioMessageChunks=remainder;
5849
- return segments;
6124
+ return {segments, remainder};
5850
6125
  }
5851
6126
 
5852
- #findSpeechPunctuationBoundary(text,end=false){
5853
- if(this.#ttsSegmentation.punctuation==='none'){
6127
+ #findSpeechPunctuationBoundary(text,end=false,segmentation=this.#ttsSegmentation){
6128
+ if(segmentation.punctuation==='none'){
5854
6129
  return -1;
5855
6130
  }
5856
- if(this.#ttsSegmentation.punctuation==='any'){
6131
+ if(segmentation.punctuation==='any'){
5857
6132
  return this.#findAnySpeechPunctuationBoundary(text,end);
5858
6133
  }
5859
6134
 
@@ -5898,8 +6173,8 @@ class AI {
5898
6173
  return -1;
5899
6174
  }
5900
6175
 
5901
- #findSpeechCadenceBoundary(text,end=false){
5902
- const wordCadence=this.#ttsSegmentation.wordCadence;
6176
+ #findSpeechCadenceBoundary(text,end=false,segmentation=this.#ttsSegmentation){
6177
+ const wordCadence=segmentation.wordCadence;
5903
6178
  if(wordCadence===null){
5904
6179
  return -1;
5905
6180
  }
@@ -6040,7 +6315,8 @@ class AI {
6040
6315
  this.#traceSpeechJob('generation.request',job,{payload,selection});
6041
6316
  const response=await this.fetchTTS(
6042
6317
  payload,
6043
- job.abortController.signal
6318
+ job.abortController.signal,
6319
+ {speechInputPrepared:true}
6044
6320
  );
6045
6321
  return this.#normalizeProviderSpeechAudio(response);
6046
6322
  }
@@ -6054,7 +6330,7 @@ class AI {
6054
6330
  entry=>entry.providerId===selection.providerId
6055
6331
  );
6056
6332
  const model=provider?.models.find(entry=>entry?.id===selection.modelId);
6057
- return typeof model?.defaultVoice==='string'&&model.defaultVoice.trim()
6333
+ return is.string(model?.defaultVoice)&&model.defaultVoice.trim()
6058
6334
  ?model.defaultVoice.trim()
6059
6335
  :null;
6060
6336
  }
@@ -6072,7 +6348,7 @@ class AI {
6072
6348
  if(speech===undefined){
6073
6349
  return this.audioFormat;
6074
6350
  }
6075
- const prototype=speech&&typeof speech==='object'
6351
+ const prototype=speech&&is.object(speech)
6076
6352
  ?Object.getPrototypeOf(speech)
6077
6353
  :null;
6078
6354
  const descriptors=prototype===Object.prototype||prototype===null
@@ -6080,10 +6356,10 @@ class AI {
6080
6356
  :null;
6081
6357
  const formats=descriptors?.responseFormats?.value;
6082
6358
  const defaultFormat=descriptors?.defaultResponseFormat?.value;
6083
- if(!Array.isArray(formats)
6359
+ if(!is.array(formats)
6084
6360
  ||formats.length<1
6085
- ||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
6086
- ||typeof defaultFormat!=='string'
6361
+ ||!formats.every(format=>is.string(format)&&format.trim()===format&&format)
6362
+ ||!is.string(defaultFormat)
6087
6363
  ||!formats.includes(defaultFormat)){
6088
6364
  throw aiProviderError(
6089
6365
  'The selected TTS provider returned an invalid speech format catalog.',
@@ -6110,7 +6386,7 @@ class AI {
6110
6386
  };
6111
6387
  }
6112
6388
 
6113
- if(response instanceof ArrayBuffer||ArrayBuffer.isView(response)){
6389
+ if(response instanceof ArrayBuffer||is.arrayBufferView(response)){
6114
6390
  const bytes=response instanceof ArrayBuffer
6115
6391
  ?new Uint8Array(response)
6116
6392
  :new Uint8Array(
@@ -6124,11 +6400,11 @@ class AI {
6124
6400
  };
6125
6401
  }
6126
6402
 
6127
- if(response&&typeof response==='object'){
6128
- if(typeof response.audioBase64==='string'){
6403
+ if(response&&is.object(response)){
6404
+ if(is.string(response.audioBase64)){
6129
6405
  return {
6130
6406
  chunks:[this.#base64ToBytes(response.audioBase64)],
6131
- type:typeof response.contentType==='string'
6407
+ type:is.string(response.contentType)
6132
6408
  ?response.contentType
6133
6409
  :this.audioType
6134
6410
  };
@@ -6142,7 +6418,7 @@ class AI {
6142
6418
  };
6143
6419
  }
6144
6420
  if(response.audio instanceof ArrayBuffer
6145
- ||ArrayBuffer.isView(response.audio)){
6421
+ ||is.arrayBufferView(response.audio)){
6146
6422
  const bytes=response.audio instanceof ArrayBuffer
6147
6423
  ?new Uint8Array(response.audio)
6148
6424
  :new Uint8Array(
@@ -6152,7 +6428,7 @@ class AI {
6152
6428
  );
6153
6429
  return {
6154
6430
  chunks:[bytes],
6155
- type:typeof response.contentType==='string'
6431
+ type:is.string(response.contentType)
6156
6432
  ?response.contentType
6157
6433
  :this.audioType
6158
6434
  };
@@ -6181,7 +6457,7 @@ class AI {
6181
6457
 
6182
6458
  const AudioContext=window.AudioContext||window.webkitAudioContext;
6183
6459
 
6184
- if(typeof AudioContext!=='function'){
6460
+ if(!is.function(AudioContext)){
6185
6461
  throw new TypeError('Audio playback is unavailable in this browser.');
6186
6462
  }
6187
6463
 
@@ -6189,13 +6465,13 @@ class AI {
6189
6465
  return this.audioContext;
6190
6466
  }
6191
6467
 
6192
- async fetchTTS(payload={},signal=null){
6468
+ async fetchTTS(payload={},signal=null,{speechInputPrepared=false}={}){
6193
6469
  const callId=this.#traceSpeech('fetchTTS.call',{payload,aborted:signal?.aborted});
6194
6470
  try{
6195
6471
  this.#assertServiceConfigured(this.ttsService,'tts');
6196
6472
  if(!payload
6197
- ||typeof payload!=='object'
6198
- ||Array.isArray(payload)
6473
+ ||!is.object(payload)
6474
+ ||is.array(payload)
6199
6475
  ||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
6200
6476
  const error=new TypeError('AI.fetchTTS requires a speech request object.');
6201
6477
  error.code='ARCANE_AI_TTS_REQUEST_INVALID';
@@ -6210,7 +6486,7 @@ class AI {
6210
6486
  'speed'
6211
6487
  ]);
6212
6488
  for(const key of Reflect.ownKeys(descriptors)){
6213
- if(typeof key==='symbol'
6489
+ if(is.symbol(key)
6214
6490
  ||!acceptedKeys.has(key)
6215
6491
  ||!Object.hasOwn(descriptors[key],'value')){
6216
6492
  const error=new TypeError(
@@ -6221,9 +6497,9 @@ class AI {
6221
6497
  }
6222
6498
  }
6223
6499
  if(signal&&(
6224
- typeof signal.aborted!=='boolean'
6225
- ||typeof signal.addEventListener!=='function'
6226
- ||typeof signal.removeEventListener!=='function'
6500
+ !is.boolean(signal.aborted)
6501
+ ||!is.function(signal.addEventListener)
6502
+ ||!is.function(signal.removeEventListener)
6227
6503
  )){
6228
6504
  const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
6229
6505
  error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
@@ -6233,16 +6509,19 @@ class AI {
6233
6509
  throw normalizeAIRequestAbort(signal.reason);
6234
6510
  }
6235
6511
 
6236
- const input=descriptors.input?.value;
6237
- if(typeof input!=='string'||!input.trim()){
6512
+ const suppliedInput=descriptors.input?.value;
6513
+ if(!is.string(suppliedInput)||!suppliedInput.trim()){
6238
6514
  const error=new TypeError('AI.fetchTTS input must be nonempty text.');
6239
6515
  error.code='ARCANE_AI_TTS_INPUT_INVALID';
6240
6516
  throw error;
6241
6517
  }
6518
+ const input=speechInputPrepared===true
6519
+ ?suppliedInput
6520
+ :stripSpeechFormatting(suppliedInput);
6242
6521
  const selection=this.#providerRuntime.selection('tts');
6243
6522
  const requestedModel=descriptors.model?.value;
6244
6523
  if(requestedModel!==undefined
6245
- &&(typeof requestedModel!=='string'
6524
+ &&(!is.string(requestedModel)
6246
6525
  ||requestedModel.trim()!==requestedModel
6247
6526
  ||!requestedModel)){
6248
6527
  const error=new TypeError(
@@ -6269,7 +6548,7 @@ class AI {
6269
6548
  }
6270
6549
  const requestedVoice=descriptors.voice?.value;
6271
6550
  if(requestedVoice!==undefined
6272
- &&(typeof requestedVoice!=='string'
6551
+ &&(!is.string(requestedVoice)
6273
6552
  ||requestedVoice.trim()!==requestedVoice
6274
6553
  ||!requestedVoice)){
6275
6554
  const error=new TypeError(
@@ -6292,7 +6571,7 @@ class AI {
6292
6571
  }
6293
6572
  const requestedResponseFormat=descriptors.responseFormat?.value;
6294
6573
  if(requestedResponseFormat!==undefined
6295
- &&(typeof requestedResponseFormat!=='string'
6574
+ &&(!is.string(requestedResponseFormat)
6296
6575
  ||requestedResponseFormat.trim()!==requestedResponseFormat
6297
6576
  ||!requestedResponseFormat)){
6298
6577
  const error=new TypeError(
@@ -6312,7 +6591,7 @@ class AI {
6312
6591
  const speed=descriptors.speed
6313
6592
  ?Number(descriptors.speed.value)
6314
6593
  :this.voiceSpeed;
6315
- if(!Number.isFinite(speed)||speed<=0){
6594
+ if(!is.finite(speed)||speed<=0){
6316
6595
  const error=new RangeError('AI.fetchTTS speed must be a positive number.');
6317
6596
  error.code='ARCANE_AI_TTS_SPEED_INVALID';
6318
6597
  throw error;
@@ -6329,7 +6608,8 @@ class AI {
6329
6608
  payload:{model,voice,input,responseFormat,speed},
6330
6609
  localOnly:false,
6331
6610
  signal
6332
- }
6611
+ },
6612
+ {speechInputPrepared:true}
6333
6613
  );
6334
6614
  this.#traceSpeech('fetchTTS.providerResult',{callId,response});
6335
6615
  if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
@@ -6359,9 +6639,9 @@ class AI {
6359
6639
  try{
6360
6640
  this.#assertServiceConfigured(this.sttService,'stt');
6361
6641
  if(signal&&(
6362
- typeof signal.aborted!=='boolean'
6363
- ||typeof signal.addEventListener!=='function'
6364
- ||typeof signal.removeEventListener!=='function'
6642
+ !is.boolean(signal.aborted)
6643
+ ||!is.function(signal.addEventListener)
6644
+ ||!is.function(signal.removeEventListener)
6365
6645
  )){
6366
6646
  const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
6367
6647
  error.code='ARCANE_AI_STT_SIGNAL_INVALID';
@@ -6378,7 +6658,7 @@ class AI {
6378
6658
  operation:'transcribe',
6379
6659
  payload:{
6380
6660
  audio:audioFile,
6381
- mimeType:typeof Blob==='function'
6661
+ mimeType:is.function(globalThis.Blob)
6382
6662
  &&audioFile instanceof Blob
6383
6663
  ?String(audioFile.type||'audio/webm')
6384
6664
  :'audio/webm',
@@ -6389,10 +6669,10 @@ class AI {
6389
6669
  }
6390
6670
  );
6391
6671
  this.#traceSpeech('fetchSTT.providerResult',{callId,response});
6392
- const text=typeof response==='string'
6672
+ const text=is.string(response)
6393
6673
  ?response
6394
6674
  :response?.text;
6395
- if(typeof text!=='string'){
6675
+ if(!is.string(text)){
6396
6676
  const error=new TypeError(
6397
6677
  'Arcane returned an invalid provider speech transcription.'
6398
6678
  );
@@ -6425,6 +6705,7 @@ class AI {
6425
6705
  const callId=this.#traceSpeech('stopAudio.call',{
6426
6706
  remainder:this.audioMessageChunks,queuedJobs:this.speechJobs.length
6427
6707
  });
6708
+ for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
6428
6709
  this.speechGeneration+=1;
6429
6710
  this.speechResumeAttempt+=1;
6430
6711
  this.speechResumePending=false;
@@ -6432,7 +6713,6 @@ class AI {
6432
6713
  this.speechScheduleContext=null;
6433
6714
  this.speechScheduleTime=0;
6434
6715
  this.audioMessageChunks='';
6435
- this.#speechTextFormat='plain';
6436
6716
  this.#markdownSpeech.reset();
6437
6717
  this.#clearSpeechUnlock();
6438
6718
 
@@ -6485,9 +6765,14 @@ class AI {
6485
6765
 
6486
6766
  let attempt=0;
6487
6767
  let context;
6768
+ let preparedGroup = null;
6488
6769
 
6489
6770
  try{
6490
6771
  context=audioContext||this.#getSpeechAudioContext();
6772
+ preparedGroup = this.speechJobs.find(function findPreparedResumeOwner(job) {
6773
+ return job.preparedPlayback?.context === context;
6774
+ })?.preparedPlayback ?? null;
6775
+ if(preparedGroup?.paused || preparedGroup?.stopped) return false;
6491
6776
  this.#traceSpeech('resumeAudio.context',{
6492
6777
  callId,state:context.state,audioTime:context.currentTime,
6493
6778
  sampleRate:context.sampleRate
@@ -6500,7 +6785,7 @@ class AI {
6500
6785
  return true;
6501
6786
  }
6502
6787
 
6503
- if(typeof context.resume!=='function'){
6788
+ if(!is.function(context.resume)){
6504
6789
  this.#waitForSpeechGesture(null,context);
6505
6790
  this.#traceSpeech('resumeAudio.result',{
6506
6791
  callId,result:false,reason:'resume-unavailable'
@@ -6511,6 +6796,12 @@ class AI {
6511
6796
  attempt=++this.speechResumeAttempt;
6512
6797
  this.speechResumePending=true;
6513
6798
  await context.resume();
6799
+ if(preparedGroup?.stopped) return false;
6800
+ if(preparedGroup?.paused) {
6801
+ await context.suspend();
6802
+ this.speechResumePending = false;
6803
+ return false;
6804
+ }
6514
6805
  this.#traceSpeech('resumeAudio.resumed',{
6515
6806
  callId,state:context.state,audioTime:context.currentTime
6516
6807
  });
@@ -6532,6 +6823,7 @@ class AI {
6532
6823
  }
6533
6824
  }catch(error){
6534
6825
  this.#traceSpeech('resumeAudio.error',{callId,error,state:context?.state});
6826
+ if(preparedGroup?.stopped || preparedGroup?.paused) return false;
6535
6827
  if(attempt&&attempt!==this.speechResumeAttempt){
6536
6828
  return context?.state==='running';
6537
6829
  }
@@ -6539,6 +6831,10 @@ class AI {
6539
6831
  if(attempt===this.speechResumeAttempt){
6540
6832
  this.speechResumePending=false;
6541
6833
  }
6834
+ if(preparedGroup && error?.name !== 'NotAllowedError') {
6835
+ for(const job of preparedGroup.jobs) this.#failSpeechJob(job, error, 'playback-resume');
6836
+ return false;
6837
+ }
6542
6838
  if(context?.state==='closed'){
6543
6839
  this.#clearSpeechUnlock();
6544
6840
  const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
@@ -6599,7 +6895,8 @@ class AI {
6599
6895
  this.speechJobs.push(job);
6600
6896
  }
6601
6897
 
6602
- if(this.muted||job.generation!==this.speechGeneration){
6898
+ if(this.muted||job.generation!==this.speechGeneration
6899
+ ||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
6603
6900
  return this.#cancelSpeechJob(job);
6604
6901
  }
6605
6902
 
@@ -6616,7 +6913,8 @@ class AI {
6616
6913
  channels:audioBuffer.numberOfChannels,audioBuffer
6617
6914
  });
6618
6915
 
6619
- if(this.muted||job.generation!==this.speechGeneration){
6916
+ if(this.muted||job.generation!==this.speechGeneration
6917
+ ||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
6620
6918
  return this.#cancelSpeechJob(job);
6621
6919
  }
6622
6920
 
@@ -6694,6 +6992,8 @@ class AI {
6694
6992
 
6695
6993
  const audioContext=job.sourceNode.context||job.audioContext;
6696
6994
 
6995
+ if(job.preparedPlayback?.paused) break;
6996
+
6697
6997
  if(audioContext.state!=='running'){
6698
6998
  this.#waitForSpeechGesture(null,audioContext);
6699
6999
 
@@ -6722,7 +7022,7 @@ class AI {
6722
7022
 
6723
7023
  try{
6724
7024
  const duration=Number(job.audioBuffer?.duration);
6725
- const hasKnownDuration=Number.isFinite(duration)&&duration>0;
7025
+ const hasKnownDuration=is.finite(duration)&&duration>0;
6726
7026
  const currentTime=Number(audioContext.currentTime)||0;
6727
7027
  const previousContext=this.speechScheduleContext;
6728
7028
  const remainingDelay=previousContext
@@ -6749,6 +7049,9 @@ class AI {
6749
7049
  }
6750
7050
  this.isSpeaking=true;
6751
7051
  job.sourceNode.start(scheduledStart);
7052
+ if(job.preparedPlayback && !job.preparedPlayback.error) {
7053
+ job.preparedPlayback.publish('scheduled');
7054
+ }
6752
7055
  this.#traceSpeechJob('playback.scheduled',job,{
6753
7056
  audioEnd:hasKnownDuration?scheduledStart+duration:null,
6754
7057
  previousScheduledEnd:this.speechScheduleTime,
@@ -6783,6 +7086,9 @@ class AI {
6783
7086
  !this.muted
6784
7087
  &&!this.speechAwaitingGesture
6785
7088
  &&!this.speechResumePending
7089
+ &&!this.speechJobs.some(function hasPausedPreparedPlayback(job) {
7090
+ return job.preparedPlayback?.paused;
7091
+ })
6786
7092
  &&!this.speechJobs.some(
6787
7093
  function hasScheduledSpeechWithoutDuration(job){
6788
7094
  return job.state==='scheduled'
@@ -6887,7 +7193,7 @@ class AI {
6887
7193
  operationId,
6888
7194
  publicDetail:completeValue({
6889
7195
  boundary:normalizedBoundary,
6890
- ...(typeof error?.code==='string'?{code:error.code}:{}),
7196
+ ...(is.string(error?.code)?{code:error.code}:{}),
6891
7197
  generation,
6892
7198
  reason
6893
7199
  })
@@ -6910,6 +7216,7 @@ class AI {
6910
7216
  }
6911
7217
 
6912
7218
  job.state='failed';
7219
+ job.preparedPlayback?.fail(error);
6913
7220
  this.#traceSpeechJob('queue.failed',job,{boundary,error});
6914
7221
 
6915
7222
  this.#publishTTSFailure(error,{
@@ -6970,7 +7277,15 @@ class AI {
6970
7277
  const target=window;
6971
7278
 
6972
7279
  this.speechAwaitingGesture=true;
7280
+ this.#speechUnlockContext=audioContext;
7281
+ const preparedJob = this.speechJobs.find(function preparedPlaybackForGesture(job) {
7282
+ return job.preparedPlayback?.context === audioContext;
7283
+ });
7284
+ if(preparedJob && !preparedJob.preparedPlayback.paused && !preparedJob.preparedPlayback.error) {
7285
+ preparedJob.preparedPlayback.publish('waiting-for-gesture');
7286
+ }
6973
7287
  this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
7288
+ if(preparedJob?.preparedPlayback.paused || preparedJob?.preparedPlayback.stopped) return false;
6974
7289
  runtime.#clearSpeechUnlock();
6975
7290
  return runtime.resumeAudio(audioContext);
6976
7291
  };
@@ -7008,6 +7323,7 @@ class AI {
7008
7323
  }
7009
7324
 
7010
7325
  this.speechUnlockHandler=null;
7326
+ this.#speechUnlockContext=null;
7011
7327
  this.speechAwaitingGesture=false;
7012
7328
  }
7013
7329
  }
@@ -7029,7 +7345,7 @@ function installAIUserReadyRegistration(){
7029
7345
  ?existingDescriptor.value
7030
7346
  :null;
7031
7347
  if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
7032
- ||typeof existing.dispose!=='function'){
7348
+ ||!is.function(existing.dispose)){
7033
7349
  throw aiInitializationError(
7034
7350
  AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
7035
7351
  AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,