arcane-os 0.5.17 → 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 (183) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +9 -9
  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 +46 -43
  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 +5 -1
  17. package/docs/reference/ai/browser-speech.md +198 -5
  18. package/docs/reference/availability-and-normalization.md +40 -0
  19. package/docs/reference/inventory/package-api.json +1 -1
  20. package/docs/reference/inventory/runtime-components.json +1 -1
  21. package/docs/reference/inventory/runtime-modules.json +20 -24
  22. package/docs/reference/protocols.md +1 -1
  23. package/docs/reference/runtime-modules.md +125 -35
  24. package/docs/reference/sdk-api.md +23 -7
  25. package/examples/wasm-ai-demo/index.html +1 -0
  26. package/node_modules/event-pubsub/node_modules/strong-type/README.md +408 -0
  27. package/node_modules/event-pubsub/node_modules/strong-type/assets/strong-type-header.png +0 -0
  28. package/node_modules/event-pubsub/node_modules/strong-type/index.js +1151 -0
  29. package/node_modules/event-pubsub/node_modules/strong-type/licence +21 -0
  30. package/node_modules/event-pubsub/node_modules/strong-type/node.js +125 -0
  31. package/node_modules/event-pubsub/node_modules/strong-type/package.json +61 -0
  32. package/node_modules/strong-type/README.md +42 -24
  33. package/node_modules/strong-type/benchmark/core.js +324 -0
  34. package/node_modules/strong-type/index.js +207 -82
  35. package/node_modules/strong-type/package.json +14 -6
  36. package/package.json +3 -3
  37. package/runtime/arcane/components/app-bar.html +5 -2
  38. package/runtime/arcane/components/assistant-panel.html +5 -2
  39. package/runtime/arcane/components/chart.html +15 -12
  40. package/runtime/arcane/components/chat.html +117 -138
  41. package/runtime/arcane/components/dashboard-config.html +6 -3
  42. package/runtime/arcane/components/data-view.html +4 -1
  43. package/runtime/arcane/components/directory-picker.html +5 -2
  44. package/runtime/arcane/components/document-inspector.html +6 -3
  45. package/runtime/arcane/components/file-drop.html +5 -2
  46. package/runtime/arcane/components/file-inspector.html +9 -6
  47. package/runtime/arcane/components/file-manager.html +16 -13
  48. package/runtime/arcane/components/local-ai-status.html +5 -2
  49. package/runtime/arcane/components/markdown-document.html +14 -11
  50. package/runtime/arcane/components/markdown-editor.html +4 -1
  51. package/runtime/arcane/components/modal.html +4 -1
  52. package/runtime/arcane/components/output-panel.html +6 -3
  53. package/runtime/arcane/components/preferences-form.html +4 -1
  54. package/runtime/arcane/components/record-timeline.html +8 -5
  55. package/runtime/arcane/components/relationship-board.html +9 -6
  56. package/runtime/arcane/components/source-code-viewer.html +12 -9
  57. package/runtime/arcane/components/source-explanation.html +8 -5
  58. package/runtime/arcane/components/speech.html +24 -21
  59. package/runtime/arcane/components/summary-strip.html +4 -1
  60. package/runtime/arcane/components/task-progress.html +6 -3
  61. package/runtime/arcane/components/terminal-workspace.html +4 -1
  62. package/runtime/arcane/components/voice-transcription.html +11 -8
  63. package/runtime/arcane/components/web-navigator.html +5 -2
  64. package/runtime/arcane/entities/ApiModelRecord.js +5 -2
  65. package/runtime/arcane/entities/Calculation.js +5 -2
  66. package/runtime/arcane/entities/Chat.js +33 -33
  67. package/runtime/arcane/entities/CommunicationMessage.js +4 -1
  68. package/runtime/arcane/entities/CommunicationThread.js +4 -1
  69. package/runtime/arcane/entities/File.js +1 -1
  70. package/runtime/arcane/entities/Image.js +8 -5
  71. package/runtime/arcane/entities/IntentEnvelope.js +20 -17
  72. package/runtime/arcane/entities/Preference.js +9 -6
  73. package/runtime/arcane/entities/Theme.js +5 -2
  74. package/runtime/arcane/entities/User.js +11 -11
  75. package/runtime/arcane/entities/Weather.js +5 -2
  76. package/runtime/arcane/modules/AI.js +491 -165
  77. package/runtime/arcane/modules/AIPreferenceRuntime.js +5 -2
  78. package/runtime/arcane/modules/AIPreferenceTuple.js +9 -6
  79. package/runtime/arcane/modules/AIProviderRuntime.js +88 -85
  80. package/runtime/arcane/modules/AIResponseURLPolicy.js +148 -62
  81. package/runtime/arcane/modules/AIRuntimeState.js +29 -26
  82. package/runtime/arcane/modules/AnsiText.js +4 -1
  83. package/runtime/arcane/modules/ApiModelDatabase.js +22 -19
  84. package/runtime/arcane/modules/AppDataScope.js +8 -5
  85. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +4 -1
  86. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +8 -5
  87. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +18 -15
  88. package/runtime/arcane/modules/AsyncBoundary.js +13 -10
  89. package/runtime/arcane/modules/BrowserTestSuite.js +19 -16
  90. package/runtime/arcane/modules/CalculatorEngine.js +5 -2
  91. package/runtime/arcane/modules/ChatRecords.js +18 -15
  92. package/runtime/arcane/modules/CommunicationAppController.js +9 -6
  93. package/runtime/arcane/modules/CommunicationHub.js +9 -6
  94. package/runtime/arcane/modules/CommunicationPreferences.js +4 -1
  95. package/runtime/arcane/modules/CommunicationProviderRegistry.js +4 -1
  96. package/runtime/arcane/modules/ComponentContracts.js +56 -53
  97. package/runtime/arcane/modules/ConfiguredAIChatSession.js +30 -27
  98. package/runtime/arcane/modules/ConversationActionItems.js +15 -12
  99. package/runtime/arcane/modules/ConversationClosingReport.js +11 -8
  100. package/runtime/arcane/modules/ConversationTimebox.js +28 -25
  101. package/runtime/arcane/modules/CoreLocalModelCatalog.js +17 -14
  102. package/runtime/arcane/modules/DBLS.js +7 -4
  103. package/runtime/arcane/modules/DBOPFS.js +7 -7
  104. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +34 -31
  105. package/runtime/arcane/modules/DevelopmentWorkspace.js +4 -1
  106. package/runtime/arcane/modules/DirectoryPicker.js +8 -5
  107. package/runtime/arcane/modules/DocumentLexicalSearch.js +11 -8
  108. package/runtime/arcane/modules/DocumentNavigation.js +7 -4
  109. package/runtime/arcane/modules/Errors.js +28 -25
  110. package/runtime/arcane/modules/HTMLImport.js +7 -4
  111. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +15 -12
  112. package/runtime/arcane/modules/LocalAIReadiness.js +21 -18
  113. package/runtime/arcane/modules/LocalAIReadinessController.js +6 -3
  114. package/runtime/arcane/modules/MD.js +1 -1
  115. package/runtime/arcane/modules/Mail.js +46 -43
  116. package/runtime/arcane/modules/MailOutbox.mjs +48 -45
  117. package/runtime/arcane/modules/MailTransport.mjs +29 -26
  118. package/runtime/arcane/modules/MemoryRecords.js +7 -4
  119. package/runtime/arcane/modules/MessageAdvisory.js +6 -3
  120. package/runtime/arcane/modules/ModelDefinition.js +7 -4
  121. package/runtime/arcane/modules/Ollama.js +6 -3
  122. package/runtime/arcane/modules/OllamaModelIdentifier.js +4 -1
  123. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +18 -15
  124. package/runtime/arcane/modules/PersistentAIChatSession.js +33 -30
  125. package/runtime/arcane/modules/PreferenceStore.js +20 -17
  126. package/runtime/arcane/modules/PreparedSpeech.js +485 -0
  127. package/runtime/arcane/modules/Questionnaire.js +6 -3
  128. package/runtime/arcane/modules/RecordLinkIndex.js +4 -1
  129. package/runtime/arcane/modules/RecordPassageIndex.js +9 -6
  130. package/runtime/arcane/modules/RecordReviewStore.js +12 -9
  131. package/runtime/arcane/modules/RiskSignalAnalyzer.js +4 -1
  132. package/runtime/arcane/modules/ScopedOPFSCache.js +6 -3
  133. package/runtime/arcane/modules/ScreenCapture.js +20 -17
  134. package/runtime/arcane/modules/SpeechPlayback.js +29 -26
  135. package/runtime/arcane/modules/StaticDocumentCatalog.js +33 -30
  136. package/runtime/arcane/modules/SystemAppearance.js +5 -2
  137. package/runtime/arcane/modules/SystemToolRegistry.js +6 -3
  138. package/runtime/arcane/modules/TerminalClient.js +14 -11
  139. package/runtime/arcane/modules/TerminalCommandRegistry.js +4 -1
  140. package/runtime/arcane/modules/ThemeBootstrap.js +9 -6
  141. package/runtime/arcane/modules/ThemeManager.js +5 -2
  142. package/runtime/arcane/modules/TimeGuard.js +1 -1
  143. package/runtime/arcane/modules/ToolCallRouter.js +11 -8
  144. package/runtime/arcane/modules/WaitForComponent.js +19 -16
  145. package/runtime/arcane/modules/YouTubeMedia.js +4 -1
  146. package/runtime/strong-type/index.js +1276 -352
  147. package/runtime/strong-type/package.json +69 -45
  148. package/src/app-descriptor.mjs +17 -14
  149. package/src/application-tests.mjs +4 -1
  150. package/src/cli/main.mjs +8 -5
  151. package/src/constants.mjs +4 -1
  152. package/src/dev-server.mjs +8 -5
  153. package/src/doctor.mjs +5 -2
  154. package/src/dom-event-instrumentation.mjs +24 -20
  155. package/src/errors.mjs +7 -3
  156. package/src/event-manager.mjs +111 -108
  157. package/src/event-queue.mjs +8 -5
  158. package/src/events.mjs +12 -9
  159. package/src/import-map.mjs +35 -27
  160. package/src/installed-sdk-runtime.mjs +4 -1
  161. package/src/integrated-provider-loader.mjs +9 -6
  162. package/src/mail-credentials.mjs +16 -13
  163. package/src/mail-server.mjs +53 -50
  164. package/src/mail.mjs +18 -15
  165. package/src/native-plan.mjs +12 -9
  166. package/src/native-provider-loader.mjs +7 -4
  167. package/src/packager/core.mjs +18 -15
  168. package/src/process.mjs +6 -3
  169. package/src/release-bundle.mjs +14 -11
  170. package/src/runtime.mjs +4 -1
  171. package/src/scaffold.mjs +9 -6
  172. package/src/sdk-browser-runtime.mjs +4 -1
  173. package/src/source-server.mjs +17 -14
  174. package/src/targets/index.mjs +5 -2
  175. package/src/templates/workspace-template.mjs +13 -6
  176. package/src/testing-loader.mjs +7 -4
  177. package/src/testing.mjs +10 -7
  178. package/src/toolchain.mjs +13 -10
  179. package/src/update-check.mjs +9 -6
  180. package/src/workspace-operation-lock.mjs +14 -11
  181. package/src/workspace-runtime.mjs +4 -1
  182. package/src/workspace.mjs +17 -14
  183. 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 {
@@ -14,6 +17,7 @@ import {
14
17
  import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
15
18
  import {arcaneLogging} from 'arcane-os/logging';
16
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
  );
@@ -2052,7 +2059,7 @@ class AI {
2052
2059
  if(value===undefined||value===null||value===''){
2053
2060
  return current[index];
2054
2061
  }
2055
- if(typeof value!=='string'||value.trim()!==value||!value){
2062
+ if(!is.string(value)||value.trim()!==value||!value){
2056
2063
  throw new TypeError('AI preferences must be nonempty trimmed strings.');
2057
2064
  }
2058
2065
  return value;
@@ -2487,7 +2494,7 @@ class AI {
2487
2494
 
2488
2495
  #browserSpeechOperationId(action){
2489
2496
  this.#browserSpeechOperationSequence=
2490
- typeof this.#browserSpeechOperationSequence==='bigint'
2497
+ is.bigint(this.#browserSpeechOperationSequence)
2491
2498
  ? this.#browserSpeechOperationSequence+1n
2492
2499
  : this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER
2493
2500
  ? BigInt(this.#browserSpeechOperationSequence)+1n
@@ -2709,7 +2716,7 @@ class AI {
2709
2716
  publicDetail:completeValue({
2710
2717
  configurationId:normalized.id,
2711
2718
  ...(descriptor?{descriptor}:{}),
2712
- ...(typeof error?.code==='string'?{code:error.code}:{}),
2719
+ ...(is.string(error?.code)?{code:error.code}:{}),
2713
2720
  reason
2714
2721
  })
2715
2722
  }
@@ -2737,7 +2744,7 @@ class AI {
2737
2744
  continue;
2738
2745
  }
2739
2746
  const catalog=providers[role].catalog();
2740
- if(catalog.length!==1||typeof catalog[0]?.id!=='string'){
2747
+ if(catalog.length!==1||!is.string(catalog[0]?.id)){
2741
2748
  throw aiBrowserSpeechError(
2742
2749
  AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
2743
2750
  AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
@@ -2951,7 +2958,7 @@ class AI {
2951
2958
  for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
2952
2959
  if(record.registrationState?.[role]===false)continue;
2953
2960
  const unregister=record.unregisters?.[role];
2954
- if(typeof unregister!=='function'){
2961
+ if(!is.function(unregister)){
2955
2962
  failures.push(aiBrowserSpeechError(
2956
2963
  AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
2957
2964
  AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
@@ -3745,7 +3752,7 @@ class AI {
3745
3752
 
3746
3753
  async setSpeechMuted(muted){
3747
3754
  const callId=this.#traceSpeech('setSpeechMuted.call',{requestedMuted:muted});
3748
- if(typeof muted!=='boolean'){
3755
+ if(!is.boolean(muted)){
3749
3756
  throw new TypeError('AI speech muted state must be a boolean.');
3750
3757
  }
3751
3758
  const generation=++this.#speechControlGeneration;
@@ -3792,12 +3799,12 @@ class AI {
3792
3799
  if(signal?.aborted){
3793
3800
  throw normalizeAIRequestAbort(signal.reason);
3794
3801
  }
3795
- const message=typeof error==='string'
3802
+ const message=is.string(error)
3796
3803
  ?error
3797
3804
  :error?.error?.message??error?.message;
3798
3805
  if(
3799
3806
  response.status!==429
3800
- ||typeof message!=='string'
3807
+ ||!is.string(message)
3801
3808
  ||!message.toLowerCase().includes('overload')
3802
3809
  ){
3803
3810
  throw error;
@@ -3836,7 +3843,7 @@ class AI {
3836
3843
  const client=globalThis.Arcane?.ollama;
3837
3844
 
3838
3845
  return this.llmService==='OLLAMA'
3839
- &&typeof client?.chat==='function'
3846
+ &&is.function(client?.chat)
3840
3847
  ?client
3841
3848
  :null;
3842
3849
  }
@@ -3847,10 +3854,10 @@ class AI {
3847
3854
  if(service!=='LOCAL_SPEACH'){
3848
3855
  return null;
3849
3856
  }
3850
- if(role==='tts'&&typeof client?.synthesize==='function'){
3857
+ if(role==='tts'&&is.function(client?.synthesize)){
3851
3858
  return client;
3852
3859
  }
3853
- if(role==='stt'&&typeof client?.transcribe==='function'){
3860
+ if(role==='stt'&&is.function(client?.transcribe)){
3854
3861
  return client;
3855
3862
  }
3856
3863
  return null;
@@ -3858,7 +3865,7 @@ class AI {
3858
3865
 
3859
3866
  async #requestBuiltInSpeechTranscription(payload={},signal=null){
3860
3867
  const audio=payload?.audio;
3861
- if(!audio||typeof audio.arrayBuffer!=='function'){
3868
+ if(!audio||!is.function(audio.arrayBuffer)){
3862
3869
  throw new TypeError('Speech transcription requires an audio Blob or File.');
3863
3870
  }
3864
3871
  if(signal?.aborted){
@@ -3880,7 +3887,7 @@ class AI {
3880
3887
  if(signal?.aborted){
3881
3888
  throw normalizeAIRequestAbort(signal.reason);
3882
3889
  }
3883
- if(!response||typeof response.text!=='string'){
3890
+ if(!response||!is.string(response.text)){
3884
3891
  throw new TypeError('Arcane returned an invalid local speech transcription.');
3885
3892
  }
3886
3893
  return response.text;
@@ -3908,7 +3915,7 @@ class AI {
3908
3915
  }
3909
3916
 
3910
3917
  async #requestBuiltInSpeechSynthesis(payload={},signal=null){
3911
- const input=typeof payload?.input==='string'?payload.input:'';
3918
+ const input=is.string(payload?.input)?payload.input:'';
3912
3919
  if(!input){
3913
3920
  throw new TypeError('Speech synthesis requires nonempty input.');
3914
3921
  }
@@ -3916,14 +3923,14 @@ class AI {
3916
3923
  throw normalizeAIRequestAbort(signal.reason);
3917
3924
  }
3918
3925
  const model=String(payload.model||this.modelTTS);
3919
- const voice=typeof payload.voice==='string'&&payload.voice.trim()
3926
+ const voice=is.string(payload.voice)&&payload.voice.trim()
3920
3927
  ?payload.voice.trim()
3921
3928
  :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
3922
3929
  if(!voice){
3923
3930
  throw new TypeError('The selected speech provider requires a voice.');
3924
3931
  }
3925
3932
  const responseFormat=String(payload.responseFormat||this.audioFormat);
3926
- const speed=Number.isFinite(payload.speed)?payload.speed:this.voiceSpeed;
3933
+ const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
3927
3934
  const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
3928
3935
  if(nativeSpeech){
3929
3936
  const response=await nativeSpeech.synthesize({
@@ -3936,7 +3943,7 @@ class AI {
3936
3943
  if(signal?.aborted){
3937
3944
  throw normalizeAIRequestAbort(signal.reason);
3938
3945
  }
3939
- if(!response||typeof response.audioBase64!=='string'){
3946
+ if(!response||!is.string(response.audioBase64)){
3940
3947
  const error=new TypeError(
3941
3948
  'The Arcane speech bridge returned invalid TTS audio.'
3942
3949
  );
@@ -3946,7 +3953,7 @@ class AI {
3946
3953
  return new Blob(
3947
3954
  [this.#base64ToBytes(response.audioBase64)],
3948
3955
  {
3949
- type:typeof response.contentType==='string'
3956
+ type:is.string(response.contentType)
3950
3957
  ?response.contentType
3951
3958
  :this.audioType
3952
3959
  }
@@ -3995,7 +4002,7 @@ class AI {
3995
4002
  }
3996
4003
 
3997
4004
  async #androidNativeHost(){
3998
- if(typeof globalThis.arcaneAndroid?.postMessage==='function'){
4005
+ if(is.function(globalThis.arcaneAndroid?.postMessage)){
3999
4006
  return true;
4000
4007
  }
4001
4008
 
@@ -4037,7 +4044,7 @@ class AI {
4037
4044
  }
4038
4045
 
4039
4046
  #base64ToBytes(value){
4040
- if(typeof value!=='string'||!value){
4047
+ if(!is.string(value)||!value){
4041
4048
  throw new TypeError('Arcane returned invalid local speech audio.');
4042
4049
  }
4043
4050
 
@@ -4052,7 +4059,7 @@ class AI {
4052
4059
  }
4053
4060
 
4054
4061
  #ollamaTools(tools=[],toolChoice='auto'){
4055
- if(!Array.isArray(tools)){
4062
+ if(!is.array(tools)){
4056
4063
  return [];
4057
4064
  }
4058
4065
 
@@ -4076,12 +4083,12 @@ class AI {
4076
4083
  const role=String(message?.role??'');
4077
4084
  const content=String(message?.content??'');
4078
4085
 
4079
- if(role==='assistant'&&Array.isArray(message?.tool_calls)){
4086
+ if(role==='assistant'&&is.array(message?.tool_calls)){
4080
4087
  const toolCalls=message.tool_calls.map(function sanitizeOllamaToolCall(call){
4081
4088
  const name=String(call?.function?.name??'').trim();
4082
4089
  const callId=String(call?.id??'').trim();
4083
4090
  let argumentValue=call?.function?.arguments;
4084
- if(typeof argumentValue==='string'){
4091
+ if(is.string(argumentValue)){
4085
4092
  try{
4086
4093
  argumentValue=JSON.parse(argumentValue);
4087
4094
  }catch(cause){
@@ -4095,8 +4102,8 @@ class AI {
4095
4102
  }
4096
4103
  if(
4097
4104
  !argumentValue
4098
- ||typeof argumentValue!=='object'
4099
- ||Array.isArray(argumentValue)
4105
+ ||!is.object(argumentValue)
4106
+ ||is.array(argumentValue)
4100
4107
  ){
4101
4108
  const error=new TypeError(
4102
4109
  'Ollama tool call arguments must contain a JSON object.'
@@ -4172,8 +4179,8 @@ class AI {
4172
4179
  return 'json';
4173
4180
  }
4174
4181
  if(
4175
- typeof value==='object'
4176
- &&!Array.isArray(value)
4182
+ is.object(value)
4183
+ &&!is.array(value)
4177
4184
  &&(
4178
4185
  Object.getPrototypeOf(value)===Object.prototype
4179
4186
  ||Object.getPrototypeOf(value)===null
@@ -4207,7 +4214,7 @@ class AI {
4207
4214
  }
4208
4215
 
4209
4216
  async #reportRequest(requestHandler,request,id,metadata){
4210
- if(typeof requestHandler!=='function'){
4217
+ if(!is.function(requestHandler)){
4211
4218
  throw new TypeError('AI onRequest callback must be a function.');
4212
4219
  }
4213
4220
  await requestHandler(request,id,metadata);
@@ -4215,31 +4222,31 @@ class AI {
4215
4222
 
4216
4223
  #providerStreamEmissions(chunk,seeThinking){
4217
4224
  const chunks=[];
4218
- if(typeof chunk==='string'){
4225
+ if(is.string(chunk)){
4219
4226
  if(chunk) chunks.push({text:chunk,thinking:false});
4220
4227
  return {chunks};
4221
4228
  }
4222
- const choices=Array.isArray(chunk?.choices)?chunk.choices:[];
4229
+ const choices=is.array(chunk?.choices)?chunk.choices:[];
4223
4230
  for(const choice of choices){
4224
4231
  const delta=choice?.delta||{};
4225
4232
  if(
4226
4233
  seeThinking
4227
- &&typeof delta.reasoning_content==='string'
4234
+ &&is.string(delta.reasoning_content)
4228
4235
  &&delta.reasoning_content
4229
4236
  ){
4230
4237
  chunks.push({text:delta.reasoning_content,thinking:true});
4231
4238
  }
4232
- if(typeof delta.content==='string'&&delta.content){
4239
+ if(is.string(delta.content)&&delta.content){
4233
4240
  chunks.push({text:delta.content,thinking:false});
4234
4241
  }
4235
4242
  }
4236
4243
  if(!choices.length){
4237
- if(seeThinking&&typeof chunk?.thinking==='string'){
4244
+ if(seeThinking&&is.string(chunk?.thinking)){
4238
4245
  chunks.push({text:chunk.thinking,thinking:true});
4239
4246
  }
4240
- const text=typeof chunk?.text==='string'
4247
+ const text=is.string(chunk?.text)
4241
4248
  ?chunk.text
4242
- :typeof chunk?.content==='string'
4249
+ :is.string(chunk?.content)
4243
4250
  ?chunk.content
4244
4251
  :'';
4245
4252
  if(text){
@@ -4250,16 +4257,16 @@ class AI {
4250
4257
  }
4251
4258
 
4252
4259
  #providerCompletionOutput(completion){
4253
- if(typeof completion==='string'){
4260
+ if(is.string(completion)){
4254
4261
  return completion;
4255
4262
  }
4256
- if(Array.isArray(completion?.choices)&&completion.choices.length>1){
4263
+ if(is.array(completion?.choices)&&completion.choices.length>1){
4257
4264
  return completion;
4258
4265
  }
4259
4266
  const structuralToolCalls=normalizeAICompletionToolCalls(completion);
4260
4267
  if(structuralToolCalls.length)return structuralToolCalls;
4261
4268
  const content=completion?.choices?.[0]?.message?.content;
4262
- return typeof content==='string'?content:completion;
4269
+ return is.string(content)?content:completion;
4263
4270
  }
4264
4271
 
4265
4272
  #requestBuiltInLLMChat(payload={},signal=null){
@@ -4331,14 +4338,14 @@ class AI {
4331
4338
  if(value===undefined){
4332
4339
  return [];
4333
4340
  }
4334
- if(!Array.isArray(value)){
4341
+ if(!is.array(value)){
4335
4342
  throw aiStructuralError(
4336
4343
  'AI_CHAT_INVALID_TOOL_CALL',
4337
4344
  'The native Ollama response contains invalid structural tool calls.'
4338
4345
  );
4339
4346
  }
4340
4347
  return value.map(function adaptNativeOllamaToolCall(call,index){
4341
- if(!call||typeof call!=='object'||Array.isArray(call)){
4348
+ if(!call||!is.object(call)||is.array(call)){
4342
4349
  throw aiStructuralError(
4343
4350
  'AI_CHAT_INVALID_TOOL_CALL',
4344
4351
  `The native Ollama structural tool call ${index+1} is invalid.`
@@ -4347,8 +4354,8 @@ class AI {
4347
4354
  const nativeFunction=call.function;
4348
4355
  if(
4349
4356
  !nativeFunction
4350
- ||typeof nativeFunction!=='object'
4351
- ||Array.isArray(nativeFunction)
4357
+ ||!is.object(nativeFunction)
4358
+ ||is.array(nativeFunction)
4352
4359
  ){
4353
4360
  throw aiStructuralError(
4354
4361
  'AI_CHAT_INVALID_TOOL_CALL',
@@ -4358,8 +4365,8 @@ class AI {
4358
4365
  let encodedArguments=nativeFunction.arguments;
4359
4366
  if(
4360
4367
  encodedArguments
4361
- &&typeof encodedArguments==='object'
4362
- &&!Array.isArray(encodedArguments)
4368
+ &&is.object(encodedArguments)
4369
+ &&!is.array(encodedArguments)
4363
4370
  ){
4364
4371
  try{
4365
4372
  encodedArguments=JSON.stringify(encodedArguments);
@@ -4393,7 +4400,7 @@ class AI {
4393
4400
  :{};
4394
4401
  const responseFields={...responseRecord};
4395
4402
  delete responseFields.message;
4396
- const responseId=typeof response?.id==='string'&&response.id
4403
+ const responseId=is.string(response?.id)&&response.id
4397
4404
  ?response.id
4398
4405
  :`ollama-${id}`;
4399
4406
  const adaptedToolCalls=this.#openAICompatibleOllamaToolCalls(
@@ -4405,7 +4412,7 @@ class AI {
4405
4412
  });
4406
4413
  const messageRecord={
4407
4414
  ...message,
4408
- role:typeof message.role==='string'&&message.role
4415
+ role:is.string(message.role)&&message.role
4409
4416
  ?message.role
4410
4417
  :'assistant',
4411
4418
  content:Object.hasOwn(message,'content')?message.content:'',
@@ -4732,8 +4739,8 @@ class AI {
4732
4739
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
4733
4740
  this.#assertServiceConfigured(this.llmService);
4734
4741
  if(signal&&(
4735
- typeof signal.aborted!=='boolean'
4736
- ||typeof signal.addEventListener!=='function'
4742
+ !is.boolean(signal.aborted)
4743
+ ||!is.function(signal.addEventListener)
4737
4744
  )){
4738
4745
  throw new TypeError('AI request signal must be an AbortSignal.');
4739
4746
  }
@@ -4971,7 +4978,7 @@ class AI {
4971
4978
  const toolFunction=toolCall.function||{};
4972
4979
  if(
4973
4980
  toolCall.index!==undefined
4974
- &&(!Number.isSafeInteger(toolCall.index)||toolCall.index<0)
4981
+ &&(!is.safeInteger(toolCall.index)||toolCall.index<0)
4975
4982
  ){
4976
4983
  throw aiStructuralError(
4977
4984
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
@@ -4991,7 +4998,7 @@ class AI {
4991
4998
  };
4992
4999
 
4993
5000
  if(toolCall.id!==undefined){
4994
- if(typeof toolCall.id!=='string'||!toolCall.id){
5001
+ if(!is.string(toolCall.id)||!toolCall.id){
4995
5002
  record.invalidIdentity=true;
4996
5003
  }else if(record.id&&record.id!==toolCall.id){
4997
5004
  record.invalidIdentity=true;
@@ -5001,7 +5008,7 @@ class AI {
5001
5008
  }
5002
5009
 
5003
5010
  if(toolCall.type!==undefined){
5004
- if(typeof toolCall.type!=='string'||!toolCall.type){
5011
+ if(!is.string(toolCall.type)||!toolCall.type){
5005
5012
  record.invalidIdentity=true;
5006
5013
  }else if(record.type&&record.type!==toolCall.type){
5007
5014
  record.invalidIdentity=true;
@@ -5011,14 +5018,14 @@ class AI {
5011
5018
  }
5012
5019
 
5013
5020
  if(toolFunction.name!==undefined){
5014
- if(typeof toolFunction.name!=='string'){
5021
+ if(!is.string(toolFunction.name)){
5015
5022
  record.invalidName=true;
5016
5023
  }else{
5017
5024
  record.name+=toolFunction.name;
5018
5025
  }
5019
5026
  }
5020
5027
 
5021
- if(typeof toolFunction.arguments==='string'){
5028
+ if(is.string(toolFunction.arguments)){
5022
5029
  record.arguments+=toolFunction.arguments;
5023
5030
  }else if(toolFunction.arguments!==undefined){
5024
5031
  record.invalidArguments=true;
@@ -5032,7 +5039,7 @@ class AI {
5032
5039
  function receiveStreamedChoice(choice={},choicePosition=0){
5033
5040
  if(
5034
5041
  choice.index!==undefined
5035
- &&(!Number.isSafeInteger(choice.index)||choice.index<0)
5042
+ &&(!is.safeInteger(choice.index)||choice.index<0)
5036
5043
  ){
5037
5044
  throw aiStructuralError(
5038
5045
  'AI_CHAT_INVALID_RESPONSE',
@@ -5064,8 +5071,8 @@ class AI {
5064
5071
  if(key==='content'||key==='reasoning'||key==='reasoning_content'){
5065
5072
  if(
5066
5073
  !replaceText
5067
- &&typeof value==='string'
5068
- &&typeof record.message[key]==='string'
5074
+ &&is.string(value)
5075
+ &&is.string(record.message[key])
5069
5076
  ){
5070
5077
  record.message[key]+=value;
5071
5078
  }else{
@@ -5076,7 +5083,7 @@ class AI {
5076
5083
  record.message[key]=value;
5077
5084
  }
5078
5085
  }
5079
- if(typeof record.message.role!=='string'||!record.message.role){
5086
+ if(!is.string(record.message.role)||!record.message.role){
5080
5087
  record.message.role='assistant';
5081
5088
  }
5082
5089
  if(!Object.hasOwn(record.message,'content')) record.message.content='';
@@ -5098,7 +5105,7 @@ class AI {
5098
5105
  for(const [key,value] of Object.entries(streamedResponse)){
5099
5106
  if(key!=='choices') streamMetadata[key]=value;
5100
5107
  }
5101
- const choices=Array.isArray(streamedResponse.choices)
5108
+ const choices=is.array(streamedResponse.choices)
5102
5109
  ?streamedResponse.choices
5103
5110
  :[];
5104
5111
  for(let choicePosition=0;choicePosition<choices.length;choicePosition+=1){
@@ -5125,7 +5132,7 @@ class AI {
5125
5132
  const choiceDelta=choice.delta??{};
5126
5133
  const choiceMessage=choice.message??null;
5127
5134
  if(Object.hasOwn(choiceDelta,'tool_calls')){
5128
- if(!Array.isArray(choiceDelta.tool_calls)){
5135
+ if(!is.array(choiceDelta.tool_calls)){
5129
5136
  throw aiStructuralError(
5130
5137
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5131
5138
  `AI stream choice ${choicePosition+1} contains invalid structural tool-call data.`
@@ -5134,7 +5141,7 @@ class AI {
5134
5141
  receiveStreamedToolCalls(choiceDelta.tool_calls,choiceIndex);
5135
5142
  }
5136
5143
  if(choiceMessage&&Object.hasOwn(choiceMessage,'tool_calls')){
5137
- if(!Array.isArray(choiceMessage.tool_calls)){
5144
+ if(!is.array(choiceMessage.tool_calls)){
5138
5145
  throw aiStructuralError(
5139
5146
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
5140
5147
  `AI stream choice ${choicePosition+1} contains invalid terminal structural tool-call data.`
@@ -5148,14 +5155,14 @@ class AI {
5148
5155
  )
5149
5156
  );
5150
5157
  }
5151
- const content=typeof choiceDelta.content==='string'
5158
+ const content=is.string(choiceDelta.content)
5152
5159
  ?choiceDelta.content
5153
5160
  :'';
5154
5161
  let reasoning='';
5155
5162
  if(seeThinking){
5156
- reasoning=typeof choiceDelta.reasoning_content==='string'
5163
+ reasoning=is.string(choiceDelta.reasoning_content)
5157
5164
  ?choiceDelta.reasoning_content
5158
- :typeof choiceDelta.reasoning==='string'
5165
+ :is.string(choiceDelta.reasoning)
5159
5166
  ?choiceDelta.reasoning
5160
5167
  :'';
5161
5168
  }
@@ -5343,7 +5350,7 @@ class AI {
5343
5350
  const hasToolCalls=structuralToolCallsByChoice.has(choicePosition);
5344
5351
  return {
5345
5352
  ...retained.choice,
5346
- index:Number.isSafeInteger(retained.choice.index)
5353
+ index:is.safeInteger(retained.choice.index)
5347
5354
  &&retained.choice.index>=0
5348
5355
  ?retained.choice.index
5349
5356
  :choicePosition,
@@ -5561,8 +5568,8 @@ class AI {
5561
5568
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
5562
5569
  this.#assertServiceConfigured(this.llmService);
5563
5570
  if(signal&&(
5564
- typeof signal.aborted!=='boolean'
5565
- ||typeof signal.addEventListener!=='function'
5571
+ !is.boolean(signal.aborted)
5572
+ ||!is.function(signal.addEventListener)
5566
5573
  )){
5567
5574
  throw new TypeError('AI request signal must be an AbortSignal.');
5568
5575
  }
@@ -5724,12 +5731,288 @@ class AI {
5724
5731
  return result;
5725
5732
  }
5726
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
+
5727
6009
  streamTTS(
5728
6010
  text='',
5729
6011
  end=false,
5730
6012
  options={}
5731
6013
  ){
5732
6014
  const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
6015
+ for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
5733
6016
  if(this.muted){
5734
6017
  if(end){
5735
6018
  this.audioMessageChunks='';
@@ -5741,7 +6024,7 @@ class AI {
5741
6024
 
5742
6025
  const {voice,speed,waitForPlayback=false}=options;
5743
6026
  const pauseAfterMs=Number(options.pauseAfterMs??0);
5744
- if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0){
6027
+ if(!is.finite(pauseAfterMs)||pauseAfterMs<0){
5745
6028
  throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
5746
6029
  }
5747
6030
 
@@ -5800,15 +6083,22 @@ class AI {
5800
6083
  }
5801
6084
 
5802
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) {
5803
6092
  const segments=[];
5804
- let remainder=this.audioMessageChunks;
6093
+ let remainder=text;
5805
6094
 
5806
6095
  while(remainder.length>0){
5807
6096
  const punctuationBoundary=this.#findSpeechPunctuationBoundary(
5808
6097
  remainder,
5809
- end
6098
+ end,
6099
+ segmentation
5810
6100
  );
5811
- const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
6101
+ const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end,segmentation);
5812
6102
  let boundary=punctuationBoundary;
5813
6103
 
5814
6104
  if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
@@ -5831,15 +6121,14 @@ class AI {
5831
6121
  }
5832
6122
  }
5833
6123
 
5834
- this.audioMessageChunks=remainder;
5835
- return segments;
6124
+ return {segments, remainder};
5836
6125
  }
5837
6126
 
5838
- #findSpeechPunctuationBoundary(text,end=false){
5839
- if(this.#ttsSegmentation.punctuation==='none'){
6127
+ #findSpeechPunctuationBoundary(text,end=false,segmentation=this.#ttsSegmentation){
6128
+ if(segmentation.punctuation==='none'){
5840
6129
  return -1;
5841
6130
  }
5842
- if(this.#ttsSegmentation.punctuation==='any'){
6131
+ if(segmentation.punctuation==='any'){
5843
6132
  return this.#findAnySpeechPunctuationBoundary(text,end);
5844
6133
  }
5845
6134
 
@@ -5884,8 +6173,8 @@ class AI {
5884
6173
  return -1;
5885
6174
  }
5886
6175
 
5887
- #findSpeechCadenceBoundary(text,end=false){
5888
- const wordCadence=this.#ttsSegmentation.wordCadence;
6176
+ #findSpeechCadenceBoundary(text,end=false,segmentation=this.#ttsSegmentation){
6177
+ const wordCadence=segmentation.wordCadence;
5889
6178
  if(wordCadence===null){
5890
6179
  return -1;
5891
6180
  }
@@ -6041,7 +6330,7 @@ class AI {
6041
6330
  entry=>entry.providerId===selection.providerId
6042
6331
  );
6043
6332
  const model=provider?.models.find(entry=>entry?.id===selection.modelId);
6044
- return typeof model?.defaultVoice==='string'&&model.defaultVoice.trim()
6333
+ return is.string(model?.defaultVoice)&&model.defaultVoice.trim()
6045
6334
  ?model.defaultVoice.trim()
6046
6335
  :null;
6047
6336
  }
@@ -6059,7 +6348,7 @@ class AI {
6059
6348
  if(speech===undefined){
6060
6349
  return this.audioFormat;
6061
6350
  }
6062
- const prototype=speech&&typeof speech==='object'
6351
+ const prototype=speech&&is.object(speech)
6063
6352
  ?Object.getPrototypeOf(speech)
6064
6353
  :null;
6065
6354
  const descriptors=prototype===Object.prototype||prototype===null
@@ -6067,10 +6356,10 @@ class AI {
6067
6356
  :null;
6068
6357
  const formats=descriptors?.responseFormats?.value;
6069
6358
  const defaultFormat=descriptors?.defaultResponseFormat?.value;
6070
- if(!Array.isArray(formats)
6359
+ if(!is.array(formats)
6071
6360
  ||formats.length<1
6072
- ||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
6073
- ||typeof defaultFormat!=='string'
6361
+ ||!formats.every(format=>is.string(format)&&format.trim()===format&&format)
6362
+ ||!is.string(defaultFormat)
6074
6363
  ||!formats.includes(defaultFormat)){
6075
6364
  throw aiProviderError(
6076
6365
  'The selected TTS provider returned an invalid speech format catalog.',
@@ -6097,7 +6386,7 @@ class AI {
6097
6386
  };
6098
6387
  }
6099
6388
 
6100
- if(response instanceof ArrayBuffer||ArrayBuffer.isView(response)){
6389
+ if(response instanceof ArrayBuffer||is.arrayBufferView(response)){
6101
6390
  const bytes=response instanceof ArrayBuffer
6102
6391
  ?new Uint8Array(response)
6103
6392
  :new Uint8Array(
@@ -6111,11 +6400,11 @@ class AI {
6111
6400
  };
6112
6401
  }
6113
6402
 
6114
- if(response&&typeof response==='object'){
6115
- if(typeof response.audioBase64==='string'){
6403
+ if(response&&is.object(response)){
6404
+ if(is.string(response.audioBase64)){
6116
6405
  return {
6117
6406
  chunks:[this.#base64ToBytes(response.audioBase64)],
6118
- type:typeof response.contentType==='string'
6407
+ type:is.string(response.contentType)
6119
6408
  ?response.contentType
6120
6409
  :this.audioType
6121
6410
  };
@@ -6129,7 +6418,7 @@ class AI {
6129
6418
  };
6130
6419
  }
6131
6420
  if(response.audio instanceof ArrayBuffer
6132
- ||ArrayBuffer.isView(response.audio)){
6421
+ ||is.arrayBufferView(response.audio)){
6133
6422
  const bytes=response.audio instanceof ArrayBuffer
6134
6423
  ?new Uint8Array(response.audio)
6135
6424
  :new Uint8Array(
@@ -6139,7 +6428,7 @@ class AI {
6139
6428
  );
6140
6429
  return {
6141
6430
  chunks:[bytes],
6142
- type:typeof response.contentType==='string'
6431
+ type:is.string(response.contentType)
6143
6432
  ?response.contentType
6144
6433
  :this.audioType
6145
6434
  };
@@ -6168,7 +6457,7 @@ class AI {
6168
6457
 
6169
6458
  const AudioContext=window.AudioContext||window.webkitAudioContext;
6170
6459
 
6171
- if(typeof AudioContext!=='function'){
6460
+ if(!is.function(AudioContext)){
6172
6461
  throw new TypeError('Audio playback is unavailable in this browser.');
6173
6462
  }
6174
6463
 
@@ -6181,8 +6470,8 @@ class AI {
6181
6470
  try{
6182
6471
  this.#assertServiceConfigured(this.ttsService,'tts');
6183
6472
  if(!payload
6184
- ||typeof payload!=='object'
6185
- ||Array.isArray(payload)
6473
+ ||!is.object(payload)
6474
+ ||is.array(payload)
6186
6475
  ||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
6187
6476
  const error=new TypeError('AI.fetchTTS requires a speech request object.');
6188
6477
  error.code='ARCANE_AI_TTS_REQUEST_INVALID';
@@ -6197,7 +6486,7 @@ class AI {
6197
6486
  'speed'
6198
6487
  ]);
6199
6488
  for(const key of Reflect.ownKeys(descriptors)){
6200
- if(typeof key==='symbol'
6489
+ if(is.symbol(key)
6201
6490
  ||!acceptedKeys.has(key)
6202
6491
  ||!Object.hasOwn(descriptors[key],'value')){
6203
6492
  const error=new TypeError(
@@ -6208,9 +6497,9 @@ class AI {
6208
6497
  }
6209
6498
  }
6210
6499
  if(signal&&(
6211
- typeof signal.aborted!=='boolean'
6212
- ||typeof signal.addEventListener!=='function'
6213
- ||typeof signal.removeEventListener!=='function'
6500
+ !is.boolean(signal.aborted)
6501
+ ||!is.function(signal.addEventListener)
6502
+ ||!is.function(signal.removeEventListener)
6214
6503
  )){
6215
6504
  const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
6216
6505
  error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
@@ -6221,7 +6510,7 @@ class AI {
6221
6510
  }
6222
6511
 
6223
6512
  const suppliedInput=descriptors.input?.value;
6224
- if(typeof suppliedInput!=='string'||!suppliedInput.trim()){
6513
+ if(!is.string(suppliedInput)||!suppliedInput.trim()){
6225
6514
  const error=new TypeError('AI.fetchTTS input must be nonempty text.');
6226
6515
  error.code='ARCANE_AI_TTS_INPUT_INVALID';
6227
6516
  throw error;
@@ -6232,7 +6521,7 @@ class AI {
6232
6521
  const selection=this.#providerRuntime.selection('tts');
6233
6522
  const requestedModel=descriptors.model?.value;
6234
6523
  if(requestedModel!==undefined
6235
- &&(typeof requestedModel!=='string'
6524
+ &&(!is.string(requestedModel)
6236
6525
  ||requestedModel.trim()!==requestedModel
6237
6526
  ||!requestedModel)){
6238
6527
  const error=new TypeError(
@@ -6259,7 +6548,7 @@ class AI {
6259
6548
  }
6260
6549
  const requestedVoice=descriptors.voice?.value;
6261
6550
  if(requestedVoice!==undefined
6262
- &&(typeof requestedVoice!=='string'
6551
+ &&(!is.string(requestedVoice)
6263
6552
  ||requestedVoice.trim()!==requestedVoice
6264
6553
  ||!requestedVoice)){
6265
6554
  const error=new TypeError(
@@ -6282,7 +6571,7 @@ class AI {
6282
6571
  }
6283
6572
  const requestedResponseFormat=descriptors.responseFormat?.value;
6284
6573
  if(requestedResponseFormat!==undefined
6285
- &&(typeof requestedResponseFormat!=='string'
6574
+ &&(!is.string(requestedResponseFormat)
6286
6575
  ||requestedResponseFormat.trim()!==requestedResponseFormat
6287
6576
  ||!requestedResponseFormat)){
6288
6577
  const error=new TypeError(
@@ -6302,7 +6591,7 @@ class AI {
6302
6591
  const speed=descriptors.speed
6303
6592
  ?Number(descriptors.speed.value)
6304
6593
  :this.voiceSpeed;
6305
- if(!Number.isFinite(speed)||speed<=0){
6594
+ if(!is.finite(speed)||speed<=0){
6306
6595
  const error=new RangeError('AI.fetchTTS speed must be a positive number.');
6307
6596
  error.code='ARCANE_AI_TTS_SPEED_INVALID';
6308
6597
  throw error;
@@ -6350,9 +6639,9 @@ class AI {
6350
6639
  try{
6351
6640
  this.#assertServiceConfigured(this.sttService,'stt');
6352
6641
  if(signal&&(
6353
- typeof signal.aborted!=='boolean'
6354
- ||typeof signal.addEventListener!=='function'
6355
- ||typeof signal.removeEventListener!=='function'
6642
+ !is.boolean(signal.aborted)
6643
+ ||!is.function(signal.addEventListener)
6644
+ ||!is.function(signal.removeEventListener)
6356
6645
  )){
6357
6646
  const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
6358
6647
  error.code='ARCANE_AI_STT_SIGNAL_INVALID';
@@ -6369,7 +6658,7 @@ class AI {
6369
6658
  operation:'transcribe',
6370
6659
  payload:{
6371
6660
  audio:audioFile,
6372
- mimeType:typeof Blob==='function'
6661
+ mimeType:is.function(globalThis.Blob)
6373
6662
  &&audioFile instanceof Blob
6374
6663
  ?String(audioFile.type||'audio/webm')
6375
6664
  :'audio/webm',
@@ -6380,10 +6669,10 @@ class AI {
6380
6669
  }
6381
6670
  );
6382
6671
  this.#traceSpeech('fetchSTT.providerResult',{callId,response});
6383
- const text=typeof response==='string'
6672
+ const text=is.string(response)
6384
6673
  ?response
6385
6674
  :response?.text;
6386
- if(typeof text!=='string'){
6675
+ if(!is.string(text)){
6387
6676
  const error=new TypeError(
6388
6677
  'Arcane returned an invalid provider speech transcription.'
6389
6678
  );
@@ -6416,6 +6705,7 @@ class AI {
6416
6705
  const callId=this.#traceSpeech('stopAudio.call',{
6417
6706
  remainder:this.audioMessageChunks,queuedJobs:this.speechJobs.length
6418
6707
  });
6708
+ for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
6419
6709
  this.speechGeneration+=1;
6420
6710
  this.speechResumeAttempt+=1;
6421
6711
  this.speechResumePending=false;
@@ -6475,9 +6765,14 @@ class AI {
6475
6765
 
6476
6766
  let attempt=0;
6477
6767
  let context;
6768
+ let preparedGroup = null;
6478
6769
 
6479
6770
  try{
6480
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;
6481
6776
  this.#traceSpeech('resumeAudio.context',{
6482
6777
  callId,state:context.state,audioTime:context.currentTime,
6483
6778
  sampleRate:context.sampleRate
@@ -6490,7 +6785,7 @@ class AI {
6490
6785
  return true;
6491
6786
  }
6492
6787
 
6493
- if(typeof context.resume!=='function'){
6788
+ if(!is.function(context.resume)){
6494
6789
  this.#waitForSpeechGesture(null,context);
6495
6790
  this.#traceSpeech('resumeAudio.result',{
6496
6791
  callId,result:false,reason:'resume-unavailable'
@@ -6501,6 +6796,12 @@ class AI {
6501
6796
  attempt=++this.speechResumeAttempt;
6502
6797
  this.speechResumePending=true;
6503
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
+ }
6504
6805
  this.#traceSpeech('resumeAudio.resumed',{
6505
6806
  callId,state:context.state,audioTime:context.currentTime
6506
6807
  });
@@ -6522,6 +6823,7 @@ class AI {
6522
6823
  }
6523
6824
  }catch(error){
6524
6825
  this.#traceSpeech('resumeAudio.error',{callId,error,state:context?.state});
6826
+ if(preparedGroup?.stopped || preparedGroup?.paused) return false;
6525
6827
  if(attempt&&attempt!==this.speechResumeAttempt){
6526
6828
  return context?.state==='running';
6527
6829
  }
@@ -6529,6 +6831,10 @@ class AI {
6529
6831
  if(attempt===this.speechResumeAttempt){
6530
6832
  this.speechResumePending=false;
6531
6833
  }
6834
+ if(preparedGroup && error?.name !== 'NotAllowedError') {
6835
+ for(const job of preparedGroup.jobs) this.#failSpeechJob(job, error, 'playback-resume');
6836
+ return false;
6837
+ }
6532
6838
  if(context?.state==='closed'){
6533
6839
  this.#clearSpeechUnlock();
6534
6840
  const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
@@ -6589,7 +6895,8 @@ class AI {
6589
6895
  this.speechJobs.push(job);
6590
6896
  }
6591
6897
 
6592
- if(this.muted||job.generation!==this.speechGeneration){
6898
+ if(this.muted||job.generation!==this.speechGeneration
6899
+ ||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
6593
6900
  return this.#cancelSpeechJob(job);
6594
6901
  }
6595
6902
 
@@ -6606,7 +6913,8 @@ class AI {
6606
6913
  channels:audioBuffer.numberOfChannels,audioBuffer
6607
6914
  });
6608
6915
 
6609
- if(this.muted||job.generation!==this.speechGeneration){
6916
+ if(this.muted||job.generation!==this.speechGeneration
6917
+ ||job.preparedPlayback?.stopped||['cancelled','failed'].includes(job.state)){
6610
6918
  return this.#cancelSpeechJob(job);
6611
6919
  }
6612
6920
 
@@ -6684,6 +6992,8 @@ class AI {
6684
6992
 
6685
6993
  const audioContext=job.sourceNode.context||job.audioContext;
6686
6994
 
6995
+ if(job.preparedPlayback?.paused) break;
6996
+
6687
6997
  if(audioContext.state!=='running'){
6688
6998
  this.#waitForSpeechGesture(null,audioContext);
6689
6999
 
@@ -6712,7 +7022,7 @@ class AI {
6712
7022
 
6713
7023
  try{
6714
7024
  const duration=Number(job.audioBuffer?.duration);
6715
- const hasKnownDuration=Number.isFinite(duration)&&duration>0;
7025
+ const hasKnownDuration=is.finite(duration)&&duration>0;
6716
7026
  const currentTime=Number(audioContext.currentTime)||0;
6717
7027
  const previousContext=this.speechScheduleContext;
6718
7028
  const remainingDelay=previousContext
@@ -6739,6 +7049,9 @@ class AI {
6739
7049
  }
6740
7050
  this.isSpeaking=true;
6741
7051
  job.sourceNode.start(scheduledStart);
7052
+ if(job.preparedPlayback && !job.preparedPlayback.error) {
7053
+ job.preparedPlayback.publish('scheduled');
7054
+ }
6742
7055
  this.#traceSpeechJob('playback.scheduled',job,{
6743
7056
  audioEnd:hasKnownDuration?scheduledStart+duration:null,
6744
7057
  previousScheduledEnd:this.speechScheduleTime,
@@ -6773,6 +7086,9 @@ class AI {
6773
7086
  !this.muted
6774
7087
  &&!this.speechAwaitingGesture
6775
7088
  &&!this.speechResumePending
7089
+ &&!this.speechJobs.some(function hasPausedPreparedPlayback(job) {
7090
+ return job.preparedPlayback?.paused;
7091
+ })
6776
7092
  &&!this.speechJobs.some(
6777
7093
  function hasScheduledSpeechWithoutDuration(job){
6778
7094
  return job.state==='scheduled'
@@ -6877,7 +7193,7 @@ class AI {
6877
7193
  operationId,
6878
7194
  publicDetail:completeValue({
6879
7195
  boundary:normalizedBoundary,
6880
- ...(typeof error?.code==='string'?{code:error.code}:{}),
7196
+ ...(is.string(error?.code)?{code:error.code}:{}),
6881
7197
  generation,
6882
7198
  reason
6883
7199
  })
@@ -6900,6 +7216,7 @@ class AI {
6900
7216
  }
6901
7217
 
6902
7218
  job.state='failed';
7219
+ job.preparedPlayback?.fail(error);
6903
7220
  this.#traceSpeechJob('queue.failed',job,{boundary,error});
6904
7221
 
6905
7222
  this.#publishTTSFailure(error,{
@@ -6960,7 +7277,15 @@ class AI {
6960
7277
  const target=window;
6961
7278
 
6962
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
+ }
6963
7287
  this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
7288
+ if(preparedJob?.preparedPlayback.paused || preparedJob?.preparedPlayback.stopped) return false;
6964
7289
  runtime.#clearSpeechUnlock();
6965
7290
  return runtime.resumeAudio(audioContext);
6966
7291
  };
@@ -6998,6 +7323,7 @@ class AI {
6998
7323
  }
6999
7324
 
7000
7325
  this.speechUnlockHandler=null;
7326
+ this.#speechUnlockContext=null;
7001
7327
  this.speechAwaitingGesture=false;
7002
7328
  }
7003
7329
  }
@@ -7019,7 +7345,7 @@ function installAIUserReadyRegistration(){
7019
7345
  ?existingDescriptor.value
7020
7346
  :null;
7021
7347
  if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
7022
- ||typeof existing.dispose!=='function'){
7348
+ ||!is.function(existing.dispose)){
7023
7349
  throw aiInitializationError(
7024
7350
  AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
7025
7351
  AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,