arcane-os 0.2.2 → 0.3.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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import './DBOPFS.js';
|
|
2
2
|
import UserEntity from '../entities/User.js';
|
|
3
|
+
import {
|
|
4
|
+
arcaneEvents,
|
|
5
|
+
createArcaneEventSource,
|
|
6
|
+
projectArcaneDOMEvent
|
|
7
|
+
} from 'arcane-os/event-manager';
|
|
3
8
|
import {getAIPreferencesForRuntime} from './AIPreferenceRuntime.js';
|
|
4
9
|
import {
|
|
5
10
|
AI_MODEL_AUTHORITY_PROTOCOL,
|
|
@@ -13,6 +18,84 @@ const LEGACY_TTS_RESPONSE_FORMAT='opus';
|
|
|
13
18
|
credentials='omit';
|
|
14
19
|
|
|
15
20
|
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
21
|
+
export const AI_READY_EVENT='ai-ready';
|
|
22
|
+
export const AI_INITIALIZATION_ERROR_CODES=Object.freeze({
|
|
23
|
+
userReadyRegistrationCollision:
|
|
24
|
+
'ARCANE_AI_USER_READY_REGISTRATION_COLLISION'
|
|
25
|
+
});
|
|
26
|
+
export const AI_INITIALIZATION_REASONS=Object.freeze({
|
|
27
|
+
initialized:'ai-initialized',
|
|
28
|
+
userReadyRegistrationCollision:'ai-user-ready-registration-collision'
|
|
29
|
+
});
|
|
30
|
+
export const AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL=
|
|
31
|
+
'arcane-ai-browser-speech-configuration/1';
|
|
32
|
+
export const AI_BROWSER_SPEECH_EVENT_TYPES=Object.freeze({
|
|
33
|
+
configurationCancelled:'ai-browser-speech-configuration-cancelled',
|
|
34
|
+
configurationError:'ai-browser-speech-configuration-error',
|
|
35
|
+
configurationStarted:'ai-browser-speech-configuration-started',
|
|
36
|
+
configured:'ai-browser-speech-configured',
|
|
37
|
+
disposed:'ai-browser-speech-disposed'
|
|
38
|
+
});
|
|
39
|
+
export const AI_BROWSER_SPEECH_ERROR_CODES=Object.freeze({
|
|
40
|
+
artifactStoreConstructionRejected:
|
|
41
|
+
'ARCANE_AI_BROWSER_SPEECH_ARTIFACT_STORE_CONSTRUCTION_REJECTED',
|
|
42
|
+
asyncTransitionRequired:
|
|
43
|
+
'ARCANE_AI_BROWSER_SPEECH_ASYNC_TRANSITION_REQUIRED',
|
|
44
|
+
configurationCancelled:'ARCANE_AI_BROWSER_SPEECH_CONFIGURATION_CANCELLED',
|
|
45
|
+
configurationContractMismatch:
|
|
46
|
+
'ARCANE_AI_BROWSER_SPEECH_CONFIGURATION_CONTRACT_MISMATCH',
|
|
47
|
+
configurationSuperseded:
|
|
48
|
+
'ARCANE_AI_BROWSER_SPEECH_CONFIGURATION_SUPERSEDED',
|
|
49
|
+
operationOptionsContractMismatch:
|
|
50
|
+
'ARCANE_AI_BROWSER_SPEECH_OPERATION_OPTIONS_CONTRACT_MISMATCH',
|
|
51
|
+
operationSequenceExhausted:
|
|
52
|
+
'ARCANE_AI_BROWSER_SPEECH_OPERATION_SEQUENCE_EXHAUSTED',
|
|
53
|
+
moduleImportRejected:'ARCANE_AI_BROWSER_SPEECH_MODULE_IMPORT_REJECTED',
|
|
54
|
+
providerConstructionRejected:
|
|
55
|
+
'ARCANE_AI_BROWSER_SPEECH_PROVIDER_CONSTRUCTION_REJECTED',
|
|
56
|
+
providerDisposalRejected:
|
|
57
|
+
'ARCANE_AI_BROWSER_SPEECH_PROVIDER_DISPOSAL_REJECTED',
|
|
58
|
+
providerRouteOwnershipMismatch:
|
|
59
|
+
'ARCANE_AI_BROWSER_SPEECH_PROVIDER_ROUTE_OWNERSHIP_MISMATCH',
|
|
60
|
+
providerUnregistrationRejected:
|
|
61
|
+
'ARCANE_AI_BROWSER_SPEECH_PROVIDER_UNREGISTRATION_REJECTED',
|
|
62
|
+
routeCommitRejected:'ARCANE_AI_BROWSER_SPEECH_ROUTE_COMMIT_REJECTED',
|
|
63
|
+
routeRollbackRejected:'ARCANE_AI_BROWSER_SPEECH_ROUTE_ROLLBACK_REJECTED',
|
|
64
|
+
routeViewUpdateRejected:
|
|
65
|
+
'ARCANE_AI_BROWSER_SPEECH_ROUTE_VIEW_UPDATE_REJECTED'
|
|
66
|
+
});
|
|
67
|
+
export const AI_BROWSER_SPEECH_REASONS=Object.freeze({
|
|
68
|
+
artifactStoreConstructionRejected:'speech-artifact-store-construction-rejected',
|
|
69
|
+
asyncTransitionRequired:'speech-configuration-async-transition-required',
|
|
70
|
+
configurationAdded:'speech-configuration-added',
|
|
71
|
+
configurationCancelled:'speech-configuration-cancelled',
|
|
72
|
+
configurationContractMismatch:'speech-configuration-contract-mismatch',
|
|
73
|
+
configurationDisposed:'speech-configuration-disposed',
|
|
74
|
+
configurationReplaced:'speech-configuration-replaced',
|
|
75
|
+
moduleImportRejected:'speech-module-import-rejected',
|
|
76
|
+
operationOptionsContractMismatch:'speech-operation-options-contract-mismatch',
|
|
77
|
+
operationSequenceExhausted:'speech-operation-sequence-exhausted',
|
|
78
|
+
providerConstructionRejected:'speech-provider-construction-rejected',
|
|
79
|
+
providerDisposalRejected:'speech-provider-disposal-rejected',
|
|
80
|
+
providerRouteOwnershipMismatch:'speech-provider-route-ownership-mismatch',
|
|
81
|
+
providerUnregistrationRejected:'speech-provider-unregistration-rejected',
|
|
82
|
+
routeCommitRejected:'speech-route-commit-rejected',
|
|
83
|
+
routeRollbackRejected:'speech-route-rollback-rejected',
|
|
84
|
+
routeViewUpdateRejected:'speech-route-view-update-rejected'
|
|
85
|
+
});
|
|
86
|
+
const AI_PUBLISH_READY=Symbol('publish-ai-ready');
|
|
87
|
+
const AI_USER_READY_REGISTRATION_KEY=Symbol.for(
|
|
88
|
+
'arcane.ai.user-ready-registration'
|
|
89
|
+
);
|
|
90
|
+
const AI_USER_READY_REGISTRATION_PROTOCOL=
|
|
91
|
+
'arcane-ai-user-ready-registration/1';
|
|
92
|
+
|
|
93
|
+
function aiInitializationError(code,reason,message){
|
|
94
|
+
const error=new Error(message);
|
|
95
|
+
error.code=code;
|
|
96
|
+
error.reason=reason;
|
|
97
|
+
return error;
|
|
98
|
+
}
|
|
16
99
|
|
|
17
100
|
function isAIRequestAbort(error,signal){
|
|
18
101
|
return signal?.aborted
|
|
@@ -228,6 +311,255 @@ function normalizeAIStartupOptions(options){
|
|
|
228
311
|
return Object.freeze({startMuted,startTranscription,signal});
|
|
229
312
|
}
|
|
230
313
|
|
|
314
|
+
function aiBrowserSpeechError(code,reason,message,cause,{committed=false,name='Error'}={}){
|
|
315
|
+
const error=cause===undefined
|
|
316
|
+
?new Error(message)
|
|
317
|
+
:new Error(message,{cause});
|
|
318
|
+
error.name=name;
|
|
319
|
+
error.code=code;
|
|
320
|
+
error.reason=reason;
|
|
321
|
+
if(committed)error.committed=true;
|
|
322
|
+
return error;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isAbortSignal(value){
|
|
326
|
+
return Boolean(value)
|
|
327
|
+
&&typeof value==='object'
|
|
328
|
+
&&typeof value.aborted==='boolean'
|
|
329
|
+
&&typeof value.addEventListener==='function'
|
|
330
|
+
&&typeof value.removeEventListener==='function';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function frozenClosedRecord(value,keys,required,label){
|
|
334
|
+
if(!value
|
|
335
|
+
||typeof value!=='object'
|
|
336
|
+
||Array.isArray(value)
|
|
337
|
+
||![Object.prototype,null].includes(Object.getPrototypeOf(value))
|
|
338
|
+
||!Object.isFrozen(value)){
|
|
339
|
+
throw aiBrowserSpeechError(
|
|
340
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
341
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
342
|
+
`${label} must be a frozen plain data record.`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const descriptors=Object.getOwnPropertyDescriptors(value);
|
|
346
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
347
|
+
if(typeof key!=='string'
|
|
348
|
+
||!keys.includes(key)
|
|
349
|
+
||!Object.hasOwn(descriptors[key],'value')){
|
|
350
|
+
throw aiBrowserSpeechError(
|
|
351
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
352
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
353
|
+
`${label} contains an unsupported or accessor field.`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for(const key of required){
|
|
358
|
+
if(!Object.hasOwn(descriptors,key)){
|
|
359
|
+
throw aiBrowserSpeechError(
|
|
360
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
361
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
362
|
+
`${label}.${key} is required.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return descriptors;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function browserSpeechIdentifier(value,label){
|
|
370
|
+
if(typeof value!=='string'
|
|
371
|
+
||value.trim()!==value
|
|
372
|
+
||value.length<1
|
|
373
|
+
||value.length>128){
|
|
374
|
+
throw aiBrowserSpeechError(
|
|
375
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
376
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
377
|
+
`${label} must be a trimmed 1-128 character string.`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeBrowserSpeechRole(value,role){
|
|
384
|
+
const label=`AI browser speech ${role}`;
|
|
385
|
+
const descriptors=frozenClosedRecord(
|
|
386
|
+
value,
|
|
387
|
+
['providerId','graph','model','runtime','security','offline'],
|
|
388
|
+
['providerId','offline'],
|
|
389
|
+
label
|
|
390
|
+
);
|
|
391
|
+
const providerId=browserSpeechIdentifier(
|
|
392
|
+
descriptors.providerId.value,
|
|
393
|
+
`${label}.providerId`
|
|
394
|
+
);
|
|
395
|
+
const hasGraph=Object.hasOwn(descriptors,'graph');
|
|
396
|
+
const hasModel=Object.hasOwn(descriptors,'model');
|
|
397
|
+
const hasRuntime=Object.hasOwn(descriptors,'runtime');
|
|
398
|
+
const hasSecurity=Object.hasOwn(descriptors,'security');
|
|
399
|
+
if(hasGraph&&(hasModel||hasRuntime)){
|
|
400
|
+
throw aiBrowserSpeechError(
|
|
401
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
402
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
403
|
+
`${label}.graph is mutually exclusive with model and runtime.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if(!hasGraph&&(!hasModel||!hasRuntime)){
|
|
407
|
+
throw aiBrowserSpeechError(
|
|
408
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
409
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
410
|
+
`${label} must provide graph or both model and runtime.`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if(hasGraph){
|
|
414
|
+
const graph=descriptors.graph.value;
|
|
415
|
+
if(!graph||typeof graph!=='object'||!Object.isFrozen(graph)){
|
|
416
|
+
throw aiBrowserSpeechError(
|
|
417
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
418
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
419
|
+
`${label}.graph must be an SDK-created frozen artifact graph.`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if(!hasSecurity){
|
|
423
|
+
throw aiBrowserSpeechError(
|
|
424
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
425
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
426
|
+
`${label}.security with secure:true is required for an artifact graph.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const security=frozenClosedRecord(
|
|
430
|
+
descriptors.security.value,
|
|
431
|
+
['secure','checks'],
|
|
432
|
+
['secure'],
|
|
433
|
+
`${label}.security`
|
|
434
|
+
);
|
|
435
|
+
if(security.secure.value!==true){
|
|
436
|
+
throw aiBrowserSpeechError(
|
|
437
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
438
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
439
|
+
`${label}.security.secure must be true for an artifact graph.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}else{
|
|
443
|
+
for(const key of ['model','runtime']){
|
|
444
|
+
const descriptor=descriptors[key].value;
|
|
445
|
+
if(!descriptor||typeof descriptor!=='object'||Array.isArray(descriptor)){
|
|
446
|
+
throw aiBrowserSpeechError(
|
|
447
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
448
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
449
|
+
`${label}.${key} must be a browser speech authority descriptor.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if(typeof descriptors.offline.value!=='boolean'){
|
|
455
|
+
throw aiBrowserSpeechError(
|
|
456
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
457
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
458
|
+
`${label}.offline must be a boolean.`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
providerId,
|
|
463
|
+
...(hasGraph
|
|
464
|
+
?{
|
|
465
|
+
graph:descriptors.graph.value,
|
|
466
|
+
security:descriptors.security.value
|
|
467
|
+
}
|
|
468
|
+
:{
|
|
469
|
+
model:descriptors.model.value,
|
|
470
|
+
runtime:descriptors.runtime.value,
|
|
471
|
+
...(hasSecurity?{security:descriptors.security.value}:{})
|
|
472
|
+
}),
|
|
473
|
+
offline:descriptors.offline.value
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizeBrowserSpeechConfiguration(value){
|
|
478
|
+
const descriptors=frozenClosedRecord(
|
|
479
|
+
value,
|
|
480
|
+
['protocol','id','dbopfs','tableName','stt','tts'],
|
|
481
|
+
['protocol','id','dbopfs'],
|
|
482
|
+
'AI browser speech configuration'
|
|
483
|
+
);
|
|
484
|
+
if(descriptors.protocol.value!==AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL){
|
|
485
|
+
throw aiBrowserSpeechError(
|
|
486
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
487
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
488
|
+
`AI browser speech configuration.protocol must be ${AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL}.`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const id=browserSpeechIdentifier(
|
|
492
|
+
descriptors.id.value,
|
|
493
|
+
'AI browser speech configuration.id'
|
|
494
|
+
);
|
|
495
|
+
const dbopfs=descriptors.dbopfs.value;
|
|
496
|
+
if(!dbopfs||typeof dbopfs!=='object'){
|
|
497
|
+
throw aiBrowserSpeechError(
|
|
498
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
499
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
500
|
+
'AI browser speech configuration.dbopfs must be an existing DBOPFS instance.'
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const tableName=descriptors.tableName
|
|
504
|
+
?browserSpeechIdentifier(
|
|
505
|
+
descriptors.tableName.value,
|
|
506
|
+
'AI browser speech configuration.tableName'
|
|
507
|
+
)
|
|
508
|
+
:undefined;
|
|
509
|
+
const roles=['stt','tts'].filter(role=>Object.hasOwn(descriptors,role));
|
|
510
|
+
if(roles.length===0){
|
|
511
|
+
throw aiBrowserSpeechError(
|
|
512
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
513
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
514
|
+
'AI browser speech configuration must provide stt, tts, or both.'
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return Object.freeze({
|
|
518
|
+
configuration:value,
|
|
519
|
+
id,
|
|
520
|
+
dbopfs,
|
|
521
|
+
...(tableName?{tableName}:{}),
|
|
522
|
+
roles:Object.freeze(roles),
|
|
523
|
+
...Object.fromEntries(roles.map(role=>[
|
|
524
|
+
role,
|
|
525
|
+
normalizeBrowserSpeechRole(descriptors[role].value,role)
|
|
526
|
+
]))
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeBrowserSpeechOperationOptions(value,label){
|
|
531
|
+
if(value===undefined)return Object.freeze({signal:null});
|
|
532
|
+
if(!value
|
|
533
|
+
||typeof value!=='object'
|
|
534
|
+
||Array.isArray(value)
|
|
535
|
+
||![Object.prototype,null].includes(Object.getPrototypeOf(value))){
|
|
536
|
+
throw aiBrowserSpeechError(
|
|
537
|
+
AI_BROWSER_SPEECH_ERROR_CODES.operationOptionsContractMismatch,
|
|
538
|
+
AI_BROWSER_SPEECH_REASONS.operationOptionsContractMismatch,
|
|
539
|
+
`${label} options must be a plain object.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
const descriptors=Object.getOwnPropertyDescriptors(value);
|
|
543
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
544
|
+
if(key!=='signal'||!Object.hasOwn(descriptors[key],'value')){
|
|
545
|
+
throw aiBrowserSpeechError(
|
|
546
|
+
AI_BROWSER_SPEECH_ERROR_CODES.operationOptionsContractMismatch,
|
|
547
|
+
AI_BROWSER_SPEECH_REASONS.operationOptionsContractMismatch,
|
|
548
|
+
`${label} options support only a signal data property.`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const signal=descriptors.signal?.value??null;
|
|
553
|
+
if(signal!==null&&!isAbortSignal(signal)){
|
|
554
|
+
throw aiBrowserSpeechError(
|
|
555
|
+
AI_BROWSER_SPEECH_ERROR_CODES.operationOptionsContractMismatch,
|
|
556
|
+
AI_BROWSER_SPEECH_REASONS.operationOptionsContractMismatch,
|
|
557
|
+
`${label} signal must be an AbortSignal.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
return Object.freeze({signal});
|
|
561
|
+
}
|
|
562
|
+
|
|
231
563
|
class AI {
|
|
232
564
|
// This is the enum section for inference configuration
|
|
233
565
|
#service = {
|
|
@@ -340,6 +672,19 @@ class AI {
|
|
|
340
672
|
return window.ai;
|
|
341
673
|
}
|
|
342
674
|
|
|
675
|
+
this.#events=createArcaneEventSource(
|
|
676
|
+
this,
|
|
677
|
+
{
|
|
678
|
+
source:'ai',
|
|
679
|
+
eventTypes:Object.freeze(
|
|
680
|
+
[
|
|
681
|
+
AI_READY_EVENT,
|
|
682
|
+
...Object.values(AI_BROWSER_SPEECH_EVENT_TYPES)
|
|
683
|
+
]
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
|
|
343
688
|
const preferences=[
|
|
344
689
|
llmService||'OPENAI',
|
|
345
690
|
sttService||'OPENAI',
|
|
@@ -353,7 +698,7 @@ class AI {
|
|
|
353
698
|
);
|
|
354
699
|
|
|
355
700
|
const runtime=this;
|
|
356
|
-
|
|
701
|
+
this.#stopOllamaReady=arcaneEvents.subscribe(
|
|
357
702
|
'arcane-ollama-ready',
|
|
358
703
|
function reconcileLegacyOllamaReadiness(){
|
|
359
704
|
runtime.#retainLegacyLLMReadiness(
|
|
@@ -364,10 +709,22 @@ class AI {
|
|
|
364
709
|
}
|
|
365
710
|
|
|
366
711
|
#providerRuntime=getAIProviderRuntime();
|
|
712
|
+
#events=null;
|
|
713
|
+
#browserSpeechConfigurationRecord=null;
|
|
714
|
+
#browserSpeechController=null;
|
|
715
|
+
#browserSpeechControllerRoles=Object.freeze([]);
|
|
716
|
+
#browserSpeechGeneration=0;
|
|
717
|
+
#browserSpeechModulePromise=null;
|
|
718
|
+
#browserSpeechOperationSequence=0;
|
|
719
|
+
#browserSpeechRetiredRecords=new Set();
|
|
720
|
+
#browserSpeechRetiredLegacyRecords=new Set();
|
|
721
|
+
#browserSpeechTransition=Promise.resolve();
|
|
367
722
|
#legacyLLMProviders=new Map();
|
|
368
723
|
#legacyLLMReadiness=Promise.resolve(null);
|
|
369
724
|
#legacySpeechProviders=new Map();
|
|
370
725
|
#legacySpeechReadiness=Promise.resolve(null);
|
|
726
|
+
#speechControlGeneration=0;
|
|
727
|
+
#stopOllamaReady=null;
|
|
371
728
|
#preferenceTuple=Object.freeze([
|
|
372
729
|
'OPENAI',
|
|
373
730
|
'OPENAI',
|
|
@@ -381,6 +738,40 @@ class AI {
|
|
|
381
738
|
return this.#providerRuntime;
|
|
382
739
|
}
|
|
383
740
|
|
|
741
|
+
[AI_PUBLISH_READY](){
|
|
742
|
+
const operationId=`${this.#events.instanceId}:ready:1`;
|
|
743
|
+
const reason=AI_INITIALIZATION_REASONS.initialized;
|
|
744
|
+
const {occurrence}=this.#events.dispatch(
|
|
745
|
+
AI_READY_EVENT,
|
|
746
|
+
Object.freeze({db:this,operationId,reason}),
|
|
747
|
+
{
|
|
748
|
+
operationId,
|
|
749
|
+
publicDetail:Object.freeze({
|
|
750
|
+
ready:true,
|
|
751
|
+
reason
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
);
|
|
755
|
+
projectArcaneDOMEvent(window,occurrence);
|
|
756
|
+
return occurrence;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
get browserSpeechConfiguration(){
|
|
760
|
+
return this.#browserSpeechRecordIsActive(
|
|
761
|
+
this.#browserSpeechConfigurationRecord
|
|
762
|
+
)
|
|
763
|
+
?this.#browserSpeechConfigurationRecord.configuration
|
|
764
|
+
:null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
get browserSpeechDescriptor(){
|
|
768
|
+
return this.#browserSpeechRecordIsActive(
|
|
769
|
+
this.#browserSpeechConfigurationRecord
|
|
770
|
+
)
|
|
771
|
+
?this.#browserSpeechConfigurationRecord.descriptor
|
|
772
|
+
:null;
|
|
773
|
+
}
|
|
774
|
+
|
|
384
775
|
get url() {
|
|
385
776
|
return `${this.#service.baseURL[this.llmService]}${this.#paths.chat[this.llmService]}`
|
|
386
777
|
}
|
|
@@ -1228,6 +1619,14 @@ class AI {
|
|
|
1228
1619
|
this.#preferenceTuple=Object.freeze(tuple.slice());
|
|
1229
1620
|
}
|
|
1230
1621
|
|
|
1622
|
+
#applySpeechPreferenceTuple(tuple){
|
|
1623
|
+
this.sttService=tuple[1];
|
|
1624
|
+
this.ttsService=tuple[2];
|
|
1625
|
+
this.modelTTS=this.#ttsModels[tuple[4]]||tuple[4];
|
|
1626
|
+
this.modelSTT=this.#sttModels[tuple[5]]||tuple[5];
|
|
1627
|
+
this.#preferenceTuple=Object.freeze(tuple.slice());
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1231
1630
|
#tupleFromProviderRoutes(selections){
|
|
1232
1631
|
const llm=selections.llm.default;
|
|
1233
1632
|
const stt=selections.stt.default;
|
|
@@ -1242,6 +1641,20 @@ class AI {
|
|
|
1242
1641
|
]);
|
|
1243
1642
|
}
|
|
1244
1643
|
|
|
1644
|
+
#tupleFromSpeechProviderRoutes(selections){
|
|
1645
|
+
const current=this.#preferenceTuple;
|
|
1646
|
+
const stt=selections.stt.default;
|
|
1647
|
+
const tts=selections.tts.default;
|
|
1648
|
+
return Object.freeze([
|
|
1649
|
+
current[0],
|
|
1650
|
+
stt?.providerId||'',
|
|
1651
|
+
tts?.providerId||'',
|
|
1652
|
+
current[3],
|
|
1653
|
+
tts?.modelId||'',
|
|
1654
|
+
stt?.modelId||''
|
|
1655
|
+
]);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1245
1658
|
#routesFromPreferenceTuple(tuple){
|
|
1246
1659
|
const roles={
|
|
1247
1660
|
llm:[
|
|
@@ -1312,6 +1725,36 @@ class AI {
|
|
|
1312
1725
|
}
|
|
1313
1726
|
}
|
|
1314
1727
|
|
|
1728
|
+
async #unloadSpeechProviderRolesForTransition(
|
|
1729
|
+
signal=null,
|
|
1730
|
+
expectedProviders=null,
|
|
1731
|
+
roles=['stt','tts']
|
|
1732
|
+
){
|
|
1733
|
+
const runtime=this;
|
|
1734
|
+
const settlements=await Promise.allSettled(
|
|
1735
|
+
roles.map(async function unloadSpeechProviderRole(role){
|
|
1736
|
+
if(expectedProviders?.[role]
|
|
1737
|
+
&&!runtime.#providerRuntime.ownsProvider(
|
|
1738
|
+
role,
|
|
1739
|
+
expectedProviders[role]
|
|
1740
|
+
)){
|
|
1741
|
+
throw runtime.#browserSpeechProviderRouteOwnershipError(
|
|
1742
|
+
`The ${role} browser speech provider identity changed before unload.`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
return role==='tts'
|
|
1746
|
+
?runtime.#providerRuntime.setSpeechMuted(true)
|
|
1747
|
+
:runtime.#providerRuntime.unload(role,{signal});
|
|
1748
|
+
})
|
|
1749
|
+
);
|
|
1750
|
+
const failure=settlements.find(function findAISpeechTransitionCleanupFailure(result){
|
|
1751
|
+
return result.status==='rejected';
|
|
1752
|
+
});
|
|
1753
|
+
if(failure){
|
|
1754
|
+
throw failure.reason;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1315
1758
|
// Set models to be used by the AI.
|
|
1316
1759
|
// Note: Only those that are defined are set.
|
|
1317
1760
|
setAI(
|
|
@@ -1343,10 +1786,12 @@ class AI {
|
|
|
1343
1786
|
modelSTT
|
|
1344
1787
|
]);
|
|
1345
1788
|
this.#assertValidProviderTuple(tuple);
|
|
1789
|
+
this.#assertSynchronousBrowserSpeechSupersession('AI.setAI');
|
|
1346
1790
|
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1347
1791
|
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1348
1792
|
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
1349
1793
|
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
1794
|
+
this.#invalidateSpeechControl();
|
|
1350
1795
|
this.#applyPreferenceTuple(tuple);
|
|
1351
1796
|
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1352
1797
|
this.#releaseInactiveLegacySpeechProviders({
|
|
@@ -1364,6 +1809,7 @@ class AI {
|
|
|
1364
1809
|
|
|
1365
1810
|
configureProviders(selections){
|
|
1366
1811
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1812
|
+
this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
|
|
1367
1813
|
this.#ensureLegacyLLMProvider(
|
|
1368
1814
|
prepared.llm.default?.providerId
|
|
1369
1815
|
);
|
|
@@ -1377,6 +1823,7 @@ class AI {
|
|
|
1377
1823
|
);
|
|
1378
1824
|
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1379
1825
|
const configured=this.#providerRuntime.configure(prepared);
|
|
1826
|
+
this.#invalidateSpeechControl();
|
|
1380
1827
|
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1381
1828
|
this.#releaseInactiveLegacyLLMProviders(
|
|
1382
1829
|
configured.llm.default?.providerId
|
|
@@ -1388,97 +1835,1403 @@ class AI {
|
|
|
1388
1835
|
this.#retainLegacyLLMReadiness(
|
|
1389
1836
|
this.#reconcileLegacyLLMReadiness()
|
|
1390
1837
|
);
|
|
1391
|
-
this.#retainLegacySpeechReadiness(
|
|
1392
|
-
this.#reconcileLegacySpeechReadiness()
|
|
1838
|
+
this.#retainLegacySpeechReadiness(
|
|
1839
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1840
|
+
);
|
|
1841
|
+
return configured;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
configureSpeechProviders(selections){
|
|
1845
|
+
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
1846
|
+
this.#assertSynchronousBrowserSpeechSupersession(
|
|
1847
|
+
'AI.configureSpeechProviders'
|
|
1848
|
+
);
|
|
1849
|
+
this.#ensureLegacySpeechProvider(
|
|
1850
|
+
'stt',
|
|
1851
|
+
prepared.stt.default?.providerId
|
|
1852
|
+
);
|
|
1853
|
+
this.#ensureLegacySpeechProvider(
|
|
1854
|
+
'tts',
|
|
1855
|
+
prepared.tts.default?.providerId
|
|
1856
|
+
);
|
|
1857
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1858
|
+
const configured=this.#providerRuntime.configureSpeech(prepared);
|
|
1859
|
+
this.#invalidateSpeechControl();
|
|
1860
|
+
this.#applySpeechPreferenceTuple(
|
|
1861
|
+
this.#tupleFromSpeechProviderRoutes(configured)
|
|
1862
|
+
);
|
|
1863
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1864
|
+
stt:configured.stt.default?.providerId,
|
|
1865
|
+
tts:configured.tts.default?.providerId
|
|
1866
|
+
});
|
|
1867
|
+
this.#retainLegacySpeechReadiness(
|
|
1868
|
+
this.#reconcileLegacySpeechReadiness()
|
|
1869
|
+
);
|
|
1870
|
+
this.muted=true;
|
|
1871
|
+
this.stopAudio();
|
|
1872
|
+
return configured;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
async transitionAI(
|
|
1876
|
+
llmService,
|
|
1877
|
+
sttService,
|
|
1878
|
+
ttsService,
|
|
1879
|
+
model,
|
|
1880
|
+
modelTTS,
|
|
1881
|
+
modelSTT
|
|
1882
|
+
){
|
|
1883
|
+
const tuple=this.#nextPreferenceTuple([
|
|
1884
|
+
llmService,
|
|
1885
|
+
sttService,
|
|
1886
|
+
ttsService,
|
|
1887
|
+
model,
|
|
1888
|
+
modelTTS,
|
|
1889
|
+
modelSTT
|
|
1890
|
+
]);
|
|
1891
|
+
this.#assertValidProviderTuple(tuple);
|
|
1892
|
+
await this.#supersedeBrowserSpeechForRouteChange();
|
|
1893
|
+
this.#invalidateSpeechControl();
|
|
1894
|
+
await this.#unloadProviderRolesForTransition();
|
|
1895
|
+
this.#ensureLegacyLLMProvider(tuple[0]);
|
|
1896
|
+
this.#ensureLegacySpeechProvider('stt',tuple[1]);
|
|
1897
|
+
this.#ensureLegacySpeechProvider('tts',tuple[2]);
|
|
1898
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
1899
|
+
this.#applyPreferenceTuple(tuple);
|
|
1900
|
+
this.#releaseInactiveLegacyLLMProviders(tuple[0]);
|
|
1901
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1902
|
+
stt:tuple[1],
|
|
1903
|
+
tts:tuple[2]
|
|
1904
|
+
});
|
|
1905
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1906
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
1907
|
+
return this.#providerRuntime.status();
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
async transitionProviders(selections){
|
|
1911
|
+
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
1912
|
+
await this.#supersedeBrowserSpeechForRouteChange();
|
|
1913
|
+
this.#ensureLegacyLLMProvider(
|
|
1914
|
+
prepared.llm.default?.providerId
|
|
1915
|
+
);
|
|
1916
|
+
this.#ensureLegacySpeechProvider(
|
|
1917
|
+
'stt',
|
|
1918
|
+
prepared.stt.default?.providerId
|
|
1919
|
+
);
|
|
1920
|
+
this.#ensureLegacySpeechProvider(
|
|
1921
|
+
'tts',
|
|
1922
|
+
prepared.tts.default?.providerId
|
|
1923
|
+
);
|
|
1924
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1925
|
+
this.#invalidateSpeechControl();
|
|
1926
|
+
await this.#unloadProviderRolesForTransition();
|
|
1927
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
1928
|
+
this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
|
|
1929
|
+
this.#releaseInactiveLegacyLLMProviders(
|
|
1930
|
+
configured.llm.default?.providerId
|
|
1931
|
+
);
|
|
1932
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1933
|
+
stt:configured.stt.default?.providerId,
|
|
1934
|
+
tts:configured.tts.default?.providerId
|
|
1935
|
+
});
|
|
1936
|
+
await this.#reconcileLegacyLLMReadiness();
|
|
1937
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
1938
|
+
return configured;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async transitionSpeechProviders(selections){
|
|
1942
|
+
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
1943
|
+
await this.#supersedeBrowserSpeechForRouteChange();
|
|
1944
|
+
this.#invalidateSpeechControl();
|
|
1945
|
+
await this.#unloadSpeechProviderRolesForTransition();
|
|
1946
|
+
this.#ensureLegacySpeechProvider(
|
|
1947
|
+
'stt',
|
|
1948
|
+
prepared.stt.default?.providerId
|
|
1949
|
+
);
|
|
1950
|
+
this.#ensureLegacySpeechProvider(
|
|
1951
|
+
'tts',
|
|
1952
|
+
prepared.tts.default?.providerId
|
|
1953
|
+
);
|
|
1954
|
+
this.#assertRegisteredLegacyRoutes(prepared);
|
|
1955
|
+
const configured=this.#providerRuntime.configureSpeech(prepared);
|
|
1956
|
+
this.#applySpeechPreferenceTuple(
|
|
1957
|
+
this.#tupleFromSpeechProviderRoutes(configured)
|
|
1958
|
+
);
|
|
1959
|
+
this.#releaseInactiveLegacySpeechProviders({
|
|
1960
|
+
stt:configured.stt.default?.providerId,
|
|
1961
|
+
tts:configured.tts.default?.providerId
|
|
1962
|
+
});
|
|
1963
|
+
await this.#reconcileLegacySpeechReadiness();
|
|
1964
|
+
this.muted=true;
|
|
1965
|
+
return configured;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
#browserSpeechOperationId(action){
|
|
1969
|
+
if(this.#browserSpeechOperationSequence===Number.MAX_SAFE_INTEGER){
|
|
1970
|
+
throw aiBrowserSpeechError(
|
|
1971
|
+
AI_BROWSER_SPEECH_ERROR_CODES.operationSequenceExhausted,
|
|
1972
|
+
AI_BROWSER_SPEECH_REASONS.operationSequenceExhausted,
|
|
1973
|
+
'The browser speech operation sequence is exhausted.'
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
this.#browserSpeechOperationSequence+=1;
|
|
1977
|
+
return `${this.#events.instanceId}:${action}:${this.#browserSpeechOperationSequence.toString(36)}`;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
#browserSpeechAbortError(controller,generation,{committed=false}={}){
|
|
1981
|
+
const reason=controller.signal.reason;
|
|
1982
|
+
if(reason?.code===AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded){
|
|
1983
|
+
if(Boolean(reason.committed)===committed)return reason;
|
|
1984
|
+
return aiBrowserSpeechError(
|
|
1985
|
+
reason.code,
|
|
1986
|
+
reason.reason||AI_BROWSER_SPEECH_REASONS.configurationReplaced,
|
|
1987
|
+
reason.message
|
|
1988
|
+
||'The browser speech configuration was superseded.',
|
|
1989
|
+
reason,
|
|
1990
|
+
{committed,name:'AbortError'}
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
if(generation!==this.#browserSpeechGeneration){
|
|
1994
|
+
return aiBrowserSpeechError(
|
|
1995
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded,
|
|
1996
|
+
AI_BROWSER_SPEECH_REASONS.configurationReplaced,
|
|
1997
|
+
'The browser speech configuration was replaced by a newer configuration.',
|
|
1998
|
+
controller.signal.reason,
|
|
1999
|
+
{committed,name:'AbortError'}
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
return aiBrowserSpeechError(
|
|
2003
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationCancelled,
|
|
2004
|
+
AI_BROWSER_SPEECH_REASONS.configurationCancelled,
|
|
2005
|
+
'The browser speech configuration was cancelled.',
|
|
2006
|
+
reason,
|
|
2007
|
+
{committed,name:'AbortError'}
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
#assertBrowserSpeechOperation(controller,generation,{committed=false}={}){
|
|
2012
|
+
if(generation!==this.#browserSpeechGeneration||controller.signal.aborted){
|
|
2013
|
+
throw this.#browserSpeechAbortError(
|
|
2014
|
+
controller,
|
|
2015
|
+
generation,
|
|
2016
|
+
{committed}
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
#invalidateSpeechControl(){
|
|
2022
|
+
this.#speechControlGeneration+=1;
|
|
2023
|
+
this.muted=true;
|
|
2024
|
+
this.stopAudio();
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
#sameBrowserSpeechSelection(left,right){
|
|
2028
|
+
if(left===null||right===null)return left===right;
|
|
2029
|
+
return Boolean(left&&right)
|
|
2030
|
+
&&left.providerId===right.providerId
|
|
2031
|
+
&&left.modelId===right.modelId
|
|
2032
|
+
&&left.localOnly===right.localOnly;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
#sameBrowserSpeechRoutes(left,right,roles=['stt','tts']){
|
|
2036
|
+
return roles.every(role=>
|
|
2037
|
+
['default','localOnly'].every(routeName=>
|
|
2038
|
+
this.#sameBrowserSpeechSelection(
|
|
2039
|
+
left?.[role]?.[routeName]??null,
|
|
2040
|
+
right?.[role]?.[routeName]??null
|
|
2041
|
+
)
|
|
2042
|
+
)
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
#browserSpeechRecordOwnsProviders(record){
|
|
2047
|
+
if(!record||record.managedRoles.length===0)return false;
|
|
2048
|
+
return record.managedRoles.every(role=>{
|
|
2049
|
+
if(record.registrationState?.[role]===false)return false;
|
|
2050
|
+
return this.#providerRuntime.ownsProvider(
|
|
2051
|
+
role,
|
|
2052
|
+
record.providers[role]
|
|
2053
|
+
);
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
#browserSpeechRecordIsActive(record){
|
|
2058
|
+
return this.#browserSpeechRecordOwnsProviders(record)
|
|
2059
|
+
&&this.#sameBrowserSpeechRoutes(
|
|
2060
|
+
this.#currentSpeechRoutes(),
|
|
2061
|
+
record.routes,
|
|
2062
|
+
record.managedRoles
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
#browserSpeechProviderRouteOwnershipError(message){
|
|
2067
|
+
return aiBrowserSpeechError(
|
|
2068
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerRouteOwnershipMismatch,
|
|
2069
|
+
AI_BROWSER_SPEECH_REASONS.providerRouteOwnershipMismatch,
|
|
2070
|
+
message
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
#browserSpeechReplacementBoundary(previousRecord,roles){
|
|
2075
|
+
const expectedProviders={stt:null,tts:null};
|
|
2076
|
+
const legacyRecords=[];
|
|
2077
|
+
for(const role of roles){
|
|
2078
|
+
if(previousRecord?.managedRoles.includes(role)){
|
|
2079
|
+
expectedProviders[role]=previousRecord.providers[role];
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2082
|
+
const selection=this.#providerRuntime.selection(role);
|
|
2083
|
+
if(!selection){
|
|
2084
|
+
expectedProviders[role]=null;
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
const record=this.#legacySpeechProviders.get(
|
|
2088
|
+
this.#legacySpeechProviderKey(role,selection.providerId)
|
|
2089
|
+
);
|
|
2090
|
+
if(!record
|
|
2091
|
+
||!this.#providerRuntime.ownsProvider(role,record.provider)){
|
|
2092
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2093
|
+
`The selected ${role} route is not owned by the replaceable AI legacy speech boundary.`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
expectedProviders[role]=record.provider;
|
|
2097
|
+
legacyRecords.push(record);
|
|
2098
|
+
}
|
|
2099
|
+
return Object.freeze({
|
|
2100
|
+
expectedProviders:Object.freeze(expectedProviders),
|
|
2101
|
+
legacyRecords:Object.freeze(legacyRecords)
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
async #cleanupRetiredLegacySpeechProviders(
|
|
2106
|
+
{signal=null,committed=false}={}
|
|
2107
|
+
){
|
|
2108
|
+
const failures=[];
|
|
2109
|
+
for(const record of [...this.#browserSpeechRetiredLegacyRecords]){
|
|
2110
|
+
try{
|
|
2111
|
+
if(this.#providerRuntime.ownsProvider(
|
|
2112
|
+
record.role,
|
|
2113
|
+
record.provider
|
|
2114
|
+
)){
|
|
2115
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2116
|
+
`The retired ${record.role} legacy speech provider still owns its registry entry.`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
await record.provider.dispose({role:record.role,signal});
|
|
2120
|
+
const key=this.#legacySpeechProviderKey(
|
|
2121
|
+
record.role,
|
|
2122
|
+
record.providerId
|
|
2123
|
+
);
|
|
2124
|
+
if(this.#legacySpeechProviders.get(key)===record){
|
|
2125
|
+
this.#legacySpeechProviders.delete(key);
|
|
2126
|
+
}
|
|
2127
|
+
this.#browserSpeechRetiredLegacyRecords.delete(record);
|
|
2128
|
+
}catch(error){
|
|
2129
|
+
failures.push(error);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if(failures.length){
|
|
2133
|
+
throw aiBrowserSpeechError(
|
|
2134
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2135
|
+
AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2136
|
+
'The replaced legacy speech providers could not be disposed.',
|
|
2137
|
+
failures.length===1
|
|
2138
|
+
?failures[0]
|
|
2139
|
+
:new AggregateError(
|
|
2140
|
+
failures,
|
|
2141
|
+
'Multiple replaced legacy speech provider disposals were rejected.'
|
|
2142
|
+
),
|
|
2143
|
+
{committed}
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
return true;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
#assertSynchronousBrowserSpeechSupersession(method){
|
|
2150
|
+
if(!this.#browserSpeechConfigurationRecord
|
|
2151
|
+
&&this.#browserSpeechRetiredRecords.size===0){
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
throw aiBrowserSpeechError(
|
|
2155
|
+
AI_BROWSER_SPEECH_ERROR_CODES.asyncTransitionRequired,
|
|
2156
|
+
AI_BROWSER_SPEECH_REASONS.asyncTransitionRequired,
|
|
2157
|
+
`${method} cannot replace SDK-owned browser speech providers synchronously; await AI.disposeBrowserSpeech() or use an asynchronous transition method.`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
async #supersedeBrowserSpeechForRouteChange(){
|
|
2162
|
+
if(!this.#browserSpeechConfigurationRecord
|
|
2163
|
+
&&this.#browserSpeechRetiredRecords.size===0){
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
return this.disposeBrowserSpeech();
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
#publishBrowserSpeechEvent(type,normalized,operationId,reason,{descriptor=null,error=null}={}){
|
|
2170
|
+
const compatibilityDetail=Object.freeze({
|
|
2171
|
+
configuration:normalized.configuration,
|
|
2172
|
+
configurationId:normalized.id,
|
|
2173
|
+
...(descriptor?{descriptor}:{}),
|
|
2174
|
+
...(error?{error}:{}),
|
|
2175
|
+
reason
|
|
2176
|
+
});
|
|
2177
|
+
return this.#events.dispatch(
|
|
2178
|
+
type,
|
|
2179
|
+
compatibilityDetail,
|
|
2180
|
+
{
|
|
2181
|
+
operationId,
|
|
2182
|
+
publicDetail:Object.freeze({
|
|
2183
|
+
configurationId:normalized.id,
|
|
2184
|
+
...(descriptor?{descriptor}:{}),
|
|
2185
|
+
...(typeof error?.code==='string'?{code:error.code}:{}),
|
|
2186
|
+
reason
|
|
2187
|
+
})
|
|
2188
|
+
}
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
#browserSpeechRoutes(normalized,providers,previousRecord){
|
|
2193
|
+
function roleRoutes(provider,catalog){
|
|
2194
|
+
const selection=Object.freeze({
|
|
2195
|
+
providerId:provider.id,
|
|
2196
|
+
modelId:catalog.id,
|
|
2197
|
+
localOnly:true
|
|
2198
|
+
});
|
|
2199
|
+
return Object.freeze({default:selection,localOnly:selection});
|
|
2200
|
+
}
|
|
2201
|
+
const currentRoutes=this.#currentSpeechRoutes();
|
|
2202
|
+
const routes={};
|
|
2203
|
+
const catalogs={};
|
|
2204
|
+
for(const role of ['stt','tts']){
|
|
2205
|
+
if(!normalized.roles.includes(role)){
|
|
2206
|
+
routes[role]=previousRecord?.managedRoles.includes(role)
|
|
2207
|
+
?previousRecord.routes[role]
|
|
2208
|
+
:currentRoutes[role];
|
|
2209
|
+
catalogs[role]=null;
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
const catalog=providers[role].catalog();
|
|
2213
|
+
if(catalog.length!==1||typeof catalog[0]?.id!=='string'){
|
|
2214
|
+
throw aiBrowserSpeechError(
|
|
2215
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2216
|
+
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
2217
|
+
`The browser speech ${role} provider must expose one admitted model.`
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
catalogs[role]=catalog[0];
|
|
2221
|
+
routes[role]=roleRoutes(providers[role],catalog[0]);
|
|
2222
|
+
}
|
|
2223
|
+
return Object.freeze({
|
|
2224
|
+
routes:Object.freeze(routes),
|
|
2225
|
+
catalogs:Object.freeze(catalogs)
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
#browserSpeechDescriptor(normalized,catalogs,previousRecord){
|
|
2230
|
+
function roleDescriptor(role,configured,catalog){
|
|
2231
|
+
return Object.freeze({
|
|
2232
|
+
role,
|
|
2233
|
+
providerId:configured.providerId,
|
|
2234
|
+
modelId:catalog.id,
|
|
2235
|
+
...(configured.graph
|
|
2236
|
+
?{artifactGraphId:catalog.artifactGraphId}
|
|
2237
|
+
:{}),
|
|
2238
|
+
offline:configured.offline,
|
|
2239
|
+
...(role==='tts'?{defaultVoice:catalog.defaultVoice}:{})
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
const roles={};
|
|
2243
|
+
for(const role of ['stt','tts']){
|
|
2244
|
+
roles[role]=normalized.roles.includes(role)
|
|
2245
|
+
?roleDescriptor(role,normalized[role],catalogs[role])
|
|
2246
|
+
:previousRecord?.descriptor[role]??null;
|
|
2247
|
+
}
|
|
2248
|
+
return Object.freeze({
|
|
2249
|
+
protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
|
|
2250
|
+
configurationId:normalized.id,
|
|
2251
|
+
...roles
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
#browserSpeechConfiguration(normalized,previousRecord){
|
|
2256
|
+
if(!previousRecord)return normalized.configuration;
|
|
2257
|
+
if(normalized.roles.length===2)return normalized.configuration;
|
|
2258
|
+
if(normalized.dbopfs!==previousRecord.dbopfs
|
|
2259
|
+
||normalized.tableName!==previousRecord.tableName){
|
|
2260
|
+
throw aiBrowserSpeechError(
|
|
2261
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
2262
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
2263
|
+
'A partial browser speech replacement must retain the active DBOPFS store and tableName.'
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
const carriedRoles=previousRecord.managedRoles.filter(
|
|
2267
|
+
role=>!normalized.roles.includes(role)
|
|
2268
|
+
);
|
|
2269
|
+
if(carriedRoles.length===0)return normalized.configuration;
|
|
2270
|
+
return Object.freeze({
|
|
2271
|
+
protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
|
|
2272
|
+
id:normalized.id,
|
|
2273
|
+
dbopfs:normalized.dbopfs,
|
|
2274
|
+
...(normalized.tableName?{tableName:normalized.tableName}:{}),
|
|
2275
|
+
...(normalized.roles.includes('stt')
|
|
2276
|
+
?{stt:normalized.configuration.stt}
|
|
2277
|
+
:previousRecord.managedRoles.includes('stt')
|
|
2278
|
+
?{stt:previousRecord.configuration.stt}
|
|
2279
|
+
:{}),
|
|
2280
|
+
...(normalized.roles.includes('tts')
|
|
2281
|
+
?{tts:normalized.configuration.tts}
|
|
2282
|
+
:previousRecord.managedRoles.includes('tts')
|
|
2283
|
+
?{tts:previousRecord.configuration.tts}
|
|
2284
|
+
:{})
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
#currentSpeechRoutes(){
|
|
2289
|
+
return Object.freeze({
|
|
2290
|
+
stt:Object.freeze({
|
|
2291
|
+
default:this.#providerRuntime.selection('stt'),
|
|
2292
|
+
localOnly:this.#providerRuntime.selection('stt',{localOnly:true})
|
|
2293
|
+
}),
|
|
2294
|
+
tts:Object.freeze({
|
|
2295
|
+
default:this.#providerRuntime.selection('tts'),
|
|
2296
|
+
localOnly:this.#providerRuntime.selection('tts',{localOnly:true})
|
|
2297
|
+
})
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
#emptySpeechRoutes(){
|
|
2302
|
+
return Object.freeze({
|
|
2303
|
+
stt:Object.freeze({default:null,localOnly:null}),
|
|
2304
|
+
tts:Object.freeze({default:null,localOnly:null})
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
async #browserSpeechModule(){
|
|
2309
|
+
if(!this.#browserSpeechModulePromise){
|
|
2310
|
+
const runtime=this;
|
|
2311
|
+
this.#browserSpeechModulePromise=import(
|
|
2312
|
+
'arcane-os/ai/browser-speech'
|
|
2313
|
+
).catch(function clearRejectedBrowserSpeechImport(error){
|
|
2314
|
+
runtime.#browserSpeechModulePromise=null;
|
|
2315
|
+
throw error;
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
return this.#browserSpeechModulePromise;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
#assertBrowserSpeechGraphs(normalized,module){
|
|
2322
|
+
for(const role of normalized.roles){
|
|
2323
|
+
const configured=normalized[role];
|
|
2324
|
+
const graph=configured.graph;
|
|
2325
|
+
if(!graph)continue;
|
|
2326
|
+
if(graph.protocol!==module.BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL
|
|
2327
|
+
||graph.role!==role
|
|
2328
|
+
||(graph.providerId!==null
|
|
2329
|
+
&&graph.providerId!==undefined
|
|
2330
|
+
&&graph.providerId!==configured.providerId)){
|
|
2331
|
+
throw aiBrowserSpeechError(
|
|
2332
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
2333
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
2334
|
+
`AI browser speech ${role}.graph does not match its role, provider, or graph protocol.`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
#browserSpeechCandidate(
|
|
2341
|
+
normalized,
|
|
2342
|
+
configuration,
|
|
2343
|
+
descriptor,
|
|
2344
|
+
providers,
|
|
2345
|
+
routes,
|
|
2346
|
+
previousRecord
|
|
2347
|
+
){
|
|
2348
|
+
const configurationByRole={};
|
|
2349
|
+
const managedRoles=Object.freeze(['stt','tts'].filter(role=>
|
|
2350
|
+
normalized.roles.includes(role)
|
|
2351
|
+
||previousRecord?.managedRoles.includes(role)
|
|
2352
|
+
));
|
|
2353
|
+
for(const role of ['stt','tts']){
|
|
2354
|
+
configurationByRole[role]=normalized.roles.includes(role)
|
|
2355
|
+
?normalized.configuration
|
|
2356
|
+
:previousRecord?.configurationByRole[role]??null;
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
configuration,
|
|
2360
|
+
configurationByRole:Object.freeze(configurationByRole),
|
|
2361
|
+
dbopfs:normalized.dbopfs,
|
|
2362
|
+
tableName:normalized.tableName,
|
|
2363
|
+
descriptor,
|
|
2364
|
+
providers,
|
|
2365
|
+
routes,
|
|
2366
|
+
managedRoles,
|
|
2367
|
+
candidateRoles:normalized.roles,
|
|
2368
|
+
unregisters:{stt:null,tts:null},
|
|
2369
|
+
registrationState:{stt:false,tts:false},
|
|
2370
|
+
retirementState:{stt:false,tts:false}
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
#freezeBrowserSpeechRecord(record){
|
|
2375
|
+
record.unregisters=Object.freeze({...record.unregisters});
|
|
2376
|
+
Object.seal(record.registrationState);
|
|
2377
|
+
Object.seal(record.retirementState);
|
|
2378
|
+
return Object.freeze(record);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
#browserSpeechRecordFromReplacement(record,replacement){
|
|
2382
|
+
return this.#freezeBrowserSpeechRecord({
|
|
2383
|
+
configuration:record.configuration,
|
|
2384
|
+
configurationByRole:record.configurationByRole,
|
|
2385
|
+
dbopfs:record.dbopfs,
|
|
2386
|
+
tableName:record.tableName,
|
|
2387
|
+
descriptor:record.descriptor,
|
|
2388
|
+
providers:record.providers,
|
|
2389
|
+
routes:replacement.routes,
|
|
2390
|
+
managedRoles:record.managedRoles,
|
|
2391
|
+
candidateRoles:record.candidateRoles,
|
|
2392
|
+
unregisters:{
|
|
2393
|
+
stt:replacement.unregisters.stt,
|
|
2394
|
+
tts:replacement.unregisters.tts
|
|
2395
|
+
},
|
|
2396
|
+
registrationState:{
|
|
2397
|
+
stt:record.managedRoles.includes('stt'),
|
|
2398
|
+
tts:record.managedRoles.includes('tts')
|
|
2399
|
+
},
|
|
2400
|
+
retirementState:{stt:false,tts:false}
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
#retireBrowserSpeechRegistration(record,roles=['stt','tts']){
|
|
2405
|
+
if(!record)return;
|
|
2406
|
+
let retired=false;
|
|
2407
|
+
for(const role of roles){
|
|
2408
|
+
if(record.registrationState[role]===false)continue;
|
|
2409
|
+
record.registrationState[role]=false;
|
|
2410
|
+
record.retirementState[role]=true;
|
|
2411
|
+
retired=true;
|
|
2412
|
+
}
|
|
2413
|
+
if(retired)this.#browserSpeechRetiredRecords.add(record);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
#unregisterBrowserSpeechRecord(
|
|
2417
|
+
record,
|
|
2418
|
+
{roles=['stt','tts'],committed=false}={}
|
|
2419
|
+
){
|
|
2420
|
+
const failures=[];
|
|
2421
|
+
for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
|
|
2422
|
+
if(record.registrationState?.[role]===false)continue;
|
|
2423
|
+
const unregister=record.unregisters?.[role];
|
|
2424
|
+
if(typeof unregister!=='function'){
|
|
2425
|
+
failures.push(aiBrowserSpeechError(
|
|
2426
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2427
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2428
|
+
`The ${role} browser speech provider ${record.providers[role].id} has no unregister handle.`
|
|
2429
|
+
));
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
try{
|
|
2433
|
+
const removed=unregister();
|
|
2434
|
+
if(removed!==true){
|
|
2435
|
+
failures.push(aiBrowserSpeechError(
|
|
2436
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2437
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2438
|
+
`The ${role} browser speech provider ${record.providers[role].id} no longer owns its registry entry.`
|
|
2439
|
+
));
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
if(record.registrationState)record.registrationState[role]=false;
|
|
2443
|
+
}catch(error){
|
|
2444
|
+
failures.push(error);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if(failures.length){
|
|
2448
|
+
throw aiBrowserSpeechError(
|
|
2449
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2450
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2451
|
+
'The browser speech providers could not be unregistered.',
|
|
2452
|
+
failures.length===1
|
|
2453
|
+
?failures[0]
|
|
2454
|
+
:new AggregateError(
|
|
2455
|
+
failures,
|
|
2456
|
+
'Multiple browser speech provider unregistrations were rejected.'
|
|
2457
|
+
),
|
|
2458
|
+
{committed}
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
async #disposeBrowserSpeechProviders(
|
|
2464
|
+
record,
|
|
2465
|
+
{roles=['stt','tts'],signal=null}={}
|
|
2466
|
+
){
|
|
2467
|
+
if(!record)return;
|
|
2468
|
+
const disposableRoles=roles.filter(role=>record.providers?.[role]);
|
|
2469
|
+
const settlements=await Promise.allSettled(
|
|
2470
|
+
disposableRoles.map(function disposeBrowserSpeechProvider(role){
|
|
2471
|
+
return record.providers[role].dispose({
|
|
2472
|
+
role,
|
|
2473
|
+
selection:record.routes[role].default,
|
|
2474
|
+
signal
|
|
2475
|
+
});
|
|
2476
|
+
})
|
|
2477
|
+
);
|
|
2478
|
+
const failures=settlements
|
|
2479
|
+
.filter(result=>result.status==='rejected')
|
|
2480
|
+
.map(result=>result.reason);
|
|
2481
|
+
if(failures.length){
|
|
2482
|
+
throw failures.length===1
|
|
2483
|
+
?failures[0]
|
|
2484
|
+
:new AggregateError(
|
|
2485
|
+
failures,
|
|
2486
|
+
'Multiple browser speech provider disposals were rejected.'
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
async #cleanupBrowserSpeechRecord(
|
|
2492
|
+
record,
|
|
2493
|
+
{signal=null,committed=false}={}
|
|
2494
|
+
){
|
|
2495
|
+
if(!record)return false;
|
|
2496
|
+
const retiredRoles=['stt','tts'].filter(
|
|
2497
|
+
role=>record.retirementState?.[role]===true
|
|
2498
|
+
);
|
|
2499
|
+
const roles=retiredRoles.length?retiredRoles:record.candidateRoles;
|
|
2500
|
+
this.#browserSpeechRetiredRecords.add(record);
|
|
2501
|
+
this.#unregisterBrowserSpeechRecord(record,{roles,committed});
|
|
2502
|
+
try{
|
|
2503
|
+
await this.#disposeBrowserSpeechProviders(record,{roles,signal});
|
|
2504
|
+
}catch(error){
|
|
2505
|
+
throw aiBrowserSpeechError(
|
|
2506
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2507
|
+
AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2508
|
+
'The browser speech providers could not be disposed.',
|
|
2509
|
+
error,
|
|
2510
|
+
{committed}
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
this.#browserSpeechRetiredRecords.delete(record);
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async #throwAfterBrowserSpeechCandidateCleanup(record,failure){
|
|
2518
|
+
try{
|
|
2519
|
+
await this.#cleanupBrowserSpeechRecord(record);
|
|
2520
|
+
}catch(cleanupError){
|
|
2521
|
+
throw aiBrowserSpeechError(
|
|
2522
|
+
cleanupError.code
|
|
2523
|
+
||AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2524
|
+
cleanupError.reason
|
|
2525
|
+
||AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2526
|
+
'Browser speech candidate cleanup was rejected after configuration rejection.',
|
|
2527
|
+
new AggregateError(
|
|
2528
|
+
[failure,cleanupError],
|
|
2529
|
+
'Browser speech configuration and candidate cleanup were both rejected.'
|
|
2530
|
+
)
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
throw failure;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
async #configureBrowserSpeechOperation(normalized,controller,generation,operationId){
|
|
2537
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2538
|
+
const previousRecord=this.#browserSpeechConfigurationRecord;
|
|
2539
|
+
if(previousRecord&&!this.#browserSpeechRecordIsActive(previousRecord)){
|
|
2540
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2541
|
+
'The prior browser speech provider or route ownership changed before replacement.'
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const configuration=this.#browserSpeechConfiguration(
|
|
2545
|
+
normalized,
|
|
2546
|
+
previousRecord
|
|
2547
|
+
);
|
|
2548
|
+
this.#publishBrowserSpeechEvent(
|
|
2549
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.configurationStarted,
|
|
2550
|
+
normalized,
|
|
2551
|
+
operationId,
|
|
2552
|
+
this.#browserSpeechConfigurationRecord
|
|
2553
|
+
?AI_BROWSER_SPEECH_REASONS.configurationReplaced
|
|
2554
|
+
:AI_BROWSER_SPEECH_REASONS.configurationAdded
|
|
2555
|
+
);
|
|
2556
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2557
|
+
|
|
2558
|
+
let module;
|
|
2559
|
+
try{
|
|
2560
|
+
module=await this.#browserSpeechModule();
|
|
2561
|
+
}catch(error){
|
|
2562
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2563
|
+
throw aiBrowserSpeechError(
|
|
2564
|
+
AI_BROWSER_SPEECH_ERROR_CODES.moduleImportRejected,
|
|
2565
|
+
AI_BROWSER_SPEECH_REASONS.moduleImportRejected,
|
|
2566
|
+
'The browser speech SDK module could not be imported.',
|
|
2567
|
+
error
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2571
|
+
this.#assertBrowserSpeechGraphs(normalized,module);
|
|
2572
|
+
|
|
2573
|
+
let store;
|
|
2574
|
+
try{
|
|
2575
|
+
store=module.createDbopfsSpeechArtifactStore({
|
|
2576
|
+
dbopfs:normalized.dbopfs,
|
|
2577
|
+
...(normalized.tableName?{tableName:normalized.tableName}:{})
|
|
2578
|
+
});
|
|
2579
|
+
}catch(error){
|
|
2580
|
+
throw aiBrowserSpeechError(
|
|
2581
|
+
AI_BROWSER_SPEECH_ERROR_CODES.artifactStoreConstructionRejected,
|
|
2582
|
+
AI_BROWSER_SPEECH_REASONS.artifactStoreConstructionRejected,
|
|
2583
|
+
'The browser speech artifact store could not be constructed.',
|
|
2584
|
+
error
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
const candidateProviders={
|
|
2589
|
+
stt:previousRecord?.providers.stt??null,
|
|
2590
|
+
tts:previousRecord?.providers.tts??null
|
|
2591
|
+
};
|
|
2592
|
+
for(const role of normalized.roles)candidateProviders[role]=null;
|
|
2593
|
+
let providers=null;
|
|
2594
|
+
let prepared=null;
|
|
2595
|
+
try{
|
|
2596
|
+
for(const role of normalized.roles){
|
|
2597
|
+
const factory=role==='stt'
|
|
2598
|
+
?module.createBrowserWhisperProvider
|
|
2599
|
+
:module.createBrowserKokoroProvider;
|
|
2600
|
+
const configured=normalized[role];
|
|
2601
|
+
candidateProviders[role]=factory({
|
|
2602
|
+
id:configured.providerId,
|
|
2603
|
+
...(configured.graph
|
|
2604
|
+
?{
|
|
2605
|
+
graph:configured.graph,
|
|
2606
|
+
security:configured.security
|
|
2607
|
+
}
|
|
2608
|
+
:{
|
|
2609
|
+
model:configured.model,
|
|
2610
|
+
runtime:configured.runtime,
|
|
2611
|
+
...(Object.hasOwn(configured,'security')
|
|
2612
|
+
?{security:configured.security}
|
|
2613
|
+
:{})
|
|
2614
|
+
}),
|
|
2615
|
+
store,
|
|
2616
|
+
offline:configured.offline
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
providers=Object.freeze({...candidateProviders});
|
|
2620
|
+
prepared=this.#browserSpeechRoutes(
|
|
2621
|
+
normalized,
|
|
2622
|
+
providers,
|
|
2623
|
+
previousRecord
|
|
2624
|
+
);
|
|
2625
|
+
}catch(error){
|
|
2626
|
+
const failure=error?.code===AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected
|
|
2627
|
+
?error
|
|
2628
|
+
:aiBrowserSpeechError(
|
|
2629
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2630
|
+
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
2631
|
+
'The browser speech providers could not be constructed.',
|
|
2632
|
+
error
|
|
2633
|
+
);
|
|
2634
|
+
if(normalized.roles.some(role=>candidateProviders[role])){
|
|
2635
|
+
const currentRoutes=this.#currentSpeechRoutes();
|
|
2636
|
+
const partialRoutes={};
|
|
2637
|
+
for(const role of ['stt','tts']){
|
|
2638
|
+
const provider=candidateProviders[role];
|
|
2639
|
+
const changed=normalized.roles.includes(role);
|
|
2640
|
+
const selection=changed&&provider
|
|
2641
|
+
?Object.freeze({
|
|
2642
|
+
providerId:provider.id,
|
|
2643
|
+
modelId:normalized[role].graph?.model.id
|
|
2644
|
+
??normalized[role].model.id,
|
|
2645
|
+
localOnly:true
|
|
2646
|
+
})
|
|
2647
|
+
:null;
|
|
2648
|
+
partialRoutes[role]=changed
|
|
2649
|
+
?Object.freeze({default:selection,localOnly:selection})
|
|
2650
|
+
:previousRecord?.managedRoles.includes(role)
|
|
2651
|
+
?previousRecord.routes[role]
|
|
2652
|
+
:currentRoutes[role];
|
|
2653
|
+
}
|
|
2654
|
+
const partial=this.#browserSpeechCandidate(
|
|
2655
|
+
normalized,
|
|
2656
|
+
configuration,
|
|
2657
|
+
null,
|
|
2658
|
+
Object.freeze({...candidateProviders}),
|
|
2659
|
+
Object.freeze(partialRoutes),
|
|
2660
|
+
previousRecord
|
|
2661
|
+
);
|
|
2662
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2663
|
+
partial,
|
|
2664
|
+
failure
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
throw failure;
|
|
2668
|
+
}
|
|
2669
|
+
const descriptor=this.#browserSpeechDescriptor(
|
|
2670
|
+
normalized,
|
|
2671
|
+
prepared.catalogs,
|
|
2672
|
+
previousRecord
|
|
2673
|
+
);
|
|
2674
|
+
const candidate=this.#browserSpeechCandidate(
|
|
2675
|
+
normalized,
|
|
2676
|
+
configuration,
|
|
2677
|
+
descriptor,
|
|
2678
|
+
providers,
|
|
2679
|
+
prepared.routes,
|
|
2680
|
+
previousRecord
|
|
2681
|
+
);
|
|
2682
|
+
let replacementBoundary;
|
|
2683
|
+
try{
|
|
2684
|
+
replacementBoundary=this.#browserSpeechReplacementBoundary(
|
|
2685
|
+
previousRecord,
|
|
2686
|
+
normalized.roles
|
|
2687
|
+
);
|
|
2688
|
+
}catch(error){
|
|
2689
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2690
|
+
candidate,
|
|
2691
|
+
error
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
try{
|
|
2695
|
+
await this.#unloadSpeechProviderRolesForTransition(
|
|
2696
|
+
controller.signal,
|
|
2697
|
+
replacementBoundary.expectedProviders,
|
|
2698
|
+
normalized.roles
|
|
2699
|
+
);
|
|
2700
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2701
|
+
}catch(error){
|
|
2702
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(candidate,error);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
let replacement;
|
|
2706
|
+
try{
|
|
2707
|
+
if(normalized.roles.length===2){
|
|
2708
|
+
replacement=this.#providerRuntime.replaceSpeechProviders({
|
|
2709
|
+
providers,
|
|
2710
|
+
routes:prepared.routes,
|
|
2711
|
+
expectedProviders:replacementBoundary.expectedProviders
|
|
2712
|
+
});
|
|
2713
|
+
}else{
|
|
2714
|
+
const role=normalized.roles[0];
|
|
2715
|
+
const roleReplacement=this.#providerRuntime.replaceSpeechProvider(
|
|
2716
|
+
role,
|
|
2717
|
+
{
|
|
2718
|
+
provider:providers[role],
|
|
2719
|
+
routes:prepared.routes[role],
|
|
2720
|
+
expectedProvider:replacementBoundary.expectedProviders[role]
|
|
2721
|
+
}
|
|
2722
|
+
);
|
|
2723
|
+
replacement=Object.freeze({
|
|
2724
|
+
routes:prepared.routes,
|
|
2725
|
+
unregisters:Object.freeze({
|
|
2726
|
+
stt:role==='stt'
|
|
2727
|
+
?roleReplacement.unregister
|
|
2728
|
+
:previousRecord?.unregisters.stt??null,
|
|
2729
|
+
tts:role==='tts'
|
|
2730
|
+
?roleReplacement.unregister
|
|
2731
|
+
:previousRecord?.unregisters.tts??null
|
|
2732
|
+
})
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}catch(error){
|
|
2736
|
+
const commitFailure=aiBrowserSpeechError(
|
|
2737
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeCommitRejected,
|
|
2738
|
+
AI_BROWSER_SPEECH_REASONS.routeCommitRejected,
|
|
2739
|
+
'The browser speech provider and route replacement could not be committed.',
|
|
2740
|
+
error
|
|
2741
|
+
);
|
|
2742
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2743
|
+
candidate,
|
|
2744
|
+
commitFailure
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
const record=this.#browserSpeechRecordFromReplacement(
|
|
2749
|
+
candidate,
|
|
2750
|
+
replacement
|
|
2751
|
+
);
|
|
2752
|
+
for(const legacyRecord of replacementBoundary.legacyRecords){
|
|
2753
|
+
this.#browserSpeechRetiredLegacyRecords.add(legacyRecord);
|
|
2754
|
+
}
|
|
2755
|
+
if(previousRecord){
|
|
2756
|
+
this.#retireBrowserSpeechRegistration(
|
|
2757
|
+
previousRecord,
|
|
2758
|
+
normalized.roles
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
this.#browserSpeechConfigurationRecord=record;
|
|
2762
|
+
try{
|
|
2763
|
+
this.#applySpeechPreferenceTuple(
|
|
2764
|
+
this.#tupleFromSpeechProviderRoutes(replacement.routes)
|
|
2765
|
+
);
|
|
2766
|
+
}catch(error){
|
|
2767
|
+
const finalizationFailure=aiBrowserSpeechError(
|
|
2768
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeViewUpdateRejected,
|
|
2769
|
+
AI_BROWSER_SPEECH_REASONS.routeViewUpdateRejected,
|
|
2770
|
+
'The committed browser speech replacement could not update the AI speech route view.',
|
|
2771
|
+
error
|
|
2772
|
+
);
|
|
2773
|
+
if(!previousRecord
|
|
2774
|
+
||normalized.roles.some(
|
|
2775
|
+
role=>!previousRecord.managedRoles.includes(role)
|
|
2776
|
+
)){
|
|
2777
|
+
finalizationFailure.committed=true;
|
|
2778
|
+
throw finalizationFailure;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
let rollback;
|
|
2782
|
+
try{
|
|
2783
|
+
if(normalized.roles.length===2){
|
|
2784
|
+
rollback=this.#providerRuntime.replaceSpeechProviders({
|
|
2785
|
+
providers:previousRecord.providers,
|
|
2786
|
+
routes:previousRecord.routes,
|
|
2787
|
+
expectedProviders:record.providers
|
|
2788
|
+
});
|
|
2789
|
+
}else{
|
|
2790
|
+
const role=normalized.roles[0];
|
|
2791
|
+
const roleRollback=this.#providerRuntime.replaceSpeechProvider(
|
|
2792
|
+
role,
|
|
2793
|
+
{
|
|
2794
|
+
provider:previousRecord.providers[role],
|
|
2795
|
+
routes:previousRecord.routes[role],
|
|
2796
|
+
expectedProvider:record.providers[role]
|
|
2797
|
+
}
|
|
2798
|
+
);
|
|
2799
|
+
rollback=Object.freeze({
|
|
2800
|
+
routes:previousRecord.routes,
|
|
2801
|
+
unregisters:Object.freeze({
|
|
2802
|
+
stt:role==='stt'
|
|
2803
|
+
?roleRollback.unregister
|
|
2804
|
+
:previousRecord.unregisters.stt,
|
|
2805
|
+
tts:role==='tts'
|
|
2806
|
+
?roleRollback.unregister
|
|
2807
|
+
:previousRecord.unregisters.tts
|
|
2808
|
+
})
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
}catch(rollbackError){
|
|
2812
|
+
const candidateCommitted=this.#browserSpeechRecordIsActive(record);
|
|
2813
|
+
if(!candidateCommitted){
|
|
2814
|
+
this.#browserSpeechConfigurationRecord=null;
|
|
2815
|
+
this.#browserSpeechRetiredRecords.add(record);
|
|
2816
|
+
}
|
|
2817
|
+
throw aiBrowserSpeechError(
|
|
2818
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeRollbackRejected,
|
|
2819
|
+
AI_BROWSER_SPEECH_REASONS.routeRollbackRejected,
|
|
2820
|
+
'The prior browser speech providers and routes could not be restored after AI speech route view rejection.',
|
|
2821
|
+
new AggregateError(
|
|
2822
|
+
[finalizationFailure,rollbackError],
|
|
2823
|
+
'Browser speech replacement finalization and rollback were both rejected.'
|
|
2824
|
+
),
|
|
2825
|
+
{committed:candidateCommitted}
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
this.#retireBrowserSpeechRegistration(record,normalized.roles);
|
|
2830
|
+
const restoredRecord=this.#browserSpeechRecordFromReplacement(
|
|
2831
|
+
previousRecord,
|
|
2832
|
+
rollback
|
|
2833
|
+
);
|
|
2834
|
+
this.#browserSpeechRetiredRecords.delete(previousRecord);
|
|
2835
|
+
this.#browserSpeechConfigurationRecord=restoredRecord;
|
|
2836
|
+
this.#applySpeechPreferenceTuple(
|
|
2837
|
+
this.#tupleFromSpeechProviderRoutes(rollback.routes)
|
|
2838
|
+
);
|
|
2839
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2840
|
+
record,
|
|
2841
|
+
finalizationFailure
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
this.#assertBrowserSpeechOperation(
|
|
2845
|
+
controller,
|
|
2846
|
+
generation,
|
|
2847
|
+
{committed:true}
|
|
2848
|
+
);
|
|
2849
|
+
await this.#cleanupRetiredLegacySpeechProviders({
|
|
2850
|
+
signal:controller.signal,
|
|
2851
|
+
committed:true
|
|
2852
|
+
});
|
|
2853
|
+
this.#assertBrowserSpeechOperation(
|
|
2854
|
+
controller,
|
|
2855
|
+
generation,
|
|
2856
|
+
{committed:true}
|
|
2857
|
+
);
|
|
2858
|
+
this.#publishBrowserSpeechEvent(
|
|
2859
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.configured,
|
|
2860
|
+
normalized,
|
|
2861
|
+
operationId,
|
|
2862
|
+
previousRecord
|
|
2863
|
+
?AI_BROWSER_SPEECH_REASONS.configurationReplaced
|
|
2864
|
+
:AI_BROWSER_SPEECH_REASONS.configurationAdded,
|
|
2865
|
+
{descriptor}
|
|
2866
|
+
);
|
|
2867
|
+
|
|
2868
|
+
for(const retiredRecord of [...this.#browserSpeechRetiredRecords]){
|
|
2869
|
+
try{
|
|
2870
|
+
await this.#cleanupBrowserSpeechRecord(
|
|
2871
|
+
retiredRecord,
|
|
2872
|
+
{signal:controller.signal,committed:true}
|
|
2873
|
+
);
|
|
2874
|
+
}catch(error){
|
|
2875
|
+
if(error?.committed===true)throw error;
|
|
2876
|
+
if(generation!==this.#browserSpeechGeneration
|
|
2877
|
+
||controller.signal.aborted){
|
|
2878
|
+
throw this.#browserSpeechAbortError(
|
|
2879
|
+
controller,
|
|
2880
|
+
generation,
|
|
2881
|
+
{committed:true}
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
throw error;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
this.#assertBrowserSpeechOperation(
|
|
2888
|
+
controller,
|
|
2889
|
+
generation,
|
|
2890
|
+
{committed:true}
|
|
1393
2891
|
);
|
|
1394
|
-
return
|
|
2892
|
+
return descriptor;
|
|
1395
2893
|
}
|
|
1396
2894
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2895
|
+
configureBrowserSpeech(configuration,options={}){
|
|
2896
|
+
const normalized=normalizeBrowserSpeechConfiguration(configuration);
|
|
2897
|
+
const operation=normalizeBrowserSpeechOperationOptions(
|
|
2898
|
+
options,
|
|
2899
|
+
'AI.configureBrowserSpeech'
|
|
2900
|
+
);
|
|
2901
|
+
if(operation.signal?.aborted){
|
|
2902
|
+
return Promise.reject(aiBrowserSpeechError(
|
|
2903
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationCancelled,
|
|
2904
|
+
AI_BROWSER_SPEECH_REASONS.configurationCancelled,
|
|
2905
|
+
'The browser speech configuration was cancelled before admission.',
|
|
2906
|
+
operation.signal.reason,
|
|
2907
|
+
{name:'AbortError'}
|
|
2908
|
+
));
|
|
2909
|
+
}
|
|
2910
|
+
if(this.#browserSpeechConfigurationRecord
|
|
2911
|
+
&&normalized.roles.every(role=>
|
|
2912
|
+
this.#browserSpeechConfigurationRecord.configurationByRole[role]
|
|
2913
|
+
===configuration
|
|
2914
|
+
)
|
|
2915
|
+
&&this.#browserSpeechRecordIsActive(
|
|
2916
|
+
this.#browserSpeechConfigurationRecord
|
|
2917
|
+
)
|
|
2918
|
+
&&this.#browserSpeechRetiredRecords.size===0
|
|
2919
|
+
&&!this.#browserSpeechController){
|
|
2920
|
+
return Promise.resolve(this.#browserSpeechConfigurationRecord.descriptor);
|
|
2921
|
+
}
|
|
2922
|
+
const operationId=this.#browserSpeechOperationId('configure-browser-speech');
|
|
2923
|
+
const generation=++this.#browserSpeechGeneration;
|
|
2924
|
+
const superseded=aiBrowserSpeechError(
|
|
2925
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded,
|
|
2926
|
+
AI_BROWSER_SPEECH_REASONS.configurationReplaced,
|
|
2927
|
+
'The browser speech configuration was replaced by a newer configuration.',
|
|
2928
|
+
undefined,
|
|
2929
|
+
{name:'AbortError'}
|
|
2930
|
+
);
|
|
2931
|
+
this.#browserSpeechController?.abort(superseded);
|
|
2932
|
+
if(normalized.roles.includes('tts'))this.#invalidateSpeechControl();
|
|
2933
|
+
const controller=new AbortController();
|
|
2934
|
+
this.#browserSpeechController=controller;
|
|
2935
|
+
this.#browserSpeechControllerRoles=normalized.roles;
|
|
2936
|
+
const forwardAbort=function cancelBrowserSpeechConfiguration(){
|
|
2937
|
+
if(!controller.signal.aborted)controller.abort(operation.signal.reason);
|
|
2938
|
+
};
|
|
2939
|
+
if(operation.signal?.aborted)forwardAbort();
|
|
2940
|
+
else operation.signal?.addEventListener('abort',forwardAbort,{once:true});
|
|
2941
|
+
const runtime=this;
|
|
2942
|
+
const scheduled=this.#browserSpeechTransition.then(
|
|
2943
|
+
async function runBrowserSpeechConfiguration(){
|
|
2944
|
+
try{
|
|
2945
|
+
return await runtime.#configureBrowserSpeechOperation(
|
|
2946
|
+
normalized,
|
|
2947
|
+
controller,
|
|
2948
|
+
generation,
|
|
2949
|
+
operationId
|
|
2950
|
+
);
|
|
2951
|
+
}catch(error){
|
|
2952
|
+
const failure=error?.committed===true
|
|
2953
|
+
?error
|
|
2954
|
+
:(generation!==runtime.#browserSpeechGeneration
|
|
2955
|
+
||controller.signal.aborted)
|
|
2956
|
+
?runtime.#browserSpeechAbortError(controller,generation)
|
|
2957
|
+
:error;
|
|
2958
|
+
runtime.#publishBrowserSpeechEvent(
|
|
2959
|
+
failure.name==='AbortError'
|
|
2960
|
+
?AI_BROWSER_SPEECH_EVENT_TYPES.configurationCancelled
|
|
2961
|
+
:AI_BROWSER_SPEECH_EVENT_TYPES.configurationError,
|
|
2962
|
+
normalized,
|
|
2963
|
+
operationId,
|
|
2964
|
+
failure.reason
|
|
2965
|
+
||AI_BROWSER_SPEECH_REASONS.routeCommitRejected,
|
|
2966
|
+
{error:failure}
|
|
2967
|
+
);
|
|
2968
|
+
throw failure;
|
|
2969
|
+
}finally{
|
|
2970
|
+
operation.signal?.removeEventListener('abort',forwardAbort);
|
|
2971
|
+
if(runtime.#browserSpeechController===controller){
|
|
2972
|
+
runtime.#browserSpeechController=null;
|
|
2973
|
+
runtime.#browserSpeechControllerRoles=Object.freeze([]);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
);
|
|
2978
|
+
this.#browserSpeechTransition=scheduled.then(
|
|
2979
|
+
function completeBrowserSpeechConfigurationLane(){},
|
|
2980
|
+
function retainBrowserSpeechConfigurationFailure(){}
|
|
2981
|
+
);
|
|
2982
|
+
return scheduled;
|
|
1429
2983
|
}
|
|
1430
2984
|
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
2985
|
+
disposeBrowserSpeech(options={}){
|
|
2986
|
+
const operation=normalizeBrowserSpeechOperationOptions(
|
|
2987
|
+
options,
|
|
2988
|
+
'AI.disposeBrowserSpeech'
|
|
1435
2989
|
);
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2990
|
+
if(operation.signal?.aborted){
|
|
2991
|
+
return Promise.reject(aiBrowserSpeechError(
|
|
2992
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationCancelled,
|
|
2993
|
+
AI_BROWSER_SPEECH_REASONS.configurationCancelled,
|
|
2994
|
+
'Browser speech disposal was cancelled before admission.',
|
|
2995
|
+
operation.signal.reason,
|
|
2996
|
+
{name:'AbortError'}
|
|
2997
|
+
));
|
|
2998
|
+
}
|
|
2999
|
+
const operationId=this.#browserSpeechOperationId('dispose-browser-speech');
|
|
3000
|
+
const generation=++this.#browserSpeechGeneration;
|
|
3001
|
+
const superseded=aiBrowserSpeechError(
|
|
3002
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded,
|
|
3003
|
+
AI_BROWSER_SPEECH_REASONS.configurationDisposed,
|
|
3004
|
+
'The browser speech configuration was superseded by disposal.',
|
|
3005
|
+
undefined,
|
|
3006
|
+
{name:'AbortError'}
|
|
1439
3007
|
);
|
|
1440
|
-
|
|
1441
|
-
'tts'
|
|
1442
|
-
|
|
3008
|
+
const invalidatesSpeech=Boolean(
|
|
3009
|
+
this.#browserSpeechControllerRoles.includes('tts')
|
|
3010
|
+
||this.#browserSpeechConfigurationRecord?.managedRoles.includes('tts')
|
|
3011
|
+
||[...this.#browserSpeechRetiredRecords].some(record=>
|
|
3012
|
+
record.managedRoles.includes('tts')
|
|
3013
|
+
||record.candidateRoles.includes('tts')
|
|
3014
|
+
)
|
|
3015
|
+
||[...this.#browserSpeechRetiredLegacyRecords].some(
|
|
3016
|
+
record=>record.role==='tts'
|
|
3017
|
+
)
|
|
1443
3018
|
);
|
|
1444
|
-
this.#
|
|
1445
|
-
this
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
this.#
|
|
1449
|
-
|
|
1450
|
-
|
|
3019
|
+
this.#browserSpeechController?.abort(superseded);
|
|
3020
|
+
if(invalidatesSpeech)this.#invalidateSpeechControl();
|
|
3021
|
+
const controller=new AbortController();
|
|
3022
|
+
this.#browserSpeechController=controller;
|
|
3023
|
+
this.#browserSpeechControllerRoles=Object.freeze([]);
|
|
3024
|
+
const forwardAbort=function cancelBrowserSpeechDisposal(){
|
|
3025
|
+
if(!controller.signal.aborted)controller.abort(operation.signal.reason);
|
|
3026
|
+
};
|
|
3027
|
+
if(operation.signal?.aborted)forwardAbort();
|
|
3028
|
+
else operation.signal?.addEventListener('abort',forwardAbort,{once:true});
|
|
3029
|
+
const runtime=this;
|
|
3030
|
+
const scheduled=this.#browserSpeechTransition.then(
|
|
3031
|
+
async function runBrowserSpeechDisposal(){
|
|
3032
|
+
let normalized=null;
|
|
3033
|
+
let descriptor=null;
|
|
3034
|
+
let changed=false;
|
|
3035
|
+
let committed=false;
|
|
3036
|
+
try{
|
|
3037
|
+
runtime.#assertBrowserSpeechOperation(controller,generation);
|
|
3038
|
+
const activeRecord=runtime.#browserSpeechConfigurationRecord;
|
|
3039
|
+
const records=new Set(runtime.#browserSpeechRetiredRecords);
|
|
3040
|
+
if(activeRecord)records.add(activeRecord);
|
|
3041
|
+
const eventRecord=activeRecord||records.values().next().value||null;
|
|
3042
|
+
if(!eventRecord
|
|
3043
|
+
&&runtime.#browserSpeechRetiredLegacyRecords.size===0){
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
if(eventRecord){
|
|
3047
|
+
normalized=normalizeBrowserSpeechConfiguration(
|
|
3048
|
+
eventRecord.configuration
|
|
3049
|
+
);
|
|
3050
|
+
descriptor=eventRecord.descriptor;
|
|
3051
|
+
}
|
|
3052
|
+
if(activeRecord){
|
|
3053
|
+
if(!runtime.#browserSpeechRecordIsActive(activeRecord)){
|
|
3054
|
+
throw runtime.#browserSpeechProviderRouteOwnershipError(
|
|
3055
|
+
'The configured browser speech provider or route ownership changed before disposal.'
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
await runtime.#unloadSpeechProviderRolesForTransition(
|
|
3059
|
+
controller.signal,
|
|
3060
|
+
activeRecord.providers,
|
|
3061
|
+
activeRecord.managedRoles
|
|
3062
|
+
);
|
|
3063
|
+
runtime.#assertBrowserSpeechOperation(controller,generation);
|
|
3064
|
+
let removed;
|
|
3065
|
+
if(activeRecord.managedRoles.length===2){
|
|
3066
|
+
removed=runtime.#providerRuntime.replaceSpeechProviders({
|
|
3067
|
+
providers:{stt:null,tts:null},
|
|
3068
|
+
routes:runtime.#emptySpeechRoutes(),
|
|
3069
|
+
expectedProviders:activeRecord.providers
|
|
3070
|
+
});
|
|
3071
|
+
}else{
|
|
3072
|
+
const role=activeRecord.managedRoles[0];
|
|
3073
|
+
const currentRoutes=runtime.#currentSpeechRoutes();
|
|
3074
|
+
const emptyRoleRoutes=Object.freeze({
|
|
3075
|
+
default:null,
|
|
3076
|
+
localOnly:null
|
|
3077
|
+
});
|
|
3078
|
+
runtime.#providerRuntime.replaceSpeechProvider(
|
|
3079
|
+
role,
|
|
3080
|
+
{
|
|
3081
|
+
provider:null,
|
|
3082
|
+
routes:emptyRoleRoutes,
|
|
3083
|
+
expectedProvider:activeRecord.providers[role]
|
|
3084
|
+
}
|
|
3085
|
+
);
|
|
3086
|
+
removed=Object.freeze({
|
|
3087
|
+
routes:Object.freeze({
|
|
3088
|
+
stt:role==='stt'
|
|
3089
|
+
?emptyRoleRoutes
|
|
3090
|
+
:currentRoutes.stt,
|
|
3091
|
+
tts:role==='tts'
|
|
3092
|
+
?emptyRoleRoutes
|
|
3093
|
+
:currentRoutes.tts
|
|
3094
|
+
})
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
runtime.#retireBrowserSpeechRegistration(
|
|
3098
|
+
activeRecord,
|
|
3099
|
+
activeRecord.managedRoles
|
|
3100
|
+
);
|
|
3101
|
+
runtime.#browserSpeechConfigurationRecord=null;
|
|
3102
|
+
changed=true;
|
|
3103
|
+
committed=true;
|
|
3104
|
+
try{
|
|
3105
|
+
runtime.#applySpeechPreferenceTuple(
|
|
3106
|
+
runtime.#tupleFromSpeechProviderRoutes(removed.routes)
|
|
3107
|
+
);
|
|
3108
|
+
}catch(error){
|
|
3109
|
+
throw aiBrowserSpeechError(
|
|
3110
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeViewUpdateRejected,
|
|
3111
|
+
AI_BROWSER_SPEECH_REASONS.routeViewUpdateRejected,
|
|
3112
|
+
'The committed browser speech removal could not update the AI speech route view.',
|
|
3113
|
+
error,
|
|
3114
|
+
{committed:true}
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
if(records.size)committed=true;
|
|
3120
|
+
for(const record of records){
|
|
3121
|
+
await runtime.#cleanupBrowserSpeechRecord(
|
|
3122
|
+
record,
|
|
3123
|
+
{signal:controller.signal,committed:true}
|
|
3124
|
+
);
|
|
3125
|
+
changed=true;
|
|
3126
|
+
}
|
|
3127
|
+
if(runtime.#browserSpeechRetiredLegacyRecords.size){
|
|
3128
|
+
await runtime.#cleanupRetiredLegacySpeechProviders({
|
|
3129
|
+
signal:controller.signal,
|
|
3130
|
+
committed:true
|
|
3131
|
+
});
|
|
3132
|
+
changed=true;
|
|
3133
|
+
}
|
|
3134
|
+
runtime.#assertBrowserSpeechOperation(
|
|
3135
|
+
controller,
|
|
3136
|
+
generation,
|
|
3137
|
+
{committed}
|
|
3138
|
+
);
|
|
3139
|
+
if(normalized){
|
|
3140
|
+
runtime.#publishBrowserSpeechEvent(
|
|
3141
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.disposed,
|
|
3142
|
+
normalized,
|
|
3143
|
+
operationId,
|
|
3144
|
+
AI_BROWSER_SPEECH_REASONS.configurationDisposed,
|
|
3145
|
+
{descriptor}
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
return changed;
|
|
3149
|
+
}catch(error){
|
|
3150
|
+
const failure=error?.committed===true
|
|
3151
|
+
?error
|
|
3152
|
+
:(generation!==runtime.#browserSpeechGeneration
|
|
3153
|
+
||controller.signal.aborted)
|
|
3154
|
+
?runtime.#browserSpeechAbortError(
|
|
3155
|
+
controller,
|
|
3156
|
+
generation,
|
|
3157
|
+
{committed}
|
|
3158
|
+
)
|
|
3159
|
+
:error;
|
|
3160
|
+
if(normalized){
|
|
3161
|
+
runtime.#publishBrowserSpeechEvent(
|
|
3162
|
+
failure.name==='AbortError'
|
|
3163
|
+
?AI_BROWSER_SPEECH_EVENT_TYPES.configurationCancelled
|
|
3164
|
+
:AI_BROWSER_SPEECH_EVENT_TYPES.configurationError,
|
|
3165
|
+
normalized,
|
|
3166
|
+
operationId,
|
|
3167
|
+
failure.reason
|
|
3168
|
+
||AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
3169
|
+
{error:failure}
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
throw failure;
|
|
3173
|
+
}finally{
|
|
3174
|
+
operation.signal?.removeEventListener('abort',forwardAbort);
|
|
3175
|
+
if(runtime.#browserSpeechController===controller){
|
|
3176
|
+
runtime.#browserSpeechController=null;
|
|
3177
|
+
runtime.#browserSpeechControllerRoles=Object.freeze([]);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
1451
3181
|
);
|
|
1452
|
-
this.#
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
await this.#reconcileLegacySpeechReadiness();
|
|
1458
|
-
return configured;
|
|
3182
|
+
this.#browserSpeechTransition=scheduled.then(
|
|
3183
|
+
function completeBrowserSpeechDisposalLane(){},
|
|
3184
|
+
function retainBrowserSpeechDisposalFailure(){}
|
|
3185
|
+
);
|
|
3186
|
+
return scheduled;
|
|
1459
3187
|
}
|
|
1460
3188
|
|
|
1461
3189
|
async startProviders(options){
|
|
1462
3190
|
const normalized=normalizeAIStartupOptions(options);
|
|
1463
|
-
this
|
|
3191
|
+
const generation=++this.#speechControlGeneration;
|
|
3192
|
+
this.muted=true;
|
|
1464
3193
|
if(normalized.startMuted){
|
|
1465
3194
|
this.stopAudio();
|
|
1466
3195
|
}
|
|
1467
|
-
|
|
3196
|
+
const handle=await this.#providerRuntime.start(normalized);
|
|
3197
|
+
if(!normalized.startMuted){
|
|
3198
|
+
const runtime=this;
|
|
3199
|
+
handle.settled.then(
|
|
3200
|
+
function admitReadyStartupSpeech(){
|
|
3201
|
+
if(generation!==runtime.#speechControlGeneration)return;
|
|
3202
|
+
const status=runtime.#providerRuntime.status('tts');
|
|
3203
|
+
runtime.muted=!(status.state==='ready'&&status.loaded===true);
|
|
3204
|
+
},
|
|
3205
|
+
function retainFailClosedStartupSpeech(){
|
|
3206
|
+
if(generation===runtime.#speechControlGeneration){
|
|
3207
|
+
runtime.muted=true;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
return handle;
|
|
1468
3213
|
}
|
|
1469
3214
|
|
|
1470
3215
|
async setSpeechMuted(muted){
|
|
1471
3216
|
if(typeof muted!=='boolean'){
|
|
1472
3217
|
throw new TypeError('AI speech muted state must be a boolean.');
|
|
1473
3218
|
}
|
|
1474
|
-
this
|
|
3219
|
+
const generation=++this.#speechControlGeneration;
|
|
3220
|
+
this.muted=true;
|
|
1475
3221
|
if(muted){
|
|
1476
3222
|
this.stopAudio();
|
|
1477
3223
|
}
|
|
1478
3224
|
if(!this.#usesProviderRuntime('tts',this.ttsService)){
|
|
3225
|
+
if(generation===this.#speechControlGeneration)this.muted=muted;
|
|
1479
3226
|
return true;
|
|
1480
3227
|
}
|
|
1481
3228
|
await this.#providerRuntime.setSpeechMuted(muted);
|
|
3229
|
+
if(generation===this.#speechControlGeneration){
|
|
3230
|
+
const status=this.#providerRuntime.status('tts');
|
|
3231
|
+
this.muted=muted
|
|
3232
|
+
||status.state!=='ready'
|
|
3233
|
+
||status.loaded!==true;
|
|
3234
|
+
}
|
|
1482
3235
|
return true;
|
|
1483
3236
|
}
|
|
1484
3237
|
|
|
@@ -1624,7 +3377,11 @@ class AI {
|
|
|
1624
3377
|
throw normalizeAIRequestAbort(signal.reason);
|
|
1625
3378
|
}
|
|
1626
3379
|
if(!response||typeof response.audioBase64!=='string'){
|
|
1627
|
-
|
|
3380
|
+
const error=new TypeError(
|
|
3381
|
+
'The Arcane speech bridge returned invalid TTS audio.'
|
|
3382
|
+
);
|
|
3383
|
+
error.code='ARCANE_AI_TTS_NATIVE_AUDIO_INVALID';
|
|
3384
|
+
throw error;
|
|
1628
3385
|
}
|
|
1629
3386
|
return new Blob(
|
|
1630
3387
|
[this.#base64ToBytes(response.audioBase64)],
|
|
@@ -1637,28 +3394,41 @@ class AI {
|
|
|
1637
3394
|
}
|
|
1638
3395
|
|
|
1639
3396
|
await this.#assertAndroidSpeechBridge(this.ttsService);
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
3397
|
+
let response;
|
|
3398
|
+
try{
|
|
3399
|
+
response=await fetch(
|
|
3400
|
+
this.urlTTS,
|
|
3401
|
+
{
|
|
3402
|
+
method:'POST',
|
|
3403
|
+
credentials,
|
|
3404
|
+
headers:this.#ttsHeaders[this.ttsService],
|
|
3405
|
+
body:JSON.stringify({
|
|
3406
|
+
model,
|
|
3407
|
+
voice,
|
|
3408
|
+
input,
|
|
3409
|
+
speed,
|
|
3410
|
+
response_format:responseFormat
|
|
3411
|
+
}),
|
|
3412
|
+
signal
|
|
3413
|
+
}
|
|
3414
|
+
);
|
|
3415
|
+
}catch(error){
|
|
3416
|
+
if(isAIRequestAbort(error,signal)){
|
|
3417
|
+
throw normalizeAIRequestAbort(error);
|
|
1658
3418
|
}
|
|
1659
|
-
|
|
3419
|
+
throw legacyAIProviderError(
|
|
3420
|
+
'The configured TTS HTTP request failed.',
|
|
3421
|
+
'ARCANE_AI_TTS_HTTP_REQUEST_FAILED',
|
|
3422
|
+
error
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
1660
3425
|
if(!response.ok){
|
|
1661
|
-
|
|
3426
|
+
const error=legacyAIProviderError(
|
|
3427
|
+
`The configured TTS HTTP response was rejected with status ${response.status}.`,
|
|
3428
|
+
'ARCANE_AI_TTS_HTTP_RESPONSE_REJECTED'
|
|
3429
|
+
);
|
|
3430
|
+
error.status=response.status;
|
|
3431
|
+
throw error;
|
|
1662
3432
|
}
|
|
1663
3433
|
const contentType=response.headers.get('content-type')||this.audioType;
|
|
1664
3434
|
return new Blob([await response.arrayBuffer()],{type:contentType});
|
|
@@ -3085,34 +4855,17 @@ class AI {
|
|
|
3085
4855
|
}
|
|
3086
4856
|
|
|
3087
4857
|
async #requestSpeechAudio(job){
|
|
3088
|
-
if(this.#usesProviderRuntime('tts',this.ttsService)){
|
|
3089
|
-
job.abortController=new AbortController();
|
|
3090
|
-
const responseFormat=this.#providerSpeechResponseFormat();
|
|
3091
|
-
const voice=this.#providerSpeechVoice();
|
|
3092
|
-
const response=await this.#providerRuntime.request(
|
|
3093
|
-
'tts',
|
|
3094
|
-
{
|
|
3095
|
-
operation:'synthesize',
|
|
3096
|
-
payload:{
|
|
3097
|
-
model:this.#providerRuntime.selection('tts')?.modelId,
|
|
3098
|
-
input:job.text,
|
|
3099
|
-
responseFormat,
|
|
3100
|
-
...(voice?{voice}:{}),
|
|
3101
|
-
speed:this.voiceSpeed
|
|
3102
|
-
},
|
|
3103
|
-
localOnly:false,
|
|
3104
|
-
signal:job.abortController.signal
|
|
3105
|
-
}
|
|
3106
|
-
);
|
|
3107
|
-
return this.#normalizeProviderSpeechAudio(response);
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
4858
|
job.abortController=new AbortController();
|
|
3111
|
-
const
|
|
4859
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
4860
|
+
const voice=selection?this.#providerSpeechVoice():null;
|
|
4861
|
+
const response=await this.fetchTTS(
|
|
3112
4862
|
{
|
|
3113
|
-
model:this.modelTTS,
|
|
4863
|
+
model:selection?.modelId||this.modelTTS,
|
|
3114
4864
|
input:job.text,
|
|
3115
|
-
|
|
4865
|
+
...(voice?{voice}:{}),
|
|
4866
|
+
responseFormat:selection
|
|
4867
|
+
?this.#providerSpeechResponseFormat()
|
|
4868
|
+
:this.audioFormat,
|
|
3116
4869
|
speed:this.voiceSpeed
|
|
3117
4870
|
},
|
|
3118
4871
|
job.abortController.signal
|
|
@@ -3234,7 +4987,19 @@ class AI {
|
|
|
3234
4987
|
}
|
|
3235
4988
|
}
|
|
3236
4989
|
|
|
3237
|
-
|
|
4990
|
+
const error=new TypeError(
|
|
4991
|
+
'The selected TTS provider returned invalid playable audio.'
|
|
4992
|
+
);
|
|
4993
|
+
error.code='ARCANE_AI_TTS_PROVIDER_AUDIO_INVALID';
|
|
4994
|
+
throw error;
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4997
|
+
async #normalizeProviderSpeechBlob(response){
|
|
4998
|
+
if(response instanceof Blob){
|
|
4999
|
+
return response;
|
|
5000
|
+
}
|
|
5001
|
+
const normalized=await this.#normalizeProviderSpeechAudio(response);
|
|
5002
|
+
return new Blob(normalized.chunks,{type:normalized.type||this.audioType});
|
|
3238
5003
|
}
|
|
3239
5004
|
|
|
3240
5005
|
#getSpeechAudioContext(){
|
|
@@ -3252,33 +5017,190 @@ class AI {
|
|
|
3252
5017
|
return this.audioContext;
|
|
3253
5018
|
}
|
|
3254
5019
|
|
|
5020
|
+
async fetchTTS(payload={},signal=null){
|
|
5021
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
5022
|
+
if(!payload
|
|
5023
|
+
||typeof payload!=='object'
|
|
5024
|
+
||Array.isArray(payload)
|
|
5025
|
+
||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
|
|
5026
|
+
const error=new TypeError('AI.fetchTTS requires a speech request object.');
|
|
5027
|
+
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
5028
|
+
throw error;
|
|
5029
|
+
}
|
|
5030
|
+
const descriptors=Object.getOwnPropertyDescriptors(payload);
|
|
5031
|
+
const acceptedKeys=new Set([
|
|
5032
|
+
'model',
|
|
5033
|
+
'voice',
|
|
5034
|
+
'input',
|
|
5035
|
+
'responseFormat',
|
|
5036
|
+
'speed'
|
|
5037
|
+
]);
|
|
5038
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
5039
|
+
if(typeof key==='symbol'
|
|
5040
|
+
||!acceptedKeys.has(key)
|
|
5041
|
+
||!Object.hasOwn(descriptors[key],'value')){
|
|
5042
|
+
const error=new TypeError(
|
|
5043
|
+
'AI.fetchTTS accepts only model, voice, input, responseFormat, and speed data properties.'
|
|
5044
|
+
);
|
|
5045
|
+
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
5046
|
+
throw error;
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
5049
|
+
if(signal&&(
|
|
5050
|
+
typeof signal.aborted!=='boolean'
|
|
5051
|
+
||typeof signal.addEventListener!=='function'
|
|
5052
|
+
||typeof signal.removeEventListener!=='function'
|
|
5053
|
+
)){
|
|
5054
|
+
const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
|
|
5055
|
+
error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
|
|
5056
|
+
throw error;
|
|
5057
|
+
}
|
|
5058
|
+
if(signal?.aborted){
|
|
5059
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
const input=descriptors.input?.value;
|
|
5063
|
+
if(typeof input!=='string'||!input.trim()){
|
|
5064
|
+
const error=new TypeError('AI.fetchTTS input must be nonempty text.');
|
|
5065
|
+
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
5066
|
+
throw error;
|
|
5067
|
+
}
|
|
5068
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
5069
|
+
const requestedModel=descriptors.model?.value;
|
|
5070
|
+
if(requestedModel!==undefined
|
|
5071
|
+
&&(typeof requestedModel!=='string'
|
|
5072
|
+
||requestedModel.trim()!==requestedModel
|
|
5073
|
+
||!requestedModel)){
|
|
5074
|
+
const error=new TypeError(
|
|
5075
|
+
'AI.fetchTTS model must be a nonempty trimmed string when provided.'
|
|
5076
|
+
);
|
|
5077
|
+
error.code='ARCANE_AI_TTS_MODEL_INVALID';
|
|
5078
|
+
throw error;
|
|
5079
|
+
}
|
|
5080
|
+
const model=requestedModel
|
|
5081
|
+
||selection?.modelId
|
|
5082
|
+
||this.modelTTS
|
|
5083
|
+
||'';
|
|
5084
|
+
if(!model){
|
|
5085
|
+
const error=new TypeError('AI.fetchTTS model must be selected explicitly.');
|
|
5086
|
+
error.code='ARCANE_AI_TTS_MODEL_REQUIRED';
|
|
5087
|
+
throw error;
|
|
5088
|
+
}
|
|
5089
|
+
if(selection&&model!==selection.modelId){
|
|
5090
|
+
const error=new TypeError(
|
|
5091
|
+
'AI.fetchTTS model must match the admitted TTS route.'
|
|
5092
|
+
);
|
|
5093
|
+
error.code='ARCANE_AI_TTS_MODEL_SELECTION_MISMATCH';
|
|
5094
|
+
throw error;
|
|
5095
|
+
}
|
|
5096
|
+
const requestedVoice=descriptors.voice?.value;
|
|
5097
|
+
if(requestedVoice!==undefined
|
|
5098
|
+
&&(typeof requestedVoice!=='string'
|
|
5099
|
+
||requestedVoice.trim()!==requestedVoice
|
|
5100
|
+
||!requestedVoice)){
|
|
5101
|
+
const error=new TypeError(
|
|
5102
|
+
'AI.fetchTTS voice must be a nonempty trimmed string when provided.'
|
|
5103
|
+
);
|
|
5104
|
+
error.code='ARCANE_AI_TTS_VOICE_INVALID';
|
|
5105
|
+
throw error;
|
|
5106
|
+
}
|
|
5107
|
+
const voice=requestedVoice
|
|
5108
|
+
?requestedVoice
|
|
5109
|
+
:selection
|
|
5110
|
+
?this.#providerSpeechVoice()
|
|
5111
|
+
:this.#legacySpeechDefaultVoice('tts',this.ttsService);
|
|
5112
|
+
if(!voice){
|
|
5113
|
+
const error=new TypeError(
|
|
5114
|
+
'AI.fetchTTS requires a caller- or model-catalog-admitted voice.'
|
|
5115
|
+
);
|
|
5116
|
+
error.code='ARCANE_AI_TTS_VOICE_REQUIRED';
|
|
5117
|
+
throw error;
|
|
5118
|
+
}
|
|
5119
|
+
const requestedResponseFormat=descriptors.responseFormat?.value;
|
|
5120
|
+
if(requestedResponseFormat!==undefined
|
|
5121
|
+
&&(typeof requestedResponseFormat!=='string'
|
|
5122
|
+
||requestedResponseFormat.trim()!==requestedResponseFormat
|
|
5123
|
+
||!requestedResponseFormat)){
|
|
5124
|
+
const error=new TypeError(
|
|
5125
|
+
'AI.fetchTTS responseFormat must be a nonempty trimmed string when provided.'
|
|
5126
|
+
);
|
|
5127
|
+
error.code='ARCANE_AI_TTS_RESPONSE_FORMAT_INVALID';
|
|
5128
|
+
throw error;
|
|
5129
|
+
}
|
|
5130
|
+
const responseFormat=requestedResponseFormat
|
|
5131
|
+
||(selection?this.#providerSpeechResponseFormat():this.audioFormat)
|
|
5132
|
+
||'';
|
|
5133
|
+
if(!responseFormat){
|
|
5134
|
+
const error=new TypeError('AI.fetchTTS responseFormat must be nonempty.');
|
|
5135
|
+
error.code='ARCANE_AI_TTS_RESPONSE_FORMAT_INVALID';
|
|
5136
|
+
throw error;
|
|
5137
|
+
}
|
|
5138
|
+
const speed=descriptors.speed
|
|
5139
|
+
?Number(descriptors.speed.value)
|
|
5140
|
+
:this.voiceSpeed;
|
|
5141
|
+
if(!Number.isFinite(speed)||speed<=0){
|
|
5142
|
+
const error=new RangeError('AI.fetchTTS speed must be a positive number.');
|
|
5143
|
+
error.code='ARCANE_AI_TTS_SPEED_INVALID';
|
|
5144
|
+
throw error;
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
if(selection){
|
|
5148
|
+
const response=await this.#providerRuntime.request(
|
|
5149
|
+
'tts',
|
|
5150
|
+
{
|
|
5151
|
+
operation:'synthesize',
|
|
5152
|
+
payload:{model,voice,input,responseFormat,speed},
|
|
5153
|
+
localOnly:false,
|
|
5154
|
+
signal
|
|
5155
|
+
}
|
|
5156
|
+
);
|
|
5157
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
5158
|
+
const audio=await this.#normalizeProviderSpeechBlob(response);
|
|
5159
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
5160
|
+
return audio;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
return this.#requestLegacySpeechSynthesis(
|
|
5164
|
+
{model,voice,input,responseFormat,speed},
|
|
5165
|
+
signal
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
|
|
3255
5169
|
async fetchSTT(
|
|
3256
5170
|
audioFile,
|
|
3257
5171
|
responseHandler=(text='')=>{},
|
|
3258
5172
|
signal=null
|
|
3259
5173
|
){
|
|
3260
5174
|
this.#assertServiceConfigured(this.sttService,'stt');
|
|
5175
|
+
if(typeof responseHandler!=='function'){
|
|
5176
|
+
const error=new TypeError('AI.fetchSTT responseHandler must be a function.');
|
|
5177
|
+
error.code='ARCANE_AI_STT_RESPONSE_HANDLER_INVALID';
|
|
5178
|
+
throw error;
|
|
5179
|
+
}
|
|
3261
5180
|
if(signal&&(
|
|
3262
5181
|
typeof signal.aborted!=='boolean'
|
|
3263
5182
|
||typeof signal.addEventListener!=='function'
|
|
5183
|
+
||typeof signal.removeEventListener!=='function'
|
|
3264
5184
|
)){
|
|
3265
|
-
|
|
5185
|
+
const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
|
|
5186
|
+
error.code='ARCANE_AI_STT_SIGNAL_INVALID';
|
|
5187
|
+
throw error;
|
|
3266
5188
|
}
|
|
3267
5189
|
if(signal?.aborted){
|
|
3268
|
-
throw normalizeAIRequestAbort();
|
|
5190
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
3269
5191
|
}
|
|
3270
5192
|
|
|
3271
5193
|
if(this.#usesProviderRuntime('stt',this.sttService)){
|
|
3272
|
-
if(!audioFile||typeof audioFile.arrayBuffer!=='function'){
|
|
3273
|
-
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
3274
|
-
}
|
|
3275
5194
|
const response=await this.#providerRuntime.request(
|
|
3276
5195
|
'stt',
|
|
3277
5196
|
{
|
|
3278
5197
|
operation:'transcribe',
|
|
3279
5198
|
payload:{
|
|
3280
5199
|
audio:audioFile,
|
|
3281
|
-
mimeType:
|
|
5200
|
+
mimeType:typeof Blob==='function'
|
|
5201
|
+
&&audioFile instanceof Blob
|
|
5202
|
+
?String(audioFile.type||'audio/webm')
|
|
5203
|
+
:'audio/webm',
|
|
3282
5204
|
model:this.#providerRuntime.selection('stt')?.modelId
|
|
3283
5205
|
},
|
|
3284
5206
|
localOnly:false,
|
|
@@ -3289,11 +5211,15 @@ class AI {
|
|
|
3289
5211
|
?response
|
|
3290
5212
|
:response?.text;
|
|
3291
5213
|
if(typeof text!=='string'){
|
|
3292
|
-
|
|
5214
|
+
const error=new TypeError(
|
|
3293
5215
|
'Arcane returned an invalid provider speech transcription.'
|
|
3294
5216
|
);
|
|
5217
|
+
error.code='ARCANE_AI_STT_PROVIDER_TRANSCRIPT_INVALID';
|
|
5218
|
+
throw error;
|
|
3295
5219
|
}
|
|
5220
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3296
5221
|
await responseHandler(text);
|
|
5222
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3297
5223
|
return text;
|
|
3298
5224
|
}
|
|
3299
5225
|
|
|
@@ -3305,7 +5231,9 @@ class AI {
|
|
|
3305
5231
|
},
|
|
3306
5232
|
signal
|
|
3307
5233
|
);
|
|
5234
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3308
5235
|
await responseHandler(text);
|
|
5236
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3309
5237
|
return text;
|
|
3310
5238
|
}
|
|
3311
5239
|
|
|
@@ -3659,13 +5587,70 @@ class AI {
|
|
|
3659
5587
|
}
|
|
3660
5588
|
}
|
|
3661
5589
|
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
)
|
|
5590
|
+
installAIUserReadyRegistration();
|
|
5591
|
+
|
|
5592
|
+
function installAIUserReadyRegistration(){
|
|
5593
|
+
if(window.user?.ready){
|
|
5594
|
+
instantiateAI();
|
|
5595
|
+
return null;
|
|
5596
|
+
}
|
|
3666
5597
|
|
|
3667
|
-
|
|
3668
|
-
|
|
5598
|
+
const existingDescriptor=Object.getOwnPropertyDescriptor(
|
|
5599
|
+
globalThis,
|
|
5600
|
+
AI_USER_READY_REGISTRATION_KEY
|
|
5601
|
+
);
|
|
5602
|
+
if(existingDescriptor){
|
|
5603
|
+
const existing=Object.hasOwn(existingDescriptor,'value')
|
|
5604
|
+
?existingDescriptor.value
|
|
5605
|
+
:null;
|
|
5606
|
+
if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
|
|
5607
|
+
||typeof existing.dispose!=='function'){
|
|
5608
|
+
throw aiInitializationError(
|
|
5609
|
+
AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
|
|
5610
|
+
AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,
|
|
5611
|
+
'The AI user-readiness registration collides with an incompatible realm owner.'
|
|
5612
|
+
);
|
|
5613
|
+
}
|
|
5614
|
+
return existing;
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
let registration;
|
|
5618
|
+
let unsubscribe=null;
|
|
5619
|
+
function disposeAIUserReadyRegistration(){
|
|
5620
|
+
unsubscribe?.();
|
|
5621
|
+
unsubscribe=null;
|
|
5622
|
+
const descriptor=Object.getOwnPropertyDescriptor(
|
|
5623
|
+
globalThis,
|
|
5624
|
+
AI_USER_READY_REGISTRATION_KEY
|
|
5625
|
+
);
|
|
5626
|
+
if(descriptor?.value===registration){
|
|
5627
|
+
delete globalThis[AI_USER_READY_REGISTRATION_KEY];
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
registration=Object.freeze({
|
|
5631
|
+
protocol:AI_USER_READY_REGISTRATION_PROTOCOL,
|
|
5632
|
+
dispose:disposeAIUserReadyRegistration
|
|
5633
|
+
});
|
|
5634
|
+
unsubscribe=arcaneEvents.subscribe(
|
|
5635
|
+
'user-entity-loaded',
|
|
5636
|
+
function initializeAIFromCanonicalUser(event){
|
|
5637
|
+
if(event?.detail?.user&&event.detail.user!==window.user)return;
|
|
5638
|
+
if(!window.user?.ready)return;
|
|
5639
|
+
registration.dispose();
|
|
5640
|
+
instantiateAI(event);
|
|
5641
|
+
}
|
|
5642
|
+
);
|
|
5643
|
+
Object.defineProperty(
|
|
5644
|
+
globalThis,
|
|
5645
|
+
AI_USER_READY_REGISTRATION_KEY,
|
|
5646
|
+
{
|
|
5647
|
+
value:registration,
|
|
5648
|
+
configurable:true,
|
|
5649
|
+
enumerable:false,
|
|
5650
|
+
writable:false
|
|
5651
|
+
}
|
|
5652
|
+
);
|
|
5653
|
+
return registration;
|
|
3669
5654
|
}
|
|
3670
5655
|
|
|
3671
5656
|
function instantiateAI(event) {
|
|
@@ -3694,13 +5679,7 @@ function instantiateAI(event) {
|
|
|
3694
5679
|
|
|
3695
5680
|
window.ai.ready=true;
|
|
3696
5681
|
|
|
3697
|
-
|
|
3698
|
-
'ai-ready', {
|
|
3699
|
-
detail: { db: window.ai }
|
|
3700
|
-
}
|
|
3701
|
-
);
|
|
3702
|
-
|
|
3703
|
-
window.dispatchEvent(aiReady);
|
|
5682
|
+
window.ai[AI_PUBLISH_READY]();
|
|
3704
5683
|
|
|
3705
5684
|
}
|
|
3706
5685
|
}
|