arcane-os 0.5.8 → 0.5.9

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +32 -27
  3. package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
  4. package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
  5. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
  6. package/browser-runtime/ai/speech-worker-client.mjs +51 -14
  7. package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
  8. package/browser-runtime/dependencies/strong-type/package.json +22 -23
  9. package/browser-runtime/event-manager.mjs +27 -41
  10. package/package.json +2 -3
  11. package/runtime/arcane/components/chat.html +1 -1
  12. package/runtime/arcane/components/speech.html +8 -38
  13. package/runtime/arcane/components/voice-transcription.html +1 -1
  14. package/runtime/arcane/css/theme.css +1 -1
  15. package/runtime/arcane/entities/Chat.js +12 -7
  16. package/runtime/arcane/modules/AI.js +506 -391
  17. package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
  18. package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
  19. package/runtime/arcane/modules/AIRuntimeState.js +2 -18
  20. package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
  21. package/runtime/arcane/modules/CalculatorEngine.js +0 -4
  22. package/runtime/arcane/modules/ChatRecords.js +169 -1
  23. package/runtime/arcane/modules/CommunicationHub.js +0 -19
  24. package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
  25. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
  26. package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
  27. package/runtime/arcane/modules/Errors.js +6 -19
  28. package/runtime/arcane/modules/Mail.js +1 -2
  29. package/runtime/arcane/modules/MailTransport.mjs +1 -3
  30. package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
  31. package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
  32. package/runtime/arcane/modules/ScreenCapture.js +2 -4
  33. package/runtime/arcane/modules/SpeechPlayback.js +0 -27
  34. package/runtime/arcane/modules/WaitForComponent.js +0 -1
  35. package/src/app-descriptor.mjs +1 -1
  36. package/src/doctor.mjs +1 -3
  37. package/src/event-manager.mjs +27 -41
  38. package/src/import-map.mjs +1 -1
  39. package/src/index.mjs +2 -9
  40. package/src/installed-sdk-runtime.mjs +1 -11
  41. package/src/mail-api.mjs +0 -1
  42. package/src/native-provider-loader.mjs +0 -9
  43. package/src/scaffold.mjs +9 -21
  44. package/src/toolchain.mjs +5 -28
  45. package/src/workspace-runtime.mjs +0 -4
  46. package/src/workspace.mjs +2 -23
@@ -16,11 +16,17 @@ import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
16
16
  const completeValue=(value)=>value;
17
17
 
18
18
  let credentials='include';
19
- const LEGACY_TTS_RESPONSE_FORMAT='opus';
19
+ const DEFAULT_TTS_RESPONSE_FORMAT='opus';
20
20
  const DEFAULT_TTS_SEGMENTATION={
21
21
  punctuation:'sentence',
22
22
  wordCadence:null
23
23
  };
24
+ const DEFAULT_BROWSER_TTS_EXECUTION={
25
+ device:'auto',
26
+ maxConcurrentRequests:2
27
+ };
28
+ const BROWSER_TTS_EXECUTION_DEVICES=new Set(['auto','webgpu','wasm']);
29
+ const MAX_BROWSER_TTS_CONCURRENT_REQUESTS=4;
24
30
  const TTS_PUNCTUATION_MODES=new Set(['sentence','any','none']);
25
31
  // A complete punctuation run is a boundary unless the whole run consists of
26
32
  // apostrophe, comma, or dash punctuation joining Unicode letters or numbers.
@@ -29,7 +35,7 @@ const TTS_ANY_PUNCTUATION=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+(?!\p{P})(?=[\s\S
29
35
  const TTS_ANY_PUNCTUATION_AT_END=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+|\p{P}+(?![\p{L}\p{N}])|\p{P}*[^\P{P}'\u2019\uFF07,\u060C\u3001\uFF0C\p{Pd}]\p{P}*)(?!\p{P})/gu;
30
36
  credentials='omit';
31
37
 
32
- const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
38
+ const BUILT_IN_AI_SERVICES=new Set(['TWIN','OLLAMA','LOCAL_SPEACH']);
33
39
  const AI_REASONING_EFFORTS=new Set(['none','low','medium','high','max']);
34
40
  export const AI_READY_EVENT='ai-ready';
35
41
  const AI_TTS_FAILURE_EVENT='ai-tts-failure';
@@ -143,7 +149,7 @@ function normalizeAIReasoningEffort(value){
143
149
  return value;
144
150
  }
145
151
 
146
- function legacyAIProviderError(message,code,cause){
152
+ function aiProviderError(message,code,cause){
147
153
  const error=cause===undefined
148
154
  ?new Error(message)
149
155
  :new Error(message,{cause});
@@ -535,7 +541,7 @@ function projectAIStreamChunk(value){
535
541
  return projectAIStreamData(value);
536
542
  }
537
543
 
538
- function createLegacyAIStreamBridge(execute,sourceSignal){
544
+ function createBuiltInAIStreamBridge(execute,sourceSignal){
539
545
  const controller=new AbortController();
540
546
  const queue=[];
541
547
  const waiters=[];
@@ -543,34 +549,34 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
543
549
  let failure=null;
544
550
  let detached=false;
545
551
 
546
- function forwardLegacyAIStreamAbort(){
552
+ function forwardBuiltInAIStreamAbort(){
547
553
  if(!controller.signal.aborted){
548
554
  controller.abort(sourceSignal?.reason);
549
555
  }
550
556
  }
551
557
 
552
- function detachLegacyAIStreamAbort(){
558
+ function detachBuiltInAIStreamAbort(){
553
559
  if(detached){
554
560
  return;
555
561
  }
556
562
  detached=true;
557
563
  sourceSignal?.removeEventListener?.(
558
564
  'abort',
559
- forwardLegacyAIStreamAbort
565
+ forwardBuiltInAIStreamAbort
560
566
  );
561
567
  }
562
568
 
563
569
  if(sourceSignal?.aborted){
564
- forwardLegacyAIStreamAbort();
570
+ forwardBuiltInAIStreamAbort();
565
571
  }else{
566
572
  sourceSignal?.addEventListener?.(
567
573
  'abort',
568
- forwardLegacyAIStreamAbort,
574
+ forwardBuiltInAIStreamAbort,
569
575
  {once:true}
570
576
  );
571
577
  }
572
578
 
573
- function emitLegacyAIStreamChunk(chunk){
579
+ function emitBuiltInAIStreamChunk(chunk){
574
580
  if(complete){
575
581
  return false;
576
582
  }
@@ -583,13 +589,13 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
583
589
  return true;
584
590
  }
585
591
 
586
- function finishLegacyAIStream(error){
592
+ function finishBuiltInAIStream(error){
587
593
  if(complete){
588
594
  return;
589
595
  }
590
596
  complete=true;
591
597
  failure=error||null;
592
- detachLegacyAIStreamAbort();
598
+ detachBuiltInAIStreamAbort();
593
599
  while(waiters.length){
594
600
  const waiter=waiters.shift();
595
601
  if(failure){
@@ -601,42 +607,42 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
601
607
  }
602
608
 
603
609
  const result=Promise.resolve().then(
604
- function executeLegacyAIStream(){
610
+ function executeBuiltInAIStream(){
605
611
  if(controller.signal.aborted){
606
612
  throw normalizeAIRequestAbort(controller.signal.reason);
607
613
  }
608
614
  return execute({
609
- emit:emitLegacyAIStreamChunk,
615
+ emit:emitBuiltInAIStreamChunk,
610
616
  signal:controller.signal
611
617
  });
612
618
  }
613
619
  ).then(
614
- function acceptLegacyAIStreamResult(value){
615
- finishLegacyAIStream(null);
620
+ function acceptBuiltInAIStreamResult(value){
621
+ finishBuiltInAIStream(null);
616
622
  return value;
617
623
  },
618
- function rejectLegacyAIStreamResult(error){
624
+ function rejectBuiltInAIStreamResult(error){
619
625
  const normalized=isAIRequestAbort(error,controller.signal)
620
626
  ?normalizeAIRequestAbort(error)
621
627
  :error;
622
- finishLegacyAIStream(normalized);
628
+ finishBuiltInAIStream(normalized);
623
629
  throw normalized;
624
630
  }
625
631
  );
626
- result.catch(function retainLegacyAIStreamFailure() {});
632
+ result.catch(function retainBuiltInAIStreamFailure() {});
627
633
 
628
- async function cancelLegacyAIStream(reason){
634
+ async function cancelBuiltInAIStream(reason){
629
635
  if(!controller.signal.aborted){
630
636
  controller.abort(reason);
631
637
  }
632
- await result.catch(function retainCancelledLegacyAIStream() {});
638
+ await result.catch(function retainCancelledBuiltInAIStream() {});
633
639
  return true;
634
640
  }
635
641
 
636
642
  const handle={
637
643
  result,
638
- cancel:cancelLegacyAIStream,
639
- next:function readLegacyAIStreamChunk(){
644
+ cancel:cancelBuiltInAIStream,
645
+ next:function readBuiltInAIStreamChunk(){
640
646
  if(queue.length){
641
647
  return Promise.resolve({value:queue.shift(),done:false});
642
648
  }
@@ -645,24 +651,24 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
645
651
  ?Promise.reject(failure)
646
652
  :Promise.resolve({value:undefined,done:true});
647
653
  }
648
- return new Promise(function waitForLegacyAIStreamChunk(resolve,reject){
654
+ return new Promise(function waitForBuiltInAIStreamChunk(resolve,reject){
649
655
  waiters.push({resolve,reject});
650
656
  });
651
657
  },
652
- return:async function returnLegacyAIStream(value){
653
- await cancelLegacyAIStream(
654
- legacyAIProviderError(
655
- 'The legacy AI stream consumer stopped before completion.',
658
+ return:async function returnBuiltInAIStream(value){
659
+ await cancelBuiltInAIStream(
660
+ aiProviderError(
661
+ 'The built-in AI stream consumer stopped before completion.',
656
662
  'ARCANE_AI_REQUEST_ABORTED'
657
663
  )
658
664
  );
659
665
  return {value,done:true};
660
666
  },
661
- throw:async function throwLegacyAIStream(error){
662
- await cancelLegacyAIStream(error);
667
+ throw:async function throwBuiltInAIStream(error){
668
+ await cancelBuiltInAIStream(error);
663
669
  throw error;
664
670
  },
665
- [Symbol.asyncIterator]:function iterateLegacyAIStream(){
671
+ [Symbol.asyncIterator]:function iterateBuiltInAIStream(){
666
672
  return this;
667
673
  }
668
674
  };
@@ -803,11 +809,46 @@ function browserSpeechIdentifier(value,label){
803
809
  return value;
804
810
  }
805
811
 
812
+ function normalizeBrowserTTSExecution(value){
813
+ if(value===undefined){
814
+ return completeValue({...DEFAULT_BROWSER_TTS_EXECUTION});
815
+ }
816
+ const descriptors=closedRecord(
817
+ value,
818
+ ['device','maxConcurrentRequests'],
819
+ [],
820
+ 'AI browser speech tts.execution'
821
+ );
822
+ const device=descriptors.device
823
+ ?descriptors.device.value
824
+ :DEFAULT_BROWSER_TTS_EXECUTION.device;
825
+ const maxConcurrentRequests=descriptors.maxConcurrentRequests
826
+ ?descriptors.maxConcurrentRequests.value
827
+ :DEFAULT_BROWSER_TTS_EXECUTION.maxConcurrentRequests;
828
+ if(typeof device!=='string'||!BROWSER_TTS_EXECUTION_DEVICES.has(device)){
829
+ throw aiBrowserSpeechError(
830
+ AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
831
+ AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
832
+ 'AI browser speech tts.execution.device must be auto, webgpu, or wasm.'
833
+ );
834
+ }
835
+ if(!Number.isSafeInteger(maxConcurrentRequests)
836
+ ||maxConcurrentRequests<1
837
+ ||maxConcurrentRequests>MAX_BROWSER_TTS_CONCURRENT_REQUESTS){
838
+ throw aiBrowserSpeechError(
839
+ AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
840
+ AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
841
+ `AI browser speech tts.execution.maxConcurrentRequests must be an integer from 1 through ${MAX_BROWSER_TTS_CONCURRENT_REQUESTS}.`
842
+ );
843
+ }
844
+ return completeValue({device,maxConcurrentRequests});
845
+ }
846
+
806
847
  function normalizeBrowserSpeechRole(value,role){
807
848
  const label=`AI browser speech ${role}`;
808
849
  const descriptors=closedRecord(
809
850
  value,
810
- ['providerId','graph','model','runtime','security','offline'],
851
+ ['providerId','graph','model','runtime','security','offline','execution'],
811
852
  ['providerId','offline'],
812
853
  label
813
854
  );
@@ -868,6 +909,13 @@ function normalizeBrowserSpeechRole(value,role){
868
909
  `${label}.offline must be a boolean.`
869
910
  );
870
911
  }
912
+ if(role==='stt'&&descriptors.execution){
913
+ throw aiBrowserSpeechError(
914
+ AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
915
+ AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
916
+ 'AI browser speech execution policy is available only for TTS.'
917
+ );
918
+ }
871
919
  return completeValue({
872
920
  providerId,
873
921
  ...(hasGraph
@@ -880,7 +928,12 @@ function normalizeBrowserSpeechRole(value,role){
880
928
  runtime:descriptors.runtime.value,
881
929
  ...(secure?{security:{secure:true}}:{})
882
930
  }),
883
- offline:descriptors.offline.value
931
+ offline:descriptors.offline.value,
932
+ ...(role==='tts'
933
+ ?{execution:normalizeBrowserTTSExecution(
934
+ descriptors.execution?.value
935
+ )}
936
+ :{})
884
937
  });
885
938
  }
886
939
 
@@ -974,9 +1027,8 @@ class AI {
974
1027
  // This is the enum section for inference configuration
975
1028
  #service = {
976
1029
  baseURL: {
977
- // OPENAI remains the legacy route identifier for compatibility;
978
- // remote LLM chat is provided by TWiN Cloud.
979
- OPENAI: 'https://inference.do-ai.run/v1'
1030
+ // TWiN Cloud owns this remote LLM route.
1031
+ TWIN: 'https://inference.do-ai.run/v1'
980
1032
  },
981
1033
  sttURL: {
982
1034
  LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
@@ -988,7 +1040,7 @@ class AI {
988
1040
 
989
1041
  #paths = {
990
1042
  chat: {
991
- OPENAI: '/chat/completions'
1043
+ TWIN: '/chat/completions'
992
1044
  },
993
1045
  stt: {
994
1046
  LOCAL_SPEACH: '/audio/transcriptions'
@@ -999,7 +1051,7 @@ class AI {
999
1051
  }
1000
1052
 
1001
1053
  #models = {
1002
- OPENAI:'openai-gpt-oss-120b'
1054
+ TWIN:'openai-gpt-oss-120b'
1003
1055
  }
1004
1056
 
1005
1057
  #sttModels = {
@@ -1018,7 +1070,7 @@ class AI {
1018
1070
  // Note: if we expand cloud providers, simply add their expected JSON metadata here
1019
1071
  get #serviceHeaders(){
1020
1072
  return {
1021
- OPENAI: {
1073
+ TWIN: {
1022
1074
  'Content-Type': 'application/json',
1023
1075
  'Authorization': `Bearer ${this.twinKey}`
1024
1076
  }
@@ -1086,10 +1138,10 @@ class AI {
1086
1138
  );
1087
1139
 
1088
1140
  const preferences=[
1089
- llmService||'OPENAI',
1141
+ llmService||'TWIN',
1090
1142
  sttService||'LOCAL_SPEACH',
1091
1143
  ttsService||'LOCAL_SPEACH',
1092
- model||'OPENAI',
1144
+ model||'TWIN',
1093
1145
  modelTTS||'LOCAL_SPEACH',
1094
1146
  modelSTT||'LOCAL_SPEACH'
1095
1147
  ];
@@ -1100,9 +1152,9 @@ class AI {
1100
1152
  const runtime=this;
1101
1153
  this.#stopOllamaReady=arcaneEvents.subscribe(
1102
1154
  'arcane-ollama-ready',
1103
- function reconcileLegacyOllamaReadiness(){
1104
- runtime.#retainLegacyLLMReadiness(
1105
- runtime.#reconcileLegacyLLMReadiness()
1155
+ function reconcileBuiltInOllamaReadiness(){
1156
+ runtime.#retainBuiltInLLMReadiness(
1157
+ runtime.#reconcileBuiltInLLMReadiness()
1106
1158
  );
1107
1159
  }
1108
1160
  );
@@ -1117,21 +1169,21 @@ class AI {
1117
1169
  #browserSpeechModulePromise=null;
1118
1170
  #browserSpeechOperationSequence=0;
1119
1171
  #browserSpeechRetiredRecords=new Set();
1120
- #browserSpeechRetiredLegacyRecords=new Set();
1172
+ #browserSpeechRetiredBuiltInRecords=new Set();
1121
1173
  #browserSpeechTransition=Promise.resolve();
1122
- #legacyLLMProviders=new Map();
1123
- #legacyLLMReadiness=Promise.resolve(null);
1124
- #legacySpeechProviders=new Map();
1125
- #legacySpeechReadiness=Promise.resolve(null);
1174
+ #builtInLLMProviders=new Map();
1175
+ #builtInLLMReadiness=Promise.resolve(null);
1176
+ #builtInSpeechProviders=new Map();
1177
+ #builtInSpeechReadiness=Promise.resolve(null);
1126
1178
  #speechControlGeneration=0;
1127
1179
  #speechFailureSequence=0;
1128
1180
  #stopOllamaReady=null;
1129
1181
  #ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
1130
1182
  #preferenceTuple=completeValue([
1131
- 'OPENAI',
1183
+ 'TWIN',
1132
1184
  'LOCAL_SPEACH',
1133
1185
  'LOCAL_SPEACH',
1134
- 'OPENAI',
1186
+ 'TWIN',
1135
1187
  'LOCAL_SPEACH',
1136
1188
  'LOCAL_SPEACH'
1137
1189
  ]);
@@ -1178,26 +1230,14 @@ class AI {
1178
1230
  return `${this.#service.baseURL[this.llmService]}${this.#paths.chat[this.llmService]}`
1179
1231
  }
1180
1232
 
1181
- set url(value) {
1182
- return false;
1183
- }
1184
-
1185
1233
  get urlTTS() {
1186
1234
  return `${this.#service.ttsURL[this.ttsService]}${this.#paths.tts[this.ttsService]}`
1187
1235
  }
1188
1236
 
1189
- set urlTTS(value) {
1190
- return false;
1191
- }
1192
-
1193
1237
  get urlSTT() {
1194
1238
  return `${this.#service.sttURL[this.sttService]}${this.#paths.stt[this.sttService]}`
1195
1239
  }
1196
1240
 
1197
- set urlSTT(value) {
1198
- return false;
1199
- }
1200
-
1201
1241
  #license='';
1202
1242
 
1203
1243
  // Browser-delivered framework code must not contain provider credentials.
@@ -1210,8 +1250,8 @@ class AI {
1210
1250
 
1211
1251
  set twinKey(value){
1212
1252
  this.#license=typeof value==='string' ? value.trim():'';
1213
- this.#retainLegacyLLMReadiness(
1214
- this.#reconcileLegacyLLMReadiness()
1253
+ this.#retainBuiltInLLMReadiness(
1254
+ this.#reconcileBuiltInLLMReadiness()
1215
1255
  );
1216
1256
  return this.#license;
1217
1257
  }
@@ -1227,9 +1267,9 @@ class AI {
1227
1267
  return this.#license;
1228
1268
  }
1229
1269
 
1230
- #legacyLLMCapability(providerId){
1231
- if(providerId==='OPENAI'){
1232
- return this.llmService==='OPENAI'
1270
+ #builtInLLMCapability(providerId){
1271
+ if(providerId==='TWIN'){
1272
+ return this.llmService==='TWIN'
1233
1273
  &&Boolean(this.model)
1234
1274
  &&Boolean(this.license)
1235
1275
  &&typeof globalThis.fetch==='function';
@@ -1242,7 +1282,7 @@ class AI {
1242
1282
  return false;
1243
1283
  }
1244
1284
 
1245
- #legacyLLMInspection(providerId,selection){
1285
+ #builtInLLMInspection(providerId,selection){
1246
1286
  const localOnly=providerId==='OLLAMA';
1247
1287
  if(!selection
1248
1288
  ||selection.providerId!==providerId
@@ -1252,10 +1292,10 @@ class AI {
1252
1292
  return completeValue({
1253
1293
  available:false,
1254
1294
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
1255
- message:'The selected legacy LLM route does not match the active AI configuration.'
1295
+ message:'The selected built-in LLM route does not match the active AI configuration.'
1256
1296
  });
1257
1297
  }
1258
- if(!this.#legacyLLMCapability(providerId)){
1298
+ if(!this.#builtInLLMCapability(providerId)){
1259
1299
  return completeValue({
1260
1300
  available:false,
1261
1301
  code:providerId==='OLLAMA'
@@ -1276,16 +1316,16 @@ class AI {
1276
1316
  });
1277
1317
  }
1278
1318
 
1279
- #createLegacyLLMProvider(providerId){
1319
+ #createBuiltInLLMProvider(providerId){
1280
1320
  const runtime=this;
1281
1321
  const localOnly=providerId==='OLLAMA';
1282
1322
  let state='unloaded';
1283
1323
  let busy=false;
1284
1324
 
1285
- function statusLegacyLLMProvider(){
1325
+ function statusBuiltInLLMProvider(){
1286
1326
  if(state==='ready'
1287
1327
  &&!busy
1288
- &&!runtime.#legacyLLMCapability(providerId)){
1328
+ &&!runtime.#builtInLLMCapability(providerId)){
1289
1329
  state='unloaded';
1290
1330
  }
1291
1331
  return completeValue({
@@ -1295,13 +1335,13 @@ class AI {
1295
1335
  });
1296
1336
  }
1297
1337
 
1298
- function assertLegacyLLMSelection(selection){
1299
- const inspection=runtime.#legacyLLMInspection(
1338
+ function assertBuiltInLLMSelection(selection){
1339
+ const inspection=runtime.#builtInLLMInspection(
1300
1340
  providerId,
1301
1341
  selection
1302
1342
  );
1303
1343
  if(!inspection.available){
1304
- throw legacyAIProviderError(
1344
+ throw aiProviderError(
1305
1345
  inspection.message,
1306
1346
  inspection.code
1307
1347
  );
@@ -1309,7 +1349,7 @@ class AI {
1309
1349
  return inspection;
1310
1350
  }
1311
1351
 
1312
- function releaseLegacyLLMRequest(){
1352
+ function releaseBuiltInLLMRequest(){
1313
1353
  busy=false;
1314
1354
  }
1315
1355
 
@@ -1318,7 +1358,7 @@ class AI {
1318
1358
  role:'llm',
1319
1359
  id:providerId,
1320
1360
  localOnly,
1321
- catalog:function catalogLegacyLLMProvider(){
1361
+ catalog:function catalogBuiltInLLMProvider(){
1322
1362
  if(runtime.llmService!==providerId||!runtime.model){
1323
1363
  return completeValue([]);
1324
1364
  }
@@ -1326,35 +1366,35 @@ class AI {
1326
1366
  completeValue({id:runtime.model})
1327
1367
  ]);
1328
1368
  },
1329
- inspect:function inspectLegacyLLMProvider(selection,{signal}={}){
1369
+ inspect:function inspectBuiltInLLMProvider(selection,{signal}={}){
1330
1370
  if(signal?.aborted){
1331
1371
  throw normalizeAIRequestAbort(signal.reason);
1332
1372
  }
1333
- return runtime.#legacyLLMInspection(providerId,selection);
1373
+ return runtime.#builtInLLMInspection(providerId,selection);
1334
1374
  },
1335
- status:statusLegacyLLMProvider,
1336
- load:function loadLegacyLLMProvider(context={}){
1375
+ status:statusBuiltInLLMProvider,
1376
+ load:function loadBuiltInLLMProvider(context={}){
1337
1377
  if(context.signal?.aborted){
1338
1378
  throw normalizeAIRequestAbort(context.signal.reason);
1339
1379
  }
1340
1380
  if(state==='disposed'){
1341
- throw legacyAIProviderError(
1342
- 'The legacy LLM provider is disposed.',
1381
+ throw aiProviderError(
1382
+ 'The built-in LLM provider is disposed.',
1343
1383
  'ARCANE_AI_PROVIDER_DISPOSED'
1344
1384
  );
1345
1385
  }
1346
1386
  if(busy){
1347
- throw legacyAIProviderError(
1348
- 'The legacy LLM provider owns an active request.',
1387
+ throw aiProviderError(
1388
+ 'The built-in LLM provider owns an active request.',
1349
1389
  'ARCANE_AI_ROLE_BUSY'
1350
1390
  );
1351
1391
  }
1352
1392
  if(typeof context.progress!=='function'){
1353
1393
  throw new TypeError(
1354
- 'Legacy LLM provider load progress must be a function.'
1394
+ 'Built-in LLM provider load progress must be a function.'
1355
1395
  );
1356
1396
  }
1357
- const inspection=assertLegacyLLMSelection(context.selection);
1397
+ const inspection=assertBuiltInLLMSelection(context.selection);
1358
1398
  state='loading';
1359
1399
  context.progress({
1360
1400
  phase:'capability',
@@ -1377,40 +1417,40 @@ class AI {
1377
1417
  });
1378
1418
  return completeValue({
1379
1419
  authority:inspection.authority,
1380
- status:statusLegacyLLMProvider()
1420
+ status:statusBuiltInLLMProvider()
1381
1421
  });
1382
1422
  },
1383
- request:function requestLegacyLLMProvider(context={}){
1423
+ request:function requestBuiltInLLMProvider(context={}){
1384
1424
  if(context.signal?.aborted){
1385
1425
  throw normalizeAIRequestAbort(context.signal.reason);
1386
1426
  }
1387
- assertLegacyLLMSelection(context.selection);
1388
- const current=statusLegacyLLMProvider();
1427
+ assertBuiltInLLMSelection(context.selection);
1428
+ const current=statusBuiltInLLMProvider();
1389
1429
  if(current.state!=='ready'||!current.loaded){
1390
- throw legacyAIProviderError(
1391
- 'The legacy LLM provider is not ready.',
1430
+ throw aiProviderError(
1431
+ 'The built-in LLM provider is not ready.',
1392
1432
  'ARCANE_AI_ROLE_NOT_READY'
1393
1433
  );
1394
1434
  }
1395
1435
  if(busy){
1396
- throw legacyAIProviderError(
1397
- 'The legacy LLM provider owns an active request.',
1436
+ throw aiProviderError(
1437
+ 'The built-in LLM provider owns an active request.',
1398
1438
  'ARCANE_AI_ROLE_BUSY'
1399
1439
  );
1400
1440
  }
1401
1441
  busy=true;
1402
1442
  if(context.operation==='chat'){
1403
1443
  return Promise.resolve(
1404
- runtime.#requestLegacyLLMChat(
1444
+ runtime.#requestBuiltInLLMChat(
1405
1445
  context.payload,
1406
1446
  context.signal
1407
1447
  )
1408
- ).finally(releaseLegacyLLMRequest);
1448
+ ).finally(releaseBuiltInLLMRequest);
1409
1449
  }
1410
1450
  if(context.operation==='stream'){
1411
- const handle=createLegacyAIStreamBridge(
1412
- function executeLegacyLLMProviderStream(bridge){
1413
- return runtime.#requestLegacyLLMStream(
1451
+ const handle=createBuiltInAIStreamBridge(
1452
+ function executeBuiltInLLMProviderStream(bridge){
1453
+ return runtime.#requestBuiltInLLMStream(
1414
1454
  context.payload,
1415
1455
  bridge
1416
1456
  );
@@ -1418,49 +1458,49 @@ class AI {
1418
1458
  context.signal
1419
1459
  );
1420
1460
  handle.result.then(
1421
- releaseLegacyLLMRequest,
1422
- releaseLegacyLLMRequest
1461
+ releaseBuiltInLLMRequest,
1462
+ releaseBuiltInLLMRequest
1423
1463
  );
1424
1464
  return handle;
1425
1465
  }
1426
1466
  busy=false;
1427
- throw legacyAIProviderError(
1428
- 'The legacy LLM provider operation is unsupported.',
1467
+ throw aiProviderError(
1468
+ 'The built-in LLM provider operation is unsupported.',
1429
1469
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
1430
1470
  );
1431
1471
  },
1432
- unload:function unloadLegacyLLMProvider(context={}){
1472
+ unload:function unloadBuiltInLLMProvider(context={}){
1433
1473
  if(context.signal?.aborted){
1434
1474
  throw normalizeAIRequestAbort(context.signal.reason);
1435
1475
  }
1436
1476
  state='unloaded';
1437
1477
  busy=false;
1438
- return statusLegacyLLMProvider();
1478
+ return statusBuiltInLLMProvider();
1439
1479
  },
1440
- dispose:function disposeLegacyLLMProvider(context={}){
1480
+ dispose:function disposeBuiltInLLMProvider(context={}){
1441
1481
  if(context.signal?.aborted){
1442
1482
  throw normalizeAIRequestAbort(context.signal.reason);
1443
1483
  }
1444
1484
  state='disposed';
1445
1485
  busy=false;
1446
- return statusLegacyLLMProvider();
1486
+ return statusBuiltInLLMProvider();
1447
1487
  }
1448
1488
  });
1449
1489
  }
1450
1490
 
1451
- #legacySpeechService(role){
1491
+ #builtInSpeechService(role){
1452
1492
  return role==='stt'?this.sttService:this.ttsService;
1453
1493
  }
1454
1494
 
1455
- #legacySpeechModel(role){
1495
+ #builtInSpeechModel(role){
1456
1496
  return role==='stt'?this.modelSTT:this.modelTTS;
1457
1497
  }
1458
1498
 
1459
- #legacySpeechProviderKey(role,providerId){
1499
+ #builtInSpeechProviderKey(role,providerId){
1460
1500
  return `${role}:${providerId}`;
1461
1501
  }
1462
1502
 
1463
- #legacySpeechDefaultVoice(role,providerId){
1503
+ #builtInSpeechDefaultVoice(role,providerId){
1464
1504
  if(role!=='tts'){
1465
1505
  return null;
1466
1506
  }
@@ -1470,9 +1510,9 @@ class AI {
1470
1510
  return null;
1471
1511
  }
1472
1512
 
1473
- #legacySpeechCapability(role,providerId){
1474
- const service=this.#legacySpeechService(role);
1475
- const model=this.#legacySpeechModel(role);
1513
+ #builtInSpeechCapability(role,providerId){
1514
+ const service=this.#builtInSpeechService(role);
1515
+ const model=this.#builtInSpeechModel(role);
1476
1516
  if(service!==providerId||!model){
1477
1517
  return false;
1478
1518
  }
@@ -1482,20 +1522,20 @@ class AI {
1482
1522
  return false;
1483
1523
  }
1484
1524
 
1485
- #legacySpeechInspection(role,providerId,selection){
1525
+ #builtInSpeechInspection(role,providerId,selection){
1486
1526
  const localOnly=providerId==='LOCAL_SPEACH';
1487
1527
  if(!selection
1488
1528
  ||selection.providerId!==providerId
1489
- ||selection.modelId!==this.#legacySpeechModel(role)
1529
+ ||selection.modelId!==this.#builtInSpeechModel(role)
1490
1530
  ||selection.localOnly!==localOnly
1491
- ||this.#legacySpeechService(role)!==providerId){
1531
+ ||this.#builtInSpeechService(role)!==providerId){
1492
1532
  return completeValue({
1493
1533
  available:false,
1494
1534
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
1495
- message:`The selected legacy ${role.toUpperCase()} route does not match the active AI configuration.`
1535
+ message:`The selected built-in ${role.toUpperCase()} route does not match the active AI configuration.`
1496
1536
  });
1497
1537
  }
1498
- if(!this.#legacySpeechCapability(role,providerId)){
1538
+ if(!this.#builtInSpeechCapability(role,providerId)){
1499
1539
  return completeValue({
1500
1540
  available:false,
1501
1541
  code:providerId==='LOCAL_SPEACH'
@@ -1516,17 +1556,17 @@ class AI {
1516
1556
  });
1517
1557
  }
1518
1558
 
1519
- #createLegacySpeechProvider(role,providerId){
1559
+ #createBuiltInSpeechProvider(role,providerId){
1520
1560
  const runtime=this;
1521
1561
  const localOnly=providerId==='LOCAL_SPEACH';
1522
1562
  const expectedOperation=role==='stt'?'transcribe':'synthesize';
1523
1563
  let state='unloaded';
1524
1564
  let busy=false;
1525
1565
 
1526
- function statusLegacySpeechProvider(){
1566
+ function statusBuiltInSpeechProvider(){
1527
1567
  if(state==='ready'
1528
1568
  &&!busy
1529
- &&!runtime.#legacySpeechCapability(role,providerId)){
1569
+ &&!runtime.#builtInSpeechCapability(role,providerId)){
1530
1570
  state='unloaded';
1531
1571
  }
1532
1572
  return completeValue({
@@ -1536,14 +1576,14 @@ class AI {
1536
1576
  });
1537
1577
  }
1538
1578
 
1539
- function assertLegacySpeechSelection(selection){
1540
- const inspection=runtime.#legacySpeechInspection(
1579
+ function assertBuiltInSpeechSelection(selection){
1580
+ const inspection=runtime.#builtInSpeechInspection(
1541
1581
  role,
1542
1582
  providerId,
1543
1583
  selection
1544
1584
  );
1545
1585
  if(!inspection.available){
1546
- throw legacyAIProviderError(
1586
+ throw aiProviderError(
1547
1587
  inspection.message,
1548
1588
  inspection.code
1549
1589
  );
@@ -1551,7 +1591,7 @@ class AI {
1551
1591
  return inspection;
1552
1592
  }
1553
1593
 
1554
- function releaseLegacySpeechRequest(){
1594
+ function releaseBuiltInSpeechRequest(){
1555
1595
  busy=false;
1556
1596
  }
1557
1597
 
@@ -1560,12 +1600,12 @@ class AI {
1560
1600
  role,
1561
1601
  id:providerId,
1562
1602
  localOnly,
1563
- catalog:function catalogLegacySpeechProvider(){
1564
- const model=runtime.#legacySpeechModel(role);
1565
- if(runtime.#legacySpeechService(role)!==providerId||!model){
1603
+ catalog:function catalogBuiltInSpeechProvider(){
1604
+ const model=runtime.#builtInSpeechModel(role);
1605
+ if(runtime.#builtInSpeechService(role)!==providerId||!model){
1566
1606
  return completeValue([]);
1567
1607
  }
1568
- const defaultVoice=runtime.#legacySpeechDefaultVoice(
1608
+ const defaultVoice=runtime.#builtInSpeechDefaultVoice(
1569
1609
  role,
1570
1610
  providerId
1571
1611
  );
@@ -1576,35 +1616,35 @@ class AI {
1576
1616
  })
1577
1617
  ]);
1578
1618
  },
1579
- inspect:function inspectLegacySpeechProvider(selection,{signal}={}){
1619
+ inspect:function inspectBuiltInSpeechProvider(selection,{signal}={}){
1580
1620
  if(signal?.aborted){
1581
1621
  throw normalizeAIRequestAbort(signal.reason);
1582
1622
  }
1583
- return runtime.#legacySpeechInspection(role,providerId,selection);
1623
+ return runtime.#builtInSpeechInspection(role,providerId,selection);
1584
1624
  },
1585
- status:statusLegacySpeechProvider,
1586
- load:function loadLegacySpeechProvider(context={}){
1625
+ status:statusBuiltInSpeechProvider,
1626
+ load:function loadBuiltInSpeechProvider(context={}){
1587
1627
  if(context.signal?.aborted){
1588
1628
  throw normalizeAIRequestAbort(context.signal.reason);
1589
1629
  }
1590
1630
  if(state==='disposed'){
1591
- throw legacyAIProviderError(
1592
- `The legacy ${role.toUpperCase()} provider is disposed.`,
1631
+ throw aiProviderError(
1632
+ `The built-in ${role.toUpperCase()} provider is disposed.`,
1593
1633
  'ARCANE_AI_PROVIDER_DISPOSED'
1594
1634
  );
1595
1635
  }
1596
1636
  if(busy){
1597
- throw legacyAIProviderError(
1598
- `The legacy ${role.toUpperCase()} provider owns an active request.`,
1637
+ throw aiProviderError(
1638
+ `The built-in ${role.toUpperCase()} provider owns an active request.`,
1599
1639
  'ARCANE_AI_ROLE_BUSY'
1600
1640
  );
1601
1641
  }
1602
1642
  if(typeof context.progress!=='function'){
1603
1643
  throw new TypeError(
1604
- `Legacy ${role.toUpperCase()} provider load progress must be a function.`
1644
+ `Built-in ${role.toUpperCase()} provider load progress must be a function.`
1605
1645
  );
1606
1646
  }
1607
- const inspection=assertLegacySpeechSelection(context.selection);
1647
+ const inspection=assertBuiltInSpeechSelection(context.selection);
1608
1648
  state='loading';
1609
1649
  context.progress({
1610
1650
  phase:'capability',
@@ -1627,135 +1667,135 @@ class AI {
1627
1667
  });
1628
1668
  return completeValue({
1629
1669
  authority:inspection.authority,
1630
- status:statusLegacySpeechProvider()
1670
+ status:statusBuiltInSpeechProvider()
1631
1671
  });
1632
1672
  },
1633
- request:function requestLegacySpeechProvider(context={}){
1673
+ request:function requestBuiltInSpeechProvider(context={}){
1634
1674
  if(context.signal?.aborted){
1635
1675
  throw normalizeAIRequestAbort(context.signal.reason);
1636
1676
  }
1637
- assertLegacySpeechSelection(context.selection);
1638
- const current=statusLegacySpeechProvider();
1677
+ assertBuiltInSpeechSelection(context.selection);
1678
+ const current=statusBuiltInSpeechProvider();
1639
1679
  if(current.state!=='ready'||!current.loaded){
1640
- throw legacyAIProviderError(
1641
- `The legacy ${role.toUpperCase()} provider is not ready.`,
1680
+ throw aiProviderError(
1681
+ `The built-in ${role.toUpperCase()} provider is not ready.`,
1642
1682
  'ARCANE_AI_ROLE_NOT_READY'
1643
1683
  );
1644
1684
  }
1645
1685
  if(busy){
1646
- throw legacyAIProviderError(
1647
- `The legacy ${role.toUpperCase()} provider owns an active request.`,
1686
+ throw aiProviderError(
1687
+ `The built-in ${role.toUpperCase()} provider owns an active request.`,
1648
1688
  'ARCANE_AI_ROLE_BUSY'
1649
1689
  );
1650
1690
  }
1651
1691
  if(context.operation!==expectedOperation){
1652
- throw legacyAIProviderError(
1653
- `The legacy ${role.toUpperCase()} provider operation is unsupported.`,
1692
+ throw aiProviderError(
1693
+ `The built-in ${role.toUpperCase()} provider operation is unsupported.`,
1654
1694
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
1655
1695
  );
1656
1696
  }
1657
1697
  busy=true;
1658
1698
  const request=role==='stt'
1659
- ?runtime.#requestLegacySpeechTranscription(
1699
+ ?runtime.#requestBuiltInSpeechTranscription(
1660
1700
  context.payload,
1661
1701
  context.signal
1662
1702
  )
1663
- :runtime.#requestLegacySpeechSynthesis(
1703
+ :runtime.#requestBuiltInSpeechSynthesis(
1664
1704
  context.payload,
1665
1705
  context.signal
1666
1706
  );
1667
- return Promise.resolve(request).finally(releaseLegacySpeechRequest);
1707
+ return Promise.resolve(request).finally(releaseBuiltInSpeechRequest);
1668
1708
  },
1669
- unload:function unloadLegacySpeechProvider(context={}){
1709
+ unload:function unloadBuiltInSpeechProvider(context={}){
1670
1710
  if(context.signal?.aborted){
1671
1711
  throw normalizeAIRequestAbort(context.signal.reason);
1672
1712
  }
1673
1713
  if(busy){
1674
- throw legacyAIProviderError(
1675
- `The legacy ${role.toUpperCase()} provider still owns an active request.`,
1714
+ throw aiProviderError(
1715
+ `The built-in ${role.toUpperCase()} provider still owns an active request.`,
1676
1716
  'ARCANE_AI_ROLE_BUSY'
1677
1717
  );
1678
1718
  }
1679
1719
  state='unloaded';
1680
- return statusLegacySpeechProvider();
1720
+ return statusBuiltInSpeechProvider();
1681
1721
  },
1682
- dispose:function disposeLegacySpeechProvider(context={}){
1722
+ dispose:function disposeBuiltInSpeechProvider(context={}){
1683
1723
  if(context.signal?.aborted){
1684
1724
  throw normalizeAIRequestAbort(context.signal.reason);
1685
1725
  }
1686
1726
  if(busy){
1687
- throw legacyAIProviderError(
1688
- `The legacy ${role.toUpperCase()} provider still owns an active request.`,
1727
+ throw aiProviderError(
1728
+ `The built-in ${role.toUpperCase()} provider still owns an active request.`,
1689
1729
  'ARCANE_AI_ROLE_BUSY'
1690
1730
  );
1691
1731
  }
1692
1732
  state='disposed';
1693
- return statusLegacySpeechProvider();
1733
+ return statusBuiltInSpeechProvider();
1694
1734
  }
1695
1735
  });
1696
1736
  }
1697
1737
 
1698
- #ensureLegacyLLMProvider(providerId){
1699
- if(providerId!=='OPENAI'&&providerId!=='OLLAMA'){
1738
+ #ensureBuiltInLLMProvider(providerId){
1739
+ if(providerId!=='TWIN'&&providerId!=='OLLAMA'){
1700
1740
  return false;
1701
1741
  }
1702
1742
  if(this.#providerRuntime.hasProvider('llm',providerId)){
1703
1743
  return false;
1704
1744
  }
1705
- const provider=this.#createLegacyLLMProvider(providerId);
1745
+ const provider=this.#createBuiltInLLMProvider(providerId);
1706
1746
  const unregister=this.#providerRuntime.register(provider);
1707
- this.#legacyLLMProviders.set(
1747
+ this.#builtInLLMProviders.set(
1708
1748
  providerId,
1709
1749
  completeValue({provider,unregister})
1710
1750
  );
1711
1751
  return true;
1712
1752
  }
1713
1753
 
1714
- #ensureLegacySpeechProvider(role,providerId){
1754
+ #ensureBuiltInSpeechProvider(role,providerId){
1715
1755
  if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
1716
1756
  return false;
1717
1757
  }
1718
1758
  if(this.#providerRuntime.hasProvider(role,providerId)){
1719
1759
  return false;
1720
1760
  }
1721
- const provider=this.#createLegacySpeechProvider(role,providerId);
1761
+ const provider=this.#createBuiltInSpeechProvider(role,providerId);
1722
1762
  const unregister=this.#providerRuntime.register(provider);
1723
- this.#legacySpeechProviders.set(
1724
- this.#legacySpeechProviderKey(role,providerId),
1763
+ this.#builtInSpeechProviders.set(
1764
+ this.#builtInSpeechProviderKey(role,providerId),
1725
1765
  completeValue({role,providerId,provider,unregister})
1726
1766
  );
1727
1767
  return true;
1728
1768
  }
1729
1769
 
1730
- #releaseInactiveLegacyLLMProviders(activeProviderId){
1731
- for(const [providerId,record] of this.#legacyLLMProviders){
1770
+ #releaseInactiveBuiltInLLMProviders(activeProviderId){
1771
+ for(const [providerId,record] of this.#builtInLLMProviders){
1732
1772
  if(providerId===activeProviderId){
1733
1773
  continue;
1734
1774
  }
1735
1775
  if(record.unregister()){
1736
- this.#legacyLLMProviders.delete(providerId);
1776
+ this.#builtInLLMProviders.delete(providerId);
1737
1777
  }
1738
1778
  }
1739
1779
  }
1740
1780
 
1741
- #releaseInactiveLegacySpeechProviders(activeProviders){
1742
- for(const [key,record] of this.#legacySpeechProviders){
1781
+ #releaseInactiveBuiltInSpeechProviders(activeProviders){
1782
+ for(const [key,record] of this.#builtInSpeechProviders){
1743
1783
  if(activeProviders[record.role]===record.providerId){
1744
1784
  continue;
1745
1785
  }
1746
1786
  if(record.unregister()){
1747
- this.#legacySpeechProviders.delete(key);
1787
+ this.#builtInSpeechProviders.delete(key);
1748
1788
  }
1749
1789
  }
1750
1790
  }
1751
1791
 
1752
- #internalLegacyLLMSelection(localOnly=false){
1792
+ #builtInLLMSelection(localOnly=false){
1753
1793
  const selection=this.#providerRuntime.selection(
1754
1794
  'llm',
1755
1795
  {localOnly}
1756
1796
  );
1757
1797
  if(!selection
1758
- ||!this.#legacyLLMProviders.has(selection.providerId)
1798
+ ||!this.#builtInLLMProviders.has(selection.providerId)
1759
1799
  ||selection.providerId!==this.llmService
1760
1800
  ||selection.modelId!==this.model){
1761
1801
  return null;
@@ -1763,44 +1803,44 @@ class AI {
1763
1803
  return selection;
1764
1804
  }
1765
1805
 
1766
- #internalLegacySpeechSelection(role,localOnly=false){
1806
+ #builtInSpeechSelection(role,localOnly=false){
1767
1807
  const selection=this.#providerRuntime.selection(role,{localOnly});
1768
1808
  if(!selection
1769
- ||!this.#legacySpeechProviders.has(
1770
- this.#legacySpeechProviderKey(role,selection.providerId)
1809
+ ||!this.#builtInSpeechProviders.has(
1810
+ this.#builtInSpeechProviderKey(role,selection.providerId)
1771
1811
  )
1772
- ||selection.providerId!==this.#legacySpeechService(role)
1773
- ||selection.modelId!==this.#legacySpeechModel(role)){
1812
+ ||selection.providerId!==this.#builtInSpeechService(role)
1813
+ ||selection.modelId!==this.#builtInSpeechModel(role)){
1774
1814
  return null;
1775
1815
  }
1776
1816
  return selection;
1777
1817
  }
1778
1818
 
1779
- #retainLegacyLLMReadiness(operation){
1780
- this.#legacyLLMReadiness=Promise.resolve(operation).catch(
1781
- function retainLegacyLLMReadinessFailure(){
1819
+ #retainBuiltInLLMReadiness(operation){
1820
+ this.#builtInLLMReadiness=Promise.resolve(operation).catch(
1821
+ function retainBuiltInLLMReadinessFailure(){
1782
1822
  return null;
1783
1823
  }
1784
1824
  );
1785
- return this.#legacyLLMReadiness;
1825
+ return this.#builtInLLMReadiness;
1786
1826
  }
1787
1827
 
1788
- #retainLegacySpeechReadiness(operation){
1789
- this.#legacySpeechReadiness=Promise.resolve(operation).catch(
1790
- function retainLegacySpeechReadinessFailure(){
1828
+ #retainBuiltInSpeechReadiness(operation){
1829
+ this.#builtInSpeechReadiness=Promise.resolve(operation).catch(
1830
+ function retainBuiltInSpeechReadinessFailure(){
1791
1831
  return null;
1792
1832
  }
1793
1833
  );
1794
- return this.#legacySpeechReadiness;
1834
+ return this.#builtInSpeechReadiness;
1795
1835
  }
1796
1836
 
1797
- #reconcileLegacyLLMReadiness(){
1798
- const selection=this.#internalLegacyLLMSelection(false);
1837
+ #reconcileBuiltInLLMReadiness(){
1838
+ const selection=this.#builtInLLMSelection(false);
1799
1839
  if(!selection){
1800
1840
  return Promise.resolve(this.#providerRuntime.status('llm'));
1801
1841
  }
1802
1842
  const status=this.#providerRuntime.status('llm');
1803
- if(this.#legacyLLMCapability(selection.providerId)){
1843
+ if(this.#builtInLLMCapability(selection.providerId)){
1804
1844
  if(status.state==='ready'&&status.loaded===true){
1805
1845
  return Promise.resolve(status);
1806
1846
  }
@@ -1815,15 +1855,15 @@ class AI {
1815
1855
  return Promise.resolve(status);
1816
1856
  }
1817
1857
 
1818
- #reconcileLegacySpeechReadiness(){
1858
+ #reconcileBuiltInSpeechReadiness(){
1819
1859
  const runtime=this;
1820
- return Promise.all(['stt','tts'].map(function reconcileLegacySpeechRole(role){
1821
- const selection=runtime.#internalLegacySpeechSelection(role,false);
1860
+ return Promise.all(['stt','tts'].map(function reconcileBuiltInSpeechRole(role){
1861
+ const selection=runtime.#builtInSpeechSelection(role,false);
1822
1862
  if(!selection){
1823
1863
  return runtime.#providerRuntime.status(role);
1824
1864
  }
1825
1865
  const status=runtime.#providerRuntime.status(role);
1826
- if(runtime.#legacySpeechCapability(role,selection.providerId)){
1866
+ if(runtime.#builtInSpeechCapability(role,selection.providerId)){
1827
1867
  return status;
1828
1868
  }
1829
1869
  if(status.loaded===true
@@ -1838,8 +1878,8 @@ class AI {
1838
1878
 
1839
1879
  get configured(){
1840
1880
  if(this.#usesProviderRuntime('llm',this.llmService)){
1841
- if(this.#internalLegacyLLMSelection(false)
1842
- &&!this.#legacyLLMCapability(this.llmService)){
1881
+ if(this.#builtInLLMSelection(false)
1882
+ &&!this.#builtInLLMCapability(this.llmService)){
1843
1883
  return false;
1844
1884
  }
1845
1885
  const state=this.#providerRuntime.status('llm');
@@ -1849,7 +1889,7 @@ class AI {
1849
1889
  return Boolean(this.model)&&Boolean(this.#nativeOllama());
1850
1890
  }
1851
1891
 
1852
- return this.llmService==='OPENAI'
1892
+ return this.llmService==='TWIN'
1853
1893
  &&Boolean(this.model)
1854
1894
  &&Boolean(this.license);
1855
1895
  }
@@ -1857,21 +1897,21 @@ class AI {
1857
1897
  #assertServiceConfigured(service=this.llmService,role='llm'){
1858
1898
  if(this.#usesProviderRuntime(role,service)){
1859
1899
  const internal=role==='llm'
1860
- ?this.#internalLegacyLLMSelection(false)
1861
- :this.#internalLegacySpeechSelection(role,false);
1900
+ ?this.#builtInLLMSelection(false)
1901
+ :this.#builtInSpeechSelection(role,false);
1862
1902
  const internalAvailable=!internal
1863
1903
  ||(role==='llm'
1864
- ?this.#legacyLLMCapability(internal.providerId)
1865
- :this.#legacySpeechCapability(role,internal.providerId));
1904
+ ?this.#builtInLLMCapability(internal.providerId)
1905
+ :this.#builtInSpeechCapability(role,internal.providerId));
1866
1906
  if(!internalAvailable){
1867
1907
  const inspection=role==='llm'
1868
- ?this.#legacyLLMInspection(internal.providerId,internal)
1869
- :this.#legacySpeechInspection(
1908
+ ?this.#builtInLLMInspection(internal.providerId,internal)
1909
+ :this.#builtInSpeechInspection(
1870
1910
  role,
1871
1911
  internal.providerId,
1872
1912
  internal
1873
1913
  );
1874
- throw legacyAIProviderError(
1914
+ throw aiProviderError(
1875
1915
  inspection.message,
1876
1916
  inspection.code
1877
1917
  );
@@ -1901,7 +1941,7 @@ class AI {
1901
1941
  throw error;
1902
1942
  }
1903
1943
 
1904
- if(service==='OPENAI'&&role==='llm'&&Boolean(this.twinKey)){
1944
+ if(service==='TWIN'&&role==='llm'&&Boolean(this.twinKey)){
1905
1945
  return true;
1906
1946
  }
1907
1947
 
@@ -1915,9 +1955,9 @@ class AI {
1915
1955
  }
1916
1956
 
1917
1957
  #shouldUseProviderRuntime(role,service,localOnly=false){
1918
- // Legacy adapters publish lifecycle without replacing established
1958
+ // Built-in adapters publish lifecycle without replacing established
1919
1959
  // public transport callbacks or their cancellation behavior.
1920
- if(role==='llm'&&this.#internalLegacyLLMSelection(localOnly)){
1960
+ if(role==='llm'&&this.#builtInLLMSelection(localOnly)){
1921
1961
  return false;
1922
1962
  }
1923
1963
  if(!localOnly){
@@ -1955,7 +1995,9 @@ class AI {
1955
1995
  speechPlaybackStarting=false;
1956
1996
  speechResumeAttempt=0;
1957
1997
  speechResumePending=false;
1958
- speechSynthesisTail=Promise.resolve();
1998
+ speechScheduleGeneration=0;
1999
+ speechScheduleContext=null;
2000
+ speechScheduleTime=0;
1959
2001
  speechUnlockHandler=null;
1960
2002
 
1961
2003
  #nextPreferenceTuple(values){
@@ -1978,7 +2020,7 @@ class AI {
1978
2020
 
1979
2021
  #assertValidProviderTuple(tuple){
1980
2022
  if(tuple[0]==='OLLAMA'){
1981
- const mappedModel=tuple[3]==='OPENAI'?null:this.#models[tuple[3]];
2023
+ const mappedModel=tuple[3]==='TWIN'?null:this.#models[tuple[3]];
1982
2024
  if(!mappedModel&&!normalizeOllamaModelIdentifier(tuple[3])){
1983
2025
  const error=new TypeError('The Ollama model preference is invalid.');
1984
2026
  error.code='AI_MODEL_INVALID';
@@ -2009,13 +2051,13 @@ class AI {
2009
2051
 
2010
2052
  #normalizedLLMModel(service,model){
2011
2053
  if(service==='OLLAMA'){
2012
- const mappedModel=model==='OPENAI'?null:this.#models[model];
2054
+ const mappedModel=model==='TWIN'?null:this.#models[model];
2013
2055
  return mappedModel
2014
2056
  ||normalizeOllamaModelIdentifier(model)
2015
2057
  ||model;
2016
2058
  }
2017
- if(service==='OPENAI'){
2018
- return model==='OPENAI'?this.#models.OPENAI:model;
2059
+ if(service==='TWIN'){
2060
+ return model==='TWIN'?this.#models.TWIN:model;
2019
2061
  }
2020
2062
  return model;
2021
2063
  }
@@ -2094,12 +2136,12 @@ class AI {
2094
2136
  const identity=providerId&&modelId
2095
2137
  ?this.#providerRuntime.providerIdentity(role,providerId)
2096
2138
  :null;
2097
- const pendingNonLegacy=Boolean(
2139
+ const pendingExternalRoute=Boolean(
2098
2140
  providerId
2099
2141
  &&modelId
2100
- &&!LEGACY_AI_SERVICES.has(providerId)
2142
+ &&!BUILT_IN_AI_SERVICES.has(providerId)
2101
2143
  );
2102
- if(!identity&&!pendingNonLegacy){
2144
+ if(!identity&&!pendingExternalRoute){
2103
2145
  selections[role]={default:null,localOnly:null};
2104
2146
  continue;
2105
2147
  }
@@ -2118,15 +2160,15 @@ class AI {
2118
2160
  return selections;
2119
2161
  }
2120
2162
 
2121
- #assertRegisteredLegacyRoutes(selections){
2163
+ #assertRegisteredBuiltInRoutes(selections){
2122
2164
  for(const role of ['llm','stt','tts']){
2123
2165
  for(const routeName of ['default','localOnly']){
2124
2166
  const selection=selections?.[role]?.[routeName];
2125
2167
  if(selection
2126
- &&LEGACY_AI_SERVICES.has(selection.providerId)
2168
+ &&BUILT_IN_AI_SERVICES.has(selection.providerId)
2127
2169
  &&!this.#providerRuntime.hasProvider(role,selection.providerId)){
2128
2170
  const error=new Error(
2129
- `Legacy AI provider ${selection.providerId} requires an explicit ${role} adapter before routing.`
2171
+ `Built-in AI provider ${selection.providerId} requires an explicit ${role} adapter before routing.`
2130
2172
  );
2131
2173
  error.code='ARCANE_AI_PROVIDER_UNAVAILABLE';
2132
2174
  throw error;
@@ -2211,22 +2253,22 @@ class AI {
2211
2253
  ]);
2212
2254
  this.#assertValidProviderTuple(tuple);
2213
2255
  this.#assertSynchronousBrowserSpeechSupersession('AI.setAI');
2214
- this.#ensureLegacyLLMProvider(tuple[0]);
2215
- this.#ensureLegacySpeechProvider('stt',tuple[1]);
2216
- this.#ensureLegacySpeechProvider('tts',tuple[2]);
2256
+ this.#ensureBuiltInLLMProvider(tuple[0]);
2257
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2258
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2217
2259
  this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2218
2260
  this.#invalidateSpeechControl();
2219
2261
  this.#applyPreferenceTuple(tuple);
2220
- this.#releaseInactiveLegacyLLMProviders(tuple[0]);
2221
- this.#releaseInactiveLegacySpeechProviders({
2262
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2263
+ this.#releaseInactiveBuiltInSpeechProviders({
2222
2264
  stt:tuple[1],
2223
2265
  tts:tuple[2]
2224
2266
  });
2225
- this.#retainLegacyLLMReadiness(
2226
- this.#reconcileLegacyLLMReadiness()
2267
+ this.#retainBuiltInLLMReadiness(
2268
+ this.#reconcileBuiltInLLMReadiness()
2227
2269
  );
2228
- this.#retainLegacySpeechReadiness(
2229
- this.#reconcileLegacySpeechReadiness()
2270
+ this.#retainBuiltInSpeechReadiness(
2271
+ this.#reconcileBuiltInSpeechReadiness()
2230
2272
  );
2231
2273
  return true;
2232
2274
  }
@@ -2235,33 +2277,33 @@ class AI {
2235
2277
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2236
2278
  this.#assertDeviceSpeechConfiguration(prepared);
2237
2279
  this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
2238
- this.#ensureLegacyLLMProvider(
2280
+ this.#ensureBuiltInLLMProvider(
2239
2281
  prepared.llm.default?.providerId
2240
2282
  );
2241
- this.#ensureLegacySpeechProvider(
2283
+ this.#ensureBuiltInSpeechProvider(
2242
2284
  'stt',
2243
2285
  prepared.stt.default?.providerId
2244
2286
  );
2245
- this.#ensureLegacySpeechProvider(
2287
+ this.#ensureBuiltInSpeechProvider(
2246
2288
  'tts',
2247
2289
  prepared.tts.default?.providerId
2248
2290
  );
2249
- this.#assertRegisteredLegacyRoutes(prepared);
2291
+ this.#assertRegisteredBuiltInRoutes(prepared);
2250
2292
  const configured=this.#providerRuntime.configure(prepared);
2251
2293
  this.#invalidateSpeechControl();
2252
2294
  this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2253
- this.#releaseInactiveLegacyLLMProviders(
2295
+ this.#releaseInactiveBuiltInLLMProviders(
2254
2296
  configured.llm.default?.providerId
2255
2297
  );
2256
- this.#releaseInactiveLegacySpeechProviders({
2298
+ this.#releaseInactiveBuiltInSpeechProviders({
2257
2299
  stt:configured.stt.default?.providerId,
2258
2300
  tts:configured.tts.default?.providerId
2259
2301
  });
2260
- this.#retainLegacyLLMReadiness(
2261
- this.#reconcileLegacyLLMReadiness()
2302
+ this.#retainBuiltInLLMReadiness(
2303
+ this.#reconcileBuiltInLLMReadiness()
2262
2304
  );
2263
- this.#retainLegacySpeechReadiness(
2264
- this.#reconcileLegacySpeechReadiness()
2305
+ this.#retainBuiltInSpeechReadiness(
2306
+ this.#reconcileBuiltInSpeechReadiness()
2265
2307
  );
2266
2308
  return configured;
2267
2309
  }
@@ -2272,26 +2314,26 @@ class AI {
2272
2314
  this.#assertSynchronousBrowserSpeechSupersession(
2273
2315
  'AI.configureSpeechProviders'
2274
2316
  );
2275
- this.#ensureLegacySpeechProvider(
2317
+ this.#ensureBuiltInSpeechProvider(
2276
2318
  'stt',
2277
2319
  prepared.stt.default?.providerId
2278
2320
  );
2279
- this.#ensureLegacySpeechProvider(
2321
+ this.#ensureBuiltInSpeechProvider(
2280
2322
  'tts',
2281
2323
  prepared.tts.default?.providerId
2282
2324
  );
2283
- this.#assertRegisteredLegacyRoutes(prepared);
2325
+ this.#assertRegisteredBuiltInRoutes(prepared);
2284
2326
  const configured=this.#providerRuntime.configureSpeech(prepared);
2285
2327
  this.#invalidateSpeechControl();
2286
2328
  this.#applySpeechPreferenceTuple(
2287
2329
  this.#tupleFromSpeechProviderRoutes(configured)
2288
2330
  );
2289
- this.#releaseInactiveLegacySpeechProviders({
2331
+ this.#releaseInactiveBuiltInSpeechProviders({
2290
2332
  stt:configured.stt.default?.providerId,
2291
2333
  tts:configured.tts.default?.providerId
2292
2334
  });
2293
- this.#retainLegacySpeechReadiness(
2294
- this.#reconcileLegacySpeechReadiness()
2335
+ this.#retainBuiltInSpeechReadiness(
2336
+ this.#reconcileBuiltInSpeechReadiness()
2295
2337
  );
2296
2338
  this.muted=true;
2297
2339
  this.stopAudio();
@@ -2318,18 +2360,18 @@ class AI {
2318
2360
  await this.#supersedeBrowserSpeechForRouteChange();
2319
2361
  this.#invalidateSpeechControl();
2320
2362
  await this.#unloadProviderRolesForTransition();
2321
- this.#ensureLegacyLLMProvider(tuple[0]);
2322
- this.#ensureLegacySpeechProvider('stt',tuple[1]);
2323
- this.#ensureLegacySpeechProvider('tts',tuple[2]);
2363
+ this.#ensureBuiltInLLMProvider(tuple[0]);
2364
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2365
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2324
2366
  this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2325
2367
  this.#applyPreferenceTuple(tuple);
2326
- this.#releaseInactiveLegacyLLMProviders(tuple[0]);
2327
- this.#releaseInactiveLegacySpeechProviders({
2368
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2369
+ this.#releaseInactiveBuiltInSpeechProviders({
2328
2370
  stt:tuple[1],
2329
2371
  tts:tuple[2]
2330
2372
  });
2331
- await this.#reconcileLegacyLLMReadiness();
2332
- await this.#reconcileLegacySpeechReadiness();
2373
+ await this.#reconcileBuiltInLLMReadiness();
2374
+ await this.#reconcileBuiltInSpeechReadiness();
2333
2375
  return this.#providerRuntime.status();
2334
2376
  }
2335
2377
 
@@ -2337,31 +2379,31 @@ class AI {
2337
2379
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2338
2380
  this.#assertDeviceSpeechConfiguration(prepared);
2339
2381
  await this.#supersedeBrowserSpeechForRouteChange();
2340
- this.#ensureLegacyLLMProvider(
2382
+ this.#ensureBuiltInLLMProvider(
2341
2383
  prepared.llm.default?.providerId
2342
2384
  );
2343
- this.#ensureLegacySpeechProvider(
2385
+ this.#ensureBuiltInSpeechProvider(
2344
2386
  'stt',
2345
2387
  prepared.stt.default?.providerId
2346
2388
  );
2347
- this.#ensureLegacySpeechProvider(
2389
+ this.#ensureBuiltInSpeechProvider(
2348
2390
  'tts',
2349
2391
  prepared.tts.default?.providerId
2350
2392
  );
2351
- this.#assertRegisteredLegacyRoutes(prepared);
2393
+ this.#assertRegisteredBuiltInRoutes(prepared);
2352
2394
  this.#invalidateSpeechControl();
2353
2395
  await this.#unloadProviderRolesForTransition();
2354
2396
  const configured=this.#providerRuntime.configure(prepared);
2355
2397
  this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2356
- this.#releaseInactiveLegacyLLMProviders(
2398
+ this.#releaseInactiveBuiltInLLMProviders(
2357
2399
  configured.llm.default?.providerId
2358
2400
  );
2359
- this.#releaseInactiveLegacySpeechProviders({
2401
+ this.#releaseInactiveBuiltInSpeechProviders({
2360
2402
  stt:configured.stt.default?.providerId,
2361
2403
  tts:configured.tts.default?.providerId
2362
2404
  });
2363
- await this.#reconcileLegacyLLMReadiness();
2364
- await this.#reconcileLegacySpeechReadiness();
2405
+ await this.#reconcileBuiltInLLMReadiness();
2406
+ await this.#reconcileBuiltInSpeechReadiness();
2365
2407
  return configured;
2366
2408
  }
2367
2409
 
@@ -2371,24 +2413,24 @@ class AI {
2371
2413
  await this.#supersedeBrowserSpeechForRouteChange();
2372
2414
  this.#invalidateSpeechControl();
2373
2415
  await this.#unloadSpeechProviderRolesForTransition();
2374
- this.#ensureLegacySpeechProvider(
2416
+ this.#ensureBuiltInSpeechProvider(
2375
2417
  'stt',
2376
2418
  prepared.stt.default?.providerId
2377
2419
  );
2378
- this.#ensureLegacySpeechProvider(
2420
+ this.#ensureBuiltInSpeechProvider(
2379
2421
  'tts',
2380
2422
  prepared.tts.default?.providerId
2381
2423
  );
2382
- this.#assertRegisteredLegacyRoutes(prepared);
2424
+ this.#assertRegisteredBuiltInRoutes(prepared);
2383
2425
  const configured=this.#providerRuntime.configureSpeech(prepared);
2384
2426
  this.#applySpeechPreferenceTuple(
2385
2427
  this.#tupleFromSpeechProviderRoutes(configured)
2386
2428
  );
2387
- this.#releaseInactiveLegacySpeechProviders({
2429
+ this.#releaseInactiveBuiltInSpeechProviders({
2388
2430
  stt:configured.stt.default?.providerId,
2389
2431
  tts:configured.tts.default?.providerId
2390
2432
  });
2391
- await this.#reconcileLegacySpeechReadiness();
2433
+ await this.#reconcileBuiltInSpeechReadiness();
2392
2434
  this.muted=true;
2393
2435
  return configured;
2394
2436
  }
@@ -2499,7 +2541,7 @@ class AI {
2499
2541
 
2500
2542
  #browserSpeechReplacementBoundary(previousRecord,roles){
2501
2543
  const expectedProviders={stt:null,tts:null};
2502
- const legacyRecords=[];
2544
+ const builtInRecords=[];
2503
2545
  for(const role of roles){
2504
2546
  if(previousRecord?.managedRoles.includes(role)){
2505
2547
  expectedProviders[role]=previousRecord.providers[role];
@@ -2510,8 +2552,8 @@ class AI {
2510
2552
  expectedProviders[role]=null;
2511
2553
  continue;
2512
2554
  }
2513
- const record=this.#legacySpeechProviders.get(
2514
- this.#legacySpeechProviderKey(role,selection.providerId)
2555
+ const record=this.#builtInSpeechProviders.get(
2556
+ this.#builtInSpeechProviderKey(role,selection.providerId)
2515
2557
  );
2516
2558
  if(!record
2517
2559
  ||!this.#providerRuntime.ownsProvider(role,record.provider)){
@@ -2524,41 +2566,41 @@ class AI {
2524
2566
  continue;
2525
2567
  }
2526
2568
  throw this.#browserSpeechProviderRouteOwnershipError(
2527
- `The selected ${role} route is not owned by the replaceable AI legacy speech boundary.`
2569
+ `The selected ${role} route is not owned by the replaceable built-in AI speech boundary.`
2528
2570
  );
2529
2571
  }
2530
2572
  expectedProviders[role]=record.provider;
2531
- legacyRecords.push(record);
2573
+ builtInRecords.push(record);
2532
2574
  }
2533
2575
  return completeValue({
2534
2576
  expectedProviders:completeValue(expectedProviders),
2535
- legacyRecords:completeValue(legacyRecords)
2577
+ builtInRecords:completeValue(builtInRecords)
2536
2578
  });
2537
2579
  }
2538
2580
 
2539
- async #cleanupRetiredLegacySpeechProviders(
2581
+ async #cleanupRetiredBuiltInSpeechProviders(
2540
2582
  {signal=null,committed=false}={}
2541
2583
  ){
2542
2584
  const failures=[];
2543
- for(const record of [...this.#browserSpeechRetiredLegacyRecords]){
2585
+ for(const record of [...this.#browserSpeechRetiredBuiltInRecords]){
2544
2586
  try{
2545
2587
  if(this.#providerRuntime.ownsProvider(
2546
2588
  record.role,
2547
2589
  record.provider
2548
2590
  )){
2549
2591
  throw this.#browserSpeechProviderRouteOwnershipError(
2550
- `The retired ${record.role} legacy speech provider still owns its registry entry.`
2592
+ `The retired ${record.role} built-in speech provider still owns its registry entry.`
2551
2593
  );
2552
2594
  }
2553
2595
  await record.provider.dispose({role:record.role,signal});
2554
- const key=this.#legacySpeechProviderKey(
2596
+ const key=this.#builtInSpeechProviderKey(
2555
2597
  record.role,
2556
2598
  record.providerId
2557
2599
  );
2558
- if(this.#legacySpeechProviders.get(key)===record){
2559
- this.#legacySpeechProviders.delete(key);
2600
+ if(this.#builtInSpeechProviders.get(key)===record){
2601
+ this.#builtInSpeechProviders.delete(key);
2560
2602
  }
2561
- this.#browserSpeechRetiredLegacyRecords.delete(record);
2603
+ this.#browserSpeechRetiredBuiltInRecords.delete(record);
2562
2604
  }catch(error){
2563
2605
  failures.push(error);
2564
2606
  }
@@ -2567,12 +2609,12 @@ class AI {
2567
2609
  throw aiBrowserSpeechError(
2568
2610
  AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
2569
2611
  AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
2570
- 'The replaced legacy speech providers could not be disposed.',
2612
+ 'The replaced built-in speech providers could not be disposed.',
2571
2613
  failures.length===1
2572
2614
  ?failures[0]
2573
2615
  :new AggregateError(
2574
2616
  failures,
2575
- 'Multiple replaced legacy speech provider disposals were rejected.'
2617
+ 'Multiple replaced built-in speech provider disposals were rejected.'
2576
2618
  ),
2577
2619
  {committed}
2578
2620
  );
@@ -2670,7 +2712,12 @@ class AI {
2670
2712
  ?{artifactGraphId:catalog.artifactGraphId}
2671
2713
  :{}),
2672
2714
  offline:configured.offline,
2673
- ...(role==='tts'?{defaultVoice:catalog.defaultVoice}:{})
2715
+ ...(role==='tts'
2716
+ ?{
2717
+ defaultVoice:catalog.defaultVoice,
2718
+ execution:configured.execution
2719
+ }
2720
+ :{})
2674
2721
  });
2675
2722
  }
2676
2723
  const roles={};
@@ -3039,7 +3086,8 @@ class AI {
3039
3086
  runtime:configured.runtime
3040
3087
  }),
3041
3088
  store,
3042
- offline:configured.offline
3089
+ offline:configured.offline,
3090
+ ...(role==='tts'?{execution:configured.execution}:{})
3043
3091
  });
3044
3092
  }
3045
3093
  providers=completeValue({...candidateProviders});
@@ -3175,8 +3223,8 @@ class AI {
3175
3223
  candidate,
3176
3224
  replacement
3177
3225
  );
3178
- for(const legacyRecord of replacementBoundary.legacyRecords){
3179
- this.#browserSpeechRetiredLegacyRecords.add(legacyRecord);
3226
+ for(const builtInRecord of replacementBoundary.builtInRecords){
3227
+ this.#browserSpeechRetiredBuiltInRecords.add(builtInRecord);
3180
3228
  }
3181
3229
  if(previousRecord){
3182
3230
  this.#retireBrowserSpeechRegistration(
@@ -3272,7 +3320,7 @@ class AI {
3272
3320
  generation,
3273
3321
  {committed:true}
3274
3322
  );
3275
- await this.#cleanupRetiredLegacySpeechProviders({
3323
+ await this.#cleanupRetiredBuiltInSpeechProviders({
3276
3324
  signal:controller.signal,
3277
3325
  committed:true
3278
3326
  });
@@ -3438,7 +3486,7 @@ class AI {
3438
3486
  record.managedRoles.includes('tts')
3439
3487
  ||record.candidateRoles.includes('tts')
3440
3488
  )
3441
- ||[...this.#browserSpeechRetiredLegacyRecords].some(
3489
+ ||[...this.#browserSpeechRetiredBuiltInRecords].some(
3442
3490
  record=>record.role==='tts'
3443
3491
  )
3444
3492
  );
@@ -3466,7 +3514,7 @@ class AI {
3466
3514
  if(activeRecord)records.add(activeRecord);
3467
3515
  const eventRecord=activeRecord||records.values().next().value||null;
3468
3516
  if(!eventRecord
3469
- &&runtime.#browserSpeechRetiredLegacyRecords.size===0){
3517
+ &&runtime.#browserSpeechRetiredBuiltInRecords.size===0){
3470
3518
  return false;
3471
3519
  }
3472
3520
  if(eventRecord){
@@ -3550,8 +3598,8 @@ class AI {
3550
3598
  );
3551
3599
  changed=true;
3552
3600
  }
3553
- if(runtime.#browserSpeechRetiredLegacyRecords.size){
3554
- await runtime.#cleanupRetiredLegacySpeechProviders({
3601
+ if(runtime.#browserSpeechRetiredBuiltInRecords.size){
3602
+ await runtime.#cleanupRetiredBuiltInSpeechProviders({
3555
3603
  signal:controller.signal,
3556
3604
  committed:true
3557
3605
  });
@@ -3722,7 +3770,7 @@ class AI {
3722
3770
  return null;
3723
3771
  }
3724
3772
 
3725
- async #requestLegacySpeechTranscription(payload={},signal=null){
3773
+ async #requestBuiltInSpeechTranscription(payload={},signal=null){
3726
3774
  const audio=payload?.audio;
3727
3775
  if(!audio||typeof audio.arrayBuffer!=='function'){
3728
3776
  throw new TypeError('Speech transcription requires an audio Blob or File.');
@@ -3773,7 +3821,7 @@ class AI {
3773
3821
  return response.text();
3774
3822
  }
3775
3823
 
3776
- async #requestLegacySpeechSynthesis(payload={},signal=null){
3824
+ async #requestBuiltInSpeechSynthesis(payload={},signal=null){
3777
3825
  const input=typeof payload?.input==='string'?payload.input:'';
3778
3826
  if(!input){
3779
3827
  throw new TypeError('Speech synthesis requires nonempty input.');
@@ -3784,7 +3832,7 @@ class AI {
3784
3832
  const model=String(payload.model||this.modelTTS);
3785
3833
  const voice=typeof payload.voice==='string'&&payload.voice.trim()
3786
3834
  ?payload.voice.trim()
3787
- :this.#legacySpeechDefaultVoice('tts',this.ttsService);
3835
+ :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
3788
3836
  if(!voice){
3789
3837
  throw new TypeError('The selected speech provider requires a voice.');
3790
3838
  }
@@ -3842,14 +3890,14 @@ class AI {
3842
3890
  if(isAIRequestAbort(error,signal)){
3843
3891
  throw normalizeAIRequestAbort(error);
3844
3892
  }
3845
- throw legacyAIProviderError(
3893
+ throw aiProviderError(
3846
3894
  'The configured TTS HTTP request failed.',
3847
3895
  'ARCANE_AI_TTS_HTTP_REQUEST_FAILED',
3848
3896
  error
3849
3897
  );
3850
3898
  }
3851
3899
  if(!response.ok){
3852
- const error=legacyAIProviderError(
3900
+ const error=aiProviderError(
3853
3901
  `The configured TTS HTTP response was rejected with status ${response.status}.`,
3854
3902
  'ARCANE_AI_TTS_HTTP_RESPONSE_REJECTED'
3855
3903
  );
@@ -4128,49 +4176,49 @@ class AI {
4128
4176
  return typeof content==='string'?content:completion;
4129
4177
  }
4130
4178
 
4131
- #requestLegacyLLMChat(payload={},signal=null){
4179
+ #requestBuiltInLLMChat(payload={},signal=null){
4132
4180
  const parallelToolCalls=payload.parallelToolCalls!==undefined
4133
4181
  ?payload.parallelToolCalls
4134
4182
  :payload.parallel_tool_calls;
4135
- return this.#fetchLegacy(
4183
+ return this.#fetchBuiltIn(
4136
4184
  payload.messages??[],
4137
- function ignoreLegacyLLMProviderResponse(){},
4185
+ function ignoreBuiltInLLMProviderResponse(){},
4138
4186
  payload.structuredOutput??false,
4139
4187
  payload.tools??[],
4140
4188
  payload.toolChoice??'auto',
4141
4189
  parallelToolCalls,
4142
4190
  payload.id??Date.now(),
4143
- function ignoreLegacyLLMProviderRequest(){},
4191
+ function ignoreBuiltInLLMProviderRequest(){},
4144
4192
  signal,
4145
4193
  payload.reasoningEffort
4146
4194
  );
4147
4195
  }
4148
4196
 
4149
- #requestLegacyLLMStream(payload={},bridge){
4197
+ #requestBuiltInLLMStream(payload={},bridge){
4150
4198
  const parallelToolCalls=payload.parallelToolCalls!==undefined
4151
4199
  ?payload.parallelToolCalls
4152
4200
  :payload.parallel_tool_calls;
4153
- function emitLegacyLLMStreamData(chunk){
4201
+ function emitBuiltInLLMStreamData(chunk){
4154
4202
  bridge.emit(chunk);
4155
4203
  }
4156
4204
 
4157
- return this.#streamLegacyMessage(
4205
+ return this.#streamBuiltInMessage(
4158
4206
  payload.messages??[],
4159
- function ignoreLegacyLLMScalarStream(){},
4160
- function ignoreLegacyLLMProviderCompletion(){},
4207
+ function ignoreBuiltInLLMScalarStream(){},
4208
+ function ignoreBuiltInLLMProviderCompletion(){},
4161
4209
  payload.tools??[],
4162
4210
  payload.toolChoice??'auto',
4163
- function retainLegacyLLMStreamToolUntilCompletion(){},
4211
+ function retainBuiltInLLMStreamToolUntilCompletion(){},
4164
4212
  parallelToolCalls,
4165
4213
  payload.id??Date.now(),
4166
4214
  payload.seeThinking??false,
4167
4215
  bridge.signal,
4168
- function ignoreLegacyLLMProviderRequest(){},
4216
+ function ignoreBuiltInLLMProviderRequest(){},
4169
4217
  payload.structuredOutput??false,
4170
4218
  false,
4171
4219
  true,
4172
- emitLegacyLLMStreamData,
4173
- function ignoreLegacyLLMStreamResult(){},
4220
+ emitBuiltInLLMStreamData,
4221
+ function ignoreBuiltInLLMStreamResult(){},
4174
4222
  payload.reasoningEffort
4175
4223
  );
4176
4224
  }
@@ -4466,13 +4514,13 @@ class AI {
4466
4514
  }
4467
4515
 
4468
4516
  try{
4469
- const completion=await this.#streamLegacyMessage(
4517
+ const completion=await this.#streamBuiltInMessage(
4470
4518
  messages,
4471
4519
  onChunk,
4472
- function retainLegacyCompletionUntilResponse(){},
4520
+ function retainBuiltInCompletionUntilResponse(){},
4473
4521
  tools,
4474
4522
  toolChoice,
4475
- function retainLegacyToolCallUntilResponse(){},
4523
+ function retainBuiltInToolCallUntilResponse(){},
4476
4524
  parallelToolCalls,
4477
4525
  id,
4478
4526
  seeThinking,
@@ -4553,7 +4601,7 @@ class AI {
4553
4601
  });
4554
4602
  }
4555
4603
 
4556
- return this.#streamLegacyMessage(
4604
+ return this.#streamBuiltInMessage(
4557
4605
  messages,
4558
4606
  streamHandler,
4559
4607
  streamComplete,
@@ -4573,7 +4621,7 @@ class AI {
4573
4621
  );
4574
4622
  }
4575
4623
 
4576
- async #streamLegacyMessage(
4624
+ async #streamBuiltInMessage(
4577
4625
  messages=[],
4578
4626
  streamHandler=function ignoreStreamChunk(){},
4579
4627
  streamComplete=function finishIgnoredStream(){},
@@ -4588,8 +4636,8 @@ class AI {
4588
4636
  structuredOutput=false,
4589
4637
  finishSpeech=true,
4590
4638
  returnCompletion=false,
4591
- dataChunkHandler=function ignoreLegacyStreamDataChunk(){},
4592
- dataResultHandler=function ignoreLegacyStreamDataResult(){},
4639
+ dataChunkHandler=function ignoreBuiltInStreamDataChunk(){},
4640
+ dataResultHandler=function ignoreBuiltInStreamDataResult(){},
4593
4641
  reasoningEffort
4594
4642
  ){
4595
4643
  let speechTurnCompleted=false;
@@ -4635,7 +4683,7 @@ class AI {
4635
4683
 
4636
4684
  if(
4637
4685
  normalizedReasoningEffort
4638
- &&(this.llmService==='OPENAI'||this.llmService==='OLLAMA')
4686
+ &&(this.llmService==='TWIN'||this.llmService==='OLLAMA')
4639
4687
  ){
4640
4688
  request.reasoning_effort=normalizedReasoningEffort;
4641
4689
  }
@@ -4648,7 +4696,7 @@ class AI {
4648
4696
  const nativeOllama=this.#nativeOllama();
4649
4697
 
4650
4698
  if(this.llmService==='OLLAMA'&&!nativeOllama){
4651
- throw legacyAIProviderError(
4699
+ throw aiProviderError(
4652
4700
  'Local AI requires the capability-gated Arcane API.',
4653
4701
  'AI_NATIVE_LOCAL_REQUIRED'
4654
4702
  );
@@ -5036,7 +5084,6 @@ class AI {
5036
5084
  const content=typeof choiceDelta.content==='string'
5037
5085
  ?choiceDelta.content
5038
5086
  :'';
5039
- let value=content;
5040
5087
  let reasoning='';
5041
5088
  if(seeThinking){
5042
5089
  reasoning=typeof choiceDelta.reasoning_content==='string'
@@ -5046,8 +5093,13 @@ class AI {
5046
5093
  :'';
5047
5094
  }
5048
5095
  isThinking=Boolean(reasoning);
5049
- if(reasoning) value=reasoning;
5050
- if(value) await streamHandler(value,`M-${id}`,isThinking);
5096
+ if(reasoning){
5097
+ await streamHandler(reasoning,`M-${id}`,true);
5098
+ }
5099
+ if(content){
5100
+ isThinking=false;
5101
+ await streamHandler(content,`M-${id}`,false);
5102
+ }
5051
5103
  }
5052
5104
  }
5053
5105
 
@@ -5214,7 +5266,7 @@ class AI {
5214
5266
  const selectedTerminalChoiceIndex=selectedChoiceIndex??completionChoiceIndexes[0];
5215
5267
  const structuralToolCalls=structuralToolCallsByChoice.get(selectedTerminalChoiceIndex)||[];
5216
5268
  const completionChoices=completionChoiceIndexes
5217
- .map(function completeLegacyStreamChoice(choicePosition){
5269
+ .map(function completeBuiltInStreamChoice(choicePosition){
5218
5270
  const retained=streamedChoicesByIndex.get(choicePosition)||{
5219
5271
  choice:{index:choicePosition},
5220
5272
  message:{role:'assistant'}
@@ -5243,7 +5295,7 @@ class AI {
5243
5295
  });
5244
5296
  const completion={
5245
5297
  ...streamMetadata,
5246
- id:Object.hasOwn(streamMetadata,'id')?streamMetadata.id:`legacy-${id}`,
5298
+ id:Object.hasOwn(streamMetadata,'id')?streamMetadata.id:`stream-${id}`,
5247
5299
  object:Object.hasOwn(streamMetadata,'object')
5248
5300
  ?streamMetadata.object
5249
5301
  :'chat.completion',
@@ -5257,7 +5309,7 @@ class AI {
5257
5309
  assertAIStreamToolCallCorrelation(
5258
5310
  structuralToolCalls,
5259
5311
  terminalToolCalls,
5260
- 'The legacy HTTP stream'
5312
+ 'The built-in HTTP stream'
5261
5313
  );
5262
5314
  await dataResultHandler(completion,id);
5263
5315
  for(const call of terminalToolCalls){
@@ -5374,7 +5426,7 @@ class AI {
5374
5426
  return response;
5375
5427
  }
5376
5428
 
5377
- return this.#fetchLegacy(
5429
+ return this.#fetchBuiltIn(
5378
5430
  messages,
5379
5431
  onResponse,
5380
5432
  structuredOutput,
@@ -5414,7 +5466,7 @@ class AI {
5414
5466
  });
5415
5467
  }
5416
5468
 
5417
- return this.#fetchLegacy(
5469
+ return this.#fetchBuiltIn(
5418
5470
  messages,
5419
5471
  responseHandler,
5420
5472
  structuredOutput,
@@ -5427,7 +5479,7 @@ class AI {
5427
5479
  );
5428
5480
  }
5429
5481
 
5430
- async #fetchLegacy(
5482
+ async #fetchBuiltIn(
5431
5483
  messages=[],
5432
5484
  responseHandler=function ignoreFetchResponse(){},
5433
5485
  structuredOutput=false,
@@ -5477,7 +5529,7 @@ class AI {
5477
5529
 
5478
5530
  if(
5479
5531
  normalizedReasoningEffort
5480
- &&(this.llmService==='OPENAI'||this.llmService==='OLLAMA')
5532
+ &&(this.llmService==='TWIN'||this.llmService==='OLLAMA')
5481
5533
  ){
5482
5534
  request.reasoning_effort=normalizedReasoningEffort;
5483
5535
  }
@@ -5485,7 +5537,7 @@ class AI {
5485
5537
  const nativeOllama=this.#nativeOllama();
5486
5538
 
5487
5539
  if(this.llmService==='OLLAMA'&&!nativeOllama){
5488
- throw legacyAIProviderError(
5540
+ throw aiProviderError(
5489
5541
  'Local AI requires the capability-gated Arcane API.',
5490
5542
  'AI_NATIVE_LOCAL_REQUIRED'
5491
5543
  );
@@ -5808,18 +5860,21 @@ class AI {
5808
5860
  #queueSpeechJob(text,generation){
5809
5861
  const job={
5810
5862
  abortController:null,
5863
+ audioBuffer:null,
5864
+ audioContext:null,
5811
5865
  generation,
5866
+ scheduledEnd:null,
5867
+ scheduledStart:null,
5812
5868
  sourceNode:null,
5813
5869
  state:'queued',
5814
5870
  text
5815
5871
  };
5816
5872
  const runtime=this;
5817
- const previous=this.speechSynthesisTail;
5818
5873
 
5819
5874
  this.speechJobs.push(job);
5820
5875
 
5821
- const synthesis=previous.then(
5822
- function synthesizeQueuedSpeech(){
5876
+ return Promise.resolve().then(
5877
+ function synthesizeAvailableSpeech(){
5823
5878
  return runtime.#prepareSpeechJob(job);
5824
5879
  }
5825
5880
  ).catch(
@@ -5831,14 +5886,6 @@ class AI {
5831
5886
  );
5832
5887
  }
5833
5888
  );
5834
-
5835
- this.speechSynthesisTail=synthesis.then(
5836
- function releaseSpeechSynthesisSlot(){
5837
- return undefined;
5838
- }
5839
- );
5840
-
5841
- return synthesis;
5842
5889
  }
5843
5890
 
5844
5891
  async #prepareSpeechJob(job){
@@ -5923,7 +5970,7 @@ class AI {
5923
5970
  ||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
5924
5971
  ||typeof defaultFormat!=='string'
5925
5972
  ||!formats.includes(defaultFormat)){
5926
- throw legacyAIProviderError(
5973
+ throw aiProviderError(
5927
5974
  'The selected TTS provider returned an invalid speech format catalog.',
5928
5975
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
5929
5976
  );
@@ -5931,10 +5978,10 @@ class AI {
5931
5978
  if(formats.includes(this.audioFormat)){
5932
5979
  return this.audioFormat;
5933
5980
  }
5934
- if(this.audioFormat===LEGACY_TTS_RESPONSE_FORMAT){
5981
+ if(this.audioFormat===DEFAULT_TTS_RESPONSE_FORMAT){
5935
5982
  return defaultFormat;
5936
5983
  }
5937
- throw legacyAIProviderError(
5984
+ throw aiProviderError(
5938
5985
  `The selected TTS provider does not support ${this.audioFormat}.`,
5939
5986
  'ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT'
5940
5987
  );
@@ -6118,7 +6165,7 @@ class AI {
6118
6165
  ?requestedVoice
6119
6166
  :selection
6120
6167
  ?this.#providerSpeechVoice()
6121
- :this.#legacySpeechDefaultVoice('tts',this.ttsService);
6168
+ :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
6122
6169
  if(!voice){
6123
6170
  const error=new TypeError(
6124
6171
  'AI.fetchTTS requires a caller- or model-catalog-admitted voice.'
@@ -6170,23 +6217,14 @@ class AI {
6170
6217
  return audio;
6171
6218
  }
6172
6219
 
6173
- return this.#requestLegacySpeechSynthesis(
6220
+ return this.#requestBuiltInSpeechSynthesis(
6174
6221
  {model,voice,input,responseFormat,speed},
6175
6222
  signal
6176
6223
  );
6177
6224
  }
6178
6225
 
6179
- async fetchSTT(
6180
- audioFile,
6181
- responseHandler=(text='')=>{},
6182
- signal=null
6183
- ){
6226
+ async fetchSTT(audioFile,signal=null){
6184
6227
  this.#assertServiceConfigured(this.sttService,'stt');
6185
- if(typeof responseHandler!=='function'){
6186
- const error=new TypeError('AI.fetchSTT responseHandler must be a function.');
6187
- error.code='ARCANE_AI_STT_RESPONSE_HANDLER_INVALID';
6188
- throw error;
6189
- }
6190
6228
  if(signal&&(
6191
6229
  typeof signal.aborted!=='boolean'
6192
6230
  ||typeof signal.addEventListener!=='function'
@@ -6228,12 +6266,10 @@ class AI {
6228
6266
  throw error;
6229
6267
  }
6230
6268
  if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6231
- await responseHandler(text);
6232
- if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6233
6269
  return text;
6234
6270
  }
6235
6271
 
6236
- const text=await this.#requestLegacySpeechTranscription(
6272
+ const text=await this.#requestBuiltInSpeechTranscription(
6237
6273
  {
6238
6274
  audio:audioFile,
6239
6275
  mimeType:String(audioFile?.type||'audio/webm'),
@@ -6242,8 +6278,6 @@ class AI {
6242
6278
  signal
6243
6279
  );
6244
6280
  if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6245
- await responseHandler(text);
6246
- if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6247
6281
  return text;
6248
6282
  }
6249
6283
 
@@ -6251,6 +6285,9 @@ class AI {
6251
6285
  this.speechGeneration+=1;
6252
6286
  this.speechResumeAttempt+=1;
6253
6287
  this.speechResumePending=false;
6288
+ this.speechScheduleGeneration=this.speechGeneration;
6289
+ this.speechScheduleContext=null;
6290
+ this.speechScheduleTime=0;
6254
6291
  this.audioMessageChunks='';
6255
6292
  this.#clearSpeechUnlock();
6256
6293
 
@@ -6306,7 +6343,7 @@ class AI {
6306
6343
  }
6307
6344
 
6308
6345
  if(typeof context.resume!=='function'){
6309
- this.#waitForSpeechGesture();
6346
+ this.#waitForSpeechGesture(null,context);
6310
6347
  return false;
6311
6348
  }
6312
6349
 
@@ -6333,7 +6370,7 @@ class AI {
6333
6370
  if(attempt===this.speechResumeAttempt){
6334
6371
  this.speechResumePending=false;
6335
6372
  }
6336
- this.#waitForSpeechGesture(error);
6373
+ this.#waitForSpeechGesture(error,context);
6337
6374
  if(error?.name!=='NotAllowedError'){
6338
6375
  this.#publishTTSFailure(error,{
6339
6376
  boundary:'playback-resume',
@@ -6343,7 +6380,7 @@ class AI {
6343
6380
  return false;
6344
6381
  }
6345
6382
 
6346
- this.#waitForSpeechGesture();
6383
+ this.#waitForSpeechGesture(null,context);
6347
6384
  return false;
6348
6385
  }
6349
6386
 
@@ -6356,7 +6393,11 @@ class AI {
6356
6393
  ){
6357
6394
  const job=speechJob||{
6358
6395
  abortController:null,
6396
+ audioBuffer:null,
6397
+ audioContext:null,
6359
6398
  generation:this.speechGeneration,
6399
+ scheduledEnd:null,
6400
+ scheduledStart:null,
6360
6401
  sourceNode:null,
6361
6402
  state:'decoding',
6362
6403
  text:''
@@ -6372,7 +6413,9 @@ class AI {
6372
6413
 
6373
6414
  try{
6374
6415
  job.state='decoding';
6375
- const playbackContext=audioContext||this.#getSpeechAudioContext();
6416
+ const playbackContext=sourceNode?.context
6417
+ ||audioContext
6418
+ ||this.#getSpeechAudioContext();
6376
6419
  const audioBlob=new Blob(audioChunks,{type:audioType});
6377
6420
  const arrayBuffer=await audioBlob.arrayBuffer();
6378
6421
  const audioBuffer=await playbackContext.decodeAudioData(arrayBuffer);
@@ -6390,6 +6433,8 @@ class AI {
6390
6433
  preparedSource.onended=function finishQueuedSpeechSource(){
6391
6434
  runtime.nextSentance(job);
6392
6435
  };
6436
+ job.audioBuffer=audioBuffer;
6437
+ job.audioContext=preparedSource.context||playbackContext;
6393
6438
  job.sourceNode=preparedSource;
6394
6439
  job.state='ready';
6395
6440
  this.sourceNodes.push(preparedSource);
@@ -6415,7 +6460,7 @@ class AI {
6415
6460
  }
6416
6461
 
6417
6462
  async #pumpSpeechPlayback(){
6418
- if(this.speechPlaybackStarting||this.isSpeaking||this.muted){
6463
+ if(this.speechPlaybackStarting||this.muted){
6419
6464
  return false;
6420
6465
  }
6421
6466
 
@@ -6423,12 +6468,21 @@ class AI {
6423
6468
  let activeJob=null;
6424
6469
 
6425
6470
  try{
6426
- while(!this.isSpeaking&&!this.muted){
6427
- const job=this.speechJobs[0];
6471
+ if(this.speechScheduleGeneration!==this.speechGeneration){
6472
+ this.speechScheduleGeneration=this.speechGeneration;
6473
+ this.speechScheduleContext=null;
6474
+ this.speechScheduleTime=0;
6475
+ }
6476
+
6477
+ let index=0;
6478
+ let scheduled=false;
6479
+
6480
+ while(index<this.speechJobs.length&&!this.muted){
6481
+ const job=this.speechJobs[index];
6428
6482
  activeJob=job||null;
6429
6483
 
6430
6484
  if(!job){
6431
- return false;
6485
+ break;
6432
6486
  }
6433
6487
 
6434
6488
  if(job.generation!==this.speechGeneration||['cancelled','failed'].includes(job.state)){
@@ -6436,25 +6490,32 @@ class AI {
6436
6490
  continue;
6437
6491
  }
6438
6492
 
6439
- if(job.state!=='ready'||!job.sourceNode?.buffer){
6440
- return false;
6493
+ if(!['ready','scheduled'].includes(job.state)||!job.sourceNode?.buffer){
6494
+ break;
6441
6495
  }
6442
6496
 
6443
- const audioContext=this.#getSpeechAudioContext();
6497
+ const audioContext=job.sourceNode.context||job.audioContext;
6444
6498
 
6445
6499
  if(audioContext.state!=='running'){
6446
- this.#waitForSpeechGesture();
6500
+ this.#waitForSpeechGesture(null,audioContext);
6447
6501
 
6448
6502
  if(!this.speechResumePending){
6449
6503
  this.resumeAudio(audioContext,false);
6450
6504
  }
6451
6505
 
6452
- return false;
6506
+ return scheduled;
6507
+ }
6508
+
6509
+ if(job.state==='scheduled'){
6510
+ if(job.scheduledEnd===null){
6511
+ break;
6512
+ }
6513
+ index+=1;
6514
+ continue;
6453
6515
  }
6454
6516
 
6455
6517
  if(
6456
6518
  this.muted
6457
- ||job!==this.speechJobs[0]
6458
6519
  ||job.generation!==this.speechGeneration
6459
6520
  ||job.state!=='ready'
6460
6521
  ){
@@ -6462,18 +6523,47 @@ class AI {
6462
6523
  }
6463
6524
 
6464
6525
  try{
6465
- job.state='playing';
6526
+ const duration=Number(job.audioBuffer?.duration);
6527
+ const hasKnownDuration=Number.isFinite(duration)&&duration>0;
6528
+ const currentTime=Number(audioContext.currentTime)||0;
6529
+ const previousContext=this.speechScheduleContext;
6530
+ const remainingDelay=previousContext
6531
+ ?Math.max(
6532
+ 0,
6533
+ this.speechScheduleTime
6534
+ -(Number(previousContext.currentTime)||0)
6535
+ )
6536
+ :0;
6537
+ const scheduledStart=Math.max(
6538
+ currentTime,
6539
+ previousContext===audioContext
6540
+ ?this.speechScheduleTime
6541
+ :currentTime+remainingDelay
6542
+ );
6543
+ job.state='scheduled';
6544
+ job.scheduledStart=scheduledStart;
6545
+ job.scheduledEnd=hasKnownDuration
6546
+ ?scheduledStart+duration
6547
+ :null;
6466
6548
  job.sourceNode.__arcaneStarted=true;
6467
- this.currentSpeechJob=job;
6549
+ if(!this.currentSpeechJob){
6550
+ this.currentSpeechJob=job;
6551
+ }
6468
6552
  this.isSpeaking=true;
6469
- await job.sourceNode.start(0);
6470
- return true;
6553
+ job.sourceNode.start(scheduledStart);
6554
+ scheduled=true;
6555
+ this.speechScheduleContext=audioContext;
6556
+ if(job.scheduledEnd===null){
6557
+ this.speechScheduleTime=0;
6558
+ break;
6559
+ }
6560
+ this.speechScheduleTime=job.scheduledEnd;
6561
+ index+=1;
6471
6562
  }catch(error){
6472
- this.currentSpeechJob=null;
6473
- this.isSpeaking=false;
6474
6563
  this.#failSpeechJob(job,error,'playback-start');
6475
6564
  }
6476
6565
  }
6566
+ return scheduled;
6477
6567
  }catch(error){
6478
6568
  if(activeJob){
6479
6569
  this.#failSpeechJob(activeJob,error,'playback-start');
@@ -6485,11 +6575,20 @@ class AI {
6485
6575
  this.speechPlaybackStarting=false;
6486
6576
 
6487
6577
  if(
6488
- !this.isSpeaking
6489
- &&!this.muted
6578
+ !this.muted
6490
6579
  &&!this.speechAwaitingGesture
6491
6580
  &&!this.speechResumePending
6492
- &&this.speechJobs[0]?.state==='ready'
6581
+ &&!this.speechJobs.some(
6582
+ function hasScheduledSpeechWithoutDuration(job){
6583
+ return job.state==='scheduled'
6584
+ &&job.scheduledEnd===null;
6585
+ }
6586
+ )
6587
+ &&this.speechJobs.find(
6588
+ function findFirstUnscheduledSpeechJob(job){
6589
+ return job.state!=='scheduled';
6590
+ }
6591
+ )?.state==='ready'
6493
6592
  ){
6494
6593
  this.#requestSpeechPlayback();
6495
6594
  }
@@ -6511,9 +6610,17 @@ class AI {
6511
6610
  this.#removeSpeechJob(job);
6512
6611
 
6513
6612
  if(this.currentSpeechJob===job){
6514
- this.currentSpeechJob=null;
6515
- this.isSpeaking=false;
6613
+ this.currentSpeechJob=this.speechJobs.find(
6614
+ function findNextScheduledSpeechJob(candidate){
6615
+ return candidate.state==='scheduled';
6616
+ }
6617
+ )||null;
6516
6618
  }
6619
+ this.isSpeaking=this.speechJobs.some(
6620
+ function hasScheduledSpeechJob(candidate){
6621
+ return candidate.state==='scheduled';
6622
+ }
6623
+ );
6517
6624
 
6518
6625
  this.#requestSpeechPlayback();
6519
6626
  return true;
@@ -6603,9 +6710,17 @@ class AI {
6603
6710
  this.#removeSpeechJob(job);
6604
6711
 
6605
6712
  if(this.currentSpeechJob===job){
6606
- this.currentSpeechJob=null;
6607
- this.isSpeaking=false;
6713
+ this.currentSpeechJob=this.speechJobs.find(
6714
+ function findRemainingScheduledSpeechJob(candidate){
6715
+ return candidate.state==='scheduled';
6716
+ }
6717
+ )||null;
6608
6718
  }
6719
+ this.isSpeaking=this.speechJobs.some(
6720
+ function hasRemainingScheduledSpeechJob(candidate){
6721
+ return candidate.state==='scheduled';
6722
+ }
6723
+ );
6609
6724
 
6610
6725
  this.#requestSpeechPlayback();
6611
6726
  return false;
@@ -6627,7 +6742,7 @@ class AI {
6627
6742
  job.sourceNode?.disconnect?.();
6628
6743
  }
6629
6744
 
6630
- #waitForSpeechGesture(error=null){
6745
+ #waitForSpeechGesture(error=null,audioContext=null){
6631
6746
  if(this.speechUnlockHandler){
6632
6747
  return false;
6633
6748
  }
@@ -6638,7 +6753,7 @@ class AI {
6638
6753
  this.speechAwaitingGesture=true;
6639
6754
  this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
6640
6755
  runtime.#clearSpeechUnlock();
6641
- runtime.resumeAudio();
6756
+ return runtime.resumeAudio(audioContext);
6642
6757
  };
6643
6758
 
6644
6759
  target.addEventListener?.(