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.
- package/CHANGELOG.md +16 -0
- package/README.md +32 -27
- package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
- package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
- package/browser-runtime/ai/speech-worker-client.mjs +51 -14
- package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
- package/browser-runtime/dependencies/strong-type/package.json +22 -23
- package/browser-runtime/event-manager.mjs +27 -41
- package/package.json +2 -3
- package/runtime/arcane/components/chat.html +1 -1
- package/runtime/arcane/components/speech.html +8 -38
- package/runtime/arcane/components/voice-transcription.html +1 -1
- package/runtime/arcane/css/theme.css +1 -1
- package/runtime/arcane/entities/Chat.js +12 -7
- package/runtime/arcane/modules/AI.js +506 -391
- package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
- package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
- package/runtime/arcane/modules/AIRuntimeState.js +2 -18
- package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
- package/runtime/arcane/modules/CalculatorEngine.js +0 -4
- package/runtime/arcane/modules/ChatRecords.js +169 -1
- package/runtime/arcane/modules/CommunicationHub.js +0 -19
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
- package/runtime/arcane/modules/Errors.js +6 -19
- package/runtime/arcane/modules/Mail.js +1 -2
- package/runtime/arcane/modules/MailTransport.mjs +1 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
- package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
- package/runtime/arcane/modules/ScreenCapture.js +2 -4
- package/runtime/arcane/modules/SpeechPlayback.js +0 -27
- package/runtime/arcane/modules/WaitForComponent.js +0 -1
- package/src/app-descriptor.mjs +1 -1
- package/src/doctor.mjs +1 -3
- package/src/event-manager.mjs +27 -41
- package/src/import-map.mjs +1 -1
- package/src/index.mjs +2 -9
- package/src/installed-sdk-runtime.mjs +1 -11
- package/src/mail-api.mjs +0 -1
- package/src/native-provider-loader.mjs +0 -9
- package/src/scaffold.mjs +9 -21
- package/src/toolchain.mjs +5 -28
- package/src/workspace-runtime.mjs +0 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
552
|
+
function forwardBuiltInAIStreamAbort(){
|
|
547
553
|
if(!controller.signal.aborted){
|
|
548
554
|
controller.abort(sourceSignal?.reason);
|
|
549
555
|
}
|
|
550
556
|
}
|
|
551
557
|
|
|
552
|
-
function
|
|
558
|
+
function detachBuiltInAIStreamAbort(){
|
|
553
559
|
if(detached){
|
|
554
560
|
return;
|
|
555
561
|
}
|
|
556
562
|
detached=true;
|
|
557
563
|
sourceSignal?.removeEventListener?.(
|
|
558
564
|
'abort',
|
|
559
|
-
|
|
565
|
+
forwardBuiltInAIStreamAbort
|
|
560
566
|
);
|
|
561
567
|
}
|
|
562
568
|
|
|
563
569
|
if(sourceSignal?.aborted){
|
|
564
|
-
|
|
570
|
+
forwardBuiltInAIStreamAbort();
|
|
565
571
|
}else{
|
|
566
572
|
sourceSignal?.addEventListener?.(
|
|
567
573
|
'abort',
|
|
568
|
-
|
|
574
|
+
forwardBuiltInAIStreamAbort,
|
|
569
575
|
{once:true}
|
|
570
576
|
);
|
|
571
577
|
}
|
|
572
578
|
|
|
573
|
-
function
|
|
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
|
|
592
|
+
function finishBuiltInAIStream(error){
|
|
587
593
|
if(complete){
|
|
588
594
|
return;
|
|
589
595
|
}
|
|
590
596
|
complete=true;
|
|
591
597
|
failure=error||null;
|
|
592
|
-
|
|
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
|
|
610
|
+
function executeBuiltInAIStream(){
|
|
605
611
|
if(controller.signal.aborted){
|
|
606
612
|
throw normalizeAIRequestAbort(controller.signal.reason);
|
|
607
613
|
}
|
|
608
614
|
return execute({
|
|
609
|
-
emit:
|
|
615
|
+
emit:emitBuiltInAIStreamChunk,
|
|
610
616
|
signal:controller.signal
|
|
611
617
|
});
|
|
612
618
|
}
|
|
613
619
|
).then(
|
|
614
|
-
function
|
|
615
|
-
|
|
620
|
+
function acceptBuiltInAIStreamResult(value){
|
|
621
|
+
finishBuiltInAIStream(null);
|
|
616
622
|
return value;
|
|
617
623
|
},
|
|
618
|
-
function
|
|
624
|
+
function rejectBuiltInAIStreamResult(error){
|
|
619
625
|
const normalized=isAIRequestAbort(error,controller.signal)
|
|
620
626
|
?normalizeAIRequestAbort(error)
|
|
621
627
|
:error;
|
|
622
|
-
|
|
628
|
+
finishBuiltInAIStream(normalized);
|
|
623
629
|
throw normalized;
|
|
624
630
|
}
|
|
625
631
|
);
|
|
626
|
-
result.catch(function
|
|
632
|
+
result.catch(function retainBuiltInAIStreamFailure() {});
|
|
627
633
|
|
|
628
|
-
async function
|
|
634
|
+
async function cancelBuiltInAIStream(reason){
|
|
629
635
|
if(!controller.signal.aborted){
|
|
630
636
|
controller.abort(reason);
|
|
631
637
|
}
|
|
632
|
-
await result.catch(function
|
|
638
|
+
await result.catch(function retainCancelledBuiltInAIStream() {});
|
|
633
639
|
return true;
|
|
634
640
|
}
|
|
635
641
|
|
|
636
642
|
const handle={
|
|
637
643
|
result,
|
|
638
|
-
cancel:
|
|
639
|
-
next:function
|
|
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
|
|
654
|
+
return new Promise(function waitForBuiltInAIStreamChunk(resolve,reject){
|
|
649
655
|
waiters.push({resolve,reject});
|
|
650
656
|
});
|
|
651
657
|
},
|
|
652
|
-
return:async function
|
|
653
|
-
await
|
|
654
|
-
|
|
655
|
-
'The
|
|
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
|
|
662
|
-
await
|
|
667
|
+
throw:async function throwBuiltInAIStream(error){
|
|
668
|
+
await cancelBuiltInAIStream(error);
|
|
663
669
|
throw error;
|
|
664
670
|
},
|
|
665
|
-
[Symbol.asyncIterator]:function
|
|
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
|
-
//
|
|
978
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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||'
|
|
1141
|
+
llmService||'TWIN',
|
|
1090
1142
|
sttService||'LOCAL_SPEACH',
|
|
1091
1143
|
ttsService||'LOCAL_SPEACH',
|
|
1092
|
-
model||'
|
|
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
|
|
1104
|
-
runtime.#
|
|
1105
|
-
runtime.#
|
|
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
|
-
#
|
|
1172
|
+
#browserSpeechRetiredBuiltInRecords=new Set();
|
|
1121
1173
|
#browserSpeechTransition=Promise.resolve();
|
|
1122
|
-
#
|
|
1123
|
-
#
|
|
1124
|
-
#
|
|
1125
|
-
#
|
|
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
|
-
'
|
|
1183
|
+
'TWIN',
|
|
1132
1184
|
'LOCAL_SPEACH',
|
|
1133
1185
|
'LOCAL_SPEACH',
|
|
1134
|
-
'
|
|
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.#
|
|
1214
|
-
this.#
|
|
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
|
-
#
|
|
1231
|
-
if(providerId==='
|
|
1232
|
-
return this.llmService==='
|
|
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
|
-
#
|
|
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
|
|
1295
|
+
message:'The selected built-in LLM route does not match the active AI configuration.'
|
|
1256
1296
|
});
|
|
1257
1297
|
}
|
|
1258
|
-
if(!this.#
|
|
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
|
-
#
|
|
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
|
|
1325
|
+
function statusBuiltInLLMProvider(){
|
|
1286
1326
|
if(state==='ready'
|
|
1287
1327
|
&&!busy
|
|
1288
|
-
&&!runtime.#
|
|
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
|
|
1299
|
-
const inspection=runtime.#
|
|
1338
|
+
function assertBuiltInLLMSelection(selection){
|
|
1339
|
+
const inspection=runtime.#builtInLLMInspection(
|
|
1300
1340
|
providerId,
|
|
1301
1341
|
selection
|
|
1302
1342
|
);
|
|
1303
1343
|
if(!inspection.available){
|
|
1304
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
1369
|
+
inspect:function inspectBuiltInLLMProvider(selection,{signal}={}){
|
|
1330
1370
|
if(signal?.aborted){
|
|
1331
1371
|
throw normalizeAIRequestAbort(signal.reason);
|
|
1332
1372
|
}
|
|
1333
|
-
return runtime.#
|
|
1373
|
+
return runtime.#builtInLLMInspection(providerId,selection);
|
|
1334
1374
|
},
|
|
1335
|
-
status:
|
|
1336
|
-
load:function
|
|
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
|
|
1342
|
-
'The
|
|
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
|
|
1348
|
-
'The
|
|
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
|
-
'
|
|
1394
|
+
'Built-in LLM provider load progress must be a function.'
|
|
1355
1395
|
);
|
|
1356
1396
|
}
|
|
1357
|
-
const inspection=
|
|
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:
|
|
1420
|
+
status:statusBuiltInLLMProvider()
|
|
1381
1421
|
});
|
|
1382
1422
|
},
|
|
1383
|
-
request:function
|
|
1423
|
+
request:function requestBuiltInLLMProvider(context={}){
|
|
1384
1424
|
if(context.signal?.aborted){
|
|
1385
1425
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1386
1426
|
}
|
|
1387
|
-
|
|
1388
|
-
const current=
|
|
1427
|
+
assertBuiltInLLMSelection(context.selection);
|
|
1428
|
+
const current=statusBuiltInLLMProvider();
|
|
1389
1429
|
if(current.state!=='ready'||!current.loaded){
|
|
1390
|
-
throw
|
|
1391
|
-
'The
|
|
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
|
|
1397
|
-
'The
|
|
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.#
|
|
1444
|
+
runtime.#requestBuiltInLLMChat(
|
|
1405
1445
|
context.payload,
|
|
1406
1446
|
context.signal
|
|
1407
1447
|
)
|
|
1408
|
-
).finally(
|
|
1448
|
+
).finally(releaseBuiltInLLMRequest);
|
|
1409
1449
|
}
|
|
1410
1450
|
if(context.operation==='stream'){
|
|
1411
|
-
const handle=
|
|
1412
|
-
function
|
|
1413
|
-
return runtime.#
|
|
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
|
-
|
|
1422
|
-
|
|
1461
|
+
releaseBuiltInLLMRequest,
|
|
1462
|
+
releaseBuiltInLLMRequest
|
|
1423
1463
|
);
|
|
1424
1464
|
return handle;
|
|
1425
1465
|
}
|
|
1426
1466
|
busy=false;
|
|
1427
|
-
throw
|
|
1428
|
-
'The
|
|
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
|
|
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
|
|
1478
|
+
return statusBuiltInLLMProvider();
|
|
1439
1479
|
},
|
|
1440
|
-
dispose:function
|
|
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
|
|
1486
|
+
return statusBuiltInLLMProvider();
|
|
1447
1487
|
}
|
|
1448
1488
|
});
|
|
1449
1489
|
}
|
|
1450
1490
|
|
|
1451
|
-
#
|
|
1491
|
+
#builtInSpeechService(role){
|
|
1452
1492
|
return role==='stt'?this.sttService:this.ttsService;
|
|
1453
1493
|
}
|
|
1454
1494
|
|
|
1455
|
-
#
|
|
1495
|
+
#builtInSpeechModel(role){
|
|
1456
1496
|
return role==='stt'?this.modelSTT:this.modelTTS;
|
|
1457
1497
|
}
|
|
1458
1498
|
|
|
1459
|
-
#
|
|
1499
|
+
#builtInSpeechProviderKey(role,providerId){
|
|
1460
1500
|
return `${role}:${providerId}`;
|
|
1461
1501
|
}
|
|
1462
1502
|
|
|
1463
|
-
#
|
|
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
|
-
#
|
|
1474
|
-
const service=this.#
|
|
1475
|
-
const model=this.#
|
|
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
|
-
#
|
|
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.#
|
|
1529
|
+
||selection.modelId!==this.#builtInSpeechModel(role)
|
|
1490
1530
|
||selection.localOnly!==localOnly
|
|
1491
|
-
||this.#
|
|
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
|
|
1535
|
+
message:`The selected built-in ${role.toUpperCase()} route does not match the active AI configuration.`
|
|
1496
1536
|
});
|
|
1497
1537
|
}
|
|
1498
|
-
if(!this.#
|
|
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
|
-
#
|
|
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
|
|
1566
|
+
function statusBuiltInSpeechProvider(){
|
|
1527
1567
|
if(state==='ready'
|
|
1528
1568
|
&&!busy
|
|
1529
|
-
&&!runtime.#
|
|
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
|
|
1540
|
-
const inspection=runtime.#
|
|
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
|
|
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
|
|
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
|
|
1564
|
-
const model=runtime.#
|
|
1565
|
-
if(runtime.#
|
|
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.#
|
|
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
|
|
1619
|
+
inspect:function inspectBuiltInSpeechProvider(selection,{signal}={}){
|
|
1580
1620
|
if(signal?.aborted){
|
|
1581
1621
|
throw normalizeAIRequestAbort(signal.reason);
|
|
1582
1622
|
}
|
|
1583
|
-
return runtime.#
|
|
1623
|
+
return runtime.#builtInSpeechInspection(role,providerId,selection);
|
|
1584
1624
|
},
|
|
1585
|
-
status:
|
|
1586
|
-
load:function
|
|
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
|
|
1592
|
-
`The
|
|
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
|
|
1598
|
-
`The
|
|
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
|
-
`
|
|
1644
|
+
`Built-in ${role.toUpperCase()} provider load progress must be a function.`
|
|
1605
1645
|
);
|
|
1606
1646
|
}
|
|
1607
|
-
const inspection=
|
|
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:
|
|
1670
|
+
status:statusBuiltInSpeechProvider()
|
|
1631
1671
|
});
|
|
1632
1672
|
},
|
|
1633
|
-
request:function
|
|
1673
|
+
request:function requestBuiltInSpeechProvider(context={}){
|
|
1634
1674
|
if(context.signal?.aborted){
|
|
1635
1675
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1636
1676
|
}
|
|
1637
|
-
|
|
1638
|
-
const current=
|
|
1677
|
+
assertBuiltInSpeechSelection(context.selection);
|
|
1678
|
+
const current=statusBuiltInSpeechProvider();
|
|
1639
1679
|
if(current.state!=='ready'||!current.loaded){
|
|
1640
|
-
throw
|
|
1641
|
-
`The
|
|
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
|
|
1647
|
-
`The
|
|
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
|
|
1653
|
-
`The
|
|
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.#
|
|
1699
|
+
?runtime.#requestBuiltInSpeechTranscription(
|
|
1660
1700
|
context.payload,
|
|
1661
1701
|
context.signal
|
|
1662
1702
|
)
|
|
1663
|
-
:runtime.#
|
|
1703
|
+
:runtime.#requestBuiltInSpeechSynthesis(
|
|
1664
1704
|
context.payload,
|
|
1665
1705
|
context.signal
|
|
1666
1706
|
);
|
|
1667
|
-
return Promise.resolve(request).finally(
|
|
1707
|
+
return Promise.resolve(request).finally(releaseBuiltInSpeechRequest);
|
|
1668
1708
|
},
|
|
1669
|
-
unload:function
|
|
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
|
|
1675
|
-
`The
|
|
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
|
|
1720
|
+
return statusBuiltInSpeechProvider();
|
|
1681
1721
|
},
|
|
1682
|
-
dispose:function
|
|
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
|
|
1688
|
-
`The
|
|
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
|
|
1733
|
+
return statusBuiltInSpeechProvider();
|
|
1694
1734
|
}
|
|
1695
1735
|
});
|
|
1696
1736
|
}
|
|
1697
1737
|
|
|
1698
|
-
#
|
|
1699
|
-
if(providerId!=='
|
|
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.#
|
|
1745
|
+
const provider=this.#createBuiltInLLMProvider(providerId);
|
|
1706
1746
|
const unregister=this.#providerRuntime.register(provider);
|
|
1707
|
-
this.#
|
|
1747
|
+
this.#builtInLLMProviders.set(
|
|
1708
1748
|
providerId,
|
|
1709
1749
|
completeValue({provider,unregister})
|
|
1710
1750
|
);
|
|
1711
1751
|
return true;
|
|
1712
1752
|
}
|
|
1713
1753
|
|
|
1714
|
-
#
|
|
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.#
|
|
1761
|
+
const provider=this.#createBuiltInSpeechProvider(role,providerId);
|
|
1722
1762
|
const unregister=this.#providerRuntime.register(provider);
|
|
1723
|
-
this.#
|
|
1724
|
-
this.#
|
|
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
|
-
#
|
|
1731
|
-
for(const [providerId,record] of this.#
|
|
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.#
|
|
1776
|
+
this.#builtInLLMProviders.delete(providerId);
|
|
1737
1777
|
}
|
|
1738
1778
|
}
|
|
1739
1779
|
}
|
|
1740
1780
|
|
|
1741
|
-
#
|
|
1742
|
-
for(const [key,record] of this.#
|
|
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.#
|
|
1787
|
+
this.#builtInSpeechProviders.delete(key);
|
|
1748
1788
|
}
|
|
1749
1789
|
}
|
|
1750
1790
|
}
|
|
1751
1791
|
|
|
1752
|
-
#
|
|
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.#
|
|
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
|
-
#
|
|
1806
|
+
#builtInSpeechSelection(role,localOnly=false){
|
|
1767
1807
|
const selection=this.#providerRuntime.selection(role,{localOnly});
|
|
1768
1808
|
if(!selection
|
|
1769
|
-
||!this.#
|
|
1770
|
-
this.#
|
|
1809
|
+
||!this.#builtInSpeechProviders.has(
|
|
1810
|
+
this.#builtInSpeechProviderKey(role,selection.providerId)
|
|
1771
1811
|
)
|
|
1772
|
-
||selection.providerId!==this.#
|
|
1773
|
-
||selection.modelId!==this.#
|
|
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
|
-
#
|
|
1780
|
-
this.#
|
|
1781
|
-
function
|
|
1819
|
+
#retainBuiltInLLMReadiness(operation){
|
|
1820
|
+
this.#builtInLLMReadiness=Promise.resolve(operation).catch(
|
|
1821
|
+
function retainBuiltInLLMReadinessFailure(){
|
|
1782
1822
|
return null;
|
|
1783
1823
|
}
|
|
1784
1824
|
);
|
|
1785
|
-
return this.#
|
|
1825
|
+
return this.#builtInLLMReadiness;
|
|
1786
1826
|
}
|
|
1787
1827
|
|
|
1788
|
-
#
|
|
1789
|
-
this.#
|
|
1790
|
-
function
|
|
1828
|
+
#retainBuiltInSpeechReadiness(operation){
|
|
1829
|
+
this.#builtInSpeechReadiness=Promise.resolve(operation).catch(
|
|
1830
|
+
function retainBuiltInSpeechReadinessFailure(){
|
|
1791
1831
|
return null;
|
|
1792
1832
|
}
|
|
1793
1833
|
);
|
|
1794
|
-
return this.#
|
|
1834
|
+
return this.#builtInSpeechReadiness;
|
|
1795
1835
|
}
|
|
1796
1836
|
|
|
1797
|
-
#
|
|
1798
|
-
const selection=this.#
|
|
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.#
|
|
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
|
-
#
|
|
1858
|
+
#reconcileBuiltInSpeechReadiness(){
|
|
1819
1859
|
const runtime=this;
|
|
1820
|
-
return Promise.all(['stt','tts'].map(function
|
|
1821
|
-
const selection=runtime.#
|
|
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.#
|
|
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.#
|
|
1842
|
-
&&!this.#
|
|
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==='
|
|
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.#
|
|
1861
|
-
:this.#
|
|
1900
|
+
?this.#builtInLLMSelection(false)
|
|
1901
|
+
:this.#builtInSpeechSelection(role,false);
|
|
1862
1902
|
const internalAvailable=!internal
|
|
1863
1903
|
||(role==='llm'
|
|
1864
|
-
?this.#
|
|
1865
|
-
:this.#
|
|
1904
|
+
?this.#builtInLLMCapability(internal.providerId)
|
|
1905
|
+
:this.#builtInSpeechCapability(role,internal.providerId));
|
|
1866
1906
|
if(!internalAvailable){
|
|
1867
1907
|
const inspection=role==='llm'
|
|
1868
|
-
?this.#
|
|
1869
|
-
:this.#
|
|
1908
|
+
?this.#builtInLLMInspection(internal.providerId,internal)
|
|
1909
|
+
:this.#builtInSpeechInspection(
|
|
1870
1910
|
role,
|
|
1871
1911
|
internal.providerId,
|
|
1872
1912
|
internal
|
|
1873
1913
|
);
|
|
1874
|
-
throw
|
|
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==='
|
|
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
|
-
//
|
|
1958
|
+
// Built-in adapters publish lifecycle without replacing established
|
|
1919
1959
|
// public transport callbacks or their cancellation behavior.
|
|
1920
|
-
if(role==='llm'&&this.#
|
|
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
|
-
|
|
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]==='
|
|
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==='
|
|
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==='
|
|
2018
|
-
return 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
|
|
2139
|
+
const pendingExternalRoute=Boolean(
|
|
2098
2140
|
providerId
|
|
2099
2141
|
&&modelId
|
|
2100
|
-
&&!
|
|
2142
|
+
&&!BUILT_IN_AI_SERVICES.has(providerId)
|
|
2101
2143
|
);
|
|
2102
|
-
if(!identity&&!
|
|
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
|
-
#
|
|
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
|
-
&&
|
|
2168
|
+
&&BUILT_IN_AI_SERVICES.has(selection.providerId)
|
|
2127
2169
|
&&!this.#providerRuntime.hasProvider(role,selection.providerId)){
|
|
2128
2170
|
const error=new Error(
|
|
2129
|
-
`
|
|
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.#
|
|
2215
|
-
this.#
|
|
2216
|
-
this.#
|
|
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.#
|
|
2221
|
-
this.#
|
|
2262
|
+
this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
|
|
2263
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2222
2264
|
stt:tuple[1],
|
|
2223
2265
|
tts:tuple[2]
|
|
2224
2266
|
});
|
|
2225
|
-
this.#
|
|
2226
|
-
this.#
|
|
2267
|
+
this.#retainBuiltInLLMReadiness(
|
|
2268
|
+
this.#reconcileBuiltInLLMReadiness()
|
|
2227
2269
|
);
|
|
2228
|
-
this.#
|
|
2229
|
-
this.#
|
|
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.#
|
|
2280
|
+
this.#ensureBuiltInLLMProvider(
|
|
2239
2281
|
prepared.llm.default?.providerId
|
|
2240
2282
|
);
|
|
2241
|
-
this.#
|
|
2283
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2242
2284
|
'stt',
|
|
2243
2285
|
prepared.stt.default?.providerId
|
|
2244
2286
|
);
|
|
2245
|
-
this.#
|
|
2287
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2246
2288
|
'tts',
|
|
2247
2289
|
prepared.tts.default?.providerId
|
|
2248
2290
|
);
|
|
2249
|
-
this.#
|
|
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.#
|
|
2295
|
+
this.#releaseInactiveBuiltInLLMProviders(
|
|
2254
2296
|
configured.llm.default?.providerId
|
|
2255
2297
|
);
|
|
2256
|
-
this.#
|
|
2298
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2257
2299
|
stt:configured.stt.default?.providerId,
|
|
2258
2300
|
tts:configured.tts.default?.providerId
|
|
2259
2301
|
});
|
|
2260
|
-
this.#
|
|
2261
|
-
this.#
|
|
2302
|
+
this.#retainBuiltInLLMReadiness(
|
|
2303
|
+
this.#reconcileBuiltInLLMReadiness()
|
|
2262
2304
|
);
|
|
2263
|
-
this.#
|
|
2264
|
-
this.#
|
|
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.#
|
|
2317
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2276
2318
|
'stt',
|
|
2277
2319
|
prepared.stt.default?.providerId
|
|
2278
2320
|
);
|
|
2279
|
-
this.#
|
|
2321
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2280
2322
|
'tts',
|
|
2281
2323
|
prepared.tts.default?.providerId
|
|
2282
2324
|
);
|
|
2283
|
-
this.#
|
|
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.#
|
|
2331
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2290
2332
|
stt:configured.stt.default?.providerId,
|
|
2291
2333
|
tts:configured.tts.default?.providerId
|
|
2292
2334
|
});
|
|
2293
|
-
this.#
|
|
2294
|
-
this.#
|
|
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.#
|
|
2322
|
-
this.#
|
|
2323
|
-
this.#
|
|
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.#
|
|
2327
|
-
this.#
|
|
2368
|
+
this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
|
|
2369
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2328
2370
|
stt:tuple[1],
|
|
2329
2371
|
tts:tuple[2]
|
|
2330
2372
|
});
|
|
2331
|
-
await this.#
|
|
2332
|
-
await this.#
|
|
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.#
|
|
2382
|
+
this.#ensureBuiltInLLMProvider(
|
|
2341
2383
|
prepared.llm.default?.providerId
|
|
2342
2384
|
);
|
|
2343
|
-
this.#
|
|
2385
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2344
2386
|
'stt',
|
|
2345
2387
|
prepared.stt.default?.providerId
|
|
2346
2388
|
);
|
|
2347
|
-
this.#
|
|
2389
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2348
2390
|
'tts',
|
|
2349
2391
|
prepared.tts.default?.providerId
|
|
2350
2392
|
);
|
|
2351
|
-
this.#
|
|
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.#
|
|
2398
|
+
this.#releaseInactiveBuiltInLLMProviders(
|
|
2357
2399
|
configured.llm.default?.providerId
|
|
2358
2400
|
);
|
|
2359
|
-
this.#
|
|
2401
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2360
2402
|
stt:configured.stt.default?.providerId,
|
|
2361
2403
|
tts:configured.tts.default?.providerId
|
|
2362
2404
|
});
|
|
2363
|
-
await this.#
|
|
2364
|
-
await this.#
|
|
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.#
|
|
2416
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2375
2417
|
'stt',
|
|
2376
2418
|
prepared.stt.default?.providerId
|
|
2377
2419
|
);
|
|
2378
|
-
this.#
|
|
2420
|
+
this.#ensureBuiltInSpeechProvider(
|
|
2379
2421
|
'tts',
|
|
2380
2422
|
prepared.tts.default?.providerId
|
|
2381
2423
|
);
|
|
2382
|
-
this.#
|
|
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.#
|
|
2429
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2388
2430
|
stt:configured.stt.default?.providerId,
|
|
2389
2431
|
tts:configured.tts.default?.providerId
|
|
2390
2432
|
});
|
|
2391
|
-
await this.#
|
|
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
|
|
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.#
|
|
2514
|
-
this.#
|
|
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
|
|
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
|
-
|
|
2573
|
+
builtInRecords.push(record);
|
|
2532
2574
|
}
|
|
2533
2575
|
return completeValue({
|
|
2534
2576
|
expectedProviders:completeValue(expectedProviders),
|
|
2535
|
-
|
|
2577
|
+
builtInRecords:completeValue(builtInRecords)
|
|
2536
2578
|
});
|
|
2537
2579
|
}
|
|
2538
2580
|
|
|
2539
|
-
async #
|
|
2581
|
+
async #cleanupRetiredBuiltInSpeechProviders(
|
|
2540
2582
|
{signal=null,committed=false}={}
|
|
2541
2583
|
){
|
|
2542
2584
|
const failures=[];
|
|
2543
|
-
for(const record of [...this.#
|
|
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}
|
|
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.#
|
|
2596
|
+
const key=this.#builtInSpeechProviderKey(
|
|
2555
2597
|
record.role,
|
|
2556
2598
|
record.providerId
|
|
2557
2599
|
);
|
|
2558
|
-
if(this.#
|
|
2559
|
-
this.#
|
|
2600
|
+
if(this.#builtInSpeechProviders.get(key)===record){
|
|
2601
|
+
this.#builtInSpeechProviders.delete(key);
|
|
2560
2602
|
}
|
|
2561
|
-
this.#
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
3179
|
-
this.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
3554
|
-
await runtime.#
|
|
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 #
|
|
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 #
|
|
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.#
|
|
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
|
|
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=
|
|
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
|
-
#
|
|
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.#
|
|
4183
|
+
return this.#fetchBuiltIn(
|
|
4136
4184
|
payload.messages??[],
|
|
4137
|
-
function
|
|
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
|
|
4191
|
+
function ignoreBuiltInLLMProviderRequest(){},
|
|
4144
4192
|
signal,
|
|
4145
4193
|
payload.reasoningEffort
|
|
4146
4194
|
);
|
|
4147
4195
|
}
|
|
4148
4196
|
|
|
4149
|
-
#
|
|
4197
|
+
#requestBuiltInLLMStream(payload={},bridge){
|
|
4150
4198
|
const parallelToolCalls=payload.parallelToolCalls!==undefined
|
|
4151
4199
|
?payload.parallelToolCalls
|
|
4152
4200
|
:payload.parallel_tool_calls;
|
|
4153
|
-
function
|
|
4201
|
+
function emitBuiltInLLMStreamData(chunk){
|
|
4154
4202
|
bridge.emit(chunk);
|
|
4155
4203
|
}
|
|
4156
4204
|
|
|
4157
|
-
return this.#
|
|
4205
|
+
return this.#streamBuiltInMessage(
|
|
4158
4206
|
payload.messages??[],
|
|
4159
|
-
function
|
|
4160
|
-
function
|
|
4207
|
+
function ignoreBuiltInLLMScalarStream(){},
|
|
4208
|
+
function ignoreBuiltInLLMProviderCompletion(){},
|
|
4161
4209
|
payload.tools??[],
|
|
4162
4210
|
payload.toolChoice??'auto',
|
|
4163
|
-
function
|
|
4211
|
+
function retainBuiltInLLMStreamToolUntilCompletion(){},
|
|
4164
4212
|
parallelToolCalls,
|
|
4165
4213
|
payload.id??Date.now(),
|
|
4166
4214
|
payload.seeThinking??false,
|
|
4167
4215
|
bridge.signal,
|
|
4168
|
-
function
|
|
4216
|
+
function ignoreBuiltInLLMProviderRequest(){},
|
|
4169
4217
|
payload.structuredOutput??false,
|
|
4170
4218
|
false,
|
|
4171
4219
|
true,
|
|
4172
|
-
|
|
4173
|
-
function
|
|
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.#
|
|
4517
|
+
const completion=await this.#streamBuiltInMessage(
|
|
4470
4518
|
messages,
|
|
4471
4519
|
onChunk,
|
|
4472
|
-
function
|
|
4520
|
+
function retainBuiltInCompletionUntilResponse(){},
|
|
4473
4521
|
tools,
|
|
4474
4522
|
toolChoice,
|
|
4475
|
-
function
|
|
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.#
|
|
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 #
|
|
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
|
|
4592
|
-
dataResultHandler=function
|
|
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==='
|
|
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
|
|
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)
|
|
5050
|
-
|
|
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
|
|
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:`
|
|
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
|
|
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.#
|
|
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.#
|
|
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 #
|
|
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==='
|
|
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
|
|
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
|
-
|
|
5822
|
-
function
|
|
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
|
|
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===
|
|
5981
|
+
if(this.audioFormat===DEFAULT_TTS_RESPONSE_FORMAT){
|
|
5935
5982
|
return defaultFormat;
|
|
5936
5983
|
}
|
|
5937
|
-
throw
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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=
|
|
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.
|
|
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
|
-
|
|
6427
|
-
|
|
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
|
-
|
|
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
|
|
6440
|
-
|
|
6493
|
+
if(!['ready','scheduled'].includes(job.state)||!job.sourceNode?.buffer){
|
|
6494
|
+
break;
|
|
6441
6495
|
}
|
|
6442
6496
|
|
|
6443
|
-
const audioContext=
|
|
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
|
|
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.
|
|
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
|
|
6549
|
+
if(!this.currentSpeechJob){
|
|
6550
|
+
this.currentSpeechJob=job;
|
|
6551
|
+
}
|
|
6468
6552
|
this.isSpeaking=true;
|
|
6469
|
-
|
|
6470
|
-
|
|
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.
|
|
6489
|
-
&&!this.muted
|
|
6578
|
+
!this.muted
|
|
6490
6579
|
&&!this.speechAwaitingGesture
|
|
6491
6580
|
&&!this.speechResumePending
|
|
6492
|
-
|
|
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=
|
|
6515
|
-
|
|
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=
|
|
6607
|
-
|
|
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?.(
|