arcane-os 0.33.5 → 0.34.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 +31 -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/browser-runtime/ai/twin-cloud.mjs +11 -4
- package/docs/reference/ai/browser-speech.md +86 -5
- package/docs/reference/ai/twin-cloud.md +5 -2
- 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-components.md +3 -1
- package/docs/reference/runtime-entities.md +40 -0
- package/docs/reference/runtime-modules.md +39 -9
- package/docs/reference/sdk-api.md +66 -2
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +15 -9
- package/runtime/arcane/components/speech.html +40 -6
- package/runtime/arcane/entities/User.js +30 -7
- package/runtime/arcane/modules/AI.js +337 -39
|
@@ -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(){
|
|
@@ -5934,7 +6206,7 @@ class AI {
|
|
|
5934
6206
|
){
|
|
5935
6207
|
const callId=this.#traceSpeech('streamTTS.call',{text,end,options});
|
|
5936
6208
|
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
5937
|
-
if(this.muted){
|
|
6209
|
+
if(this.muted&&!this.speechActivationPending){
|
|
5938
6210
|
if(end){
|
|
5939
6211
|
this.audioMessageChunks='';
|
|
5940
6212
|
this.#markdownSpeech.reset();
|
|
@@ -5963,7 +6235,9 @@ class AI {
|
|
|
5963
6235
|
}
|
|
5964
6236
|
|
|
5965
6237
|
try{
|
|
5966
|
-
|
|
6238
|
+
if(!this.speechActivationPending){
|
|
6239
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
6240
|
+
}
|
|
5967
6241
|
}catch(error){
|
|
5968
6242
|
this.#traceSpeech('streamTTS.error',{callId,error});
|
|
5969
6243
|
this.#publishTTSFailure(error,{
|
|
@@ -5999,7 +6273,7 @@ class AI {
|
|
|
5999
6273
|
}
|
|
6000
6274
|
|
|
6001
6275
|
#finishStreamingSpeech(){
|
|
6002
|
-
if(this.muted){
|
|
6276
|
+
if(this.muted&&!this.speechActivationPending){
|
|
6003
6277
|
for(const playback of this.#preparedSpeechPlaybacks) playback.stop();
|
|
6004
6278
|
this.audioMessageChunks='';
|
|
6005
6279
|
this.#markdownSpeech.reset();
|
|
@@ -6163,7 +6437,7 @@ class AI {
|
|
|
6163
6437
|
const job={
|
|
6164
6438
|
diagnosticId:++this.#speechJobSequence,
|
|
6165
6439
|
diagnosticCallId:options.diagnosticCallId,
|
|
6166
|
-
abortController:
|
|
6440
|
+
abortController:new AbortController(),
|
|
6167
6441
|
audioBuffer:null,
|
|
6168
6442
|
audioContext:null,
|
|
6169
6443
|
generation,
|
|
@@ -6203,7 +6477,31 @@ class AI {
|
|
|
6203
6477
|
return playback||preparation;
|
|
6204
6478
|
}
|
|
6205
6479
|
|
|
6480
|
+
#waitForSpeechOperation(operation,signal){
|
|
6481
|
+
return new Promise(function observeSpeechOperation(resolve,reject){
|
|
6482
|
+
let settled=false;
|
|
6483
|
+
function finish(value,error){
|
|
6484
|
+
if(settled)return;
|
|
6485
|
+
settled=true;
|
|
6486
|
+
signal.removeEventListener('abort',cancel);
|
|
6487
|
+
if(error)reject(error);
|
|
6488
|
+
else resolve(value);
|
|
6489
|
+
}
|
|
6490
|
+
function cancel(){finish(false);}
|
|
6491
|
+
signal.addEventListener('abort',cancel,{once:true});
|
|
6492
|
+
if(signal.aborted){cancel();return;}
|
|
6493
|
+
operation.then(
|
|
6494
|
+
function speechOperationCompleted(value){finish(value);},
|
|
6495
|
+
function speechOperationFailed(error){finish(false,error);}
|
|
6496
|
+
);
|
|
6497
|
+
});
|
|
6498
|
+
}
|
|
6499
|
+
|
|
6206
6500
|
async #prepareSpeechJob(job){
|
|
6501
|
+
const activation=this.#speechActivation;
|
|
6502
|
+
if(activation&&job.generation===this.speechGeneration){
|
|
6503
|
+
await this.#waitForSpeechOperation(activation.promise,job.abortController.signal);
|
|
6504
|
+
}
|
|
6207
6505
|
if(job.generation!==this.speechGeneration||this.muted){
|
|
6208
6506
|
return this.#cancelSpeechJob(job);
|
|
6209
6507
|
}
|
|
@@ -6229,7 +6527,7 @@ class AI {
|
|
|
6229
6527
|
}
|
|
6230
6528
|
|
|
6231
6529
|
async #requestSpeechAudio(job){
|
|
6232
|
-
job.abortController
|
|
6530
|
+
job.abortController??=new AbortController();
|
|
6233
6531
|
const selection=this.#providerRuntime.selection('tts');
|
|
6234
6532
|
const voice=job.voice===undefined
|
|
6235
6533
|
?(selection?this.#providerSpeechVoice():null)
|