arcane-os 0.5.7 → 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 +25 -0
  2. package/README.md +39 -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 +569 -402
  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,8 @@ 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']);
39
+ const AI_REASONING_EFFORTS=new Set(['none','low','medium','high','max']);
33
40
  export const AI_READY_EVENT='ai-ready';
34
41
  const AI_TTS_FAILURE_EVENT='ai-tts-failure';
35
42
  export const AI_INITIALIZATION_ERROR_CODES=completeValue({
@@ -128,7 +135,21 @@ function normalizeAIRequestAbort(error){
128
135
  return normalized;
129
136
  }
130
137
 
131
- function legacyAIProviderError(message,code,cause){
138
+ function normalizeAIReasoningEffort(value){
139
+ if(value===undefined||value===null||value===''){
140
+ return '';
141
+ }
142
+ if(typeof value!=='string'||!AI_REASONING_EFFORTS.has(value)){
143
+ const error=new TypeError(
144
+ 'AI reasoningEffort must be none, low, medium, high, or max.'
145
+ );
146
+ error.code='AI_REASONING_EFFORT_INVALID';
147
+ throw error;
148
+ }
149
+ return value;
150
+ }
151
+
152
+ function aiProviderError(message,code,cause){
132
153
  const error=cause===undefined
133
154
  ?new Error(message)
134
155
  :new Error(message,{cause});
@@ -520,7 +541,7 @@ function projectAIStreamChunk(value){
520
541
  return projectAIStreamData(value);
521
542
  }
522
543
 
523
- function createLegacyAIStreamBridge(execute,sourceSignal){
544
+ function createBuiltInAIStreamBridge(execute,sourceSignal){
524
545
  const controller=new AbortController();
525
546
  const queue=[];
526
547
  const waiters=[];
@@ -528,34 +549,34 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
528
549
  let failure=null;
529
550
  let detached=false;
530
551
 
531
- function forwardLegacyAIStreamAbort(){
552
+ function forwardBuiltInAIStreamAbort(){
532
553
  if(!controller.signal.aborted){
533
554
  controller.abort(sourceSignal?.reason);
534
555
  }
535
556
  }
536
557
 
537
- function detachLegacyAIStreamAbort(){
558
+ function detachBuiltInAIStreamAbort(){
538
559
  if(detached){
539
560
  return;
540
561
  }
541
562
  detached=true;
542
563
  sourceSignal?.removeEventListener?.(
543
564
  'abort',
544
- forwardLegacyAIStreamAbort
565
+ forwardBuiltInAIStreamAbort
545
566
  );
546
567
  }
547
568
 
548
569
  if(sourceSignal?.aborted){
549
- forwardLegacyAIStreamAbort();
570
+ forwardBuiltInAIStreamAbort();
550
571
  }else{
551
572
  sourceSignal?.addEventListener?.(
552
573
  'abort',
553
- forwardLegacyAIStreamAbort,
574
+ forwardBuiltInAIStreamAbort,
554
575
  {once:true}
555
576
  );
556
577
  }
557
578
 
558
- function emitLegacyAIStreamChunk(chunk){
579
+ function emitBuiltInAIStreamChunk(chunk){
559
580
  if(complete){
560
581
  return false;
561
582
  }
@@ -568,13 +589,13 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
568
589
  return true;
569
590
  }
570
591
 
571
- function finishLegacyAIStream(error){
592
+ function finishBuiltInAIStream(error){
572
593
  if(complete){
573
594
  return;
574
595
  }
575
596
  complete=true;
576
597
  failure=error||null;
577
- detachLegacyAIStreamAbort();
598
+ detachBuiltInAIStreamAbort();
578
599
  while(waiters.length){
579
600
  const waiter=waiters.shift();
580
601
  if(failure){
@@ -586,42 +607,42 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
586
607
  }
587
608
 
588
609
  const result=Promise.resolve().then(
589
- function executeLegacyAIStream(){
610
+ function executeBuiltInAIStream(){
590
611
  if(controller.signal.aborted){
591
612
  throw normalizeAIRequestAbort(controller.signal.reason);
592
613
  }
593
614
  return execute({
594
- emit:emitLegacyAIStreamChunk,
615
+ emit:emitBuiltInAIStreamChunk,
595
616
  signal:controller.signal
596
617
  });
597
618
  }
598
619
  ).then(
599
- function acceptLegacyAIStreamResult(value){
600
- finishLegacyAIStream(null);
620
+ function acceptBuiltInAIStreamResult(value){
621
+ finishBuiltInAIStream(null);
601
622
  return value;
602
623
  },
603
- function rejectLegacyAIStreamResult(error){
624
+ function rejectBuiltInAIStreamResult(error){
604
625
  const normalized=isAIRequestAbort(error,controller.signal)
605
626
  ?normalizeAIRequestAbort(error)
606
627
  :error;
607
- finishLegacyAIStream(normalized);
628
+ finishBuiltInAIStream(normalized);
608
629
  throw normalized;
609
630
  }
610
631
  );
611
- result.catch(function retainLegacyAIStreamFailure() {});
632
+ result.catch(function retainBuiltInAIStreamFailure() {});
612
633
 
613
- async function cancelLegacyAIStream(reason){
634
+ async function cancelBuiltInAIStream(reason){
614
635
  if(!controller.signal.aborted){
615
636
  controller.abort(reason);
616
637
  }
617
- await result.catch(function retainCancelledLegacyAIStream() {});
638
+ await result.catch(function retainCancelledBuiltInAIStream() {});
618
639
  return true;
619
640
  }
620
641
 
621
642
  const handle={
622
643
  result,
623
- cancel:cancelLegacyAIStream,
624
- next:function readLegacyAIStreamChunk(){
644
+ cancel:cancelBuiltInAIStream,
645
+ next:function readBuiltInAIStreamChunk(){
625
646
  if(queue.length){
626
647
  return Promise.resolve({value:queue.shift(),done:false});
627
648
  }
@@ -630,24 +651,24 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
630
651
  ?Promise.reject(failure)
631
652
  :Promise.resolve({value:undefined,done:true});
632
653
  }
633
- return new Promise(function waitForLegacyAIStreamChunk(resolve,reject){
654
+ return new Promise(function waitForBuiltInAIStreamChunk(resolve,reject){
634
655
  waiters.push({resolve,reject});
635
656
  });
636
657
  },
637
- return:async function returnLegacyAIStream(value){
638
- await cancelLegacyAIStream(
639
- legacyAIProviderError(
640
- '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.',
641
662
  'ARCANE_AI_REQUEST_ABORTED'
642
663
  )
643
664
  );
644
665
  return {value,done:true};
645
666
  },
646
- throw:async function throwLegacyAIStream(error){
647
- await cancelLegacyAIStream(error);
667
+ throw:async function throwBuiltInAIStream(error){
668
+ await cancelBuiltInAIStream(error);
648
669
  throw error;
649
670
  },
650
- [Symbol.asyncIterator]:function iterateLegacyAIStream(){
671
+ [Symbol.asyncIterator]:function iterateBuiltInAIStream(){
651
672
  return this;
652
673
  }
653
674
  };
@@ -788,11 +809,46 @@ function browserSpeechIdentifier(value,label){
788
809
  return value;
789
810
  }
790
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
+
791
847
  function normalizeBrowserSpeechRole(value,role){
792
848
  const label=`AI browser speech ${role}`;
793
849
  const descriptors=closedRecord(
794
850
  value,
795
- ['providerId','graph','model','runtime','security','offline'],
851
+ ['providerId','graph','model','runtime','security','offline','execution'],
796
852
  ['providerId','offline'],
797
853
  label
798
854
  );
@@ -853,6 +909,13 @@ function normalizeBrowserSpeechRole(value,role){
853
909
  `${label}.offline must be a boolean.`
854
910
  );
855
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
+ }
856
919
  return completeValue({
857
920
  providerId,
858
921
  ...(hasGraph
@@ -865,7 +928,12 @@ function normalizeBrowserSpeechRole(value,role){
865
928
  runtime:descriptors.runtime.value,
866
929
  ...(secure?{security:{secure:true}}:{})
867
930
  }),
868
- offline:descriptors.offline.value
931
+ offline:descriptors.offline.value,
932
+ ...(role==='tts'
933
+ ?{execution:normalizeBrowserTTSExecution(
934
+ descriptors.execution?.value
935
+ )}
936
+ :{})
869
937
  });
870
938
  }
871
939
 
@@ -959,9 +1027,8 @@ class AI {
959
1027
  // This is the enum section for inference configuration
960
1028
  #service = {
961
1029
  baseURL: {
962
- // OPENAI remains the legacy route identifier for compatibility;
963
- // remote LLM chat is provided by TWiN Cloud.
964
- OPENAI: 'https://inference.do-ai.run/v1'
1030
+ // TWiN Cloud owns this remote LLM route.
1031
+ TWIN: 'https://inference.do-ai.run/v1'
965
1032
  },
966
1033
  sttURL: {
967
1034
  LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
@@ -973,7 +1040,7 @@ class AI {
973
1040
 
974
1041
  #paths = {
975
1042
  chat: {
976
- OPENAI: '/chat/completions'
1043
+ TWIN: '/chat/completions'
977
1044
  },
978
1045
  stt: {
979
1046
  LOCAL_SPEACH: '/audio/transcriptions'
@@ -984,7 +1051,7 @@ class AI {
984
1051
  }
985
1052
 
986
1053
  #models = {
987
- OPENAI:'openai-gpt-oss-120b'
1054
+ TWIN:'openai-gpt-oss-120b'
988
1055
  }
989
1056
 
990
1057
  #sttModels = {
@@ -1003,7 +1070,7 @@ class AI {
1003
1070
  // Note: if we expand cloud providers, simply add their expected JSON metadata here
1004
1071
  get #serviceHeaders(){
1005
1072
  return {
1006
- OPENAI: {
1073
+ TWIN: {
1007
1074
  'Content-Type': 'application/json',
1008
1075
  'Authorization': `Bearer ${this.twinKey}`
1009
1076
  }
@@ -1071,10 +1138,10 @@ class AI {
1071
1138
  );
1072
1139
 
1073
1140
  const preferences=[
1074
- llmService||'OPENAI',
1141
+ llmService||'TWIN',
1075
1142
  sttService||'LOCAL_SPEACH',
1076
1143
  ttsService||'LOCAL_SPEACH',
1077
- model||'OPENAI',
1144
+ model||'TWIN',
1078
1145
  modelTTS||'LOCAL_SPEACH',
1079
1146
  modelSTT||'LOCAL_SPEACH'
1080
1147
  ];
@@ -1085,9 +1152,9 @@ class AI {
1085
1152
  const runtime=this;
1086
1153
  this.#stopOllamaReady=arcaneEvents.subscribe(
1087
1154
  'arcane-ollama-ready',
1088
- function reconcileLegacyOllamaReadiness(){
1089
- runtime.#retainLegacyLLMReadiness(
1090
- runtime.#reconcileLegacyLLMReadiness()
1155
+ function reconcileBuiltInOllamaReadiness(){
1156
+ runtime.#retainBuiltInLLMReadiness(
1157
+ runtime.#reconcileBuiltInLLMReadiness()
1091
1158
  );
1092
1159
  }
1093
1160
  );
@@ -1102,21 +1169,21 @@ class AI {
1102
1169
  #browserSpeechModulePromise=null;
1103
1170
  #browserSpeechOperationSequence=0;
1104
1171
  #browserSpeechRetiredRecords=new Set();
1105
- #browserSpeechRetiredLegacyRecords=new Set();
1172
+ #browserSpeechRetiredBuiltInRecords=new Set();
1106
1173
  #browserSpeechTransition=Promise.resolve();
1107
- #legacyLLMProviders=new Map();
1108
- #legacyLLMReadiness=Promise.resolve(null);
1109
- #legacySpeechProviders=new Map();
1110
- #legacySpeechReadiness=Promise.resolve(null);
1174
+ #builtInLLMProviders=new Map();
1175
+ #builtInLLMReadiness=Promise.resolve(null);
1176
+ #builtInSpeechProviders=new Map();
1177
+ #builtInSpeechReadiness=Promise.resolve(null);
1111
1178
  #speechControlGeneration=0;
1112
1179
  #speechFailureSequence=0;
1113
1180
  #stopOllamaReady=null;
1114
1181
  #ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
1115
1182
  #preferenceTuple=completeValue([
1116
- 'OPENAI',
1183
+ 'TWIN',
1117
1184
  'LOCAL_SPEACH',
1118
1185
  'LOCAL_SPEACH',
1119
- 'OPENAI',
1186
+ 'TWIN',
1120
1187
  'LOCAL_SPEACH',
1121
1188
  'LOCAL_SPEACH'
1122
1189
  ]);
@@ -1163,26 +1230,14 @@ class AI {
1163
1230
  return `${this.#service.baseURL[this.llmService]}${this.#paths.chat[this.llmService]}`
1164
1231
  }
1165
1232
 
1166
- set url(value) {
1167
- return false;
1168
- }
1169
-
1170
1233
  get urlTTS() {
1171
1234
  return `${this.#service.ttsURL[this.ttsService]}${this.#paths.tts[this.ttsService]}`
1172
1235
  }
1173
1236
 
1174
- set urlTTS(value) {
1175
- return false;
1176
- }
1177
-
1178
1237
  get urlSTT() {
1179
1238
  return `${this.#service.sttURL[this.sttService]}${this.#paths.stt[this.sttService]}`
1180
1239
  }
1181
1240
 
1182
- set urlSTT(value) {
1183
- return false;
1184
- }
1185
-
1186
1241
  #license='';
1187
1242
 
1188
1243
  // Browser-delivered framework code must not contain provider credentials.
@@ -1195,8 +1250,8 @@ class AI {
1195
1250
 
1196
1251
  set twinKey(value){
1197
1252
  this.#license=typeof value==='string' ? value.trim():'';
1198
- this.#retainLegacyLLMReadiness(
1199
- this.#reconcileLegacyLLMReadiness()
1253
+ this.#retainBuiltInLLMReadiness(
1254
+ this.#reconcileBuiltInLLMReadiness()
1200
1255
  );
1201
1256
  return this.#license;
1202
1257
  }
@@ -1212,9 +1267,9 @@ class AI {
1212
1267
  return this.#license;
1213
1268
  }
1214
1269
 
1215
- #legacyLLMCapability(providerId){
1216
- if(providerId==='OPENAI'){
1217
- return this.llmService==='OPENAI'
1270
+ #builtInLLMCapability(providerId){
1271
+ if(providerId==='TWIN'){
1272
+ return this.llmService==='TWIN'
1218
1273
  &&Boolean(this.model)
1219
1274
  &&Boolean(this.license)
1220
1275
  &&typeof globalThis.fetch==='function';
@@ -1227,7 +1282,7 @@ class AI {
1227
1282
  return false;
1228
1283
  }
1229
1284
 
1230
- #legacyLLMInspection(providerId,selection){
1285
+ #builtInLLMInspection(providerId,selection){
1231
1286
  const localOnly=providerId==='OLLAMA';
1232
1287
  if(!selection
1233
1288
  ||selection.providerId!==providerId
@@ -1237,10 +1292,10 @@ class AI {
1237
1292
  return completeValue({
1238
1293
  available:false,
1239
1294
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
1240
- 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.'
1241
1296
  });
1242
1297
  }
1243
- if(!this.#legacyLLMCapability(providerId)){
1298
+ if(!this.#builtInLLMCapability(providerId)){
1244
1299
  return completeValue({
1245
1300
  available:false,
1246
1301
  code:providerId==='OLLAMA'
@@ -1261,16 +1316,16 @@ class AI {
1261
1316
  });
1262
1317
  }
1263
1318
 
1264
- #createLegacyLLMProvider(providerId){
1319
+ #createBuiltInLLMProvider(providerId){
1265
1320
  const runtime=this;
1266
1321
  const localOnly=providerId==='OLLAMA';
1267
1322
  let state='unloaded';
1268
1323
  let busy=false;
1269
1324
 
1270
- function statusLegacyLLMProvider(){
1325
+ function statusBuiltInLLMProvider(){
1271
1326
  if(state==='ready'
1272
1327
  &&!busy
1273
- &&!runtime.#legacyLLMCapability(providerId)){
1328
+ &&!runtime.#builtInLLMCapability(providerId)){
1274
1329
  state='unloaded';
1275
1330
  }
1276
1331
  return completeValue({
@@ -1280,13 +1335,13 @@ class AI {
1280
1335
  });
1281
1336
  }
1282
1337
 
1283
- function assertLegacyLLMSelection(selection){
1284
- const inspection=runtime.#legacyLLMInspection(
1338
+ function assertBuiltInLLMSelection(selection){
1339
+ const inspection=runtime.#builtInLLMInspection(
1285
1340
  providerId,
1286
1341
  selection
1287
1342
  );
1288
1343
  if(!inspection.available){
1289
- throw legacyAIProviderError(
1344
+ throw aiProviderError(
1290
1345
  inspection.message,
1291
1346
  inspection.code
1292
1347
  );
@@ -1294,7 +1349,7 @@ class AI {
1294
1349
  return inspection;
1295
1350
  }
1296
1351
 
1297
- function releaseLegacyLLMRequest(){
1352
+ function releaseBuiltInLLMRequest(){
1298
1353
  busy=false;
1299
1354
  }
1300
1355
 
@@ -1303,7 +1358,7 @@ class AI {
1303
1358
  role:'llm',
1304
1359
  id:providerId,
1305
1360
  localOnly,
1306
- catalog:function catalogLegacyLLMProvider(){
1361
+ catalog:function catalogBuiltInLLMProvider(){
1307
1362
  if(runtime.llmService!==providerId||!runtime.model){
1308
1363
  return completeValue([]);
1309
1364
  }
@@ -1311,35 +1366,35 @@ class AI {
1311
1366
  completeValue({id:runtime.model})
1312
1367
  ]);
1313
1368
  },
1314
- inspect:function inspectLegacyLLMProvider(selection,{signal}={}){
1369
+ inspect:function inspectBuiltInLLMProvider(selection,{signal}={}){
1315
1370
  if(signal?.aborted){
1316
1371
  throw normalizeAIRequestAbort(signal.reason);
1317
1372
  }
1318
- return runtime.#legacyLLMInspection(providerId,selection);
1373
+ return runtime.#builtInLLMInspection(providerId,selection);
1319
1374
  },
1320
- status:statusLegacyLLMProvider,
1321
- load:function loadLegacyLLMProvider(context={}){
1375
+ status:statusBuiltInLLMProvider,
1376
+ load:function loadBuiltInLLMProvider(context={}){
1322
1377
  if(context.signal?.aborted){
1323
1378
  throw normalizeAIRequestAbort(context.signal.reason);
1324
1379
  }
1325
1380
  if(state==='disposed'){
1326
- throw legacyAIProviderError(
1327
- 'The legacy LLM provider is disposed.',
1381
+ throw aiProviderError(
1382
+ 'The built-in LLM provider is disposed.',
1328
1383
  'ARCANE_AI_PROVIDER_DISPOSED'
1329
1384
  );
1330
1385
  }
1331
1386
  if(busy){
1332
- throw legacyAIProviderError(
1333
- 'The legacy LLM provider owns an active request.',
1387
+ throw aiProviderError(
1388
+ 'The built-in LLM provider owns an active request.',
1334
1389
  'ARCANE_AI_ROLE_BUSY'
1335
1390
  );
1336
1391
  }
1337
1392
  if(typeof context.progress!=='function'){
1338
1393
  throw new TypeError(
1339
- 'Legacy LLM provider load progress must be a function.'
1394
+ 'Built-in LLM provider load progress must be a function.'
1340
1395
  );
1341
1396
  }
1342
- const inspection=assertLegacyLLMSelection(context.selection);
1397
+ const inspection=assertBuiltInLLMSelection(context.selection);
1343
1398
  state='loading';
1344
1399
  context.progress({
1345
1400
  phase:'capability',
@@ -1362,40 +1417,40 @@ class AI {
1362
1417
  });
1363
1418
  return completeValue({
1364
1419
  authority:inspection.authority,
1365
- status:statusLegacyLLMProvider()
1420
+ status:statusBuiltInLLMProvider()
1366
1421
  });
1367
1422
  },
1368
- request:function requestLegacyLLMProvider(context={}){
1423
+ request:function requestBuiltInLLMProvider(context={}){
1369
1424
  if(context.signal?.aborted){
1370
1425
  throw normalizeAIRequestAbort(context.signal.reason);
1371
1426
  }
1372
- assertLegacyLLMSelection(context.selection);
1373
- const current=statusLegacyLLMProvider();
1427
+ assertBuiltInLLMSelection(context.selection);
1428
+ const current=statusBuiltInLLMProvider();
1374
1429
  if(current.state!=='ready'||!current.loaded){
1375
- throw legacyAIProviderError(
1376
- 'The legacy LLM provider is not ready.',
1430
+ throw aiProviderError(
1431
+ 'The built-in LLM provider is not ready.',
1377
1432
  'ARCANE_AI_ROLE_NOT_READY'
1378
1433
  );
1379
1434
  }
1380
1435
  if(busy){
1381
- throw legacyAIProviderError(
1382
- 'The legacy LLM provider owns an active request.',
1436
+ throw aiProviderError(
1437
+ 'The built-in LLM provider owns an active request.',
1383
1438
  'ARCANE_AI_ROLE_BUSY'
1384
1439
  );
1385
1440
  }
1386
1441
  busy=true;
1387
1442
  if(context.operation==='chat'){
1388
1443
  return Promise.resolve(
1389
- runtime.#requestLegacyLLMChat(
1444
+ runtime.#requestBuiltInLLMChat(
1390
1445
  context.payload,
1391
1446
  context.signal
1392
1447
  )
1393
- ).finally(releaseLegacyLLMRequest);
1448
+ ).finally(releaseBuiltInLLMRequest);
1394
1449
  }
1395
1450
  if(context.operation==='stream'){
1396
- const handle=createLegacyAIStreamBridge(
1397
- function executeLegacyLLMProviderStream(bridge){
1398
- return runtime.#requestLegacyLLMStream(
1451
+ const handle=createBuiltInAIStreamBridge(
1452
+ function executeBuiltInLLMProviderStream(bridge){
1453
+ return runtime.#requestBuiltInLLMStream(
1399
1454
  context.payload,
1400
1455
  bridge
1401
1456
  );
@@ -1403,49 +1458,49 @@ class AI {
1403
1458
  context.signal
1404
1459
  );
1405
1460
  handle.result.then(
1406
- releaseLegacyLLMRequest,
1407
- releaseLegacyLLMRequest
1461
+ releaseBuiltInLLMRequest,
1462
+ releaseBuiltInLLMRequest
1408
1463
  );
1409
1464
  return handle;
1410
1465
  }
1411
1466
  busy=false;
1412
- throw legacyAIProviderError(
1413
- 'The legacy LLM provider operation is unsupported.',
1467
+ throw aiProviderError(
1468
+ 'The built-in LLM provider operation is unsupported.',
1414
1469
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
1415
1470
  );
1416
1471
  },
1417
- unload:function unloadLegacyLLMProvider(context={}){
1472
+ unload:function unloadBuiltInLLMProvider(context={}){
1418
1473
  if(context.signal?.aborted){
1419
1474
  throw normalizeAIRequestAbort(context.signal.reason);
1420
1475
  }
1421
1476
  state='unloaded';
1422
1477
  busy=false;
1423
- return statusLegacyLLMProvider();
1478
+ return statusBuiltInLLMProvider();
1424
1479
  },
1425
- dispose:function disposeLegacyLLMProvider(context={}){
1480
+ dispose:function disposeBuiltInLLMProvider(context={}){
1426
1481
  if(context.signal?.aborted){
1427
1482
  throw normalizeAIRequestAbort(context.signal.reason);
1428
1483
  }
1429
1484
  state='disposed';
1430
1485
  busy=false;
1431
- return statusLegacyLLMProvider();
1486
+ return statusBuiltInLLMProvider();
1432
1487
  }
1433
1488
  });
1434
1489
  }
1435
1490
 
1436
- #legacySpeechService(role){
1491
+ #builtInSpeechService(role){
1437
1492
  return role==='stt'?this.sttService:this.ttsService;
1438
1493
  }
1439
1494
 
1440
- #legacySpeechModel(role){
1495
+ #builtInSpeechModel(role){
1441
1496
  return role==='stt'?this.modelSTT:this.modelTTS;
1442
1497
  }
1443
1498
 
1444
- #legacySpeechProviderKey(role,providerId){
1499
+ #builtInSpeechProviderKey(role,providerId){
1445
1500
  return `${role}:${providerId}`;
1446
1501
  }
1447
1502
 
1448
- #legacySpeechDefaultVoice(role,providerId){
1503
+ #builtInSpeechDefaultVoice(role,providerId){
1449
1504
  if(role!=='tts'){
1450
1505
  return null;
1451
1506
  }
@@ -1455,9 +1510,9 @@ class AI {
1455
1510
  return null;
1456
1511
  }
1457
1512
 
1458
- #legacySpeechCapability(role,providerId){
1459
- const service=this.#legacySpeechService(role);
1460
- const model=this.#legacySpeechModel(role);
1513
+ #builtInSpeechCapability(role,providerId){
1514
+ const service=this.#builtInSpeechService(role);
1515
+ const model=this.#builtInSpeechModel(role);
1461
1516
  if(service!==providerId||!model){
1462
1517
  return false;
1463
1518
  }
@@ -1467,20 +1522,20 @@ class AI {
1467
1522
  return false;
1468
1523
  }
1469
1524
 
1470
- #legacySpeechInspection(role,providerId,selection){
1525
+ #builtInSpeechInspection(role,providerId,selection){
1471
1526
  const localOnly=providerId==='LOCAL_SPEACH';
1472
1527
  if(!selection
1473
1528
  ||selection.providerId!==providerId
1474
- ||selection.modelId!==this.#legacySpeechModel(role)
1529
+ ||selection.modelId!==this.#builtInSpeechModel(role)
1475
1530
  ||selection.localOnly!==localOnly
1476
- ||this.#legacySpeechService(role)!==providerId){
1531
+ ||this.#builtInSpeechService(role)!==providerId){
1477
1532
  return completeValue({
1478
1533
  available:false,
1479
1534
  code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
1480
- 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.`
1481
1536
  });
1482
1537
  }
1483
- if(!this.#legacySpeechCapability(role,providerId)){
1538
+ if(!this.#builtInSpeechCapability(role,providerId)){
1484
1539
  return completeValue({
1485
1540
  available:false,
1486
1541
  code:providerId==='LOCAL_SPEACH'
@@ -1501,17 +1556,17 @@ class AI {
1501
1556
  });
1502
1557
  }
1503
1558
 
1504
- #createLegacySpeechProvider(role,providerId){
1559
+ #createBuiltInSpeechProvider(role,providerId){
1505
1560
  const runtime=this;
1506
1561
  const localOnly=providerId==='LOCAL_SPEACH';
1507
1562
  const expectedOperation=role==='stt'?'transcribe':'synthesize';
1508
1563
  let state='unloaded';
1509
1564
  let busy=false;
1510
1565
 
1511
- function statusLegacySpeechProvider(){
1566
+ function statusBuiltInSpeechProvider(){
1512
1567
  if(state==='ready'
1513
1568
  &&!busy
1514
- &&!runtime.#legacySpeechCapability(role,providerId)){
1569
+ &&!runtime.#builtInSpeechCapability(role,providerId)){
1515
1570
  state='unloaded';
1516
1571
  }
1517
1572
  return completeValue({
@@ -1521,14 +1576,14 @@ class AI {
1521
1576
  });
1522
1577
  }
1523
1578
 
1524
- function assertLegacySpeechSelection(selection){
1525
- const inspection=runtime.#legacySpeechInspection(
1579
+ function assertBuiltInSpeechSelection(selection){
1580
+ const inspection=runtime.#builtInSpeechInspection(
1526
1581
  role,
1527
1582
  providerId,
1528
1583
  selection
1529
1584
  );
1530
1585
  if(!inspection.available){
1531
- throw legacyAIProviderError(
1586
+ throw aiProviderError(
1532
1587
  inspection.message,
1533
1588
  inspection.code
1534
1589
  );
@@ -1536,7 +1591,7 @@ class AI {
1536
1591
  return inspection;
1537
1592
  }
1538
1593
 
1539
- function releaseLegacySpeechRequest(){
1594
+ function releaseBuiltInSpeechRequest(){
1540
1595
  busy=false;
1541
1596
  }
1542
1597
 
@@ -1545,12 +1600,12 @@ class AI {
1545
1600
  role,
1546
1601
  id:providerId,
1547
1602
  localOnly,
1548
- catalog:function catalogLegacySpeechProvider(){
1549
- const model=runtime.#legacySpeechModel(role);
1550
- if(runtime.#legacySpeechService(role)!==providerId||!model){
1603
+ catalog:function catalogBuiltInSpeechProvider(){
1604
+ const model=runtime.#builtInSpeechModel(role);
1605
+ if(runtime.#builtInSpeechService(role)!==providerId||!model){
1551
1606
  return completeValue([]);
1552
1607
  }
1553
- const defaultVoice=runtime.#legacySpeechDefaultVoice(
1608
+ const defaultVoice=runtime.#builtInSpeechDefaultVoice(
1554
1609
  role,
1555
1610
  providerId
1556
1611
  );
@@ -1561,35 +1616,35 @@ class AI {
1561
1616
  })
1562
1617
  ]);
1563
1618
  },
1564
- inspect:function inspectLegacySpeechProvider(selection,{signal}={}){
1619
+ inspect:function inspectBuiltInSpeechProvider(selection,{signal}={}){
1565
1620
  if(signal?.aborted){
1566
1621
  throw normalizeAIRequestAbort(signal.reason);
1567
1622
  }
1568
- return runtime.#legacySpeechInspection(role,providerId,selection);
1623
+ return runtime.#builtInSpeechInspection(role,providerId,selection);
1569
1624
  },
1570
- status:statusLegacySpeechProvider,
1571
- load:function loadLegacySpeechProvider(context={}){
1625
+ status:statusBuiltInSpeechProvider,
1626
+ load:function loadBuiltInSpeechProvider(context={}){
1572
1627
  if(context.signal?.aborted){
1573
1628
  throw normalizeAIRequestAbort(context.signal.reason);
1574
1629
  }
1575
1630
  if(state==='disposed'){
1576
- throw legacyAIProviderError(
1577
- `The legacy ${role.toUpperCase()} provider is disposed.`,
1631
+ throw aiProviderError(
1632
+ `The built-in ${role.toUpperCase()} provider is disposed.`,
1578
1633
  'ARCANE_AI_PROVIDER_DISPOSED'
1579
1634
  );
1580
1635
  }
1581
1636
  if(busy){
1582
- throw legacyAIProviderError(
1583
- `The legacy ${role.toUpperCase()} provider owns an active request.`,
1637
+ throw aiProviderError(
1638
+ `The built-in ${role.toUpperCase()} provider owns an active request.`,
1584
1639
  'ARCANE_AI_ROLE_BUSY'
1585
1640
  );
1586
1641
  }
1587
1642
  if(typeof context.progress!=='function'){
1588
1643
  throw new TypeError(
1589
- `Legacy ${role.toUpperCase()} provider load progress must be a function.`
1644
+ `Built-in ${role.toUpperCase()} provider load progress must be a function.`
1590
1645
  );
1591
1646
  }
1592
- const inspection=assertLegacySpeechSelection(context.selection);
1647
+ const inspection=assertBuiltInSpeechSelection(context.selection);
1593
1648
  state='loading';
1594
1649
  context.progress({
1595
1650
  phase:'capability',
@@ -1612,135 +1667,135 @@ class AI {
1612
1667
  });
1613
1668
  return completeValue({
1614
1669
  authority:inspection.authority,
1615
- status:statusLegacySpeechProvider()
1670
+ status:statusBuiltInSpeechProvider()
1616
1671
  });
1617
1672
  },
1618
- request:function requestLegacySpeechProvider(context={}){
1673
+ request:function requestBuiltInSpeechProvider(context={}){
1619
1674
  if(context.signal?.aborted){
1620
1675
  throw normalizeAIRequestAbort(context.signal.reason);
1621
1676
  }
1622
- assertLegacySpeechSelection(context.selection);
1623
- const current=statusLegacySpeechProvider();
1677
+ assertBuiltInSpeechSelection(context.selection);
1678
+ const current=statusBuiltInSpeechProvider();
1624
1679
  if(current.state!=='ready'||!current.loaded){
1625
- throw legacyAIProviderError(
1626
- `The legacy ${role.toUpperCase()} provider is not ready.`,
1680
+ throw aiProviderError(
1681
+ `The built-in ${role.toUpperCase()} provider is not ready.`,
1627
1682
  'ARCANE_AI_ROLE_NOT_READY'
1628
1683
  );
1629
1684
  }
1630
1685
  if(busy){
1631
- throw legacyAIProviderError(
1632
- `The legacy ${role.toUpperCase()} provider owns an active request.`,
1686
+ throw aiProviderError(
1687
+ `The built-in ${role.toUpperCase()} provider owns an active request.`,
1633
1688
  'ARCANE_AI_ROLE_BUSY'
1634
1689
  );
1635
1690
  }
1636
1691
  if(context.operation!==expectedOperation){
1637
- throw legacyAIProviderError(
1638
- `The legacy ${role.toUpperCase()} provider operation is unsupported.`,
1692
+ throw aiProviderError(
1693
+ `The built-in ${role.toUpperCase()} provider operation is unsupported.`,
1639
1694
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
1640
1695
  );
1641
1696
  }
1642
1697
  busy=true;
1643
1698
  const request=role==='stt'
1644
- ?runtime.#requestLegacySpeechTranscription(
1699
+ ?runtime.#requestBuiltInSpeechTranscription(
1645
1700
  context.payload,
1646
1701
  context.signal
1647
1702
  )
1648
- :runtime.#requestLegacySpeechSynthesis(
1703
+ :runtime.#requestBuiltInSpeechSynthesis(
1649
1704
  context.payload,
1650
1705
  context.signal
1651
1706
  );
1652
- return Promise.resolve(request).finally(releaseLegacySpeechRequest);
1707
+ return Promise.resolve(request).finally(releaseBuiltInSpeechRequest);
1653
1708
  },
1654
- unload:function unloadLegacySpeechProvider(context={}){
1709
+ unload:function unloadBuiltInSpeechProvider(context={}){
1655
1710
  if(context.signal?.aborted){
1656
1711
  throw normalizeAIRequestAbort(context.signal.reason);
1657
1712
  }
1658
1713
  if(busy){
1659
- throw legacyAIProviderError(
1660
- `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.`,
1661
1716
  'ARCANE_AI_ROLE_BUSY'
1662
1717
  );
1663
1718
  }
1664
1719
  state='unloaded';
1665
- return statusLegacySpeechProvider();
1720
+ return statusBuiltInSpeechProvider();
1666
1721
  },
1667
- dispose:function disposeLegacySpeechProvider(context={}){
1722
+ dispose:function disposeBuiltInSpeechProvider(context={}){
1668
1723
  if(context.signal?.aborted){
1669
1724
  throw normalizeAIRequestAbort(context.signal.reason);
1670
1725
  }
1671
1726
  if(busy){
1672
- throw legacyAIProviderError(
1673
- `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.`,
1674
1729
  'ARCANE_AI_ROLE_BUSY'
1675
1730
  );
1676
1731
  }
1677
1732
  state='disposed';
1678
- return statusLegacySpeechProvider();
1733
+ return statusBuiltInSpeechProvider();
1679
1734
  }
1680
1735
  });
1681
1736
  }
1682
1737
 
1683
- #ensureLegacyLLMProvider(providerId){
1684
- if(providerId!=='OPENAI'&&providerId!=='OLLAMA'){
1738
+ #ensureBuiltInLLMProvider(providerId){
1739
+ if(providerId!=='TWIN'&&providerId!=='OLLAMA'){
1685
1740
  return false;
1686
1741
  }
1687
1742
  if(this.#providerRuntime.hasProvider('llm',providerId)){
1688
1743
  return false;
1689
1744
  }
1690
- const provider=this.#createLegacyLLMProvider(providerId);
1745
+ const provider=this.#createBuiltInLLMProvider(providerId);
1691
1746
  const unregister=this.#providerRuntime.register(provider);
1692
- this.#legacyLLMProviders.set(
1747
+ this.#builtInLLMProviders.set(
1693
1748
  providerId,
1694
1749
  completeValue({provider,unregister})
1695
1750
  );
1696
1751
  return true;
1697
1752
  }
1698
1753
 
1699
- #ensureLegacySpeechProvider(role,providerId){
1754
+ #ensureBuiltInSpeechProvider(role,providerId){
1700
1755
  if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
1701
1756
  return false;
1702
1757
  }
1703
1758
  if(this.#providerRuntime.hasProvider(role,providerId)){
1704
1759
  return false;
1705
1760
  }
1706
- const provider=this.#createLegacySpeechProvider(role,providerId);
1761
+ const provider=this.#createBuiltInSpeechProvider(role,providerId);
1707
1762
  const unregister=this.#providerRuntime.register(provider);
1708
- this.#legacySpeechProviders.set(
1709
- this.#legacySpeechProviderKey(role,providerId),
1763
+ this.#builtInSpeechProviders.set(
1764
+ this.#builtInSpeechProviderKey(role,providerId),
1710
1765
  completeValue({role,providerId,provider,unregister})
1711
1766
  );
1712
1767
  return true;
1713
1768
  }
1714
1769
 
1715
- #releaseInactiveLegacyLLMProviders(activeProviderId){
1716
- for(const [providerId,record] of this.#legacyLLMProviders){
1770
+ #releaseInactiveBuiltInLLMProviders(activeProviderId){
1771
+ for(const [providerId,record] of this.#builtInLLMProviders){
1717
1772
  if(providerId===activeProviderId){
1718
1773
  continue;
1719
1774
  }
1720
1775
  if(record.unregister()){
1721
- this.#legacyLLMProviders.delete(providerId);
1776
+ this.#builtInLLMProviders.delete(providerId);
1722
1777
  }
1723
1778
  }
1724
1779
  }
1725
1780
 
1726
- #releaseInactiveLegacySpeechProviders(activeProviders){
1727
- for(const [key,record] of this.#legacySpeechProviders){
1781
+ #releaseInactiveBuiltInSpeechProviders(activeProviders){
1782
+ for(const [key,record] of this.#builtInSpeechProviders){
1728
1783
  if(activeProviders[record.role]===record.providerId){
1729
1784
  continue;
1730
1785
  }
1731
1786
  if(record.unregister()){
1732
- this.#legacySpeechProviders.delete(key);
1787
+ this.#builtInSpeechProviders.delete(key);
1733
1788
  }
1734
1789
  }
1735
1790
  }
1736
1791
 
1737
- #internalLegacyLLMSelection(localOnly=false){
1792
+ #builtInLLMSelection(localOnly=false){
1738
1793
  const selection=this.#providerRuntime.selection(
1739
1794
  'llm',
1740
1795
  {localOnly}
1741
1796
  );
1742
1797
  if(!selection
1743
- ||!this.#legacyLLMProviders.has(selection.providerId)
1798
+ ||!this.#builtInLLMProviders.has(selection.providerId)
1744
1799
  ||selection.providerId!==this.llmService
1745
1800
  ||selection.modelId!==this.model){
1746
1801
  return null;
@@ -1748,44 +1803,44 @@ class AI {
1748
1803
  return selection;
1749
1804
  }
1750
1805
 
1751
- #internalLegacySpeechSelection(role,localOnly=false){
1806
+ #builtInSpeechSelection(role,localOnly=false){
1752
1807
  const selection=this.#providerRuntime.selection(role,{localOnly});
1753
1808
  if(!selection
1754
- ||!this.#legacySpeechProviders.has(
1755
- this.#legacySpeechProviderKey(role,selection.providerId)
1809
+ ||!this.#builtInSpeechProviders.has(
1810
+ this.#builtInSpeechProviderKey(role,selection.providerId)
1756
1811
  )
1757
- ||selection.providerId!==this.#legacySpeechService(role)
1758
- ||selection.modelId!==this.#legacySpeechModel(role)){
1812
+ ||selection.providerId!==this.#builtInSpeechService(role)
1813
+ ||selection.modelId!==this.#builtInSpeechModel(role)){
1759
1814
  return null;
1760
1815
  }
1761
1816
  return selection;
1762
1817
  }
1763
1818
 
1764
- #retainLegacyLLMReadiness(operation){
1765
- this.#legacyLLMReadiness=Promise.resolve(operation).catch(
1766
- function retainLegacyLLMReadinessFailure(){
1819
+ #retainBuiltInLLMReadiness(operation){
1820
+ this.#builtInLLMReadiness=Promise.resolve(operation).catch(
1821
+ function retainBuiltInLLMReadinessFailure(){
1767
1822
  return null;
1768
1823
  }
1769
1824
  );
1770
- return this.#legacyLLMReadiness;
1825
+ return this.#builtInLLMReadiness;
1771
1826
  }
1772
1827
 
1773
- #retainLegacySpeechReadiness(operation){
1774
- this.#legacySpeechReadiness=Promise.resolve(operation).catch(
1775
- function retainLegacySpeechReadinessFailure(){
1828
+ #retainBuiltInSpeechReadiness(operation){
1829
+ this.#builtInSpeechReadiness=Promise.resolve(operation).catch(
1830
+ function retainBuiltInSpeechReadinessFailure(){
1776
1831
  return null;
1777
1832
  }
1778
1833
  );
1779
- return this.#legacySpeechReadiness;
1834
+ return this.#builtInSpeechReadiness;
1780
1835
  }
1781
1836
 
1782
- #reconcileLegacyLLMReadiness(){
1783
- const selection=this.#internalLegacyLLMSelection(false);
1837
+ #reconcileBuiltInLLMReadiness(){
1838
+ const selection=this.#builtInLLMSelection(false);
1784
1839
  if(!selection){
1785
1840
  return Promise.resolve(this.#providerRuntime.status('llm'));
1786
1841
  }
1787
1842
  const status=this.#providerRuntime.status('llm');
1788
- if(this.#legacyLLMCapability(selection.providerId)){
1843
+ if(this.#builtInLLMCapability(selection.providerId)){
1789
1844
  if(status.state==='ready'&&status.loaded===true){
1790
1845
  return Promise.resolve(status);
1791
1846
  }
@@ -1800,15 +1855,15 @@ class AI {
1800
1855
  return Promise.resolve(status);
1801
1856
  }
1802
1857
 
1803
- #reconcileLegacySpeechReadiness(){
1858
+ #reconcileBuiltInSpeechReadiness(){
1804
1859
  const runtime=this;
1805
- return Promise.all(['stt','tts'].map(function reconcileLegacySpeechRole(role){
1806
- const selection=runtime.#internalLegacySpeechSelection(role,false);
1860
+ return Promise.all(['stt','tts'].map(function reconcileBuiltInSpeechRole(role){
1861
+ const selection=runtime.#builtInSpeechSelection(role,false);
1807
1862
  if(!selection){
1808
1863
  return runtime.#providerRuntime.status(role);
1809
1864
  }
1810
1865
  const status=runtime.#providerRuntime.status(role);
1811
- if(runtime.#legacySpeechCapability(role,selection.providerId)){
1866
+ if(runtime.#builtInSpeechCapability(role,selection.providerId)){
1812
1867
  return status;
1813
1868
  }
1814
1869
  if(status.loaded===true
@@ -1823,8 +1878,8 @@ class AI {
1823
1878
 
1824
1879
  get configured(){
1825
1880
  if(this.#usesProviderRuntime('llm',this.llmService)){
1826
- if(this.#internalLegacyLLMSelection(false)
1827
- &&!this.#legacyLLMCapability(this.llmService)){
1881
+ if(this.#builtInLLMSelection(false)
1882
+ &&!this.#builtInLLMCapability(this.llmService)){
1828
1883
  return false;
1829
1884
  }
1830
1885
  const state=this.#providerRuntime.status('llm');
@@ -1834,7 +1889,7 @@ class AI {
1834
1889
  return Boolean(this.model)&&Boolean(this.#nativeOllama());
1835
1890
  }
1836
1891
 
1837
- return this.llmService==='OPENAI'
1892
+ return this.llmService==='TWIN'
1838
1893
  &&Boolean(this.model)
1839
1894
  &&Boolean(this.license);
1840
1895
  }
@@ -1842,21 +1897,21 @@ class AI {
1842
1897
  #assertServiceConfigured(service=this.llmService,role='llm'){
1843
1898
  if(this.#usesProviderRuntime(role,service)){
1844
1899
  const internal=role==='llm'
1845
- ?this.#internalLegacyLLMSelection(false)
1846
- :this.#internalLegacySpeechSelection(role,false);
1900
+ ?this.#builtInLLMSelection(false)
1901
+ :this.#builtInSpeechSelection(role,false);
1847
1902
  const internalAvailable=!internal
1848
1903
  ||(role==='llm'
1849
- ?this.#legacyLLMCapability(internal.providerId)
1850
- :this.#legacySpeechCapability(role,internal.providerId));
1904
+ ?this.#builtInLLMCapability(internal.providerId)
1905
+ :this.#builtInSpeechCapability(role,internal.providerId));
1851
1906
  if(!internalAvailable){
1852
1907
  const inspection=role==='llm'
1853
- ?this.#legacyLLMInspection(internal.providerId,internal)
1854
- :this.#legacySpeechInspection(
1908
+ ?this.#builtInLLMInspection(internal.providerId,internal)
1909
+ :this.#builtInSpeechInspection(
1855
1910
  role,
1856
1911
  internal.providerId,
1857
1912
  internal
1858
1913
  );
1859
- throw legacyAIProviderError(
1914
+ throw aiProviderError(
1860
1915
  inspection.message,
1861
1916
  inspection.code
1862
1917
  );
@@ -1886,7 +1941,7 @@ class AI {
1886
1941
  throw error;
1887
1942
  }
1888
1943
 
1889
- if(service==='OPENAI'&&role==='llm'&&Boolean(this.twinKey)){
1944
+ if(service==='TWIN'&&role==='llm'&&Boolean(this.twinKey)){
1890
1945
  return true;
1891
1946
  }
1892
1947
 
@@ -1900,9 +1955,9 @@ class AI {
1900
1955
  }
1901
1956
 
1902
1957
  #shouldUseProviderRuntime(role,service,localOnly=false){
1903
- // Legacy adapters publish lifecycle without replacing established
1958
+ // Built-in adapters publish lifecycle without replacing established
1904
1959
  // public transport callbacks or their cancellation behavior.
1905
- if(role==='llm'&&this.#internalLegacyLLMSelection(localOnly)){
1960
+ if(role==='llm'&&this.#builtInLLMSelection(localOnly)){
1906
1961
  return false;
1907
1962
  }
1908
1963
  if(!localOnly){
@@ -1940,7 +1995,9 @@ class AI {
1940
1995
  speechPlaybackStarting=false;
1941
1996
  speechResumeAttempt=0;
1942
1997
  speechResumePending=false;
1943
- speechSynthesisTail=Promise.resolve();
1998
+ speechScheduleGeneration=0;
1999
+ speechScheduleContext=null;
2000
+ speechScheduleTime=0;
1944
2001
  speechUnlockHandler=null;
1945
2002
 
1946
2003
  #nextPreferenceTuple(values){
@@ -1963,7 +2020,7 @@ class AI {
1963
2020
 
1964
2021
  #assertValidProviderTuple(tuple){
1965
2022
  if(tuple[0]==='OLLAMA'){
1966
- const mappedModel=tuple[3]==='OPENAI'?null:this.#models[tuple[3]];
2023
+ const mappedModel=tuple[3]==='TWIN'?null:this.#models[tuple[3]];
1967
2024
  if(!mappedModel&&!normalizeOllamaModelIdentifier(tuple[3])){
1968
2025
  const error=new TypeError('The Ollama model preference is invalid.');
1969
2026
  error.code='AI_MODEL_INVALID';
@@ -1994,13 +2051,13 @@ class AI {
1994
2051
 
1995
2052
  #normalizedLLMModel(service,model){
1996
2053
  if(service==='OLLAMA'){
1997
- const mappedModel=model==='OPENAI'?null:this.#models[model];
2054
+ const mappedModel=model==='TWIN'?null:this.#models[model];
1998
2055
  return mappedModel
1999
2056
  ||normalizeOllamaModelIdentifier(model)
2000
2057
  ||model;
2001
2058
  }
2002
- if(service==='OPENAI'){
2003
- return this.#models.OPENAI;
2059
+ if(service==='TWIN'){
2060
+ return model==='TWIN'?this.#models.TWIN:model;
2004
2061
  }
2005
2062
  return model;
2006
2063
  }
@@ -2079,12 +2136,12 @@ class AI {
2079
2136
  const identity=providerId&&modelId
2080
2137
  ?this.#providerRuntime.providerIdentity(role,providerId)
2081
2138
  :null;
2082
- const pendingNonLegacy=Boolean(
2139
+ const pendingExternalRoute=Boolean(
2083
2140
  providerId
2084
2141
  &&modelId
2085
- &&!LEGACY_AI_SERVICES.has(providerId)
2142
+ &&!BUILT_IN_AI_SERVICES.has(providerId)
2086
2143
  );
2087
- if(!identity&&!pendingNonLegacy){
2144
+ if(!identity&&!pendingExternalRoute){
2088
2145
  selections[role]={default:null,localOnly:null};
2089
2146
  continue;
2090
2147
  }
@@ -2103,15 +2160,15 @@ class AI {
2103
2160
  return selections;
2104
2161
  }
2105
2162
 
2106
- #assertRegisteredLegacyRoutes(selections){
2163
+ #assertRegisteredBuiltInRoutes(selections){
2107
2164
  for(const role of ['llm','stt','tts']){
2108
2165
  for(const routeName of ['default','localOnly']){
2109
2166
  const selection=selections?.[role]?.[routeName];
2110
2167
  if(selection
2111
- &&LEGACY_AI_SERVICES.has(selection.providerId)
2168
+ &&BUILT_IN_AI_SERVICES.has(selection.providerId)
2112
2169
  &&!this.#providerRuntime.hasProvider(role,selection.providerId)){
2113
2170
  const error=new Error(
2114
- `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.`
2115
2172
  );
2116
2173
  error.code='ARCANE_AI_PROVIDER_UNAVAILABLE';
2117
2174
  throw error;
@@ -2196,22 +2253,22 @@ class AI {
2196
2253
  ]);
2197
2254
  this.#assertValidProviderTuple(tuple);
2198
2255
  this.#assertSynchronousBrowserSpeechSupersession('AI.setAI');
2199
- this.#ensureLegacyLLMProvider(tuple[0]);
2200
- this.#ensureLegacySpeechProvider('stt',tuple[1]);
2201
- this.#ensureLegacySpeechProvider('tts',tuple[2]);
2256
+ this.#ensureBuiltInLLMProvider(tuple[0]);
2257
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2258
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2202
2259
  this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2203
2260
  this.#invalidateSpeechControl();
2204
2261
  this.#applyPreferenceTuple(tuple);
2205
- this.#releaseInactiveLegacyLLMProviders(tuple[0]);
2206
- this.#releaseInactiveLegacySpeechProviders({
2262
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2263
+ this.#releaseInactiveBuiltInSpeechProviders({
2207
2264
  stt:tuple[1],
2208
2265
  tts:tuple[2]
2209
2266
  });
2210
- this.#retainLegacyLLMReadiness(
2211
- this.#reconcileLegacyLLMReadiness()
2267
+ this.#retainBuiltInLLMReadiness(
2268
+ this.#reconcileBuiltInLLMReadiness()
2212
2269
  );
2213
- this.#retainLegacySpeechReadiness(
2214
- this.#reconcileLegacySpeechReadiness()
2270
+ this.#retainBuiltInSpeechReadiness(
2271
+ this.#reconcileBuiltInSpeechReadiness()
2215
2272
  );
2216
2273
  return true;
2217
2274
  }
@@ -2220,33 +2277,33 @@ class AI {
2220
2277
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2221
2278
  this.#assertDeviceSpeechConfiguration(prepared);
2222
2279
  this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
2223
- this.#ensureLegacyLLMProvider(
2280
+ this.#ensureBuiltInLLMProvider(
2224
2281
  prepared.llm.default?.providerId
2225
2282
  );
2226
- this.#ensureLegacySpeechProvider(
2283
+ this.#ensureBuiltInSpeechProvider(
2227
2284
  'stt',
2228
2285
  prepared.stt.default?.providerId
2229
2286
  );
2230
- this.#ensureLegacySpeechProvider(
2287
+ this.#ensureBuiltInSpeechProvider(
2231
2288
  'tts',
2232
2289
  prepared.tts.default?.providerId
2233
2290
  );
2234
- this.#assertRegisteredLegacyRoutes(prepared);
2291
+ this.#assertRegisteredBuiltInRoutes(prepared);
2235
2292
  const configured=this.#providerRuntime.configure(prepared);
2236
2293
  this.#invalidateSpeechControl();
2237
2294
  this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2238
- this.#releaseInactiveLegacyLLMProviders(
2295
+ this.#releaseInactiveBuiltInLLMProviders(
2239
2296
  configured.llm.default?.providerId
2240
2297
  );
2241
- this.#releaseInactiveLegacySpeechProviders({
2298
+ this.#releaseInactiveBuiltInSpeechProviders({
2242
2299
  stt:configured.stt.default?.providerId,
2243
2300
  tts:configured.tts.default?.providerId
2244
2301
  });
2245
- this.#retainLegacyLLMReadiness(
2246
- this.#reconcileLegacyLLMReadiness()
2302
+ this.#retainBuiltInLLMReadiness(
2303
+ this.#reconcileBuiltInLLMReadiness()
2247
2304
  );
2248
- this.#retainLegacySpeechReadiness(
2249
- this.#reconcileLegacySpeechReadiness()
2305
+ this.#retainBuiltInSpeechReadiness(
2306
+ this.#reconcileBuiltInSpeechReadiness()
2250
2307
  );
2251
2308
  return configured;
2252
2309
  }
@@ -2257,26 +2314,26 @@ class AI {
2257
2314
  this.#assertSynchronousBrowserSpeechSupersession(
2258
2315
  'AI.configureSpeechProviders'
2259
2316
  );
2260
- this.#ensureLegacySpeechProvider(
2317
+ this.#ensureBuiltInSpeechProvider(
2261
2318
  'stt',
2262
2319
  prepared.stt.default?.providerId
2263
2320
  );
2264
- this.#ensureLegacySpeechProvider(
2321
+ this.#ensureBuiltInSpeechProvider(
2265
2322
  'tts',
2266
2323
  prepared.tts.default?.providerId
2267
2324
  );
2268
- this.#assertRegisteredLegacyRoutes(prepared);
2325
+ this.#assertRegisteredBuiltInRoutes(prepared);
2269
2326
  const configured=this.#providerRuntime.configureSpeech(prepared);
2270
2327
  this.#invalidateSpeechControl();
2271
2328
  this.#applySpeechPreferenceTuple(
2272
2329
  this.#tupleFromSpeechProviderRoutes(configured)
2273
2330
  );
2274
- this.#releaseInactiveLegacySpeechProviders({
2331
+ this.#releaseInactiveBuiltInSpeechProviders({
2275
2332
  stt:configured.stt.default?.providerId,
2276
2333
  tts:configured.tts.default?.providerId
2277
2334
  });
2278
- this.#retainLegacySpeechReadiness(
2279
- this.#reconcileLegacySpeechReadiness()
2335
+ this.#retainBuiltInSpeechReadiness(
2336
+ this.#reconcileBuiltInSpeechReadiness()
2280
2337
  );
2281
2338
  this.muted=true;
2282
2339
  this.stopAudio();
@@ -2303,18 +2360,18 @@ class AI {
2303
2360
  await this.#supersedeBrowserSpeechForRouteChange();
2304
2361
  this.#invalidateSpeechControl();
2305
2362
  await this.#unloadProviderRolesForTransition();
2306
- this.#ensureLegacyLLMProvider(tuple[0]);
2307
- this.#ensureLegacySpeechProvider('stt',tuple[1]);
2308
- this.#ensureLegacySpeechProvider('tts',tuple[2]);
2363
+ this.#ensureBuiltInLLMProvider(tuple[0]);
2364
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2365
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2309
2366
  this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2310
2367
  this.#applyPreferenceTuple(tuple);
2311
- this.#releaseInactiveLegacyLLMProviders(tuple[0]);
2312
- this.#releaseInactiveLegacySpeechProviders({
2368
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2369
+ this.#releaseInactiveBuiltInSpeechProviders({
2313
2370
  stt:tuple[1],
2314
2371
  tts:tuple[2]
2315
2372
  });
2316
- await this.#reconcileLegacyLLMReadiness();
2317
- await this.#reconcileLegacySpeechReadiness();
2373
+ await this.#reconcileBuiltInLLMReadiness();
2374
+ await this.#reconcileBuiltInSpeechReadiness();
2318
2375
  return this.#providerRuntime.status();
2319
2376
  }
2320
2377
 
@@ -2322,31 +2379,31 @@ class AI {
2322
2379
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2323
2380
  this.#assertDeviceSpeechConfiguration(prepared);
2324
2381
  await this.#supersedeBrowserSpeechForRouteChange();
2325
- this.#ensureLegacyLLMProvider(
2382
+ this.#ensureBuiltInLLMProvider(
2326
2383
  prepared.llm.default?.providerId
2327
2384
  );
2328
- this.#ensureLegacySpeechProvider(
2385
+ this.#ensureBuiltInSpeechProvider(
2329
2386
  'stt',
2330
2387
  prepared.stt.default?.providerId
2331
2388
  );
2332
- this.#ensureLegacySpeechProvider(
2389
+ this.#ensureBuiltInSpeechProvider(
2333
2390
  'tts',
2334
2391
  prepared.tts.default?.providerId
2335
2392
  );
2336
- this.#assertRegisteredLegacyRoutes(prepared);
2393
+ this.#assertRegisteredBuiltInRoutes(prepared);
2337
2394
  this.#invalidateSpeechControl();
2338
2395
  await this.#unloadProviderRolesForTransition();
2339
2396
  const configured=this.#providerRuntime.configure(prepared);
2340
2397
  this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2341
- this.#releaseInactiveLegacyLLMProviders(
2398
+ this.#releaseInactiveBuiltInLLMProviders(
2342
2399
  configured.llm.default?.providerId
2343
2400
  );
2344
- this.#releaseInactiveLegacySpeechProviders({
2401
+ this.#releaseInactiveBuiltInSpeechProviders({
2345
2402
  stt:configured.stt.default?.providerId,
2346
2403
  tts:configured.tts.default?.providerId
2347
2404
  });
2348
- await this.#reconcileLegacyLLMReadiness();
2349
- await this.#reconcileLegacySpeechReadiness();
2405
+ await this.#reconcileBuiltInLLMReadiness();
2406
+ await this.#reconcileBuiltInSpeechReadiness();
2350
2407
  return configured;
2351
2408
  }
2352
2409
 
@@ -2356,24 +2413,24 @@ class AI {
2356
2413
  await this.#supersedeBrowserSpeechForRouteChange();
2357
2414
  this.#invalidateSpeechControl();
2358
2415
  await this.#unloadSpeechProviderRolesForTransition();
2359
- this.#ensureLegacySpeechProvider(
2416
+ this.#ensureBuiltInSpeechProvider(
2360
2417
  'stt',
2361
2418
  prepared.stt.default?.providerId
2362
2419
  );
2363
- this.#ensureLegacySpeechProvider(
2420
+ this.#ensureBuiltInSpeechProvider(
2364
2421
  'tts',
2365
2422
  prepared.tts.default?.providerId
2366
2423
  );
2367
- this.#assertRegisteredLegacyRoutes(prepared);
2424
+ this.#assertRegisteredBuiltInRoutes(prepared);
2368
2425
  const configured=this.#providerRuntime.configureSpeech(prepared);
2369
2426
  this.#applySpeechPreferenceTuple(
2370
2427
  this.#tupleFromSpeechProviderRoutes(configured)
2371
2428
  );
2372
- this.#releaseInactiveLegacySpeechProviders({
2429
+ this.#releaseInactiveBuiltInSpeechProviders({
2373
2430
  stt:configured.stt.default?.providerId,
2374
2431
  tts:configured.tts.default?.providerId
2375
2432
  });
2376
- await this.#reconcileLegacySpeechReadiness();
2433
+ await this.#reconcileBuiltInSpeechReadiness();
2377
2434
  this.muted=true;
2378
2435
  return configured;
2379
2436
  }
@@ -2484,7 +2541,7 @@ class AI {
2484
2541
 
2485
2542
  #browserSpeechReplacementBoundary(previousRecord,roles){
2486
2543
  const expectedProviders={stt:null,tts:null};
2487
- const legacyRecords=[];
2544
+ const builtInRecords=[];
2488
2545
  for(const role of roles){
2489
2546
  if(previousRecord?.managedRoles.includes(role)){
2490
2547
  expectedProviders[role]=previousRecord.providers[role];
@@ -2495,8 +2552,8 @@ class AI {
2495
2552
  expectedProviders[role]=null;
2496
2553
  continue;
2497
2554
  }
2498
- const record=this.#legacySpeechProviders.get(
2499
- this.#legacySpeechProviderKey(role,selection.providerId)
2555
+ const record=this.#builtInSpeechProviders.get(
2556
+ this.#builtInSpeechProviderKey(role,selection.providerId)
2500
2557
  );
2501
2558
  if(!record
2502
2559
  ||!this.#providerRuntime.ownsProvider(role,record.provider)){
@@ -2509,41 +2566,41 @@ class AI {
2509
2566
  continue;
2510
2567
  }
2511
2568
  throw this.#browserSpeechProviderRouteOwnershipError(
2512
- `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.`
2513
2570
  );
2514
2571
  }
2515
2572
  expectedProviders[role]=record.provider;
2516
- legacyRecords.push(record);
2573
+ builtInRecords.push(record);
2517
2574
  }
2518
2575
  return completeValue({
2519
2576
  expectedProviders:completeValue(expectedProviders),
2520
- legacyRecords:completeValue(legacyRecords)
2577
+ builtInRecords:completeValue(builtInRecords)
2521
2578
  });
2522
2579
  }
2523
2580
 
2524
- async #cleanupRetiredLegacySpeechProviders(
2581
+ async #cleanupRetiredBuiltInSpeechProviders(
2525
2582
  {signal=null,committed=false}={}
2526
2583
  ){
2527
2584
  const failures=[];
2528
- for(const record of [...this.#browserSpeechRetiredLegacyRecords]){
2585
+ for(const record of [...this.#browserSpeechRetiredBuiltInRecords]){
2529
2586
  try{
2530
2587
  if(this.#providerRuntime.ownsProvider(
2531
2588
  record.role,
2532
2589
  record.provider
2533
2590
  )){
2534
2591
  throw this.#browserSpeechProviderRouteOwnershipError(
2535
- `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.`
2536
2593
  );
2537
2594
  }
2538
2595
  await record.provider.dispose({role:record.role,signal});
2539
- const key=this.#legacySpeechProviderKey(
2596
+ const key=this.#builtInSpeechProviderKey(
2540
2597
  record.role,
2541
2598
  record.providerId
2542
2599
  );
2543
- if(this.#legacySpeechProviders.get(key)===record){
2544
- this.#legacySpeechProviders.delete(key);
2600
+ if(this.#builtInSpeechProviders.get(key)===record){
2601
+ this.#builtInSpeechProviders.delete(key);
2545
2602
  }
2546
- this.#browserSpeechRetiredLegacyRecords.delete(record);
2603
+ this.#browserSpeechRetiredBuiltInRecords.delete(record);
2547
2604
  }catch(error){
2548
2605
  failures.push(error);
2549
2606
  }
@@ -2552,12 +2609,12 @@ class AI {
2552
2609
  throw aiBrowserSpeechError(
2553
2610
  AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
2554
2611
  AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
2555
- 'The replaced legacy speech providers could not be disposed.',
2612
+ 'The replaced built-in speech providers could not be disposed.',
2556
2613
  failures.length===1
2557
2614
  ?failures[0]
2558
2615
  :new AggregateError(
2559
2616
  failures,
2560
- 'Multiple replaced legacy speech provider disposals were rejected.'
2617
+ 'Multiple replaced built-in speech provider disposals were rejected.'
2561
2618
  ),
2562
2619
  {committed}
2563
2620
  );
@@ -2655,7 +2712,12 @@ class AI {
2655
2712
  ?{artifactGraphId:catalog.artifactGraphId}
2656
2713
  :{}),
2657
2714
  offline:configured.offline,
2658
- ...(role==='tts'?{defaultVoice:catalog.defaultVoice}:{})
2715
+ ...(role==='tts'
2716
+ ?{
2717
+ defaultVoice:catalog.defaultVoice,
2718
+ execution:configured.execution
2719
+ }
2720
+ :{})
2659
2721
  });
2660
2722
  }
2661
2723
  const roles={};
@@ -3024,7 +3086,8 @@ class AI {
3024
3086
  runtime:configured.runtime
3025
3087
  }),
3026
3088
  store,
3027
- offline:configured.offline
3089
+ offline:configured.offline,
3090
+ ...(role==='tts'?{execution:configured.execution}:{})
3028
3091
  });
3029
3092
  }
3030
3093
  providers=completeValue({...candidateProviders});
@@ -3160,8 +3223,8 @@ class AI {
3160
3223
  candidate,
3161
3224
  replacement
3162
3225
  );
3163
- for(const legacyRecord of replacementBoundary.legacyRecords){
3164
- this.#browserSpeechRetiredLegacyRecords.add(legacyRecord);
3226
+ for(const builtInRecord of replacementBoundary.builtInRecords){
3227
+ this.#browserSpeechRetiredBuiltInRecords.add(builtInRecord);
3165
3228
  }
3166
3229
  if(previousRecord){
3167
3230
  this.#retireBrowserSpeechRegistration(
@@ -3257,7 +3320,7 @@ class AI {
3257
3320
  generation,
3258
3321
  {committed:true}
3259
3322
  );
3260
- await this.#cleanupRetiredLegacySpeechProviders({
3323
+ await this.#cleanupRetiredBuiltInSpeechProviders({
3261
3324
  signal:controller.signal,
3262
3325
  committed:true
3263
3326
  });
@@ -3423,7 +3486,7 @@ class AI {
3423
3486
  record.managedRoles.includes('tts')
3424
3487
  ||record.candidateRoles.includes('tts')
3425
3488
  )
3426
- ||[...this.#browserSpeechRetiredLegacyRecords].some(
3489
+ ||[...this.#browserSpeechRetiredBuiltInRecords].some(
3427
3490
  record=>record.role==='tts'
3428
3491
  )
3429
3492
  );
@@ -3451,7 +3514,7 @@ class AI {
3451
3514
  if(activeRecord)records.add(activeRecord);
3452
3515
  const eventRecord=activeRecord||records.values().next().value||null;
3453
3516
  if(!eventRecord
3454
- &&runtime.#browserSpeechRetiredLegacyRecords.size===0){
3517
+ &&runtime.#browserSpeechRetiredBuiltInRecords.size===0){
3455
3518
  return false;
3456
3519
  }
3457
3520
  if(eventRecord){
@@ -3535,8 +3598,8 @@ class AI {
3535
3598
  );
3536
3599
  changed=true;
3537
3600
  }
3538
- if(runtime.#browserSpeechRetiredLegacyRecords.size){
3539
- await runtime.#cleanupRetiredLegacySpeechProviders({
3601
+ if(runtime.#browserSpeechRetiredBuiltInRecords.size){
3602
+ await runtime.#cleanupRetiredBuiltInSpeechProviders({
3540
3603
  signal:controller.signal,
3541
3604
  committed:true
3542
3605
  });
@@ -3707,7 +3770,7 @@ class AI {
3707
3770
  return null;
3708
3771
  }
3709
3772
 
3710
- async #requestLegacySpeechTranscription(payload={},signal=null){
3773
+ async #requestBuiltInSpeechTranscription(payload={},signal=null){
3711
3774
  const audio=payload?.audio;
3712
3775
  if(!audio||typeof audio.arrayBuffer!=='function'){
3713
3776
  throw new TypeError('Speech transcription requires an audio Blob or File.');
@@ -3758,7 +3821,7 @@ class AI {
3758
3821
  return response.text();
3759
3822
  }
3760
3823
 
3761
- async #requestLegacySpeechSynthesis(payload={},signal=null){
3824
+ async #requestBuiltInSpeechSynthesis(payload={},signal=null){
3762
3825
  const input=typeof payload?.input==='string'?payload.input:'';
3763
3826
  if(!input){
3764
3827
  throw new TypeError('Speech synthesis requires nonempty input.');
@@ -3769,7 +3832,7 @@ class AI {
3769
3832
  const model=String(payload.model||this.modelTTS);
3770
3833
  const voice=typeof payload.voice==='string'&&payload.voice.trim()
3771
3834
  ?payload.voice.trim()
3772
- :this.#legacySpeechDefaultVoice('tts',this.ttsService);
3835
+ :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
3773
3836
  if(!voice){
3774
3837
  throw new TypeError('The selected speech provider requires a voice.');
3775
3838
  }
@@ -3827,14 +3890,14 @@ class AI {
3827
3890
  if(isAIRequestAbort(error,signal)){
3828
3891
  throw normalizeAIRequestAbort(error);
3829
3892
  }
3830
- throw legacyAIProviderError(
3893
+ throw aiProviderError(
3831
3894
  'The configured TTS HTTP request failed.',
3832
3895
  'ARCANE_AI_TTS_HTTP_REQUEST_FAILED',
3833
3896
  error
3834
3897
  );
3835
3898
  }
3836
3899
  if(!response.ok){
3837
- const error=legacyAIProviderError(
3900
+ const error=aiProviderError(
3838
3901
  `The configured TTS HTTP response was rejected with status ${response.status}.`,
3839
3902
  'ARCANE_AI_TTS_HTTP_RESPONSE_REJECTED'
3840
3903
  );
@@ -4113,47 +4176,50 @@ class AI {
4113
4176
  return typeof content==='string'?content:completion;
4114
4177
  }
4115
4178
 
4116
- #requestLegacyLLMChat(payload={},signal=null){
4179
+ #requestBuiltInLLMChat(payload={},signal=null){
4117
4180
  const parallelToolCalls=payload.parallelToolCalls!==undefined
4118
4181
  ?payload.parallelToolCalls
4119
4182
  :payload.parallel_tool_calls;
4120
- return this.#fetchLegacy(
4183
+ return this.#fetchBuiltIn(
4121
4184
  payload.messages??[],
4122
- function ignoreLegacyLLMProviderResponse(){},
4185
+ function ignoreBuiltInLLMProviderResponse(){},
4123
4186
  payload.structuredOutput??false,
4124
4187
  payload.tools??[],
4125
4188
  payload.toolChoice??'auto',
4126
4189
  parallelToolCalls,
4127
4190
  payload.id??Date.now(),
4128
- function ignoreLegacyLLMProviderRequest(){},
4129
- signal
4191
+ function ignoreBuiltInLLMProviderRequest(){},
4192
+ signal,
4193
+ payload.reasoningEffort
4130
4194
  );
4131
4195
  }
4132
4196
 
4133
- #requestLegacyLLMStream(payload={},bridge){
4197
+ #requestBuiltInLLMStream(payload={},bridge){
4134
4198
  const parallelToolCalls=payload.parallelToolCalls!==undefined
4135
4199
  ?payload.parallelToolCalls
4136
4200
  :payload.parallel_tool_calls;
4137
- function emitLegacyLLMStreamData(chunk){
4201
+ function emitBuiltInLLMStreamData(chunk){
4138
4202
  bridge.emit(chunk);
4139
4203
  }
4140
4204
 
4141
- return this.#streamLegacyMessage(
4205
+ return this.#streamBuiltInMessage(
4142
4206
  payload.messages??[],
4143
- function ignoreLegacyLLMScalarStream(){},
4144
- function ignoreLegacyLLMProviderCompletion(){},
4207
+ function ignoreBuiltInLLMScalarStream(){},
4208
+ function ignoreBuiltInLLMProviderCompletion(){},
4145
4209
  payload.tools??[],
4146
4210
  payload.toolChoice??'auto',
4147
- function retainLegacyLLMStreamToolUntilCompletion(){},
4211
+ function retainBuiltInLLMStreamToolUntilCompletion(){},
4148
4212
  parallelToolCalls,
4149
4213
  payload.id??Date.now(),
4150
4214
  payload.seeThinking??false,
4151
4215
  bridge.signal,
4152
- function ignoreLegacyLLMProviderRequest(){},
4216
+ function ignoreBuiltInLLMProviderRequest(){},
4153
4217
  payload.structuredOutput??false,
4154
4218
  false,
4155
4219
  true,
4156
- emitLegacyLLMStreamData
4220
+ emitBuiltInLLMStreamData,
4221
+ function ignoreBuiltInLLMStreamResult(){},
4222
+ payload.reasoningEffort
4157
4223
  );
4158
4224
  }
4159
4225
 
@@ -4323,9 +4389,13 @@ class AI {
4323
4389
  minP,
4324
4390
  seed,
4325
4391
  stop,
4326
- templateOptions
4392
+ templateOptions,
4393
+ reasoningEffort
4327
4394
  }={}){
4328
4395
  validateAIStructuralRequest(messages,tools,parallelToolCalls);
4396
+ const normalizedReasoningEffort=normalizeAIReasoningEffort(
4397
+ reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
4398
+ );
4329
4399
  if(localOnly!==true&&localOnly!==false){
4330
4400
  throw new TypeError('AI localOnly must be a boolean.');
4331
4401
  }
@@ -4355,7 +4425,10 @@ class AI {
4355
4425
  ...(minP!==undefined?{minP}:{}),
4356
4426
  ...(seed!==undefined?{seed}:{}),
4357
4427
  ...(stop!==undefined?{stop}:{}),
4358
- ...(templateOptions!==undefined?{templateOptions}:{})
4428
+ ...(templateOptions!==undefined?{templateOptions}:{}),
4429
+ ...(normalizedReasoningEffort
4430
+ ?{reasoningEffort:normalizedReasoningEffort}
4431
+ :{})
4359
4432
  };
4360
4433
  const displayId=`M-${id}`;
4361
4434
  let handle=null;
@@ -4441,13 +4514,13 @@ class AI {
4441
4514
  }
4442
4515
 
4443
4516
  try{
4444
- const completion=await this.#streamLegacyMessage(
4517
+ const completion=await this.#streamBuiltInMessage(
4445
4518
  messages,
4446
4519
  onChunk,
4447
- function retainLegacyCompletionUntilResponse(){},
4520
+ function retainBuiltInCompletionUntilResponse(){},
4448
4521
  tools,
4449
4522
  toolChoice,
4450
- function retainLegacyToolCallUntilResponse(){},
4523
+ function retainBuiltInToolCallUntilResponse(){},
4451
4524
  parallelToolCalls,
4452
4525
  id,
4453
4526
  seeThinking,
@@ -4457,7 +4530,8 @@ class AI {
4457
4530
  false,
4458
4531
  true,
4459
4532
  onDataChunk,
4460
- onDataResult
4533
+ onDataResult,
4534
+ normalizedReasoningEffort
4461
4535
  );
4462
4536
  const structuralToolCalls=normalizeAICompletionToolCalls(
4463
4537
  completion
@@ -4527,7 +4601,7 @@ class AI {
4527
4601
  });
4528
4602
  }
4529
4603
 
4530
- return this.#streamLegacyMessage(
4604
+ return this.#streamBuiltInMessage(
4531
4605
  messages,
4532
4606
  streamHandler,
4533
4607
  streamComplete,
@@ -4547,7 +4621,7 @@ class AI {
4547
4621
  );
4548
4622
  }
4549
4623
 
4550
- async #streamLegacyMessage(
4624
+ async #streamBuiltInMessage(
4551
4625
  messages=[],
4552
4626
  streamHandler=function ignoreStreamChunk(){},
4553
4627
  streamComplete=function finishIgnoredStream(){},
@@ -4562,8 +4636,9 @@ class AI {
4562
4636
  structuredOutput=false,
4563
4637
  finishSpeech=true,
4564
4638
  returnCompletion=false,
4565
- dataChunkHandler=function ignoreLegacyStreamDataChunk(){},
4566
- dataResultHandler=function ignoreLegacyStreamDataResult(){}
4639
+ dataChunkHandler=function ignoreBuiltInStreamDataChunk(){},
4640
+ dataResultHandler=function ignoreBuiltInStreamDataResult(){},
4641
+ reasoningEffort
4567
4642
  ){
4568
4643
  let speechTurnCompleted=false;
4569
4644
 
@@ -4583,6 +4658,9 @@ class AI {
4583
4658
  structuredOutput
4584
4659
  );
4585
4660
 
4661
+ const normalizedReasoningEffort=normalizeAIReasoningEffort(
4662
+ reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
4663
+ );
4586
4664
  const request={
4587
4665
  model:this.model,
4588
4666
  messages:messages,
@@ -4603,8 +4681,11 @@ class AI {
4603
4681
  }
4604
4682
  }
4605
4683
 
4606
- if(this.llmService==='OLLAMA'&&this.reasoningEffort){
4607
- request.reasoning_effort=this.reasoningEffort;
4684
+ if(
4685
+ normalizedReasoningEffort
4686
+ &&(this.llmService==='TWIN'||this.llmService==='OLLAMA')
4687
+ ){
4688
+ request.reasoning_effort=normalizedReasoningEffort;
4608
4689
  }
4609
4690
 
4610
4691
  let isThinking=true;
@@ -4615,7 +4696,7 @@ class AI {
4615
4696
  const nativeOllama=this.#nativeOllama();
4616
4697
 
4617
4698
  if(this.llmService==='OLLAMA'&&!nativeOllama){
4618
- throw legacyAIProviderError(
4699
+ throw aiProviderError(
4619
4700
  'Local AI requires the capability-gated Arcane API.',
4620
4701
  'AI_NATIVE_LOCAL_REQUIRED'
4621
4702
  );
@@ -4631,7 +4712,9 @@ class AI {
4631
4712
  model:this.model,
4632
4713
  messages:ollamaMessages,
4633
4714
  stream:true,
4634
- ...(this.reasoningEffort?{think:this.reasoningEffort}:{}),
4715
+ ...(normalizedReasoningEffort
4716
+ ?{think:normalizedReasoningEffort}
4717
+ :{}),
4635
4718
  ...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
4636
4719
  ...(ollamaTools.length?{tools:ollamaTools}:{})
4637
4720
  };
@@ -5001,7 +5084,6 @@ class AI {
5001
5084
  const content=typeof choiceDelta.content==='string'
5002
5085
  ?choiceDelta.content
5003
5086
  :'';
5004
- let value=content;
5005
5087
  let reasoning='';
5006
5088
  if(seeThinking){
5007
5089
  reasoning=typeof choiceDelta.reasoning_content==='string'
@@ -5011,8 +5093,13 @@ class AI {
5011
5093
  :'';
5012
5094
  }
5013
5095
  isThinking=Boolean(reasoning);
5014
- if(reasoning) value=reasoning;
5015
- 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
+ }
5016
5103
  }
5017
5104
  }
5018
5105
 
@@ -5179,7 +5266,7 @@ class AI {
5179
5266
  const selectedTerminalChoiceIndex=selectedChoiceIndex??completionChoiceIndexes[0];
5180
5267
  const structuralToolCalls=structuralToolCallsByChoice.get(selectedTerminalChoiceIndex)||[];
5181
5268
  const completionChoices=completionChoiceIndexes
5182
- .map(function completeLegacyStreamChoice(choicePosition){
5269
+ .map(function completeBuiltInStreamChoice(choicePosition){
5183
5270
  const retained=streamedChoicesByIndex.get(choicePosition)||{
5184
5271
  choice:{index:choicePosition},
5185
5272
  message:{role:'assistant'}
@@ -5208,7 +5295,7 @@ class AI {
5208
5295
  });
5209
5296
  const completion={
5210
5297
  ...streamMetadata,
5211
- id:Object.hasOwn(streamMetadata,'id')?streamMetadata.id:`legacy-${id}`,
5298
+ id:Object.hasOwn(streamMetadata,'id')?streamMetadata.id:`stream-${id}`,
5212
5299
  object:Object.hasOwn(streamMetadata,'object')
5213
5300
  ?streamMetadata.object
5214
5301
  :'chat.completion',
@@ -5222,7 +5309,7 @@ class AI {
5222
5309
  assertAIStreamToolCallCorrelation(
5223
5310
  structuralToolCalls,
5224
5311
  terminalToolCalls,
5225
- 'The legacy HTTP stream'
5312
+ 'The built-in HTTP stream'
5226
5313
  );
5227
5314
  await dataResultHandler(completion,id);
5228
5315
  for(const call of terminalToolCalls){
@@ -5272,9 +5359,13 @@ class AI {
5272
5359
  minP,
5273
5360
  seed,
5274
5361
  stop,
5275
- templateOptions
5362
+ templateOptions,
5363
+ reasoningEffort
5276
5364
  }={}){
5277
5365
  validateAIStructuralRequest(messages,tools,parallelToolCalls);
5366
+ const normalizedReasoningEffort=normalizeAIReasoningEffort(
5367
+ reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
5368
+ );
5278
5369
  if(localOnly!==true&&localOnly!==false){
5279
5370
  throw new TypeError('AI localOnly must be a boolean.');
5280
5371
  }
@@ -5306,7 +5397,10 @@ class AI {
5306
5397
  ...(minP!==undefined?{minP}:{}),
5307
5398
  ...(seed!==undefined?{seed}:{}),
5308
5399
  ...(stop!==undefined?{stop}:{}),
5309
- ...(templateOptions!==undefined?{templateOptions}:{})
5400
+ ...(templateOptions!==undefined?{templateOptions}:{}),
5401
+ ...(normalizedReasoningEffort
5402
+ ?{reasoningEffort:normalizedReasoningEffort}
5403
+ :{})
5310
5404
  };
5311
5405
  await this.#reportRequest(onRequest,request,id);
5312
5406
  if(signal?.aborted){
@@ -5332,7 +5426,7 @@ class AI {
5332
5426
  return response;
5333
5427
  }
5334
5428
 
5335
- return this.fetch(
5429
+ return this.#fetchBuiltIn(
5336
5430
  messages,
5337
5431
  onResponse,
5338
5432
  structuredOutput,
@@ -5341,7 +5435,8 @@ class AI {
5341
5435
  parallelToolCalls,
5342
5436
  id,
5343
5437
  onRequest,
5344
- signal
5438
+ signal,
5439
+ normalizedReasoningEffort
5345
5440
  );
5346
5441
  }
5347
5442
 
@@ -5354,7 +5449,7 @@ class AI {
5354
5449
  parallel_tool_calls,
5355
5450
  id=Date.now(),
5356
5451
  requestHandler=function ignoreFetchRequest(){},
5357
- signal=null,
5452
+ signal=null
5358
5453
  ){
5359
5454
  if(this.#shouldUseProviderRuntime('llm',this.llmService,false)){
5360
5455
  return this.fetchRequest({
@@ -5371,7 +5466,7 @@ class AI {
5371
5466
  });
5372
5467
  }
5373
5468
 
5374
- return this.#fetchLegacy(
5469
+ return this.#fetchBuiltIn(
5375
5470
  messages,
5376
5471
  responseHandler,
5377
5472
  structuredOutput,
@@ -5384,7 +5479,7 @@ class AI {
5384
5479
  );
5385
5480
  }
5386
5481
 
5387
- async #fetchLegacy(
5482
+ async #fetchBuiltIn(
5388
5483
  messages=[],
5389
5484
  responseHandler=function ignoreFetchResponse(){},
5390
5485
  structuredOutput=false,
@@ -5394,6 +5489,7 @@ class AI {
5394
5489
  id=Date.now(),
5395
5490
  requestHandler=function ignoreFetchRequest(){},
5396
5491
  signal=null,
5492
+ reasoningEffort
5397
5493
  ){
5398
5494
  validateAIStructuralRequest(messages,tools,parallel_tool_calls);
5399
5495
  this.#assertServiceConfigured(this.llmService);
@@ -5408,6 +5504,9 @@ class AI {
5408
5504
  }
5409
5505
  const structuredOutputFormat=this.#structuredOutputFormat(structuredOutput);
5410
5506
 
5507
+ const normalizedReasoningEffort=normalizeAIReasoningEffort(
5508
+ reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
5509
+ );
5411
5510
  const request={
5412
5511
  model:this.model,
5413
5512
  messages:messages,
@@ -5428,14 +5527,17 @@ class AI {
5428
5527
  }
5429
5528
  }
5430
5529
 
5431
- if(this.llmService==='OLLAMA'&&this.reasoningEffort){
5432
- request.reasoning_effort=this.reasoningEffort;
5530
+ if(
5531
+ normalizedReasoningEffort
5532
+ &&(this.llmService==='TWIN'||this.llmService==='OLLAMA')
5533
+ ){
5534
+ request.reasoning_effort=normalizedReasoningEffort;
5433
5535
  }
5434
5536
 
5435
5537
  const nativeOllama=this.#nativeOllama();
5436
5538
 
5437
5539
  if(this.llmService==='OLLAMA'&&!nativeOllama){
5438
- throw legacyAIProviderError(
5540
+ throw aiProviderError(
5439
5541
  'Local AI requires the capability-gated Arcane API.',
5440
5542
  'AI_NATIVE_LOCAL_REQUIRED'
5441
5543
  );
@@ -5448,7 +5550,9 @@ class AI {
5448
5550
  model:this.model,
5449
5551
  messages:ollamaMessages,
5450
5552
  stream:false,
5451
- ...(this.reasoningEffort?{think:this.reasoningEffort}:{}),
5553
+ ...(normalizedReasoningEffort
5554
+ ?{think:normalizedReasoningEffort}
5555
+ :{}),
5452
5556
  ...(structuredOutputFormat?{format:structuredOutputFormat}:{}),
5453
5557
  ...(ollamaTools.length?{tools:ollamaTools}:{})
5454
5558
  };
@@ -5756,18 +5860,21 @@ class AI {
5756
5860
  #queueSpeechJob(text,generation){
5757
5861
  const job={
5758
5862
  abortController:null,
5863
+ audioBuffer:null,
5864
+ audioContext:null,
5759
5865
  generation,
5866
+ scheduledEnd:null,
5867
+ scheduledStart:null,
5760
5868
  sourceNode:null,
5761
5869
  state:'queued',
5762
5870
  text
5763
5871
  };
5764
5872
  const runtime=this;
5765
- const previous=this.speechSynthesisTail;
5766
5873
 
5767
5874
  this.speechJobs.push(job);
5768
5875
 
5769
- const synthesis=previous.then(
5770
- function synthesizeQueuedSpeech(){
5876
+ return Promise.resolve().then(
5877
+ function synthesizeAvailableSpeech(){
5771
5878
  return runtime.#prepareSpeechJob(job);
5772
5879
  }
5773
5880
  ).catch(
@@ -5779,14 +5886,6 @@ class AI {
5779
5886
  );
5780
5887
  }
5781
5888
  );
5782
-
5783
- this.speechSynthesisTail=synthesis.then(
5784
- function releaseSpeechSynthesisSlot(){
5785
- return undefined;
5786
- }
5787
- );
5788
-
5789
- return synthesis;
5790
5889
  }
5791
5890
 
5792
5891
  async #prepareSpeechJob(job){
@@ -5871,7 +5970,7 @@ class AI {
5871
5970
  ||!formats.every(format=>typeof format==='string'&&format.trim()===format&&format)
5872
5971
  ||typeof defaultFormat!=='string'
5873
5972
  ||!formats.includes(defaultFormat)){
5874
- throw legacyAIProviderError(
5973
+ throw aiProviderError(
5875
5974
  'The selected TTS provider returned an invalid speech format catalog.',
5876
5975
  'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
5877
5976
  );
@@ -5879,10 +5978,10 @@ class AI {
5879
5978
  if(formats.includes(this.audioFormat)){
5880
5979
  return this.audioFormat;
5881
5980
  }
5882
- if(this.audioFormat===LEGACY_TTS_RESPONSE_FORMAT){
5981
+ if(this.audioFormat===DEFAULT_TTS_RESPONSE_FORMAT){
5883
5982
  return defaultFormat;
5884
5983
  }
5885
- throw legacyAIProviderError(
5984
+ throw aiProviderError(
5886
5985
  `The selected TTS provider does not support ${this.audioFormat}.`,
5887
5986
  'ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT'
5888
5987
  );
@@ -6066,7 +6165,7 @@ class AI {
6066
6165
  ?requestedVoice
6067
6166
  :selection
6068
6167
  ?this.#providerSpeechVoice()
6069
- :this.#legacySpeechDefaultVoice('tts',this.ttsService);
6168
+ :this.#builtInSpeechDefaultVoice('tts',this.ttsService);
6070
6169
  if(!voice){
6071
6170
  const error=new TypeError(
6072
6171
  'AI.fetchTTS requires a caller- or model-catalog-admitted voice.'
@@ -6118,23 +6217,14 @@ class AI {
6118
6217
  return audio;
6119
6218
  }
6120
6219
 
6121
- return this.#requestLegacySpeechSynthesis(
6220
+ return this.#requestBuiltInSpeechSynthesis(
6122
6221
  {model,voice,input,responseFormat,speed},
6123
6222
  signal
6124
6223
  );
6125
6224
  }
6126
6225
 
6127
- async fetchSTT(
6128
- audioFile,
6129
- responseHandler=(text='')=>{},
6130
- signal=null
6131
- ){
6226
+ async fetchSTT(audioFile,signal=null){
6132
6227
  this.#assertServiceConfigured(this.sttService,'stt');
6133
- if(typeof responseHandler!=='function'){
6134
- const error=new TypeError('AI.fetchSTT responseHandler must be a function.');
6135
- error.code='ARCANE_AI_STT_RESPONSE_HANDLER_INVALID';
6136
- throw error;
6137
- }
6138
6228
  if(signal&&(
6139
6229
  typeof signal.aborted!=='boolean'
6140
6230
  ||typeof signal.addEventListener!=='function'
@@ -6176,12 +6266,10 @@ class AI {
6176
6266
  throw error;
6177
6267
  }
6178
6268
  if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6179
- await responseHandler(text);
6180
- if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6181
6269
  return text;
6182
6270
  }
6183
6271
 
6184
- const text=await this.#requestLegacySpeechTranscription(
6272
+ const text=await this.#requestBuiltInSpeechTranscription(
6185
6273
  {
6186
6274
  audio:audioFile,
6187
6275
  mimeType:String(audioFile?.type||'audio/webm'),
@@ -6190,8 +6278,6 @@ class AI {
6190
6278
  signal
6191
6279
  );
6192
6280
  if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6193
- await responseHandler(text);
6194
- if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
6195
6281
  return text;
6196
6282
  }
6197
6283
 
@@ -6199,6 +6285,9 @@ class AI {
6199
6285
  this.speechGeneration+=1;
6200
6286
  this.speechResumeAttempt+=1;
6201
6287
  this.speechResumePending=false;
6288
+ this.speechScheduleGeneration=this.speechGeneration;
6289
+ this.speechScheduleContext=null;
6290
+ this.speechScheduleTime=0;
6202
6291
  this.audioMessageChunks='';
6203
6292
  this.#clearSpeechUnlock();
6204
6293
 
@@ -6254,7 +6343,7 @@ class AI {
6254
6343
  }
6255
6344
 
6256
6345
  if(typeof context.resume!=='function'){
6257
- this.#waitForSpeechGesture();
6346
+ this.#waitForSpeechGesture(null,context);
6258
6347
  return false;
6259
6348
  }
6260
6349
 
@@ -6281,7 +6370,7 @@ class AI {
6281
6370
  if(attempt===this.speechResumeAttempt){
6282
6371
  this.speechResumePending=false;
6283
6372
  }
6284
- this.#waitForSpeechGesture(error);
6373
+ this.#waitForSpeechGesture(error,context);
6285
6374
  if(error?.name!=='NotAllowedError'){
6286
6375
  this.#publishTTSFailure(error,{
6287
6376
  boundary:'playback-resume',
@@ -6291,7 +6380,7 @@ class AI {
6291
6380
  return false;
6292
6381
  }
6293
6382
 
6294
- this.#waitForSpeechGesture();
6383
+ this.#waitForSpeechGesture(null,context);
6295
6384
  return false;
6296
6385
  }
6297
6386
 
@@ -6304,7 +6393,11 @@ class AI {
6304
6393
  ){
6305
6394
  const job=speechJob||{
6306
6395
  abortController:null,
6396
+ audioBuffer:null,
6397
+ audioContext:null,
6307
6398
  generation:this.speechGeneration,
6399
+ scheduledEnd:null,
6400
+ scheduledStart:null,
6308
6401
  sourceNode:null,
6309
6402
  state:'decoding',
6310
6403
  text:''
@@ -6320,7 +6413,9 @@ class AI {
6320
6413
 
6321
6414
  try{
6322
6415
  job.state='decoding';
6323
- const playbackContext=audioContext||this.#getSpeechAudioContext();
6416
+ const playbackContext=sourceNode?.context
6417
+ ||audioContext
6418
+ ||this.#getSpeechAudioContext();
6324
6419
  const audioBlob=new Blob(audioChunks,{type:audioType});
6325
6420
  const arrayBuffer=await audioBlob.arrayBuffer();
6326
6421
  const audioBuffer=await playbackContext.decodeAudioData(arrayBuffer);
@@ -6338,6 +6433,8 @@ class AI {
6338
6433
  preparedSource.onended=function finishQueuedSpeechSource(){
6339
6434
  runtime.nextSentance(job);
6340
6435
  };
6436
+ job.audioBuffer=audioBuffer;
6437
+ job.audioContext=preparedSource.context||playbackContext;
6341
6438
  job.sourceNode=preparedSource;
6342
6439
  job.state='ready';
6343
6440
  this.sourceNodes.push(preparedSource);
@@ -6363,7 +6460,7 @@ class AI {
6363
6460
  }
6364
6461
 
6365
6462
  async #pumpSpeechPlayback(){
6366
- if(this.speechPlaybackStarting||this.isSpeaking||this.muted){
6463
+ if(this.speechPlaybackStarting||this.muted){
6367
6464
  return false;
6368
6465
  }
6369
6466
 
@@ -6371,12 +6468,21 @@ class AI {
6371
6468
  let activeJob=null;
6372
6469
 
6373
6470
  try{
6374
- while(!this.isSpeaking&&!this.muted){
6375
- 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];
6376
6482
  activeJob=job||null;
6377
6483
 
6378
6484
  if(!job){
6379
- return false;
6485
+ break;
6380
6486
  }
6381
6487
 
6382
6488
  if(job.generation!==this.speechGeneration||['cancelled','failed'].includes(job.state)){
@@ -6384,25 +6490,32 @@ class AI {
6384
6490
  continue;
6385
6491
  }
6386
6492
 
6387
- if(job.state!=='ready'||!job.sourceNode?.buffer){
6388
- return false;
6493
+ if(!['ready','scheduled'].includes(job.state)||!job.sourceNode?.buffer){
6494
+ break;
6389
6495
  }
6390
6496
 
6391
- const audioContext=this.#getSpeechAudioContext();
6497
+ const audioContext=job.sourceNode.context||job.audioContext;
6392
6498
 
6393
6499
  if(audioContext.state!=='running'){
6394
- this.#waitForSpeechGesture();
6500
+ this.#waitForSpeechGesture(null,audioContext);
6395
6501
 
6396
6502
  if(!this.speechResumePending){
6397
6503
  this.resumeAudio(audioContext,false);
6398
6504
  }
6399
6505
 
6400
- 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;
6401
6515
  }
6402
6516
 
6403
6517
  if(
6404
6518
  this.muted
6405
- ||job!==this.speechJobs[0]
6406
6519
  ||job.generation!==this.speechGeneration
6407
6520
  ||job.state!=='ready'
6408
6521
  ){
@@ -6410,18 +6523,47 @@ class AI {
6410
6523
  }
6411
6524
 
6412
6525
  try{
6413
- 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;
6414
6548
  job.sourceNode.__arcaneStarted=true;
6415
- this.currentSpeechJob=job;
6549
+ if(!this.currentSpeechJob){
6550
+ this.currentSpeechJob=job;
6551
+ }
6416
6552
  this.isSpeaking=true;
6417
- await job.sourceNode.start(0);
6418
- 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;
6419
6562
  }catch(error){
6420
- this.currentSpeechJob=null;
6421
- this.isSpeaking=false;
6422
6563
  this.#failSpeechJob(job,error,'playback-start');
6423
6564
  }
6424
6565
  }
6566
+ return scheduled;
6425
6567
  }catch(error){
6426
6568
  if(activeJob){
6427
6569
  this.#failSpeechJob(activeJob,error,'playback-start');
@@ -6433,11 +6575,20 @@ class AI {
6433
6575
  this.speechPlaybackStarting=false;
6434
6576
 
6435
6577
  if(
6436
- !this.isSpeaking
6437
- &&!this.muted
6578
+ !this.muted
6438
6579
  &&!this.speechAwaitingGesture
6439
6580
  &&!this.speechResumePending
6440
- &&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'
6441
6592
  ){
6442
6593
  this.#requestSpeechPlayback();
6443
6594
  }
@@ -6459,9 +6610,17 @@ class AI {
6459
6610
  this.#removeSpeechJob(job);
6460
6611
 
6461
6612
  if(this.currentSpeechJob===job){
6462
- this.currentSpeechJob=null;
6463
- this.isSpeaking=false;
6613
+ this.currentSpeechJob=this.speechJobs.find(
6614
+ function findNextScheduledSpeechJob(candidate){
6615
+ return candidate.state==='scheduled';
6616
+ }
6617
+ )||null;
6464
6618
  }
6619
+ this.isSpeaking=this.speechJobs.some(
6620
+ function hasScheduledSpeechJob(candidate){
6621
+ return candidate.state==='scheduled';
6622
+ }
6623
+ );
6465
6624
 
6466
6625
  this.#requestSpeechPlayback();
6467
6626
  return true;
@@ -6551,9 +6710,17 @@ class AI {
6551
6710
  this.#removeSpeechJob(job);
6552
6711
 
6553
6712
  if(this.currentSpeechJob===job){
6554
- this.currentSpeechJob=null;
6555
- this.isSpeaking=false;
6713
+ this.currentSpeechJob=this.speechJobs.find(
6714
+ function findRemainingScheduledSpeechJob(candidate){
6715
+ return candidate.state==='scheduled';
6716
+ }
6717
+ )||null;
6556
6718
  }
6719
+ this.isSpeaking=this.speechJobs.some(
6720
+ function hasRemainingScheduledSpeechJob(candidate){
6721
+ return candidate.state==='scheduled';
6722
+ }
6723
+ );
6557
6724
 
6558
6725
  this.#requestSpeechPlayback();
6559
6726
  return false;
@@ -6575,7 +6742,7 @@ class AI {
6575
6742
  job.sourceNode?.disconnect?.();
6576
6743
  }
6577
6744
 
6578
- #waitForSpeechGesture(error=null){
6745
+ #waitForSpeechGesture(error=null,audioContext=null){
6579
6746
  if(this.speechUnlockHandler){
6580
6747
  return false;
6581
6748
  }
@@ -6586,7 +6753,7 @@ class AI {
6586
6753
  this.speechAwaitingGesture=true;
6587
6754
  this.speechUnlockHandler=function unlockSpeechFromUserGesture(){
6588
6755
  runtime.#clearSpeechUnlock();
6589
- runtime.resumeAudio();
6756
+ return runtime.resumeAudio(audioContext);
6590
6757
  };
6591
6758
 
6592
6759
  target.addEventListener?.(