arcane-os 0.94.0 → 0.96.0
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 +36 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-model-assets.mjs +74 -0
- package/browser-runtime/core/client.mjs +195 -0
- package/docs/reference/README.md +9 -0
- package/docs/reference/ai/browser-speech.md +61 -10
- package/docs/reference/core-client.md +35 -0
- package/docs/reference/core-native-packaging.md +38 -6
- package/docs/reference/inventory/package-api.json +133 -5
- package/docs/reference/inventory/runtime-modules.json +39 -13
- package/docs/reference/model-assets.md +164 -3
- package/docs/reference/native-magpie.md +98 -0
- package/docs/reference/native-nemotron.md +204 -0
- package/docs/reference/native-speech.md +250 -5
- package/docs/reference/runtime-modules.md +157 -11
- package/docs/reference/sdk-api.md +247 -4
- package/package.json +7 -3
- package/runtime/arcane/modules/AI.js +114 -14
- package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
- package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
- package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
- package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
- package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
- package/src/core/host.mjs +1 -0
- package/src/core/services/local-ai.mjs +46 -12
- package/src/core/services/model-assets.mjs +491 -59
- package/src/core/services/speech.mjs +237 -4
- package/src/local-ai/magpie/build.mjs +36 -0
- package/src/local-ai/magpie/helper.mjs +254 -0
- package/src/local-ai/magpie/index.mjs +488 -0
- package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
- package/src/local-ai/magpie/native/main.cpp +270 -0
- package/src/local-ai/nemo-models.mjs +160 -0
- package/src/local-ai/nemotron/build.mjs +53 -0
- package/src/local-ai/nemotron/helper.mjs +308 -0
- package/src/local-ai/nemotron/index.mjs +693 -0
- package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
- package/src/local-ai/nemotron/native/main.cpp +282 -0
- package/src/native/macos-provider.mjs +14 -7
- package/src/native/portable-assembly.mjs +180 -0
- package/src/native/portable-provider.mjs +17 -161
- package/src/native/publish-output.mjs +101 -0
- package/src/native/windows-provider.mjs +12 -6
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
|
|
29
29
|
import {prepareSpeech, readPreparedSpeech} from './PreparedSpeech.js';
|
|
30
30
|
import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
|
|
31
|
+
import createNativeSpeechCapture from './NativeSpeechCapture.js';
|
|
32
|
+
import prepareNativeSpeechPlayback from './NativeSpeechPlayback.js';
|
|
31
33
|
|
|
32
34
|
const completeValue=(value)=>value;
|
|
33
35
|
|
|
@@ -1150,14 +1152,18 @@ class AI {
|
|
|
1150
1152
|
}
|
|
1151
1153
|
);
|
|
1152
1154
|
|
|
1155
|
+
const speechOnly=llmService===null&&model===null;
|
|
1153
1156
|
const preferences=[
|
|
1154
|
-
llmService||'TWIN',
|
|
1157
|
+
speechOnly?'':llmService||'TWIN',
|
|
1155
1158
|
sttService||'LOCAL_SPEACH',
|
|
1156
1159
|
ttsService||'LOCAL_SPEACH',
|
|
1157
|
-
model||'TWIN',
|
|
1160
|
+
speechOnly?'':model||'TWIN',
|
|
1158
1161
|
modelTTS||'LOCAL_SPEACH',
|
|
1159
1162
|
modelSTT||'LOCAL_SPEACH'
|
|
1160
1163
|
];
|
|
1164
|
+
// Explicitly absent LLM selection must not briefly register or load a
|
|
1165
|
+
// default provider. setAI retains empty entries from this initial tuple.
|
|
1166
|
+
if(speechOnly)this.#preferenceTuple=preferences.slice();
|
|
1161
1167
|
this.setAI(
|
|
1162
1168
|
...preferences
|
|
1163
1169
|
);
|
|
@@ -2008,13 +2014,15 @@ class AI {
|
|
|
2008
2014
|
if(runtime.#builtInSpeechService(role)!==providerId||!model){
|
|
2009
2015
|
return completeValue([]);
|
|
2010
2016
|
}
|
|
2011
|
-
const defaultVoice=runtime.#builtInSpeechDefaultVoice(
|
|
2017
|
+
const defaultVoice=nativeWatch?.snapshot?.roles?.[role]?.defaultVoice??runtime.#builtInSpeechDefaultVoice(
|
|
2012
2018
|
role,
|
|
2013
2019
|
providerId
|
|
2014
2020
|
);
|
|
2015
2021
|
return completeValue([
|
|
2016
2022
|
completeValue({
|
|
2017
2023
|
id:model,
|
|
2024
|
+
...(nativeWatch?.snapshot?.roles?.[role]?.speech
|
|
2025
|
+
?{speech:nativeWatch.snapshot.roles[role].speech}:{}),
|
|
2018
2026
|
...(defaultVoice?{defaultVoice}:{})
|
|
2019
2027
|
})
|
|
2020
2028
|
]);
|
|
@@ -2026,6 +2034,39 @@ class AI {
|
|
|
2026
2034
|
return runtime.#builtInSpeechInspection(role,providerId,selection);
|
|
2027
2035
|
},
|
|
2028
2036
|
status:statusBuiltInSpeechProvider,
|
|
2037
|
+
...(role==='stt'?{
|
|
2038
|
+
supportsCapture:function supportsNativeCapture(){
|
|
2039
|
+
return nativeWatch?.snapshot?.roles?.stt?.supportsStreaming===true;
|
|
2040
|
+
},
|
|
2041
|
+
createCapture:function createBuiltInNativeCapture(options={}){
|
|
2042
|
+
if(nativeWatch?.snapshot?.roles?.stt?.supportsStreaming!==true)return null;
|
|
2043
|
+
const capture=createNativeSpeechCapture({
|
|
2044
|
+
...options,
|
|
2045
|
+
speech:nativeWatch.speech,
|
|
2046
|
+
model:nativeWatch.modelId
|
|
2047
|
+
});
|
|
2048
|
+
const operation={watch:nativeWatch,requestId:null,signal:null};
|
|
2049
|
+
const done=capture.done.finally(function releaseNativeCapture(){
|
|
2050
|
+
if(nativeRequest===operation)releaseBuiltInSpeechRequest();
|
|
2051
|
+
});
|
|
2052
|
+
done.catch(function observeNativeCaptureCompletion(error){
|
|
2053
|
+
if(!isAIRequestAbort(error))arcaneLogging.error('Native transcription capture failed.',error);
|
|
2054
|
+
});
|
|
2055
|
+
return {
|
|
2056
|
+
...capture,
|
|
2057
|
+
done,
|
|
2058
|
+
start:function startBuiltInNativeCapture(options={}){
|
|
2059
|
+
if(!nativeSpeechReady()||busy){
|
|
2060
|
+
throw aiProviderError('The native transcription model is not ready.','ARCANE_AI_ROLE_NOT_READY');
|
|
2061
|
+
}
|
|
2062
|
+
busy=true;
|
|
2063
|
+
operation.signal=options.signal;
|
|
2064
|
+
nativeRequest=operation;
|
|
2065
|
+
return capture.start(options);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
}:{}),
|
|
2029
2070
|
load:async function loadBuiltInSpeechProvider(context={}){
|
|
2030
2071
|
if(context.signal?.aborted){
|
|
2031
2072
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
@@ -2086,7 +2127,7 @@ class AI {
|
|
|
2086
2127
|
throw error;
|
|
2087
2128
|
}
|
|
2088
2129
|
},
|
|
2089
|
-
request:function requestBuiltInSpeechProvider(context={}){
|
|
2130
|
+
request:function requestBuiltInSpeechProvider(context={}, {speechStream=null}={}){
|
|
2090
2131
|
if(context.signal?.aborted){
|
|
2091
2132
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
2092
2133
|
}
|
|
@@ -2126,7 +2167,8 @@ class AI {
|
|
|
2126
2167
|
)
|
|
2127
2168
|
:runtime.#requestBuiltInSpeechSynthesis(
|
|
2128
2169
|
context.payload,
|
|
2129
|
-
context.signal
|
|
2170
|
+
context.signal,
|
|
2171
|
+
speechStream
|
|
2130
2172
|
);
|
|
2131
2173
|
return Promise.resolve(request).finally(releaseBuiltInSpeechRequest);
|
|
2132
2174
|
},
|
|
@@ -2548,6 +2590,7 @@ class AI {
|
|
|
2548
2590
|
|
|
2549
2591
|
audioMessageChunks='';
|
|
2550
2592
|
#markdownSpeech=new MarkdownSpeech();
|
|
2593
|
+
#streamSpeechOptions=null;
|
|
2551
2594
|
sourceNodes=[];
|
|
2552
2595
|
isSpeaking=false;
|
|
2553
2596
|
audioContext=null;
|
|
@@ -4684,7 +4727,7 @@ class AI {
|
|
|
4684
4727
|
return response.text();
|
|
4685
4728
|
}
|
|
4686
4729
|
|
|
4687
|
-
async #requestBuiltInSpeechSynthesis(payload={},signal=null){
|
|
4730
|
+
async #requestBuiltInSpeechSynthesis(payload={},signal=null,speechStream=null){
|
|
4688
4731
|
const input=is.string(payload?.input)?payload.input:'';
|
|
4689
4732
|
if(!input){
|
|
4690
4733
|
throw new TypeError('Speech synthesis requires nonempty input.');
|
|
@@ -4703,9 +4746,15 @@ class AI {
|
|
|
4703
4746
|
const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
|
|
4704
4747
|
const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
|
|
4705
4748
|
if(nativeSpeech){
|
|
4749
|
+
if(speechStream){
|
|
4750
|
+
return nativeSpeech.synthesizeStream({
|
|
4751
|
+
...payload,model,voice,input,speed
|
|
4752
|
+
},{...speechStream,signal});
|
|
4753
|
+
}
|
|
4706
4754
|
let response;
|
|
4707
4755
|
try{
|
|
4708
4756
|
response=await nativeSpeech.synthesize({
|
|
4757
|
+
...payload,
|
|
4709
4758
|
model,
|
|
4710
4759
|
voice,
|
|
4711
4760
|
input,
|
|
@@ -6871,12 +6920,24 @@ class AI {
|
|
|
6871
6920
|
if(end){
|
|
6872
6921
|
this.audioMessageChunks='';
|
|
6873
6922
|
this.#markdownSpeech.reset();
|
|
6923
|
+
this.#streamSpeechOptions=null;
|
|
6874
6924
|
}
|
|
6875
6925
|
this.#traceSpeech('streamTTS.result',{callId,result:false,reason:'muted'});
|
|
6876
6926
|
return Promise.resolve(false);
|
|
6877
6927
|
}
|
|
6878
6928
|
|
|
6879
|
-
|
|
6929
|
+
// A final flush belongs to the same utterance selection. An explicit
|
|
6930
|
+
// language change drains the previous text before accepting new text.
|
|
6931
|
+
let preceding=null;
|
|
6932
|
+
if(this.#streamSpeechOptions&&Object.hasOwn(options,'languageCode')
|
|
6933
|
+
&&options.languageCode!==this.#streamSpeechOptions.languageCode){
|
|
6934
|
+
preceding=this.streamTTS('',true,this.#streamSpeechOptions);
|
|
6935
|
+
}
|
|
6936
|
+
const selectedOptions={...this.#streamSpeechOptions,...options};
|
|
6937
|
+
this.#streamSpeechOptions={
|
|
6938
|
+
voice:selectedOptions.voice,speed:selectedOptions.speed,languageCode:selectedOptions.languageCode
|
|
6939
|
+
};
|
|
6940
|
+
const {voice,speed,languageCode,waitForPlayback=false}=selectedOptions;
|
|
6880
6941
|
const pauseAfterMs=Number(options.pauseAfterMs??0);
|
|
6881
6942
|
if(!is.finite(pauseAfterMs)||pauseAfterMs<0){
|
|
6882
6943
|
throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
|
|
@@ -6885,6 +6946,7 @@ class AI {
|
|
|
6885
6946
|
const speechText=this.#markdownSpeech.append(String(text||''),end);
|
|
6886
6947
|
this.audioMessageChunks+=speechText;
|
|
6887
6948
|
const outputs=this.#extractSpeechSegments(end);
|
|
6949
|
+
if(end)this.#streamSpeechOptions=null;
|
|
6888
6950
|
this.#traceSpeech('streamTTS.segments',{
|
|
6889
6951
|
callId,segments:outputs,remainder:this.audioMessageChunks,
|
|
6890
6952
|
segmentation:this.ttsSegmentation,speechText
|
|
@@ -6892,7 +6954,7 @@ class AI {
|
|
|
6892
6954
|
|
|
6893
6955
|
if(!outputs.length){
|
|
6894
6956
|
this.#traceSpeech('streamTTS.result',{callId,result:true,reason:'buffered'});
|
|
6895
|
-
return Promise.resolve(true);
|
|
6957
|
+
return preceding??Promise.resolve(true);
|
|
6896
6958
|
}
|
|
6897
6959
|
|
|
6898
6960
|
try{
|
|
@@ -6909,7 +6971,7 @@ class AI {
|
|
|
6909
6971
|
}
|
|
6910
6972
|
|
|
6911
6973
|
const generation=this.speechGeneration;
|
|
6912
|
-
const jobs=[];
|
|
6974
|
+
const jobs=preceding?[preceding]:[];
|
|
6913
6975
|
const runtime=this;
|
|
6914
6976
|
|
|
6915
6977
|
for(const [index,output] of outputs.entries()){
|
|
@@ -6917,6 +6979,7 @@ class AI {
|
|
|
6917
6979
|
diagnosticCallId:callId,
|
|
6918
6980
|
voice,
|
|
6919
6981
|
speed,
|
|
6982
|
+
languageCode,
|
|
6920
6983
|
waitForPlayback,
|
|
6921
6984
|
pauseAfterMs:index===outputs.length-1?pauseAfterMs:0
|
|
6922
6985
|
}));
|
|
@@ -6938,6 +7001,7 @@ class AI {
|
|
|
6938
7001
|
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
6939
7002
|
this.audioMessageChunks='';
|
|
6940
7003
|
this.#markdownSpeech.reset();
|
|
7004
|
+
this.#streamSpeechOptions=null;
|
|
6941
7005
|
return;
|
|
6942
7006
|
}
|
|
6943
7007
|
this.finishTTS();
|
|
@@ -7111,6 +7175,7 @@ class AI {
|
|
|
7111
7175
|
text,
|
|
7112
7176
|
voice:options.voice,
|
|
7113
7177
|
speed:options.speed,
|
|
7178
|
+
languageCode:options.languageCode,
|
|
7114
7179
|
pauseAfterMs:options.pauseAfterMs??0,
|
|
7115
7180
|
resolvePlayback:null
|
|
7116
7181
|
};
|
|
@@ -7206,6 +7271,7 @@ class AI {
|
|
|
7206
7271
|
const payload={
|
|
7207
7272
|
model:selection?.modelId||this.modelTTS,
|
|
7208
7273
|
input:job.text,
|
|
7274
|
+
...(job.languageCode!==undefined?{languageCode:job.languageCode}:{}),
|
|
7209
7275
|
...(job.voice!==undefined||voice?{voice}:{}),
|
|
7210
7276
|
...(!native?{responseFormat:selection
|
|
7211
7277
|
?this.#providerSpeechResponseFormat()
|
|
@@ -7570,6 +7636,34 @@ class AI {
|
|
|
7570
7636
|
|
|
7571
7637
|
async prepareTTSPlayback(payload={},signal=null,preparation={}){
|
|
7572
7638
|
const model=this.#providerSpeechModel();
|
|
7639
|
+
if(model?.speech?.streaming===true){
|
|
7640
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
7641
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
7642
|
+
const runtime=this;
|
|
7643
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
7644
|
+
return prepareNativeSpeechPlayback({
|
|
7645
|
+
request:{
|
|
7646
|
+
...payload,
|
|
7647
|
+
model:payload.model??selection.modelId,
|
|
7648
|
+
voice:payload.voice??this.#providerSpeechVoice(),
|
|
7649
|
+
speed:payload.speed??this.voiceSpeed
|
|
7650
|
+
},
|
|
7651
|
+
signal,
|
|
7652
|
+
getAudioContext:function getOwnedSpeechOutput(){
|
|
7653
|
+
return runtime.#getSpeechAudioContext();
|
|
7654
|
+
},
|
|
7655
|
+
speech:{
|
|
7656
|
+
synthesizeStream:function requestNativeSpeechStream(request,options){
|
|
7657
|
+
return runtime.#providerRuntime.request('tts',{
|
|
7658
|
+
operation:'synthesize',payload:request,localOnly:false,signal:options.signal
|
|
7659
|
+
},{
|
|
7660
|
+
...preparation,
|
|
7661
|
+
speechStream:{onAudio:options.onAudio,onProgress:options.onProgress,onRequest:options.onRequest}
|
|
7662
|
+
});
|
|
7663
|
+
}
|
|
7664
|
+
}
|
|
7665
|
+
});
|
|
7666
|
+
}
|
|
7573
7667
|
if(model?.speech?.playback!=='native'){
|
|
7574
7668
|
return this.fetchTTS(payload,signal,preparation);
|
|
7575
7669
|
}
|
|
@@ -7618,6 +7712,7 @@ class AI {
|
|
|
7618
7712
|
'input',
|
|
7619
7713
|
'textFormat',
|
|
7620
7714
|
'responseFormat',
|
|
7715
|
+
'languageCode',
|
|
7621
7716
|
'speed'
|
|
7622
7717
|
]);
|
|
7623
7718
|
for(const key of Reflect.ownKeys(descriptors)){
|
|
@@ -7625,7 +7720,7 @@ class AI {
|
|
|
7625
7720
|
||!acceptedKeys.has(key)
|
|
7626
7721
|
||!Object.hasOwn(descriptors[key],'value')){
|
|
7627
7722
|
const error=new TypeError(
|
|
7628
|
-
'AI.fetchTTS accepts only model, voice, input, textFormat, responseFormat, and speed data properties.'
|
|
7723
|
+
'AI.fetchTTS accepts only model, voice, input, textFormat, responseFormat, languageCode, and speed data properties.'
|
|
7629
7724
|
);
|
|
7630
7725
|
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
7631
7726
|
throw error;
|
|
@@ -7732,15 +7827,19 @@ class AI {
|
|
|
7732
7827
|
throw error;
|
|
7733
7828
|
}
|
|
7734
7829
|
|
|
7830
|
+
const speechRequest={
|
|
7831
|
+
model,voice,input,responseFormat,speed,
|
|
7832
|
+
...(descriptors.languageCode?{languageCode:descriptors.languageCode.value}:{})
|
|
7833
|
+
};
|
|
7735
7834
|
if(selection){
|
|
7736
7835
|
this.#traceSpeech('fetchTTS.dispatch',{
|
|
7737
|
-
callId,selection,payload:
|
|
7836
|
+
callId,selection,payload:speechRequest
|
|
7738
7837
|
});
|
|
7739
7838
|
const response=await this.#providerRuntime.request(
|
|
7740
7839
|
'tts',
|
|
7741
7840
|
{
|
|
7742
7841
|
operation:'synthesize',
|
|
7743
|
-
payload:
|
|
7842
|
+
payload:speechRequest,
|
|
7744
7843
|
localOnly:false,
|
|
7745
7844
|
signal
|
|
7746
7845
|
},
|
|
@@ -7755,10 +7854,10 @@ class AI {
|
|
|
7755
7854
|
}
|
|
7756
7855
|
|
|
7757
7856
|
this.#traceSpeech('fetchTTS.dispatch',{
|
|
7758
|
-
callId,service:this.ttsService,payload:
|
|
7857
|
+
callId,service:this.ttsService,payload:speechRequest
|
|
7759
7858
|
});
|
|
7760
7859
|
const audio=await this.#requestBuiltInSpeechSynthesis(
|
|
7761
|
-
|
|
7860
|
+
speechRequest,
|
|
7762
7861
|
signal
|
|
7763
7862
|
);
|
|
7764
7863
|
this.#traceSpeech('fetchTTS.result',{callId,audio});
|
|
@@ -7859,6 +7958,7 @@ class AI {
|
|
|
7859
7958
|
this.speechScheduleTime=0;
|
|
7860
7959
|
this.audioMessageChunks='';
|
|
7861
7960
|
this.#markdownSpeech.reset();
|
|
7961
|
+
this.#streamSpeechOptions=null;
|
|
7862
7962
|
this.#clearSpeechUnlock();
|
|
7863
7963
|
|
|
7864
7964
|
for(const job of this.speechJobs){
|
|
@@ -1368,6 +1368,9 @@ function validateProvider(provider) {
|
|
|
1368
1368
|
}
|
|
1369
1369
|
if (provider.role === 'stt' && is.function(provider.createCapture)) {
|
|
1370
1370
|
record.createCapture = provider.createCapture.bind(provider);
|
|
1371
|
+
if (is.function(provider.supportsCapture)) {
|
|
1372
|
+
record.supportsCapture = provider.supportsCapture.bind(provider);
|
|
1373
|
+
}
|
|
1371
1374
|
}
|
|
1372
1375
|
const admitted = completeValue(record);
|
|
1373
1376
|
PROVIDER_RECORDS.set(provider, admitted);
|
|
@@ -3360,14 +3363,14 @@ export class AIProviderRuntime {
|
|
|
3360
3363
|
return this.#disposeAllPromise;
|
|
3361
3364
|
}
|
|
3362
3365
|
|
|
3363
|
-
request(role, options = {}, {speechInputPrepared = false, observeToolText = null, onRetry = null} = {}) {
|
|
3366
|
+
request(role, options = {}, {speechInputPrepared = false, observeToolText = null, onRetry = null, speechStream = null} = {}) {
|
|
3364
3367
|
if (SPEECH_ROLES.includes(role)) {
|
|
3365
3368
|
arcaneLogging.debug('[Arcane speech runtime] request', role, options);
|
|
3366
3369
|
}
|
|
3367
|
-
return this.#requestRole(role, options, false, speechInputPrepared, observeToolText, onRetry);
|
|
3370
|
+
return this.#requestRole(role, options, false, speechInputPrepared, observeToolText, onRetry, speechStream);
|
|
3368
3371
|
}
|
|
3369
3372
|
|
|
3370
|
-
#requestRole(role, options, queued, speechInputPrepared = false, observeToolText = null, onRetry = null) {
|
|
3373
|
+
#requestRole(role, options, queued, speechInputPrepared = false, observeToolText = null, onRetry = null, speechStream = null) {
|
|
3371
3374
|
this.#assertOpen();
|
|
3372
3375
|
this.#assertNotConfiguring();
|
|
3373
3376
|
assertRole(role);
|
|
@@ -3503,7 +3506,7 @@ export class AIProviderRuntime {
|
|
|
3503
3506
|
}
|
|
3504
3507
|
if ((!queued && slot.requestQueue.length)
|
|
3505
3508
|
|| slot.activeRequests.size >= maxConcurrentRequests) {
|
|
3506
|
-
return this.#enqueueRoleRequest(slot, options, observeToolText, onRetry);
|
|
3509
|
+
return this.#enqueueRoleRequest(slot, options, observeToolText, onRetry, speechStream);
|
|
3507
3510
|
}
|
|
3508
3511
|
if (!slot.ready
|
|
3509
3512
|
|| providerStatus.state !== 'ready'
|
|
@@ -4064,7 +4067,7 @@ export class AIProviderRuntime {
|
|
|
4064
4067
|
arcaneLogging.debug('[Arcane speech runtime] provider.request', {generation, requestSequence, operationId}, providerRequest);
|
|
4065
4068
|
}
|
|
4066
4069
|
const result = role === 'tts'
|
|
4067
|
-
? await provider.request(providerRequest, {speechInputPrepared: true})
|
|
4070
|
+
? await provider.request(providerRequest, {speechInputPrepared: true, speechStream})
|
|
4068
4071
|
: role === 'llm'
|
|
4069
4072
|
? await provider.request(
|
|
4070
4073
|
providerRequest,
|
|
@@ -4088,9 +4091,14 @@ export class AIProviderRuntime {
|
|
|
4088
4091
|
)
|
|
4089
4092
|
: result;
|
|
4090
4093
|
} catch (error) {
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
+
// Streaming speech joins its native cancellation before release.
|
|
4095
|
+
// An aborted signal alone must not conceal a cleanup failure.
|
|
4096
|
+
const speechCancelled = speechStream && !error?.errors
|
|
4097
|
+
&& (error === controller.signal.reason || error?.name === 'AbortError'
|
|
4098
|
+
|| error?.code === 'ARCANE_REQUEST_ABORTED');
|
|
4099
|
+
const normalized = speechStream
|
|
4100
|
+
? speechCancelled ? normalizedAbort(controller.signal.reason ?? error) : error
|
|
4101
|
+
: isAbort(error, controller.signal) ? normalizedAbort(error) : error;
|
|
4094
4102
|
if (SPEECH_ROLES.includes(role)) {
|
|
4095
4103
|
arcaneLogging.debug('[Arcane speech runtime] provider.request.error', {role, generation, requestSequence, operationId}, error);
|
|
4096
4104
|
}
|
|
@@ -4134,7 +4142,8 @@ export class AIProviderRuntime {
|
|
|
4134
4142
|
const provider = providerId === null
|
|
4135
4143
|
? this.#providerFor(slot)
|
|
4136
4144
|
: this.#providers.get(providerKey('stt', providerId));
|
|
4137
|
-
return is.function(provider?.createCapture)
|
|
4145
|
+
return is.function(provider?.createCapture)
|
|
4146
|
+
&& (!is.function(provider.supportsCapture) || provider.supportsCapture());
|
|
4138
4147
|
}
|
|
4139
4148
|
|
|
4140
4149
|
createTranscriptionCapture(options = {}) {
|
|
@@ -4147,6 +4156,7 @@ export class AIProviderRuntime {
|
|
|
4147
4156
|
const runtime = this;
|
|
4148
4157
|
const generation = slot.generation;
|
|
4149
4158
|
const capture = provider.createCapture(options);
|
|
4159
|
+
if (!capture) return null;
|
|
4150
4160
|
const controller = new AbortController();
|
|
4151
4161
|
let requestRecord = null;
|
|
4152
4162
|
let detachSignal = null;
|
|
@@ -4185,7 +4195,7 @@ export class AIProviderRuntime {
|
|
|
4185
4195
|
// permission retains the originating user gesture. It is never queued.
|
|
4186
4196
|
return {
|
|
4187
4197
|
done,
|
|
4188
|
-
start({signal} = {}) {
|
|
4198
|
+
start({signal, ...captureOptions} = {}) {
|
|
4189
4199
|
if (started || closed) return Promise.resolve(false);
|
|
4190
4200
|
try {
|
|
4191
4201
|
runtime.#assertOpen();
|
|
@@ -4217,7 +4227,7 @@ export class AIProviderRuntime {
|
|
|
4217
4227
|
};
|
|
4218
4228
|
slot.activeRequests.set(requestSequence, requestRecord);
|
|
4219
4229
|
runtime.#publishRoleRequestState(slot);
|
|
4220
|
-
const starting = capture.start({signal: controller.signal});
|
|
4230
|
+
const starting = capture.start({...captureOptions, signal: controller.signal});
|
|
4221
4231
|
return Promise.resolve(starting).catch(
|
|
4222
4232
|
function releaseFailedTranscriptionCaptureStart(error) {
|
|
4223
4233
|
capture.cancel();
|
|
@@ -4230,16 +4240,22 @@ export class AIProviderRuntime {
|
|
|
4230
4240
|
}
|
|
4231
4241
|
},
|
|
4232
4242
|
stop() {
|
|
4233
|
-
capture.stop();
|
|
4234
|
-
return done;
|
|
4243
|
+
return Promise.resolve(capture.stop()).then(function captureStopped() { return done; });
|
|
4235
4244
|
},
|
|
4245
|
+
...(is.function(capture.forceEndpoint) ? {
|
|
4246
|
+
forceEndpoint() {
|
|
4247
|
+
return capture.forceEndpoint();
|
|
4248
|
+
}
|
|
4249
|
+
} : {}),
|
|
4236
4250
|
cancel() {
|
|
4237
4251
|
controller.abort();
|
|
4238
4252
|
capture.cancel();
|
|
4253
|
+
return done;
|
|
4239
4254
|
},
|
|
4240
4255
|
destroy() {
|
|
4241
4256
|
controller.abort();
|
|
4242
4257
|
capture.destroy();
|
|
4258
|
+
return done;
|
|
4243
4259
|
}
|
|
4244
4260
|
};
|
|
4245
4261
|
}
|
|
@@ -4432,7 +4448,7 @@ export class AIProviderRuntime {
|
|
|
4432
4448
|
);
|
|
4433
4449
|
}
|
|
4434
4450
|
|
|
4435
|
-
#enqueueRoleRequest(slot, options, observeToolText = null, onRetry = null) {
|
|
4451
|
+
#enqueueRoleRequest(slot, options, observeToolText = null, onRetry = null, speechStream = null) {
|
|
4436
4452
|
if (SPEECH_ROLES.includes(slot.role)) {
|
|
4437
4453
|
arcaneLogging.debug('[Arcane speech runtime] queue.enqueue', {
|
|
4438
4454
|
role: slot.role,
|
|
@@ -4448,6 +4464,7 @@ export class AIProviderRuntime {
|
|
|
4448
4464
|
options,
|
|
4449
4465
|
observeToolText,
|
|
4450
4466
|
onRetry,
|
|
4467
|
+
speechStream,
|
|
4451
4468
|
resolve,
|
|
4452
4469
|
reject,
|
|
4453
4470
|
detachSignal: null
|
|
@@ -4520,7 +4537,7 @@ export class AIProviderRuntime {
|
|
|
4520
4537
|
let operation;
|
|
4521
4538
|
try {
|
|
4522
4539
|
operation = runtime.#requestRole(
|
|
4523
|
-
slot.role, entry.options, true, false, entry.observeToolText, entry.onRetry
|
|
4540
|
+
slot.role, entry.options, true, false, entry.observeToolText, entry.onRetry, entry.speechStream
|
|
4524
4541
|
);
|
|
4525
4542
|
} catch (error) {
|
|
4526
4543
|
if (SPEECH_ROLES.includes(slot.role)) {
|