arcane-os 0.33.6 → 0.34.1
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 +33 -0
- package/README.md +2 -2
- package/browser-runtime/ai/browser-speech.mjs +1 -0
- package/browser-runtime/ai/digitalocean-speech.mjs +468 -0
- package/docs/reference/ai/browser-speech.md +86 -5
- package/docs/reference/ai/twin-cloud.md +32 -0
- package/docs/reference/core/arcane-entities.md +1 -1
- package/docs/reference/inventory/package-api.json +15 -1
- package/docs/reference/inventory/runtime-entities.json +1 -1
- package/docs/reference/runtime-entities.md +40 -0
- package/docs/reference/runtime-modules.md +54 -9
- package/docs/reference/sdk-api.md +66 -2
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +8 -6
- package/runtime/arcane/components/speech.html +40 -6
- package/runtime/arcane/entities/User.js +30 -7
- package/runtime/arcane/modules/AI.js +364 -54
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
projectArcaneDOMEvent
|
|
10
10
|
} from 'arcane-os/event-manager';
|
|
11
11
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
12
|
+
import {subscribeAIRuntimeState} from './AIRuntimeState.js';
|
|
12
13
|
import {
|
|
13
14
|
AI_MODEL_AUTHORITY_PROTOCOL,
|
|
14
15
|
AI_PROVIDER_PROTOCOL,
|
|
@@ -1171,8 +1172,11 @@ class AI {
|
|
|
1171
1172
|
#builtInLLMProviders=new Map();
|
|
1172
1173
|
#builtInLLMReadiness=Promise.resolve(null);
|
|
1173
1174
|
#builtInSpeechProviders=new Map();
|
|
1175
|
+
#configuredSpeechProviders=new Map();
|
|
1174
1176
|
#builtInSpeechReadiness=Promise.resolve(null);
|
|
1175
1177
|
#speechControlGeneration=0;
|
|
1178
|
+
#speechActivation=null;
|
|
1179
|
+
#speechProviderConfiguration=null;
|
|
1176
1180
|
#speechFailureSequence=0;
|
|
1177
1181
|
#speechDiagnosticSequence=0;
|
|
1178
1182
|
#speechJobSequence=0;
|
|
@@ -1233,6 +1237,10 @@ class AI {
|
|
|
1233
1237
|
return this.#providerRuntime;
|
|
1234
1238
|
}
|
|
1235
1239
|
|
|
1240
|
+
get speechActivationPending(){
|
|
1241
|
+
return this.#speechActivation!==null;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1236
1244
|
[AI_PUBLISH_READY](){
|
|
1237
1245
|
const operationId=`${this.#events.instanceId}:ready:1`;
|
|
1238
1246
|
const reason=AI_INITIALIZATION_REASONS.initialized;
|
|
@@ -2056,10 +2064,6 @@ class AI {
|
|
|
2056
2064
|
}
|
|
2057
2065
|
return value;
|
|
2058
2066
|
});
|
|
2059
|
-
next[1]='LOCAL_SPEACH';
|
|
2060
|
-
next[2]='LOCAL_SPEACH';
|
|
2061
|
-
next[4]='LOCAL_SPEACH';
|
|
2062
|
-
next[5]='LOCAL_SPEACH';
|
|
2063
2067
|
return completeValue(next);
|
|
2064
2068
|
}
|
|
2065
2069
|
|
|
@@ -2075,21 +2079,12 @@ class AI {
|
|
|
2075
2079
|
}
|
|
2076
2080
|
|
|
2077
2081
|
#assertDeviceSpeechConfiguration(configuration){
|
|
2078
|
-
for(const
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
:'Speech synthesis';
|
|
2085
|
-
const error=new TypeError(
|
|
2086
|
-
`${operation} supports on-device providers only.`
|
|
2087
|
-
);
|
|
2088
|
-
error.code=role==='stt'
|
|
2089
|
-
?'AI_STT_DEVICE_ONLY'
|
|
2090
|
-
:'AI_TTS_DEVICE_ONLY';
|
|
2091
|
-
throw error;
|
|
2092
|
-
}
|
|
2082
|
+
for(const routeName of ['default','localOnly']){
|
|
2083
|
+
const selection=configuration?.stt?.[routeName];
|
|
2084
|
+
if(selection&&selection.localOnly!==true){
|
|
2085
|
+
const error=new TypeError('Speech recognition supports on-device providers only.');
|
|
2086
|
+
error.code='AI_STT_DEVICE_ONLY';
|
|
2087
|
+
throw error;
|
|
2093
2088
|
}
|
|
2094
2089
|
}
|
|
2095
2090
|
}
|
|
@@ -2537,6 +2532,8 @@ class AI {
|
|
|
2537
2532
|
|
|
2538
2533
|
#invalidateSpeechControl(){
|
|
2539
2534
|
this.#speechControlGeneration+=1;
|
|
2535
|
+
this.#speechActivation?.controller.abort();
|
|
2536
|
+
this.#speechActivation=null;
|
|
2540
2537
|
this.muted=true;
|
|
2541
2538
|
this.stopAudio();
|
|
2542
2539
|
}
|
|
@@ -2601,7 +2598,10 @@ class AI {
|
|
|
2601
2598
|
expectedProviders[role]=null;
|
|
2602
2599
|
continue;
|
|
2603
2600
|
}
|
|
2604
|
-
const
|
|
2601
|
+
const configured=this.#configuredSpeechProviders.get(role);
|
|
2602
|
+
const record=configured?.providerId===selection.providerId
|
|
2603
|
+
?configured
|
|
2604
|
+
:this.#builtInSpeechProviders.get(
|
|
2605
2605
|
this.#builtInSpeechProviderKey(role,selection.providerId)
|
|
2606
2606
|
);
|
|
2607
2607
|
if(!record
|
|
@@ -2615,7 +2615,7 @@ class AI {
|
|
|
2615
2615
|
continue;
|
|
2616
2616
|
}
|
|
2617
2617
|
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2618
|
-
`The selected ${role} route is not owned by
|
|
2618
|
+
`The selected ${role} route is not owned by this AI speech configuration.`
|
|
2619
2619
|
);
|
|
2620
2620
|
}
|
|
2621
2621
|
expectedProviders[role]=record.provider;
|
|
@@ -2649,6 +2649,9 @@ class AI {
|
|
|
2649
2649
|
if(this.#builtInSpeechProviders.get(key)===record){
|
|
2650
2650
|
this.#builtInSpeechProviders.delete(key);
|
|
2651
2651
|
}
|
|
2652
|
+
if(this.#configuredSpeechProviders.get(record.role)===record){
|
|
2653
|
+
this.#configuredSpeechProviders.delete(record.role);
|
|
2654
|
+
}
|
|
2652
2655
|
this.#browserSpeechRetiredBuiltInRecords.delete(record);
|
|
2653
2656
|
}catch(error){
|
|
2654
2657
|
failures.push(error);
|
|
@@ -2658,12 +2661,12 @@ class AI {
|
|
|
2658
2661
|
throw aiBrowserSpeechError(
|
|
2659
2662
|
AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2660
2663
|
AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2661
|
-
'The replaced
|
|
2664
|
+
'The replaced speech providers could not be disposed.',
|
|
2662
2665
|
failures.length===1
|
|
2663
2666
|
?failures[0]
|
|
2664
2667
|
:new AggregateError(
|
|
2665
2668
|
failures,
|
|
2666
|
-
'Multiple replaced
|
|
2669
|
+
'Multiple replaced speech provider disposals were rejected.'
|
|
2667
2670
|
),
|
|
2668
2671
|
{committed}
|
|
2669
2672
|
);
|
|
@@ -2673,17 +2676,21 @@ class AI {
|
|
|
2673
2676
|
|
|
2674
2677
|
#assertSynchronousBrowserSpeechSupersession(method){
|
|
2675
2678
|
if(!this.#browserSpeechConfigurationRecord
|
|
2676
|
-
&&this.#browserSpeechRetiredRecords.size===0
|
|
2679
|
+
&&this.#browserSpeechRetiredRecords.size===0
|
|
2680
|
+
&&this.#configuredSpeechProviders.size===0){
|
|
2677
2681
|
return;
|
|
2678
2682
|
}
|
|
2679
2683
|
throw aiBrowserSpeechError(
|
|
2680
2684
|
AI_BROWSER_SPEECH_ERROR_CODES.asyncTransitionRequired,
|
|
2681
2685
|
AI_BROWSER_SPEECH_REASONS.asyncTransitionRequired,
|
|
2682
|
-
`${method} cannot replace SDK-owned
|
|
2686
|
+
`${method} cannot replace SDK-owned speech providers synchronously; use configureSpeechProvider, disposeBrowserSpeech, or an asynchronous transition method.`
|
|
2683
2687
|
);
|
|
2684
2688
|
}
|
|
2685
2689
|
|
|
2686
2690
|
async #supersedeBrowserSpeechForRouteChange(){
|
|
2691
|
+
for(const role of [...this.#configuredSpeechProviders.keys()]){
|
|
2692
|
+
await this.configureSpeechProvider(role,null);
|
|
2693
|
+
}
|
|
2687
2694
|
if(!this.#browserSpeechConfigurationRecord
|
|
2688
2695
|
&&this.#browserSpeechRetiredRecords.size===0){
|
|
2689
2696
|
return false;
|
|
@@ -3416,6 +3423,183 @@ class AI {
|
|
|
3416
3423
|
return descriptor;
|
|
3417
3424
|
}
|
|
3418
3425
|
|
|
3426
|
+
configureSpeechProvider(role,provider,options={}){
|
|
3427
|
+
if(role!=='stt'&&role!=='tts'){
|
|
3428
|
+
throw new TypeError('AI.configureSpeechProvider requires stt or tts.');
|
|
3429
|
+
}
|
|
3430
|
+
if(provider!==null&&provider?.role!==role){
|
|
3431
|
+
throw new TypeError('The speech provider must implement the selected role.');
|
|
3432
|
+
}
|
|
3433
|
+
if(role==='stt'&&provider&&provider.localOnly!==true){
|
|
3434
|
+
const error=new TypeError('Speech recognition supports on-device providers only.');
|
|
3435
|
+
error.code='AI_STT_DEVICE_ONLY';
|
|
3436
|
+
throw error;
|
|
3437
|
+
}
|
|
3438
|
+
const {modelId,signal,expectedProvider}=options;
|
|
3439
|
+
const runtime=this;
|
|
3440
|
+
const speechConfiguration=role==='tts'
|
|
3441
|
+
?{generation:this.#speechControlGeneration,promise:null}
|
|
3442
|
+
:null;
|
|
3443
|
+
if(speechConfiguration)this.#speechProviderConfiguration=speechConfiguration;
|
|
3444
|
+
// Use the existing configuration lane. Unlike a replacement of the
|
|
3445
|
+
// same browser configuration, a disjoint role must not cancel its peer.
|
|
3446
|
+
const scheduled=this.#browserSpeechTransition.then(
|
|
3447
|
+
async function configureSelectedSpeechRole(){
|
|
3448
|
+
let committed=false;
|
|
3449
|
+
let candidateDisposalAttempted=false;
|
|
3450
|
+
const alreadyOwned=provider
|
|
3451
|
+
&&runtime.#providerRuntime.ownsProvider(role,provider);
|
|
3452
|
+
try{
|
|
3453
|
+
signal?.throwIfAborted();
|
|
3454
|
+
if(expectedProvider!==undefined
|
|
3455
|
+
&&(!runtime.#providerRuntime.ownsProvider(role,expectedProvider)
|
|
3456
|
+
||runtime.#providerRuntime.selection(role)?.providerId
|
|
3457
|
+
!==expectedProvider?.id)){
|
|
3458
|
+
if(provider&&!alreadyOwned){
|
|
3459
|
+
candidateDisposalAttempted=true;
|
|
3460
|
+
await provider.dispose({role});
|
|
3461
|
+
}
|
|
3462
|
+
return false;
|
|
3463
|
+
}
|
|
3464
|
+
const previousBrowser=runtime.#browserSpeechConfigurationRecord;
|
|
3465
|
+
if(previousBrowser&&!runtime.#browserSpeechRecordIsActive(previousBrowser)){
|
|
3466
|
+
throw runtime.#browserSpeechProviderRouteOwnershipError(
|
|
3467
|
+
'Browser speech ownership changed before single-role replacement.'
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
const boundary=runtime.#browserSpeechReplacementBoundary(
|
|
3471
|
+
previousBrowser,[role]
|
|
3472
|
+
);
|
|
3473
|
+
const selectedModelId=provider
|
|
3474
|
+
?modelId??provider.catalog()[0]?.id
|
|
3475
|
+
:null;
|
|
3476
|
+
if(provider&&!selectedModelId){
|
|
3477
|
+
throw new TypeError('A speech provider model must be selected.');
|
|
3478
|
+
}
|
|
3479
|
+
const selection=provider
|
|
3480
|
+
?{providerId:provider.id,modelId:selectedModelId,localOnly:provider.localOnly}
|
|
3481
|
+
:null;
|
|
3482
|
+
const routes={
|
|
3483
|
+
default:selection,
|
|
3484
|
+
localOnly:selection?.localOnly===true?selection:null
|
|
3485
|
+
};
|
|
3486
|
+
if(provider===boundary.expectedProviders[role]
|
|
3487
|
+
&&runtime.#sameBrowserSpeechSelection(
|
|
3488
|
+
runtime.#providerRuntime.selection(role),selection
|
|
3489
|
+
)){
|
|
3490
|
+
return selection;
|
|
3491
|
+
}
|
|
3492
|
+
const priorSelection=runtime.#providerRuntime.selection(role);
|
|
3493
|
+
const hydratesPendingSelection=provider
|
|
3494
|
+
&&boundary.expectedProviders[role]===null
|
|
3495
|
+
&&priorSelection?.providerId===provider.id
|
|
3496
|
+
&&priorSelection.modelId===selectedModelId;
|
|
3497
|
+
const preservesNewerActivation=speechConfiguration
|
|
3498
|
+
&&runtime.#speechActivation?.configuration
|
|
3499
|
+
&&runtime.#speechActivation.generation>speechConfiguration.generation;
|
|
3500
|
+
if(role==='tts'&&!hydratesPendingSelection&&!preservesNewerActivation){
|
|
3501
|
+
runtime.#invalidateSpeechControl();
|
|
3502
|
+
}
|
|
3503
|
+
await runtime.#unloadSpeechProviderRolesForTransition(
|
|
3504
|
+
signal,boundary.expectedProviders,[role]
|
|
3505
|
+
);
|
|
3506
|
+
signal?.throwIfAborted();
|
|
3507
|
+
const replacement=runtime.#providerRuntime.replaceSpeechProvider(
|
|
3508
|
+
role,{
|
|
3509
|
+
provider,routes,
|
|
3510
|
+
expectedProvider:boundary.expectedProviders[role]
|
|
3511
|
+
}
|
|
3512
|
+
);
|
|
3513
|
+
committed=true;
|
|
3514
|
+
if(provider){
|
|
3515
|
+
runtime.#configuredSpeechProviders.set(role,{
|
|
3516
|
+
role,providerId:provider.id,provider,
|
|
3517
|
+
unregister:replacement.unregister
|
|
3518
|
+
});
|
|
3519
|
+
}else{
|
|
3520
|
+
runtime.#configuredSpeechProviders.delete(role);
|
|
3521
|
+
}
|
|
3522
|
+
for(const record of boundary.builtInRecords){
|
|
3523
|
+
if(record.provider!==provider){
|
|
3524
|
+
runtime.#browserSpeechRetiredBuiltInRecords.add(record);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
if(previousBrowser?.managedRoles.includes(role)){
|
|
3528
|
+
const remaining=previousBrowser.managedRoles.filter(
|
|
3529
|
+
function retainOtherSpeechRole(candidate){return candidate!==role;}
|
|
3530
|
+
);
|
|
3531
|
+
runtime.#retireBrowserSpeechRegistration(previousBrowser,[role]);
|
|
3532
|
+
if(remaining.length){
|
|
3533
|
+
const configuration={...previousBrowser.configuration};
|
|
3534
|
+
delete configuration[role];
|
|
3535
|
+
runtime.#browserSpeechConfigurationRecord={
|
|
3536
|
+
...previousBrowser,
|
|
3537
|
+
configuration,
|
|
3538
|
+
configurationByRole:{...previousBrowser.configurationByRole,[role]:null},
|
|
3539
|
+
descriptor:{...previousBrowser.descriptor,[role]:null},
|
|
3540
|
+
providers:{...previousBrowser.providers,[role]:null},
|
|
3541
|
+
routes:runtime.#currentSpeechRoutes(),
|
|
3542
|
+
managedRoles:remaining,
|
|
3543
|
+
candidateRoles:remaining,
|
|
3544
|
+
unregisters:{...previousBrowser.unregisters,[role]:null},
|
|
3545
|
+
registrationState:{...previousBrowser.registrationState,[role]:false},
|
|
3546
|
+
retirementState:{stt:false,tts:false}
|
|
3547
|
+
};
|
|
3548
|
+
}else{
|
|
3549
|
+
runtime.#browserSpeechConfigurationRecord=null;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
runtime.#applySpeechPreferenceTuple(
|
|
3553
|
+
runtime.#tupleFromSpeechProviderRoutes(runtime.#currentSpeechRoutes())
|
|
3554
|
+
);
|
|
3555
|
+
await runtime.#cleanupRetiredBuiltInSpeechProviders({committed:true});
|
|
3556
|
+
if(previousBrowser?.retirementState[role]){
|
|
3557
|
+
await runtime.#cleanupBrowserSpeechRecord(previousBrowser,{committed:true});
|
|
3558
|
+
}
|
|
3559
|
+
return selection;
|
|
3560
|
+
}catch(error){
|
|
3561
|
+
if(committed){
|
|
3562
|
+
const committedError=error instanceof Error
|
|
3563
|
+
?error
|
|
3564
|
+
:new Error('Speech provider cleanup failed.',{cause:error});
|
|
3565
|
+
committedError.committed=true;
|
|
3566
|
+
throw committedError;
|
|
3567
|
+
}
|
|
3568
|
+
if(role==='tts'&&provider
|
|
3569
|
+
&&runtime.#providerRuntime.selection('tts')?.providerId===provider.id
|
|
3570
|
+
&&!runtime.#providerRuntime.hasProvider('tts',provider.id)
|
|
3571
|
+
&&!(runtime.#speechActivation?.configuration
|
|
3572
|
+
&&runtime.#speechActivation.generation>speechConfiguration.generation)){
|
|
3573
|
+
runtime.#invalidateSpeechControl();
|
|
3574
|
+
}
|
|
3575
|
+
if(provider&&!alreadyOwned&&!candidateDisposalAttempted
|
|
3576
|
+
&&!runtime.#providerRuntime.ownsProvider(role,provider)){
|
|
3577
|
+
try{
|
|
3578
|
+
await provider.dispose({role});
|
|
3579
|
+
}catch(cleanupError){
|
|
3580
|
+
throw new AggregateError(
|
|
3581
|
+
[error,cleanupError],
|
|
3582
|
+
'Speech configuration and candidate disposal failed.'
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
throw error;
|
|
3587
|
+
}finally{
|
|
3588
|
+
if(speechConfiguration
|
|
3589
|
+
&&runtime.#speechProviderConfiguration===speechConfiguration){
|
|
3590
|
+
runtime.#speechProviderConfiguration=null;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
);
|
|
3595
|
+
if(speechConfiguration)speechConfiguration.promise=scheduled;
|
|
3596
|
+
this.#browserSpeechTransition=scheduled.then(
|
|
3597
|
+
function completeSingleSpeechRoleConfiguration(){},
|
|
3598
|
+
function retainSingleSpeechRoleConfigurationFailure(){}
|
|
3599
|
+
);
|
|
3600
|
+
return scheduled;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3419
3603
|
configureBrowserSpeech(configuration,options={}){
|
|
3420
3604
|
this.#traceSpeech('configureBrowserSpeech.call',{configuration,options});
|
|
3421
3605
|
const normalized=normalizeBrowserSpeechConfiguration(configuration);
|
|
@@ -3719,6 +3903,8 @@ class AI {
|
|
|
3719
3903
|
async startProviders(options){
|
|
3720
3904
|
const normalized=normalizeAIStartupOptions(options);
|
|
3721
3905
|
const generation=++this.#speechControlGeneration;
|
|
3906
|
+
this.#speechActivation?.controller.abort();
|
|
3907
|
+
this.#speechActivation=null;
|
|
3722
3908
|
this.muted=true;
|
|
3723
3909
|
if(normalized.startMuted){
|
|
3724
3910
|
this.stopAudio();
|
|
@@ -3742,30 +3928,116 @@ class AI {
|
|
|
3742
3928
|
return handle;
|
|
3743
3929
|
}
|
|
3744
3930
|
|
|
3931
|
+
#waitForSelectedSpeechProvider(signal){
|
|
3932
|
+
const runtime=this;
|
|
3933
|
+
const selected=this.#providerRuntime.selection('tts');
|
|
3934
|
+
if(!selected)return Promise.resolve(false);
|
|
3935
|
+
return new Promise(function observeSelectedSpeechRegistration(resolve,reject){
|
|
3936
|
+
let unsubscribe=null;
|
|
3937
|
+
let settled=false;
|
|
3938
|
+
function finish(value,error){
|
|
3939
|
+
if(settled)return;
|
|
3940
|
+
settled=true;
|
|
3941
|
+
signal.removeEventListener('abort',cancel);
|
|
3942
|
+
unsubscribe?.();
|
|
3943
|
+
if(error)reject(error);
|
|
3944
|
+
else resolve(value);
|
|
3945
|
+
}
|
|
3946
|
+
function cancel(){
|
|
3947
|
+
finish(false,signal.reason??new DOMException('Speech activation cancelled.','AbortError'));
|
|
3948
|
+
}
|
|
3949
|
+
function observe(snapshot){
|
|
3950
|
+
const current=runtime.#providerRuntime.selection('tts');
|
|
3951
|
+
if(current?.providerId!==selected.providerId
|
|
3952
|
+
||current?.modelId!==selected.modelId){
|
|
3953
|
+
finish(false);
|
|
3954
|
+
}else if(runtime.#providerRuntime.hasProvider('tts',current.providerId)){
|
|
3955
|
+
finish(true);
|
|
3956
|
+
}else if(snapshot.roles.tts.state==='disposed'){
|
|
3957
|
+
finish(false);
|
|
3958
|
+
}else if(snapshot.roles.tts.state==='error'){
|
|
3959
|
+
finish(false,snapshot.roles.tts.error
|
|
3960
|
+
??new Error('Speech provider registration failed.'));
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
signal.addEventListener('abort',cancel,{once:true});
|
|
3964
|
+
if(signal.aborted){cancel();return;}
|
|
3965
|
+
unsubscribe=subscribeAIRuntimeState(observe);
|
|
3966
|
+
if(settled)unsubscribe();
|
|
3967
|
+
});
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3745
3970
|
async setSpeechMuted(muted){
|
|
3746
3971
|
const callId=this.#traceSpeech('setSpeechMuted.call',{requestedMuted:muted});
|
|
3747
3972
|
if(!is.boolean(muted)){
|
|
3748
3973
|
throw new TypeError('AI speech muted state must be a boolean.');
|
|
3749
3974
|
}
|
|
3975
|
+
const configuration=this.#speechProviderConfiguration;
|
|
3976
|
+
if(!muted&&this.#speechActivation){
|
|
3977
|
+
if(!configuration||this.#speechActivation.configuration===configuration){
|
|
3978
|
+
return this.#speechActivation.promise;
|
|
3979
|
+
}
|
|
3980
|
+
this.#invalidateSpeechControl();
|
|
3981
|
+
}
|
|
3750
3982
|
const generation=++this.#speechControlGeneration;
|
|
3751
3983
|
this.muted=true;
|
|
3752
3984
|
if(muted){
|
|
3985
|
+
this.#speechActivation?.controller.abort();
|
|
3986
|
+
this.#speechActivation=null;
|
|
3753
3987
|
this.stopAudio();
|
|
3754
3988
|
}
|
|
3755
|
-
if(!this.#usesProviderRuntime('tts',this.ttsService)
|
|
3989
|
+
if(!this.#usesProviderRuntime('tts',this.ttsService)
|
|
3990
|
+
&&(muted||!configuration)){
|
|
3756
3991
|
if(generation===this.#speechControlGeneration)this.muted=muted;
|
|
3757
3992
|
this.#traceSpeech('setSpeechMuted.result',{callId,result:true});
|
|
3758
3993
|
return true;
|
|
3759
3994
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
this.muted=muted
|
|
3764
|
-
||status.state!=='ready'
|
|
3765
|
-
||status.loaded!==true;
|
|
3995
|
+
if(muted){
|
|
3996
|
+
await this.#providerRuntime.setSpeechMuted(true);
|
|
3997
|
+
return true;
|
|
3766
3998
|
}
|
|
3767
|
-
this
|
|
3768
|
-
|
|
3999
|
+
const runtime=this;
|
|
4000
|
+
// A pending route owns the next utterance. Release old-route audio now,
|
|
4001
|
+
// before callers can queue text for this explicit activation.
|
|
4002
|
+
if(configuration)this.stopAudio();
|
|
4003
|
+
const activation={controller:new AbortController(),promise:null,generation,configuration};
|
|
4004
|
+
this.#speechActivation=activation;
|
|
4005
|
+
activation.promise=Promise.resolve().then(async function activateRequestedSpeech(){
|
|
4006
|
+
try{
|
|
4007
|
+
if(configuration){
|
|
4008
|
+
const configured=await runtime.#waitForSpeechOperation(
|
|
4009
|
+
configuration.promise,activation.controller.signal
|
|
4010
|
+
);
|
|
4011
|
+
activation.controller.signal.throwIfAborted();
|
|
4012
|
+
if(!configured)return false;
|
|
4013
|
+
}
|
|
4014
|
+
// Selection may precede registration. Observe the owner's sticky
|
|
4015
|
+
// state rather than treating that ordinary waiting state as failure.
|
|
4016
|
+
await runtime.#waitForSpeechOperation(
|
|
4017
|
+
runtime.#browserSpeechTransition,activation.controller.signal
|
|
4018
|
+
);
|
|
4019
|
+
if(!await runtime.#waitForSelectedSpeechProvider(activation.controller.signal)){
|
|
4020
|
+
return false;
|
|
4021
|
+
}
|
|
4022
|
+
// A registration event can fire before its configuration finishes.
|
|
4023
|
+
await runtime.#waitForSpeechOperation(
|
|
4024
|
+
runtime.#browserSpeechTransition,activation.controller.signal
|
|
4025
|
+
);
|
|
4026
|
+
activation.controller.signal.throwIfAborted();
|
|
4027
|
+
await runtime.#providerRuntime.setSpeechMuted(false);
|
|
4028
|
+
if(runtime.#speechActivation!==activation)return false;
|
|
4029
|
+
const status=runtime.#providerRuntime.status('tts');
|
|
4030
|
+
runtime.muted=status.state!=='ready'||status.loaded!==true;
|
|
4031
|
+
return !runtime.muted;
|
|
4032
|
+
}catch(error){
|
|
4033
|
+
if(activation.controller.signal.aborted)return false;
|
|
4034
|
+
throw error;
|
|
4035
|
+
}finally{
|
|
4036
|
+
if(runtime.#speechActivation===activation)runtime.#speechActivation=null;
|
|
4037
|
+
runtime.#traceSpeech('setSpeechMuted.result',{callId,result:!runtime.muted});
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
return activation.promise;
|
|
3769
4041
|
}
|
|
3770
4042
|
|
|
3771
4043
|
#nativeOllama(){
|
|
@@ -4171,7 +4443,8 @@ class AI {
|
|
|
4171
4443
|
function ignoreBuiltInLLMProviderRequest(){},
|
|
4172
4444
|
signal,
|
|
4173
4445
|
payload.reasoningEffort,
|
|
4174
|
-
onRetry
|
|
4446
|
+
onRetry,
|
|
4447
|
+
payload.model
|
|
4175
4448
|
);
|
|
4176
4449
|
}
|
|
4177
4450
|
|
|
@@ -4202,7 +4475,8 @@ class AI {
|
|
|
4202
4475
|
function ignoreBuiltInLLMStreamResult(){},
|
|
4203
4476
|
payload.reasoningEffort,
|
|
4204
4477
|
observeToolText,
|
|
4205
|
-
onRetry
|
|
4478
|
+
onRetry,
|
|
4479
|
+
payload.model
|
|
4206
4480
|
);
|
|
4207
4481
|
}
|
|
4208
4482
|
|
|
@@ -4283,7 +4557,7 @@ class AI {
|
|
|
4283
4557
|
});
|
|
4284
4558
|
}
|
|
4285
4559
|
|
|
4286
|
-
#openAICompatibleOllamaResponse(response={},id=Date.now()){
|
|
4560
|
+
#openAICompatibleOllamaResponse(response={},id=Date.now(),model=this.model){
|
|
4287
4561
|
const responseRecord=isPlainAIRecord(response)?response:{};
|
|
4288
4562
|
const message=isPlainAIRecord(responseRecord.message)
|
|
4289
4563
|
?responseRecord.message
|
|
@@ -4325,7 +4599,7 @@ class AI {
|
|
|
4325
4599
|
:Math.floor(Date.now()/1000),
|
|
4326
4600
|
model:Object.hasOwn(responseRecord,'model')
|
|
4327
4601
|
?responseRecord.model
|
|
4328
|
-
:
|
|
4602
|
+
:model,
|
|
4329
4603
|
choices:[
|
|
4330
4604
|
{
|
|
4331
4605
|
index:0,
|
|
@@ -4348,6 +4622,7 @@ class AI {
|
|
|
4348
4622
|
|
|
4349
4623
|
async streamRequest({
|
|
4350
4624
|
messages=[],
|
|
4625
|
+
model,
|
|
4351
4626
|
structuredOutput=false,
|
|
4352
4627
|
localOnly=false,
|
|
4353
4628
|
onChunk=function ignoreStreamChunk(){},
|
|
@@ -4404,6 +4679,7 @@ class AI {
|
|
|
4404
4679
|
if(this.#shouldUseProviderRuntime('llm',this.llmService,localOnly)){
|
|
4405
4680
|
const request={
|
|
4406
4681
|
messages,
|
|
4682
|
+
...(model!==undefined?{model}:{}),
|
|
4407
4683
|
structuredOutput,
|
|
4408
4684
|
tools,
|
|
4409
4685
|
toolChoice,
|
|
@@ -4529,7 +4805,8 @@ class AI {
|
|
|
4529
4805
|
onDataResult,
|
|
4530
4806
|
normalizedReasoningEffort,
|
|
4531
4807
|
observeToolText,
|
|
4532
|
-
onRetry
|
|
4808
|
+
onRetry,
|
|
4809
|
+
model
|
|
4533
4810
|
);
|
|
4534
4811
|
const structuralToolCalls=normalizeAICompletionToolCalls(
|
|
4535
4812
|
completion
|
|
@@ -4638,7 +4915,8 @@ class AI {
|
|
|
4638
4915
|
dataResultHandler=function ignoreBuiltInStreamDataResult(){},
|
|
4639
4916
|
reasoningEffort,
|
|
4640
4917
|
observeToolText=null,
|
|
4641
|
-
onRetry = null
|
|
4918
|
+
onRetry = null,
|
|
4919
|
+
model=this.model
|
|
4642
4920
|
){
|
|
4643
4921
|
let speechTurnCompleted=false;
|
|
4644
4922
|
|
|
@@ -4662,7 +4940,7 @@ class AI {
|
|
|
4662
4940
|
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
4663
4941
|
);
|
|
4664
4942
|
const request={
|
|
4665
|
-
model
|
|
4943
|
+
model,
|
|
4666
4944
|
messages:messages,
|
|
4667
4945
|
stream:true
|
|
4668
4946
|
}
|
|
@@ -4709,7 +4987,7 @@ class AI {
|
|
|
4709
4987
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
4710
4988
|
const adaptNativeToolCalls=value=>this.#openAICompatibleOllamaToolCalls(value,id);
|
|
4711
4989
|
const ollamaRequest={
|
|
4712
|
-
model
|
|
4990
|
+
model,
|
|
4713
4991
|
messages:ollamaMessages,
|
|
4714
4992
|
stream:true,
|
|
4715
4993
|
...(normalizedReasoningEffort
|
|
@@ -4819,7 +5097,8 @@ class AI {
|
|
|
4819
5097
|
:nativeContent
|
|
4820
5098
|
}
|
|
4821
5099
|
},
|
|
4822
|
-
id
|
|
5100
|
+
id,
|
|
5101
|
+
model
|
|
4823
5102
|
);
|
|
4824
5103
|
const structuralToolCalls=normalizeAICompletionToolCalls(
|
|
4825
5104
|
nativeCompletion
|
|
@@ -5311,7 +5590,7 @@ class AI {
|
|
|
5311
5590
|
created:Object.hasOwn(streamMetadata,'created')
|
|
5312
5591
|
?streamMetadata.created
|
|
5313
5592
|
:Math.floor(Date.now()/1000),
|
|
5314
|
-
model:Object.hasOwn(streamMetadata,'model')?streamMetadata.model:
|
|
5593
|
+
model:Object.hasOwn(streamMetadata,'model')?streamMetadata.model:model,
|
|
5315
5594
|
choices:completionChoices
|
|
5316
5595
|
};
|
|
5317
5596
|
const terminalToolCalls=normalizeAICompletionToolCalls(completion);
|
|
@@ -5356,6 +5635,7 @@ class AI {
|
|
|
5356
5635
|
|
|
5357
5636
|
async fetchRequest({
|
|
5358
5637
|
messages=[],
|
|
5638
|
+
model,
|
|
5359
5639
|
structuredOutput=false,
|
|
5360
5640
|
localOnly=false,
|
|
5361
5641
|
tools=[],
|
|
@@ -5398,6 +5678,7 @@ class AI {
|
|
|
5398
5678
|
}
|
|
5399
5679
|
const request={
|
|
5400
5680
|
messages,
|
|
5681
|
+
...(model!==undefined?{model}:{}),
|
|
5401
5682
|
structuredOutput,
|
|
5402
5683
|
tools,
|
|
5403
5684
|
toolChoice,
|
|
@@ -5454,7 +5735,8 @@ class AI {
|
|
|
5454
5735
|
onRequest,
|
|
5455
5736
|
signal,
|
|
5456
5737
|
normalizedReasoningEffort,
|
|
5457
|
-
onRetry
|
|
5738
|
+
onRetry,
|
|
5739
|
+
model
|
|
5458
5740
|
);
|
|
5459
5741
|
}
|
|
5460
5742
|
|
|
@@ -5508,7 +5790,8 @@ class AI {
|
|
|
5508
5790
|
requestHandler=function ignoreFetchRequest(){},
|
|
5509
5791
|
signal=null,
|
|
5510
5792
|
reasoningEffort,
|
|
5511
|
-
onRetry = null
|
|
5793
|
+
onRetry = null,
|
|
5794
|
+
model=this.model
|
|
5512
5795
|
){
|
|
5513
5796
|
validateAIStructuralRequest(messages,tools,parallel_tool_calls);
|
|
5514
5797
|
this.#assertServiceConfigured(this.llmService);
|
|
@@ -5527,7 +5810,7 @@ class AI {
|
|
|
5527
5810
|
reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
|
|
5528
5811
|
);
|
|
5529
5812
|
const request={
|
|
5530
|
-
model
|
|
5813
|
+
model,
|
|
5531
5814
|
messages:messages,
|
|
5532
5815
|
stream:false
|
|
5533
5816
|
}
|
|
@@ -5566,7 +5849,7 @@ class AI {
|
|
|
5566
5849
|
const ollamaTools=this.#ollamaTools(tools,tool_choice);
|
|
5567
5850
|
const ollamaMessages=this.#ollamaMessages(messages,tool_choice);
|
|
5568
5851
|
const nativeRequest={
|
|
5569
|
-
model
|
|
5852
|
+
model,
|
|
5570
5853
|
messages:ollamaMessages,
|
|
5571
5854
|
stream:false,
|
|
5572
5855
|
...(normalizedReasoningEffort
|
|
@@ -5597,7 +5880,8 @@ class AI {
|
|
|
5597
5880
|
}
|
|
5598
5881
|
const responseJSON=this.#openAICompatibleOllamaResponse(
|
|
5599
5882
|
nativeResponse,
|
|
5600
|
-
id
|
|
5883
|
+
id,
|
|
5884
|
+
model
|
|
5601
5885
|
);
|
|
5602
5886
|
this.#assertRequiredOllamaToolCall(
|
|
5603
5887
|
normalizeAICompletionToolCalls(responseJSON),
|
|
@@ -5934,7 +6218,7 @@ class AI {
|
|
|
5934
6218
|
){
|
|
5935
6219
|
const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
|
|
5936
6220
|
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
5937
|
-
if(this.muted){
|
|
6221
|
+
if(this.muted&&!this.speechActivationPending){
|
|
5938
6222
|
if(end){
|
|
5939
6223
|
this.audioMessageChunks='';
|
|
5940
6224
|
this.#markdownSpeech.reset();
|
|
@@ -5963,7 +6247,9 @@ class AI {
|
|
|
5963
6247
|
}
|
|
5964
6248
|
|
|
5965
6249
|
try{
|
|
5966
|
-
|
|
6250
|
+
if(!this.speechActivationPending){
|
|
6251
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
6252
|
+
}
|
|
5967
6253
|
}catch(error){
|
|
5968
6254
|
this.#traceSpeech('streamTTS.error',{callId,error});
|
|
5969
6255
|
this.#publishTTSFailure(error,{
|
|
@@ -5999,7 +6285,7 @@ class AI {
|
|
|
5999
6285
|
}
|
|
6000
6286
|
|
|
6001
6287
|
#finishStreamingSpeech(){
|
|
6002
|
-
if(this.muted){
|
|
6288
|
+
if(this.muted&&!this.speechActivationPending){
|
|
6003
6289
|
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
6004
6290
|
this.audioMessageChunks='';
|
|
6005
6291
|
this.#markdownSpeech.reset();
|
|
@@ -6163,7 +6449,7 @@ class AI {
|
|
|
6163
6449
|
const job={
|
|
6164
6450
|
diagnosticId:++this.#speechJobSequence,
|
|
6165
6451
|
diagnosticCallId:options.diagnosticCallId,
|
|
6166
|
-
abortController:
|
|
6452
|
+
abortController:new AbortController(),
|
|
6167
6453
|
audioBuffer:null,
|
|
6168
6454
|
audioContext:null,
|
|
6169
6455
|
generation,
|
|
@@ -6203,7 +6489,31 @@ class AI {
|
|
|
6203
6489
|
return playback||preparation;
|
|
6204
6490
|
}
|
|
6205
6491
|
|
|
6492
|
+
#waitForSpeechOperation(operation,signal){
|
|
6493
|
+
return new Promise(function observeSpeechOperation(resolve,reject){
|
|
6494
|
+
let settled=false;
|
|
6495
|
+
function finish(value,error){
|
|
6496
|
+
if(settled)return;
|
|
6497
|
+
settled=true;
|
|
6498
|
+
signal.removeEventListener('abort',cancel);
|
|
6499
|
+
if(error)reject(error);
|
|
6500
|
+
else resolve(value);
|
|
6501
|
+
}
|
|
6502
|
+
function cancel(){finish(false);}
|
|
6503
|
+
signal.addEventListener('abort',cancel,{once:true});
|
|
6504
|
+
if(signal.aborted){cancel();return;}
|
|
6505
|
+
operation.then(
|
|
6506
|
+
function speechOperationCompleted(value){finish(value);},
|
|
6507
|
+
function speechOperationFailed(error){finish(false,error);}
|
|
6508
|
+
);
|
|
6509
|
+
});
|
|
6510
|
+
}
|
|
6511
|
+
|
|
6206
6512
|
async #prepareSpeechJob(job){
|
|
6513
|
+
const activation=this.#speechActivation;
|
|
6514
|
+
if(activation&&job.generation===this.speechGeneration){
|
|
6515
|
+
await this.#waitForSpeechOperation(activation.promise,job.abortController.signal);
|
|
6516
|
+
}
|
|
6207
6517
|
if(job.generation!==this.speechGeneration||this.muted){
|
|
6208
6518
|
return this.#cancelSpeechJob(job);
|
|
6209
6519
|
}
|
|
@@ -6229,7 +6539,7 @@ class AI {
|
|
|
6229
6539
|
}
|
|
6230
6540
|
|
|
6231
6541
|
async #requestSpeechAudio(job){
|
|
6232
|
-
job.abortController
|
|
6542
|
+
job.abortController??=new AbortController();
|
|
6233
6543
|
const selection=this.#providerRuntime.selection('tts');
|
|
6234
6544
|
const voice=job.voice===undefined
|
|
6235
6545
|
?(selection?this.#providerSpeechVoice():null)
|