arcane-os 0.2.1 → 0.2.2
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 +17 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +1 -0
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +79 -13
- package/docs/reference/ai/browser-speech.md +336 -0
- package/docs/reference/ai/browser-wasm.md +207 -82
- package/docs/reference/availability-and-normalization.md +30 -4
- package/docs/reference/behavioral-testing.md +4 -1
- package/docs/reference/cli.md +29 -10
- package/docs/reference/core/arcane-ai-contracts.md +43 -9
- package/docs/reference/inventory/package-api.json +110 -14
- package/docs/reference/inventory/runtime-components.json +19 -6
- package/docs/reference/inventory/runtime-modules.json +113 -9
- package/docs/reference/protocols.md +260 -38
- package/docs/reference/runtime-components.md +108 -15
- package/docs/reference/runtime-modules.md +422 -8
- package/docs/reference/sdk-api.md +626 -85
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +15 -15
- package/runtime/arcane/components/chat.html +17 -52
- package/runtime/arcane/components/speech.html +339 -15
- package/runtime/arcane/modules/AI.js +556 -128
- package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
- package/runtime/arcane/modules/AIRuntimeState.js +23 -4
- package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
- package/schemas/arcane-lock.schema.json +2 -2
|
@@ -176,7 +176,11 @@ function createLegacyAIStreamBridge(execute,sourceSignal){
|
|
|
176
176
|
|
|
177
177
|
function normalizeAIStartupOptions(options){
|
|
178
178
|
if(options===undefined){
|
|
179
|
-
return Object.freeze({
|
|
179
|
+
return Object.freeze({
|
|
180
|
+
startMuted:true,
|
|
181
|
+
startTranscription:false,
|
|
182
|
+
signal:null
|
|
183
|
+
});
|
|
180
184
|
}
|
|
181
185
|
if(!options||typeof options!=='object'||Array.isArray(options)){
|
|
182
186
|
throw new TypeError('AI startup options must be a plain object.');
|
|
@@ -187,7 +191,11 @@ function normalizeAIStartupOptions(options){
|
|
|
187
191
|
}
|
|
188
192
|
const descriptors=Object.getOwnPropertyDescriptors(options);
|
|
189
193
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
190
|
-
if(typeof key==='symbol'||(
|
|
194
|
+
if(typeof key==='symbol'||(
|
|
195
|
+
key!=='startMuted'
|
|
196
|
+
&&key!=='startTranscription'
|
|
197
|
+
&&key!=='signal'
|
|
198
|
+
)){
|
|
191
199
|
throw new TypeError('AI startup options contain an unknown option.');
|
|
192
200
|
}
|
|
193
201
|
if(!Object.hasOwn(descriptors[key],'value')){
|
|
@@ -197,12 +205,18 @@ function normalizeAIStartupOptions(options){
|
|
|
197
205
|
const startMuted=Object.hasOwn(descriptors,'startMuted')
|
|
198
206
|
?descriptors.startMuted.value
|
|
199
207
|
:true;
|
|
208
|
+
const startTranscription=Object.hasOwn(descriptors,'startTranscription')
|
|
209
|
+
?descriptors.startTranscription.value
|
|
210
|
+
:false;
|
|
200
211
|
const signal=Object.hasOwn(descriptors,'signal')
|
|
201
212
|
?descriptors.signal.value
|
|
202
213
|
:null;
|
|
203
214
|
if(typeof startMuted!=='boolean'){
|
|
204
215
|
throw new TypeError('AI startup startMuted must be a boolean.');
|
|
205
216
|
}
|
|
217
|
+
if(typeof startTranscription!=='boolean'){
|
|
218
|
+
throw new TypeError('AI startup startTranscription must be a boolean.');
|
|
219
|
+
}
|
|
206
220
|
if(signal!==null&&signal!==undefined&&(
|
|
207
221
|
typeof signal!=='object'
|
|
208
222
|
||typeof signal.aborted!=='boolean'
|
|
@@ -211,7 +225,7 @@ function normalizeAIStartupOptions(options){
|
|
|
211
225
|
)){
|
|
212
226
|
throw new TypeError('AI startup signal must be an AbortSignal.');
|
|
213
227
|
}
|
|
214
|
-
return Object.freeze({startMuted,signal});
|
|
228
|
+
return Object.freeze({startMuted,startTranscription,signal});
|
|
215
229
|
}
|
|
216
230
|
|
|
217
231
|
class AI {
|
|
@@ -352,6 +366,8 @@ class AI {
|
|
|
352
366
|
#providerRuntime=getAIProviderRuntime();
|
|
353
367
|
#legacyLLMProviders=new Map();
|
|
354
368
|
#legacyLLMReadiness=Promise.resolve(null);
|
|
369
|
+
#legacySpeechProviders=new Map();
|
|
370
|
+
#legacySpeechReadiness=Promise.resolve(null);
|
|
355
371
|
#preferenceTuple=Object.freeze([
|
|
356
372
|
'OPENAI',
|
|
357
373
|
'OPENAI',
|
|
@@ -402,6 +418,9 @@ class AI {
|
|
|
402
418
|
this.#retainLegacyLLMReadiness(
|
|
403
419
|
this.#reconcileLegacyLLMReadiness()
|
|
404
420
|
);
|
|
421
|
+
this.#retainLegacySpeechReadiness(
|
|
422
|
+
this.#reconcileLegacySpeechReadiness()
|
|
423
|
+
);
|
|
405
424
|
return this.#license;
|
|
406
425
|
}
|
|
407
426
|
|
|
@@ -627,6 +646,263 @@ class AI {
|
|
|
627
646
|
});
|
|
628
647
|
}
|
|
629
648
|
|
|
649
|
+
#legacySpeechService(role){
|
|
650
|
+
return role==='stt'?this.sttService:this.ttsService;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
#legacySpeechModel(role){
|
|
654
|
+
return role==='stt'?this.modelSTT:this.modelTTS;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#legacySpeechProviderKey(role,providerId){
|
|
658
|
+
return `${role}:${providerId}`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
#legacySpeechDefaultVoice(role,providerId){
|
|
662
|
+
if(role!=='tts'){
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if(providerId==='OPENAI'){
|
|
666
|
+
const selected=typeof globalThis.window?.user?.AI_voice==='string'
|
|
667
|
+
?globalThis.window.user.AI_voice.trim()
|
|
668
|
+
:'';
|
|
669
|
+
return selected||'alloy';
|
|
670
|
+
}
|
|
671
|
+
if(providerId==='LOCAL_SPEACH'){
|
|
672
|
+
return 'af_heart';
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#legacySpeechCapability(role,providerId){
|
|
678
|
+
const service=this.#legacySpeechService(role);
|
|
679
|
+
const model=this.#legacySpeechModel(role);
|
|
680
|
+
if(service!==providerId||!model){
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
if(providerId==='OPENAI'){
|
|
684
|
+
return Boolean(this.license)&&typeof globalThis.fetch==='function';
|
|
685
|
+
}
|
|
686
|
+
if(providerId==='LOCAL_SPEACH'){
|
|
687
|
+
return Boolean(this.#nativeSpeech(service,role));
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#legacySpeechInspection(role,providerId,selection){
|
|
693
|
+
const localOnly=providerId==='LOCAL_SPEACH';
|
|
694
|
+
if(!selection
|
|
695
|
+
||selection.providerId!==providerId
|
|
696
|
+
||selection.modelId!==this.#legacySpeechModel(role)
|
|
697
|
+
||selection.localOnly!==localOnly
|
|
698
|
+
||this.#legacySpeechService(role)!==providerId){
|
|
699
|
+
return Object.freeze({
|
|
700
|
+
available:false,
|
|
701
|
+
code:'ARCANE_AI_MODEL_AUTHORITY_REQUIRED',
|
|
702
|
+
message:`The selected legacy ${role.toUpperCase()} route does not match the active AI configuration.`
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
if(!this.#legacySpeechCapability(role,providerId)){
|
|
706
|
+
return Object.freeze({
|
|
707
|
+
available:false,
|
|
708
|
+
code:providerId==='LOCAL_SPEACH'
|
|
709
|
+
?'AI_NATIVE_LOCAL_REQUIRED'
|
|
710
|
+
:'AI_PROVIDER_NOT_CONFIGURED',
|
|
711
|
+
message:providerId==='LOCAL_SPEACH'
|
|
712
|
+
?`Local ${role.toUpperCase()} requires the capability-gated Arcane API.`
|
|
713
|
+
:'AI provider is not configured.'
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze({
|
|
717
|
+
available:true,
|
|
718
|
+
authority:Object.freeze({
|
|
719
|
+
protocol:AI_MODEL_AUTHORITY_PROTOCOL,
|
|
720
|
+
providerId,
|
|
721
|
+
modelId:selection.modelId,
|
|
722
|
+
admitted:true
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#createLegacySpeechProvider(role,providerId){
|
|
728
|
+
const runtime=this;
|
|
729
|
+
const localOnly=providerId==='LOCAL_SPEACH';
|
|
730
|
+
const expectedOperation=role==='stt'?'transcribe':'synthesize';
|
|
731
|
+
let state='unloaded';
|
|
732
|
+
let busy=false;
|
|
733
|
+
|
|
734
|
+
function statusLegacySpeechProvider(){
|
|
735
|
+
if(state==='ready'
|
|
736
|
+
&&!busy
|
|
737
|
+
&&!runtime.#legacySpeechCapability(role,providerId)){
|
|
738
|
+
state='unloaded';
|
|
739
|
+
}
|
|
740
|
+
return Object.freeze({
|
|
741
|
+
state,
|
|
742
|
+
loaded:state==='ready',
|
|
743
|
+
busy
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function assertLegacySpeechSelection(selection){
|
|
748
|
+
const inspection=runtime.#legacySpeechInspection(
|
|
749
|
+
role,
|
|
750
|
+
providerId,
|
|
751
|
+
selection
|
|
752
|
+
);
|
|
753
|
+
if(!inspection.available){
|
|
754
|
+
throw legacyAIProviderError(
|
|
755
|
+
inspection.message,
|
|
756
|
+
inspection.code
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return inspection;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function releaseLegacySpeechRequest(){
|
|
763
|
+
busy=false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return Object.freeze({
|
|
767
|
+
protocol:AI_PROVIDER_PROTOCOL,
|
|
768
|
+
role,
|
|
769
|
+
id:providerId,
|
|
770
|
+
localOnly,
|
|
771
|
+
catalog:function catalogLegacySpeechProvider(){
|
|
772
|
+
const model=runtime.#legacySpeechModel(role);
|
|
773
|
+
if(runtime.#legacySpeechService(role)!==providerId||!model){
|
|
774
|
+
return Object.freeze([]);
|
|
775
|
+
}
|
|
776
|
+
const defaultVoice=runtime.#legacySpeechDefaultVoice(
|
|
777
|
+
role,
|
|
778
|
+
providerId
|
|
779
|
+
);
|
|
780
|
+
return Object.freeze([
|
|
781
|
+
Object.freeze({
|
|
782
|
+
id:model,
|
|
783
|
+
...(defaultVoice?{defaultVoice}:{})
|
|
784
|
+
})
|
|
785
|
+
]);
|
|
786
|
+
},
|
|
787
|
+
inspect:function inspectLegacySpeechProvider(selection,{signal}={}){
|
|
788
|
+
if(signal?.aborted){
|
|
789
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
790
|
+
}
|
|
791
|
+
return runtime.#legacySpeechInspection(role,providerId,selection);
|
|
792
|
+
},
|
|
793
|
+
status:statusLegacySpeechProvider,
|
|
794
|
+
load:function loadLegacySpeechProvider(context={}){
|
|
795
|
+
if(context.signal?.aborted){
|
|
796
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
797
|
+
}
|
|
798
|
+
if(state==='disposed'){
|
|
799
|
+
throw legacyAIProviderError(
|
|
800
|
+
`The legacy ${role.toUpperCase()} provider is disposed.`,
|
|
801
|
+
'ARCANE_AI_PROVIDER_DISPOSED'
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if(busy){
|
|
805
|
+
throw legacyAIProviderError(
|
|
806
|
+
`The legacy ${role.toUpperCase()} provider owns an active request.`,
|
|
807
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if(typeof context.progress!=='function'){
|
|
811
|
+
throw new TypeError(
|
|
812
|
+
`Legacy ${role.toUpperCase()} provider load progress must be a function.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const inspection=assertLegacySpeechSelection(context.selection);
|
|
816
|
+
state='loading';
|
|
817
|
+
context.progress({
|
|
818
|
+
phase:'capability',
|
|
819
|
+
completed:0,
|
|
820
|
+
total:1,
|
|
821
|
+
unit:'items',
|
|
822
|
+
heartbeat:false
|
|
823
|
+
});
|
|
824
|
+
if(context.signal?.aborted){
|
|
825
|
+
state='unloaded';
|
|
826
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
827
|
+
}
|
|
828
|
+
state='ready';
|
|
829
|
+
context.progress({
|
|
830
|
+
phase:'capability',
|
|
831
|
+
completed:1,
|
|
832
|
+
total:1,
|
|
833
|
+
unit:'items',
|
|
834
|
+
heartbeat:false
|
|
835
|
+
});
|
|
836
|
+
return Object.freeze({
|
|
837
|
+
authority:inspection.authority,
|
|
838
|
+
status:statusLegacySpeechProvider()
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
request:function requestLegacySpeechProvider(context={}){
|
|
842
|
+
if(context.signal?.aborted){
|
|
843
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
844
|
+
}
|
|
845
|
+
assertLegacySpeechSelection(context.selection);
|
|
846
|
+
const current=statusLegacySpeechProvider();
|
|
847
|
+
if(current.state!=='ready'||!current.loaded){
|
|
848
|
+
throw legacyAIProviderError(
|
|
849
|
+
`The legacy ${role.toUpperCase()} provider is not ready.`,
|
|
850
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if(busy){
|
|
854
|
+
throw legacyAIProviderError(
|
|
855
|
+
`The legacy ${role.toUpperCase()} provider owns an active request.`,
|
|
856
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if(context.operation!==expectedOperation){
|
|
860
|
+
throw legacyAIProviderError(
|
|
861
|
+
`The legacy ${role.toUpperCase()} provider operation is unsupported.`,
|
|
862
|
+
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
busy=true;
|
|
866
|
+
const request=role==='stt'
|
|
867
|
+
?runtime.#requestLegacySpeechTranscription(
|
|
868
|
+
context.payload,
|
|
869
|
+
context.signal
|
|
870
|
+
)
|
|
871
|
+
:runtime.#requestLegacySpeechSynthesis(
|
|
872
|
+
context.payload,
|
|
873
|
+
context.signal
|
|
874
|
+
);
|
|
875
|
+
return Promise.resolve(request).finally(releaseLegacySpeechRequest);
|
|
876
|
+
},
|
|
877
|
+
unload:function unloadLegacySpeechProvider(context={}){
|
|
878
|
+
if(context.signal?.aborted){
|
|
879
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
880
|
+
}
|
|
881
|
+
if(busy){
|
|
882
|
+
throw legacyAIProviderError(
|
|
883
|
+
`The legacy ${role.toUpperCase()} provider still owns an active request.`,
|
|
884
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
state='unloaded';
|
|
888
|
+
return statusLegacySpeechProvider();
|
|
889
|
+
},
|
|
890
|
+
dispose:function disposeLegacySpeechProvider(context={}){
|
|
891
|
+
if(context.signal?.aborted){
|
|
892
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
893
|
+
}
|
|
894
|
+
if(busy){
|
|
895
|
+
throw legacyAIProviderError(
|
|
896
|
+
`The legacy ${role.toUpperCase()} provider still owns an active request.`,
|
|
897
|
+
'ARCANE_AI_ROLE_BUSY'
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
state='disposed';
|
|
901
|
+
return statusLegacySpeechProvider();
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
630
906
|
#ensureLegacyLLMProvider(providerId){
|
|
631
907
|
if(providerId!=='OPENAI'&&providerId!=='OLLAMA'){
|
|
632
908
|
return false;
|
|
@@ -643,6 +919,23 @@ class AI {
|
|
|
643
919
|
return true;
|
|
644
920
|
}
|
|
645
921
|
|
|
922
|
+
#ensureLegacySpeechProvider(role,providerId){
|
|
923
|
+
if(!['stt','tts'].includes(role)
|
|
924
|
+
||!['OPENAI','LOCAL_SPEACH'].includes(providerId)){
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if(this.#providerRuntime.hasProvider(role,providerId)){
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
const provider=this.#createLegacySpeechProvider(role,providerId);
|
|
931
|
+
const unregister=this.#providerRuntime.register(provider);
|
|
932
|
+
this.#legacySpeechProviders.set(
|
|
933
|
+
this.#legacySpeechProviderKey(role,providerId),
|
|
934
|
+
Object.freeze({role,providerId,provider,unregister})
|
|
935
|
+
);
|
|
936
|
+
return true;
|
|
937
|
+
}
|
|
938
|
+
|
|
646
939
|
#releaseInactiveLegacyLLMProviders(activeProviderId){
|
|
647
940
|
for(const [providerId,record] of this.#legacyLLMProviders){
|
|
648
941
|
if(providerId===activeProviderId){
|
|
@@ -654,6 +947,17 @@ class AI {
|
|
|
654
947
|
}
|
|
655
948
|
}
|
|
656
949
|
|
|
950
|
+
#releaseInactiveLegacySpeechProviders(activeProviders){
|
|
951
|
+
for(const [key,record] of this.#legacySpeechProviders){
|
|
952
|
+
if(activeProviders[record.role]===record.providerId){
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if(record.unregister()){
|
|
956
|
+
this.#legacySpeechProviders.delete(key);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
657
961
|
#internalLegacyLLMSelection(localOnly=false){
|
|
658
962
|
const selection=this.#providerRuntime.selection(
|
|
659
963
|
'llm',
|
|
@@ -668,6 +972,19 @@ class AI {
|
|
|
668
972
|
return selection;
|
|
669
973
|
}
|
|
670
974
|
|
|
975
|
+
#internalLegacySpeechSelection(role,localOnly=false){
|
|
976
|
+
const selection=this.#providerRuntime.selection(role,{localOnly});
|
|
977
|
+
if(!selection
|
|
978
|
+
||!this.#legacySpeechProviders.has(
|
|
979
|
+
this.#legacySpeechProviderKey(role,selection.providerId)
|
|
980
|
+
)
|
|
981
|
+
||selection.providerId!==this.#legacySpeechService(role)
|
|
982
|
+
||selection.modelId!==this.#legacySpeechModel(role)){
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
return selection;
|
|
986
|
+
}
|
|
987
|
+
|
|
671
988
|
#retainLegacyLLMReadiness(operation){
|
|
672
989
|
this.#legacyLLMReadiness=Promise.resolve(operation).catch(
|
|
673
990
|
function retainLegacyLLMReadinessFailure(){
|
|
@@ -677,6 +994,15 @@ class AI {
|
|
|
677
994
|
return this.#legacyLLMReadiness;
|
|
678
995
|
}
|
|
679
996
|
|
|
997
|
+
#retainLegacySpeechReadiness(operation){
|
|
998
|
+
this.#legacySpeechReadiness=Promise.resolve(operation).catch(
|
|
999
|
+
function retainLegacySpeechReadinessFailure(){
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
1003
|
+
return this.#legacySpeechReadiness;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
680
1006
|
#reconcileLegacyLLMReadiness(){
|
|
681
1007
|
const selection=this.#internalLegacyLLMSelection(false);
|
|
682
1008
|
if(!selection){
|
|
@@ -698,6 +1024,27 @@ class AI {
|
|
|
698
1024
|
return Promise.resolve(status);
|
|
699
1025
|
}
|
|
700
1026
|
|
|
1027
|
+
#reconcileLegacySpeechReadiness(){
|
|
1028
|
+
const runtime=this;
|
|
1029
|
+
return Promise.all(['stt','tts'].map(function reconcileLegacySpeechRole(role){
|
|
1030
|
+
const selection=runtime.#internalLegacySpeechSelection(role,false);
|
|
1031
|
+
if(!selection){
|
|
1032
|
+
return runtime.#providerRuntime.status(role);
|
|
1033
|
+
}
|
|
1034
|
+
const status=runtime.#providerRuntime.status(role);
|
|
1035
|
+
if(runtime.#legacySpeechCapability(role,selection.providerId)){
|
|
1036
|
+
return status;
|
|
1037
|
+
}
|
|
1038
|
+
if(status.loaded===true
|
|
1039
|
+
||status.busy===true
|
|
1040
|
+
||status.state==='loading'
|
|
1041
|
+
||status.state==='unloading'){
|
|
1042
|
+
return runtime.#providerRuntime.unload(role);
|
|
1043
|
+
}
|
|
1044
|
+
return status;
|
|
1045
|
+
}));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
701
1048
|
get configured(){
|
|
702
1049
|
if(this.#usesProviderRuntime('llm',this.llmService)){
|
|
703
1050
|
if(this.#internalLegacyLLMSelection(false)
|
|
@@ -720,12 +1067,19 @@ class AI {
|
|
|
720
1067
|
if(this.#usesProviderRuntime(role,service)){
|
|
721
1068
|
const internal=role==='llm'
|
|
722
1069
|
?this.#internalLegacyLLMSelection(false)
|
|
723
|
-
:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
internal.providerId
|
|
727
|
-
internal
|
|
728
|
-
|
|
1070
|
+
:this.#internalLegacySpeechSelection(role,false);
|
|
1071
|
+
const internalAvailable=!internal
|
|
1072
|
+
||(role==='llm'
|
|
1073
|
+
?this.#legacyLLMCapability(internal.providerId)
|
|
1074
|
+
:this.#legacySpeechCapability(role,internal.providerId));
|
|
1075
|
+
if(!internalAvailable){
|
|
1076
|
+
const inspection=role==='llm'
|
|
1077
|
+
?this.#legacyLLMInspection(internal.providerId,internal)
|
|
1078
|
+
:this.#legacySpeechInspection(
|
|
1079
|
+
role,
|
|
1080
|
+
internal.providerId,
|
|
1081
|
+
internal
|
|
1082
|
+
);
|
|
729
1083
|
throw legacyAIProviderError(
|
|
730
1084
|
inspection.message,
|
|
731
1085
|
inspection.code
|
|
@@ -894,8 +1248,8 @@ class AI {
|
|
|
894
1248
|
tuple[0],
|
|
895
1249
|
this.#normalizedLLMModel(tuple[0],tuple[3])
|
|
896
1250
|
],
|
|
897
|
-
stt:[tuple[1],tuple[5]],
|
|
898
|
-
tts:[tuple[2],tuple[4]]
|
|
1251
|
+
stt:[tuple[1],this.#sttModels[tuple[5]]||tuple[5]],
|
|
1252
|
+
tts:[tuple[2],this.#ttsModels[tuple[4]]||tuple[4]]
|
|
899
1253
|
};
|
|
900
1254
|
const selections={};
|
|
901
1255
|
for(const role of ['llm','stt','tts']){
|
|
@@ -990,12 +1344,21 @@ class AI {
|
|
|
990
1344
|
]);
|
|
991
1345
|
this.#assertValidProviderTuple(tuple);
|
|
992
1346
|
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1347
|
+
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1348
|
+
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
993
1349
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
994
1350
|
this.#applyPreferenceTuple(tuple);
|
|
995
1351
|
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1352
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1353
|
+
stt:tuple[1],
|
|
1354
|
+
tts:tuple[2]
|
|
1355
|
+
});
|
|
996
1356
|
this.#retainLegacyLLMReadiness(
|
|
997
1357
|
this.#reconcileLegacyLLMReadiness()
|
|
998
1358
|
);
|
|
1359
|
+
this.#retainLegacySpeechReadiness(
|
|
1360
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1361
|
+
);
|
|
999
1362
|
return true;
|
|
1000
1363
|
}
|
|
1001
1364
|
|
|
@@ -1004,15 +1367,30 @@ class AI {
|
|
|
1004
1367
|
this.#ensureLegacyLLMProvider(
|
|
1005
1368
|
prepared.llm.default?.providerId
|
|
1006
1369
|
);
|
|
1370
|
+
this.#ensureLegacySpeechProvider(
|
|
1371
|
+
'stt',
|
|
1372
|
+
prepared.stt.default?.providerId
|
|
1373
|
+
);
|
|
1374
|
+
this.#ensureLegacySpeechProvider(
|
|
1375
|
+
'tts',
|
|
1376
|
+
prepared.tts.default?.providerId
|
|
1377
|
+
);
|
|
1007
1378
|
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1008
1379
|
const configured=this.#providerRuntime.configure(prepared);
|
|
1009
1380
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1010
1381
|
this.#releaseInactiveLegacyLLMProviders(
|
|
1011
1382
|
configured.llm.default?.providerId
|
|
1012
1383
|
);
|
|
1384
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1385
|
+
stt:configured.stt.default?.providerId,
|
|
1386
|
+
tts:configured.tts.default?.providerId
|
|
1387
|
+
});
|
|
1013
1388
|
this.#retainLegacyLLMReadiness(
|
|
1014
1389
|
this.#reconcileLegacyLLMReadiness()
|
|
1015
1390
|
);
|
|
1391
|
+
this.#retainLegacySpeechReadiness(
|
|
1392
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1393
|
+
);
|
|
1016
1394
|
return configured;
|
|
1017
1395
|
}
|
|
1018
1396
|
|
|
@@ -1036,10 +1414,17 @@ class AI {
|
|
|
1036
1414
|
this.stopAudio();
|
|
1037
1415
|
await this.#unloadProviderRolesForTransition();
|
|
1038
1416
|
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1417
|
+
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1418
|
+
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
1039
1419
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
1040
1420
|
this.#applyPreferenceTuple(tuple);
|
|
1041
1421
|
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1422
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1423
|
+
stt:tuple[1],
|
|
1424
|
+
tts:tuple[2]
|
|
1425
|
+
});
|
|
1042
1426
|
await this.#reconcileLegacyLLMReadiness();
|
|
1427
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
1043
1428
|
return this.#providerRuntime.status();
|
|
1044
1429
|
}
|
|
1045
1430
|
|
|
@@ -1048,6 +1433,14 @@ class AI {
|
|
|
1048
1433
|
this.#ensureLegacyLLMProvider(
|
|
1049
1434
|
prepared.llm.default?.providerId
|
|
1050
1435
|
);
|
|
1436
|
+
this.#ensureLegacySpeechProvider(
|
|
1437
|
+
'stt',
|
|
1438
|
+
prepared.stt.default?.providerId
|
|
1439
|
+
);
|
|
1440
|
+
this.#ensureLegacySpeechProvider(
|
|
1441
|
+
'tts',
|
|
1442
|
+
prepared.tts.default?.providerId
|
|
1443
|
+
);
|
|
1051
1444
|
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1052
1445
|
this.stopAudio();
|
|
1053
1446
|
await this.#unloadProviderRolesForTransition();
|
|
@@ -1056,7 +1449,12 @@ class AI {
|
|
|
1056
1449
|
this.#releaseInactiveLegacyLLMProviders(
|
|
1057
1450
|
configured.llm.default?.providerId
|
|
1058
1451
|
);
|
|
1452
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1453
|
+
stt:configured.stt.default?.providerId,
|
|
1454
|
+
tts:configured.tts.default?.providerId
|
|
1455
|
+
});
|
|
1059
1456
|
await this.#reconcileLegacyLLMReadiness();
|
|
1457
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
1060
1458
|
return configured;
|
|
1061
1459
|
}
|
|
1062
1460
|
|
|
@@ -1145,6 +1543,127 @@ class AI {
|
|
|
1145
1543
|
return null;
|
|
1146
1544
|
}
|
|
1147
1545
|
|
|
1546
|
+
async #requestLegacySpeechTranscription(payload={},signal=null){
|
|
1547
|
+
const audio=payload?.audio;
|
|
1548
|
+
if(!audio||typeof audio.arrayBuffer!=='function'){
|
|
1549
|
+
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
1550
|
+
}
|
|
1551
|
+
if(signal?.aborted){
|
|
1552
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1553
|
+
}
|
|
1554
|
+
const mimeType=String(payload.mimeType||audio.type||'audio/webm');
|
|
1555
|
+
const model=String(payload.model||this.modelSTT);
|
|
1556
|
+
const nativeSpeech=this.#nativeSpeech(this.sttService,'stt');
|
|
1557
|
+
if(nativeSpeech){
|
|
1558
|
+
const audioBytes=await audio.arrayBuffer();
|
|
1559
|
+
if(signal?.aborted){
|
|
1560
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1561
|
+
}
|
|
1562
|
+
const response=await nativeSpeech.transcribe({
|
|
1563
|
+
audioBase64:this.#arrayBufferToBase64(audioBytes),
|
|
1564
|
+
mimeType,
|
|
1565
|
+
model
|
|
1566
|
+
});
|
|
1567
|
+
if(signal?.aborted){
|
|
1568
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1569
|
+
}
|
|
1570
|
+
if(!response||typeof response.text!=='string'){
|
|
1571
|
+
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
1572
|
+
}
|
|
1573
|
+
return response.text;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
await this.#assertAndroidSpeechBridge(this.sttService);
|
|
1577
|
+
const formData=new FormData();
|
|
1578
|
+
formData.append('file',audio);
|
|
1579
|
+
formData.append('model',model);
|
|
1580
|
+
formData.append('response_format','text');
|
|
1581
|
+
const response=await fetch(
|
|
1582
|
+
this.urlSTT,
|
|
1583
|
+
{
|
|
1584
|
+
method:'POST',
|
|
1585
|
+
credentials,
|
|
1586
|
+
headers:this.#sttHeaders[this.sttService],
|
|
1587
|
+
body:formData,
|
|
1588
|
+
signal
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
if(!response.ok){
|
|
1592
|
+
throw new Error(`Speech transcription failed with status ${response.status}.`);
|
|
1593
|
+
}
|
|
1594
|
+
return response.text();
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async #requestLegacySpeechSynthesis(payload={},signal=null){
|
|
1598
|
+
const input=typeof payload?.input==='string'?payload.input:'';
|
|
1599
|
+
if(!input){
|
|
1600
|
+
throw new TypeError('Speech synthesis requires nonempty input.');
|
|
1601
|
+
}
|
|
1602
|
+
if(signal?.aborted){
|
|
1603
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1604
|
+
}
|
|
1605
|
+
const model=String(payload.model||this.modelTTS);
|
|
1606
|
+
const voice=typeof payload.voice==='string'&&payload.voice.trim()
|
|
1607
|
+
?payload.voice.trim()
|
|
1608
|
+
:this.#legacySpeechDefaultVoice('tts',this.ttsService);
|
|
1609
|
+
if(!voice){
|
|
1610
|
+
throw new TypeError('The selected speech provider requires a voice.');
|
|
1611
|
+
}
|
|
1612
|
+
const responseFormat=String(payload.responseFormat||this.audioFormat);
|
|
1613
|
+
const speed=Number.isFinite(payload.speed)?payload.speed:this.voiceSpeed;
|
|
1614
|
+
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
1615
|
+
if(nativeSpeech){
|
|
1616
|
+
const response=await nativeSpeech.synthesize({
|
|
1617
|
+
model,
|
|
1618
|
+
voice,
|
|
1619
|
+
input,
|
|
1620
|
+
responseFormat,
|
|
1621
|
+
speed
|
|
1622
|
+
});
|
|
1623
|
+
if(signal?.aborted){
|
|
1624
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1625
|
+
}
|
|
1626
|
+
if(!response||typeof response.audioBase64!=='string'){
|
|
1627
|
+
throw new TypeError('Arcane returned an invalid local speech response.');
|
|
1628
|
+
}
|
|
1629
|
+
return new Blob(
|
|
1630
|
+
[this.#base64ToBytes(response.audioBase64)],
|
|
1631
|
+
{
|
|
1632
|
+
type:typeof response.contentType==='string'
|
|
1633
|
+
?response.contentType
|
|
1634
|
+
:this.audioType
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
await this.#assertAndroidSpeechBridge(this.ttsService);
|
|
1640
|
+
const personality=await window.user?.personality
|
|
1641
|
+
||'A behavioral health technician with a slight veteran feel on occasion.';
|
|
1642
|
+
const religion=await window.user?.religion||'caring';
|
|
1643
|
+
const response=await fetch(
|
|
1644
|
+
this.urlTTS,
|
|
1645
|
+
{
|
|
1646
|
+
method:'POST',
|
|
1647
|
+
credentials,
|
|
1648
|
+
headers:this.#ttsHeaders[this.ttsService],
|
|
1649
|
+
body:JSON.stringify({
|
|
1650
|
+
model,
|
|
1651
|
+
voice,
|
|
1652
|
+
input,
|
|
1653
|
+
speed,
|
|
1654
|
+
instructions:`${personality} and sounding a bit ${religion}`,
|
|
1655
|
+
response_format:responseFormat
|
|
1656
|
+
}),
|
|
1657
|
+
signal
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
if(!response.ok){
|
|
1661
|
+
throw new Error(`Speech synthesis failed with status ${response.status}.`);
|
|
1662
|
+
}
|
|
1663
|
+
const contentType=response.headers.get('content-type')||this.audioType;
|
|
1664
|
+
return new Blob([await response.arrayBuffer()],{type:contentType});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1148
1667
|
async #androidNativeHost(){
|
|
1149
1668
|
if(typeof globalThis.arcaneAndroid?.postMessage==='function'){
|
|
1150
1669
|
return true;
|
|
@@ -2569,15 +3088,16 @@ class AI {
|
|
|
2569
3088
|
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
2570
3089
|
job.abortController=new AbortController();
|
|
2571
3090
|
const responseFormat=this.#providerSpeechResponseFormat();
|
|
3091
|
+
const voice=this.#providerSpeechVoice();
|
|
2572
3092
|
const response=await this.#providerRuntime.request(
|
|
2573
3093
|
'tts',
|
|
2574
3094
|
{
|
|
2575
3095
|
operation:'synthesize',
|
|
2576
3096
|
payload:{
|
|
2577
3097
|
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
2578
|
-
voice:String(window.user?.AI_voice||'af_heart'),
|
|
2579
3098
|
input:job.text,
|
|
2580
3099
|
responseFormat,
|
|
3100
|
+
...(voice?{voice}:{}),
|
|
2581
3101
|
speed:this.voiceSpeed
|
|
2582
3102
|
},
|
|
2583
3103
|
localOnly:false,
|
|
@@ -2587,83 +3107,31 @@ class AI {
|
|
|
2587
3107
|
return this.#normalizeProviderSpeechAudio(response);
|
|
2588
3108
|
}
|
|
2589
3109
|
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
const response=await nativeSpeech.synthesize({
|
|
3110
|
+
job.abortController=new AbortController();
|
|
3111
|
+
const response=await this.#requestLegacySpeechSynthesis(
|
|
3112
|
+
{
|
|
2594
3113
|
model:this.modelTTS,
|
|
2595
|
-
voice:String(window.user?.AI_voice||'af_heart'),
|
|
2596
3114
|
input:job.text,
|
|
2597
3115
|
responseFormat:this.audioFormat,
|
|
2598
3116
|
speed:this.voiceSpeed
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
if(!response||typeof response.audioBase64!=='string'){
|
|
2602
|
-
throw new TypeError('Arcane returned an invalid local speech response.');
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
return {
|
|
2606
|
-
chunks:[this.#base64ToBytes(response.audioBase64)],
|
|
2607
|
-
type:typeof response.contentType==='string'
|
|
2608
|
-
?response.contentType
|
|
2609
|
-
:this.audioType
|
|
2610
|
-
};
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
await this.#assertAndroidSpeechBridge(this.ttsService);
|
|
2614
|
-
|
|
2615
|
-
job.abortController=new AbortController();
|
|
2616
|
-
const personality=await window.user?.personality
|
|
2617
|
-
||'A behavioral health technician with a slight veteran feel on occasion.';
|
|
2618
|
-
const religion=await window.user?.religion||'caring';
|
|
2619
|
-
const request={
|
|
2620
|
-
model:this.modelTTS,
|
|
2621
|
-
voice:window.user?.AI_voice,
|
|
2622
|
-
input:job.text,
|
|
2623
|
-
speed:this.voiceSpeed,
|
|
2624
|
-
instructions:`${personality} and sounding a bit ${religion}`,
|
|
2625
|
-
response_format:this.audioFormat
|
|
2626
|
-
};
|
|
2627
|
-
const response=await fetch(
|
|
2628
|
-
this.urlTTS,
|
|
2629
|
-
{
|
|
2630
|
-
method:'POST',
|
|
2631
|
-
credentials,
|
|
2632
|
-
headers:this.#ttsHeaders[this.ttsService],
|
|
2633
|
-
body:JSON.stringify(request),
|
|
2634
|
-
signal:job.abortController.signal
|
|
2635
|
-
}
|
|
3117
|
+
},
|
|
3118
|
+
job.abortController.signal
|
|
2636
3119
|
);
|
|
3120
|
+
return this.#normalizeProviderSpeechAudio(response);
|
|
3121
|
+
}
|
|
2637
3122
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
const reader=response.body?.getReader?.();
|
|
2643
|
-
|
|
2644
|
-
if(!reader){
|
|
2645
|
-
throw new TypeError('Speech synthesis response body is not readable.');
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
const chunks=[];
|
|
2649
|
-
|
|
2650
|
-
try{
|
|
2651
|
-
while(true){
|
|
2652
|
-
const {done,value}=await reader.read();
|
|
2653
|
-
|
|
2654
|
-
if(done){
|
|
2655
|
-
break;
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
if(value){
|
|
2659
|
-
chunks.push(value);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
}finally{
|
|
2663
|
-
reader.releaseLock?.();
|
|
3123
|
+
#providerSpeechVoice(){
|
|
3124
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
3125
|
+
if(!selection){
|
|
3126
|
+
return null;
|
|
2664
3127
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
3128
|
+
const provider=this.#providerRuntime.catalog('tts').find(
|
|
3129
|
+
entry=>entry.providerId===selection.providerId
|
|
3130
|
+
);
|
|
3131
|
+
const model=provider?.models.find(entry=>entry?.id===selection.modelId);
|
|
3132
|
+
return typeof model?.defaultVoice==='string'&&model.defaultVoice.trim()
|
|
3133
|
+
?model.defaultVoice.trim()
|
|
3134
|
+
:null;
|
|
2667
3135
|
}
|
|
2668
3136
|
|
|
2669
3137
|
#providerSpeechResponseFormat(){
|
|
@@ -2829,55 +3297,15 @@ class AI {
|
|
|
2829
3297
|
return text;
|
|
2830
3298
|
}
|
|
2831
3299
|
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
if(nativeSpeech){
|
|
2835
|
-
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
2836
|
-
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
const response=await nativeSpeech.transcribe({
|
|
2840
|
-
audioBase64:this.#arrayBufferToBase64(await audioFile.arrayBuffer()),
|
|
2841
|
-
mimeType:String(audioFile.type||'audio/webm'),
|
|
2842
|
-
model:this.modelSTT
|
|
2843
|
-
});
|
|
2844
|
-
|
|
2845
|
-
if(!response||typeof response.text!=='string'){
|
|
2846
|
-
throw new TypeError('Arcane returned an invalid local speech transcription.');
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
await responseHandler(response.text);
|
|
2850
|
-
return response.text;
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
await this.#assertAndroidSpeechBridge(this.sttService);
|
|
2854
|
-
|
|
2855
|
-
const formData = new FormData();
|
|
2856
|
-
formData.append('file', audioFile);
|
|
2857
|
-
formData.append('model', this.modelSTT);
|
|
2858
|
-
formData.append('response_format', 'text');
|
|
2859
|
-
|
|
2860
|
-
const response = await fetch(
|
|
2861
|
-
this.urlSTT,
|
|
3300
|
+
const text=await this.#requestLegacySpeechTranscription(
|
|
2862
3301
|
{
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
}
|
|
3302
|
+
audio:audioFile,
|
|
3303
|
+
mimeType:String(audioFile?.type||'audio/webm'),
|
|
3304
|
+
model:this.modelSTT
|
|
3305
|
+
},
|
|
3306
|
+
signal
|
|
2869
3307
|
);
|
|
2870
|
-
|
|
2871
|
-
if(!response.ok){
|
|
2872
|
-
throw new Error(`Speech transcription failed with status ${response.status}.`);
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
const text = await response.text();
|
|
2876
|
-
|
|
2877
|
-
//async
|
|
2878
3308
|
await responseHandler(text);
|
|
2879
|
-
|
|
2880
|
-
//sync
|
|
2881
3309
|
return text;
|
|
2882
3310
|
}
|
|
2883
3311
|
|