arcane-os 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -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 +3242 -391
- package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1205 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +2 -2
- package/docs/reference/behavioral-testing.md +28 -5
- package/docs/reference/cli.md +115 -7
- 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 +86 -37
- package/docs/reference/inventory/runtime-modules.json +126 -49
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +185 -33
- package/docs/reference/runtime-components.md +192 -80
- package/docs/reference/runtime-modules.md +589 -70
- package/docs/reference/sdk-api.md +1017 -24
- 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 +453 -131
- 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 +244 -72
- 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 +2117 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +235 -37
- 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 +130 -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-runtime.mjs +804 -22
- 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,1411 @@ 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
|
+
const pendingIdentity=this.#providerRuntime.providerIdentity(
|
|
2093
|
+
role,
|
|
2094
|
+
selection.providerId
|
|
2095
|
+
);
|
|
2096
|
+
if(pendingIdentity===null&&selection.localOnly===null){
|
|
2097
|
+
expectedProviders[role]=null;
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2101
|
+
`The selected ${role} route is not owned by the replaceable AI legacy speech boundary.`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
expectedProviders[role]=record.provider;
|
|
2105
|
+
legacyRecords.push(record);
|
|
2106
|
+
}
|
|
2107
|
+
return Object.freeze({
|
|
2108
|
+
expectedProviders:Object.freeze(expectedProviders),
|
|
2109
|
+
legacyRecords:Object.freeze(legacyRecords)
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
async #cleanupRetiredLegacySpeechProviders(
|
|
2114
|
+
{signal=null,committed=false}={}
|
|
2115
|
+
){
|
|
2116
|
+
const failures=[];
|
|
2117
|
+
for(const record of [...this.#browserSpeechRetiredLegacyRecords]){
|
|
2118
|
+
try{
|
|
2119
|
+
if(this.#providerRuntime.ownsProvider(
|
|
2120
|
+
record.role,
|
|
2121
|
+
record.provider
|
|
2122
|
+
)){
|
|
2123
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2124
|
+
`The retired ${record.role} legacy speech provider still owns its registry entry.`
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
await record.provider.dispose({role:record.role,signal});
|
|
2128
|
+
const key=this.#legacySpeechProviderKey(
|
|
2129
|
+
record.role,
|
|
2130
|
+
record.providerId
|
|
2131
|
+
);
|
|
2132
|
+
if(this.#legacySpeechProviders.get(key)===record){
|
|
2133
|
+
this.#legacySpeechProviders.delete(key);
|
|
2134
|
+
}
|
|
2135
|
+
this.#browserSpeechRetiredLegacyRecords.delete(record);
|
|
2136
|
+
}catch(error){
|
|
2137
|
+
failures.push(error);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
if(failures.length){
|
|
2141
|
+
throw aiBrowserSpeechError(
|
|
2142
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2143
|
+
AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2144
|
+
'The replaced legacy speech providers could not be disposed.',
|
|
2145
|
+
failures.length===1
|
|
2146
|
+
?failures[0]
|
|
2147
|
+
:new AggregateError(
|
|
2148
|
+
failures,
|
|
2149
|
+
'Multiple replaced legacy speech provider disposals were rejected.'
|
|
2150
|
+
),
|
|
2151
|
+
{committed}
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
return true;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
#assertSynchronousBrowserSpeechSupersession(method){
|
|
2158
|
+
if(!this.#browserSpeechConfigurationRecord
|
|
2159
|
+
&&this.#browserSpeechRetiredRecords.size===0){
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
throw aiBrowserSpeechError(
|
|
2163
|
+
AI_BROWSER_SPEECH_ERROR_CODES.asyncTransitionRequired,
|
|
2164
|
+
AI_BROWSER_SPEECH_REASONS.asyncTransitionRequired,
|
|
2165
|
+
`${method} cannot replace SDK-owned browser speech providers synchronously; await AI.disposeBrowserSpeech() or use an asynchronous transition method.`
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
async #supersedeBrowserSpeechForRouteChange(){
|
|
2170
|
+
if(!this.#browserSpeechConfigurationRecord
|
|
2171
|
+
&&this.#browserSpeechRetiredRecords.size===0){
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
return this.disposeBrowserSpeech();
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
#publishBrowserSpeechEvent(type,normalized,operationId,reason,{descriptor=null,error=null}={}){
|
|
2178
|
+
const compatibilityDetail=Object.freeze({
|
|
2179
|
+
configuration:normalized.configuration,
|
|
2180
|
+
configurationId:normalized.id,
|
|
2181
|
+
...(descriptor?{descriptor}:{}),
|
|
2182
|
+
...(error?{error}:{}),
|
|
2183
|
+
reason
|
|
2184
|
+
});
|
|
2185
|
+
return this.#events.dispatch(
|
|
2186
|
+
type,
|
|
2187
|
+
compatibilityDetail,
|
|
2188
|
+
{
|
|
2189
|
+
operationId,
|
|
2190
|
+
publicDetail:Object.freeze({
|
|
2191
|
+
configurationId:normalized.id,
|
|
2192
|
+
...(descriptor?{descriptor}:{}),
|
|
2193
|
+
...(typeof error?.code==='string'?{code:error.code}:{}),
|
|
2194
|
+
reason
|
|
2195
|
+
})
|
|
2196
|
+
}
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
#browserSpeechRoutes(normalized,providers,previousRecord){
|
|
2201
|
+
function roleRoutes(provider,catalog){
|
|
2202
|
+
const selection=Object.freeze({
|
|
2203
|
+
providerId:provider.id,
|
|
2204
|
+
modelId:catalog.id,
|
|
2205
|
+
localOnly:true
|
|
2206
|
+
});
|
|
2207
|
+
return Object.freeze({default:selection,localOnly:selection});
|
|
2208
|
+
}
|
|
2209
|
+
const currentRoutes=this.#currentSpeechRoutes();
|
|
2210
|
+
const routes={};
|
|
2211
|
+
const catalogs={};
|
|
2212
|
+
for(const role of ['stt','tts']){
|
|
2213
|
+
if(!normalized.roles.includes(role)){
|
|
2214
|
+
routes[role]=previousRecord?.managedRoles.includes(role)
|
|
2215
|
+
?previousRecord.routes[role]
|
|
2216
|
+
:currentRoutes[role];
|
|
2217
|
+
catalogs[role]=null;
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
const catalog=providers[role].catalog();
|
|
2221
|
+
if(catalog.length!==1||typeof catalog[0]?.id!=='string'){
|
|
2222
|
+
throw aiBrowserSpeechError(
|
|
2223
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2224
|
+
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
2225
|
+
`The browser speech ${role} provider must expose one admitted model.`
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
catalogs[role]=catalog[0];
|
|
2229
|
+
routes[role]=roleRoutes(providers[role],catalog[0]);
|
|
2230
|
+
}
|
|
2231
|
+
return Object.freeze({
|
|
2232
|
+
routes:Object.freeze(routes),
|
|
2233
|
+
catalogs:Object.freeze(catalogs)
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
#browserSpeechDescriptor(normalized,catalogs,previousRecord){
|
|
2238
|
+
function roleDescriptor(role,configured,catalog){
|
|
2239
|
+
return Object.freeze({
|
|
2240
|
+
role,
|
|
2241
|
+
providerId:configured.providerId,
|
|
2242
|
+
modelId:catalog.id,
|
|
2243
|
+
...(configured.graph
|
|
2244
|
+
?{artifactGraphId:catalog.artifactGraphId}
|
|
2245
|
+
:{}),
|
|
2246
|
+
offline:configured.offline,
|
|
2247
|
+
...(role==='tts'?{defaultVoice:catalog.defaultVoice}:{})
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
const roles={};
|
|
2251
|
+
for(const role of ['stt','tts']){
|
|
2252
|
+
roles[role]=normalized.roles.includes(role)
|
|
2253
|
+
?roleDescriptor(role,normalized[role],catalogs[role])
|
|
2254
|
+
:previousRecord?.descriptor[role]??null;
|
|
2255
|
+
}
|
|
2256
|
+
return Object.freeze({
|
|
2257
|
+
protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
|
|
2258
|
+
configurationId:normalized.id,
|
|
2259
|
+
...roles
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
#browserSpeechConfiguration(normalized,previousRecord){
|
|
2264
|
+
if(!previousRecord)return normalized.configuration;
|
|
2265
|
+
if(normalized.roles.length===2)return normalized.configuration;
|
|
2266
|
+
if(normalized.dbopfs!==previousRecord.dbopfs
|
|
2267
|
+
||normalized.tableName!==previousRecord.tableName){
|
|
2268
|
+
throw aiBrowserSpeechError(
|
|
2269
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
2270
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
2271
|
+
'A partial browser speech replacement must retain the active DBOPFS store and tableName.'
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
const carriedRoles=previousRecord.managedRoles.filter(
|
|
2275
|
+
role=>!normalized.roles.includes(role)
|
|
2276
|
+
);
|
|
2277
|
+
if(carriedRoles.length===0)return normalized.configuration;
|
|
2278
|
+
return Object.freeze({
|
|
2279
|
+
protocol:AI_BROWSER_SPEECH_CONFIGURATION_PROTOCOL,
|
|
2280
|
+
id:normalized.id,
|
|
2281
|
+
dbopfs:normalized.dbopfs,
|
|
2282
|
+
...(normalized.tableName?{tableName:normalized.tableName}:{}),
|
|
2283
|
+
...(normalized.roles.includes('stt')
|
|
2284
|
+
?{stt:normalized.configuration.stt}
|
|
2285
|
+
:previousRecord.managedRoles.includes('stt')
|
|
2286
|
+
?{stt:previousRecord.configuration.stt}
|
|
2287
|
+
:{}),
|
|
2288
|
+
...(normalized.roles.includes('tts')
|
|
2289
|
+
?{tts:normalized.configuration.tts}
|
|
2290
|
+
:previousRecord.managedRoles.includes('tts')
|
|
2291
|
+
?{tts:previousRecord.configuration.tts}
|
|
2292
|
+
:{})
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
#currentSpeechRoutes(){
|
|
2297
|
+
return Object.freeze({
|
|
2298
|
+
stt:Object.freeze({
|
|
2299
|
+
default:this.#providerRuntime.selection('stt'),
|
|
2300
|
+
localOnly:this.#providerRuntime.selection('stt',{localOnly:true})
|
|
2301
|
+
}),
|
|
2302
|
+
tts:Object.freeze({
|
|
2303
|
+
default:this.#providerRuntime.selection('tts'),
|
|
2304
|
+
localOnly:this.#providerRuntime.selection('tts',{localOnly:true})
|
|
2305
|
+
})
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
#emptySpeechRoutes(){
|
|
2310
|
+
return Object.freeze({
|
|
2311
|
+
stt:Object.freeze({default:null,localOnly:null}),
|
|
2312
|
+
tts:Object.freeze({default:null,localOnly:null})
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
async #browserSpeechModule(){
|
|
2317
|
+
if(!this.#browserSpeechModulePromise){
|
|
2318
|
+
const runtime=this;
|
|
2319
|
+
this.#browserSpeechModulePromise=import(
|
|
2320
|
+
'arcane-os/ai/browser-speech'
|
|
2321
|
+
).catch(function clearRejectedBrowserSpeechImport(error){
|
|
2322
|
+
runtime.#browserSpeechModulePromise=null;
|
|
2323
|
+
throw error;
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
return this.#browserSpeechModulePromise;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
#assertBrowserSpeechGraphs(normalized,module){
|
|
2330
|
+
for(const role of normalized.roles){
|
|
2331
|
+
const configured=normalized[role];
|
|
2332
|
+
const graph=configured.graph;
|
|
2333
|
+
if(!graph)continue;
|
|
2334
|
+
if(graph.protocol!==module.BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL
|
|
2335
|
+
||graph.role!==role
|
|
2336
|
+
||(graph.providerId!==null
|
|
2337
|
+
&&graph.providerId!==undefined
|
|
2338
|
+
&&graph.providerId!==configured.providerId)){
|
|
2339
|
+
throw aiBrowserSpeechError(
|
|
2340
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationContractMismatch,
|
|
2341
|
+
AI_BROWSER_SPEECH_REASONS.configurationContractMismatch,
|
|
2342
|
+
`AI browser speech ${role}.graph does not match its role, provider, or graph protocol.`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
#browserSpeechCandidate(
|
|
2349
|
+
normalized,
|
|
2350
|
+
configuration,
|
|
2351
|
+
descriptor,
|
|
2352
|
+
providers,
|
|
2353
|
+
routes,
|
|
2354
|
+
previousRecord
|
|
2355
|
+
){
|
|
2356
|
+
const configurationByRole={};
|
|
2357
|
+
const managedRoles=Object.freeze(['stt','tts'].filter(role=>
|
|
2358
|
+
normalized.roles.includes(role)
|
|
2359
|
+
||previousRecord?.managedRoles.includes(role)
|
|
2360
|
+
));
|
|
2361
|
+
for(const role of ['stt','tts']){
|
|
2362
|
+
configurationByRole[role]=normalized.roles.includes(role)
|
|
2363
|
+
?normalized.configuration
|
|
2364
|
+
:previousRecord?.configurationByRole[role]??null;
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
configuration,
|
|
2368
|
+
configurationByRole:Object.freeze(configurationByRole),
|
|
2369
|
+
dbopfs:normalized.dbopfs,
|
|
2370
|
+
tableName:normalized.tableName,
|
|
2371
|
+
descriptor,
|
|
2372
|
+
providers,
|
|
2373
|
+
routes,
|
|
2374
|
+
managedRoles,
|
|
2375
|
+
candidateRoles:normalized.roles,
|
|
2376
|
+
unregisters:{stt:null,tts:null},
|
|
2377
|
+
registrationState:{stt:false,tts:false},
|
|
2378
|
+
retirementState:{stt:false,tts:false}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
#freezeBrowserSpeechRecord(record){
|
|
2383
|
+
record.unregisters=Object.freeze({...record.unregisters});
|
|
2384
|
+
Object.seal(record.registrationState);
|
|
2385
|
+
Object.seal(record.retirementState);
|
|
2386
|
+
return Object.freeze(record);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
#browserSpeechRecordFromReplacement(record,replacement){
|
|
2390
|
+
return this.#freezeBrowserSpeechRecord({
|
|
2391
|
+
configuration:record.configuration,
|
|
2392
|
+
configurationByRole:record.configurationByRole,
|
|
2393
|
+
dbopfs:record.dbopfs,
|
|
2394
|
+
tableName:record.tableName,
|
|
2395
|
+
descriptor:record.descriptor,
|
|
2396
|
+
providers:record.providers,
|
|
2397
|
+
routes:replacement.routes,
|
|
2398
|
+
managedRoles:record.managedRoles,
|
|
2399
|
+
candidateRoles:record.candidateRoles,
|
|
2400
|
+
unregisters:{
|
|
2401
|
+
stt:replacement.unregisters.stt,
|
|
2402
|
+
tts:replacement.unregisters.tts
|
|
2403
|
+
},
|
|
2404
|
+
registrationState:{
|
|
2405
|
+
stt:record.managedRoles.includes('stt'),
|
|
2406
|
+
tts:record.managedRoles.includes('tts')
|
|
2407
|
+
},
|
|
2408
|
+
retirementState:{stt:false,tts:false}
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
#retireBrowserSpeechRegistration(record,roles=['stt','tts']){
|
|
2413
|
+
if(!record)return;
|
|
2414
|
+
let retired=false;
|
|
2415
|
+
for(const role of roles){
|
|
2416
|
+
if(record.registrationState[role]===false)continue;
|
|
2417
|
+
record.registrationState[role]=false;
|
|
2418
|
+
record.retirementState[role]=true;
|
|
2419
|
+
retired=true;
|
|
2420
|
+
}
|
|
2421
|
+
if(retired)this.#browserSpeechRetiredRecords.add(record);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
#unregisterBrowserSpeechRecord(
|
|
2425
|
+
record,
|
|
2426
|
+
{roles=['stt','tts'],committed=false}={}
|
|
2427
|
+
){
|
|
2428
|
+
const failures=[];
|
|
2429
|
+
for(const role of ['tts','stt'].filter(role=>roles.includes(role))){
|
|
2430
|
+
if(record.registrationState?.[role]===false)continue;
|
|
2431
|
+
const unregister=record.unregisters?.[role];
|
|
2432
|
+
if(typeof unregister!=='function'){
|
|
2433
|
+
failures.push(aiBrowserSpeechError(
|
|
2434
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2435
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2436
|
+
`The ${role} browser speech provider ${record.providers[role].id} has no unregister handle.`
|
|
2437
|
+
));
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
try{
|
|
2441
|
+
const removed=unregister();
|
|
2442
|
+
if(removed!==true){
|
|
2443
|
+
failures.push(aiBrowserSpeechError(
|
|
2444
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2445
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2446
|
+
`The ${role} browser speech provider ${record.providers[role].id} no longer owns its registry entry.`
|
|
2447
|
+
));
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
if(record.registrationState)record.registrationState[role]=false;
|
|
2451
|
+
}catch(error){
|
|
2452
|
+
failures.push(error);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if(failures.length){
|
|
2456
|
+
throw aiBrowserSpeechError(
|
|
2457
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerUnregistrationRejected,
|
|
2458
|
+
AI_BROWSER_SPEECH_REASONS.providerUnregistrationRejected,
|
|
2459
|
+
'The browser speech providers could not be unregistered.',
|
|
2460
|
+
failures.length===1
|
|
2461
|
+
?failures[0]
|
|
2462
|
+
:new AggregateError(
|
|
2463
|
+
failures,
|
|
2464
|
+
'Multiple browser speech provider unregistrations were rejected.'
|
|
2465
|
+
),
|
|
2466
|
+
{committed}
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
async #disposeBrowserSpeechProviders(
|
|
2472
|
+
record,
|
|
2473
|
+
{roles=['stt','tts'],signal=null}={}
|
|
2474
|
+
){
|
|
2475
|
+
if(!record)return;
|
|
2476
|
+
const disposableRoles=roles.filter(role=>record.providers?.[role]);
|
|
2477
|
+
const settlements=await Promise.allSettled(
|
|
2478
|
+
disposableRoles.map(function disposeBrowserSpeechProvider(role){
|
|
2479
|
+
return record.providers[role].dispose({
|
|
2480
|
+
role,
|
|
2481
|
+
selection:record.routes[role].default,
|
|
2482
|
+
signal
|
|
2483
|
+
});
|
|
2484
|
+
})
|
|
2485
|
+
);
|
|
2486
|
+
const failures=settlements
|
|
2487
|
+
.filter(result=>result.status==='rejected')
|
|
2488
|
+
.map(result=>result.reason);
|
|
2489
|
+
if(failures.length){
|
|
2490
|
+
throw failures.length===1
|
|
2491
|
+
?failures[0]
|
|
2492
|
+
:new AggregateError(
|
|
2493
|
+
failures,
|
|
2494
|
+
'Multiple browser speech provider disposals were rejected.'
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
async #cleanupBrowserSpeechRecord(
|
|
2500
|
+
record,
|
|
2501
|
+
{signal=null,committed=false}={}
|
|
2502
|
+
){
|
|
2503
|
+
if(!record)return false;
|
|
2504
|
+
const retiredRoles=['stt','tts'].filter(
|
|
2505
|
+
role=>record.retirementState?.[role]===true
|
|
2506
|
+
);
|
|
2507
|
+
const roles=retiredRoles.length?retiredRoles:record.candidateRoles;
|
|
2508
|
+
this.#browserSpeechRetiredRecords.add(record);
|
|
2509
|
+
this.#unregisterBrowserSpeechRecord(record,{roles,committed});
|
|
2510
|
+
try{
|
|
2511
|
+
await this.#disposeBrowserSpeechProviders(record,{roles,signal});
|
|
2512
|
+
}catch(error){
|
|
2513
|
+
throw aiBrowserSpeechError(
|
|
2514
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2515
|
+
AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2516
|
+
'The browser speech providers could not be disposed.',
|
|
2517
|
+
error,
|
|
2518
|
+
{committed}
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
this.#browserSpeechRetiredRecords.delete(record);
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
async #throwAfterBrowserSpeechCandidateCleanup(record,failure){
|
|
2526
|
+
try{
|
|
2527
|
+
await this.#cleanupBrowserSpeechRecord(record);
|
|
2528
|
+
}catch(cleanupError){
|
|
2529
|
+
throw aiBrowserSpeechError(
|
|
2530
|
+
cleanupError.code
|
|
2531
|
+
||AI_BROWSER_SPEECH_ERROR_CODES.providerDisposalRejected,
|
|
2532
|
+
cleanupError.reason
|
|
2533
|
+
||AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
2534
|
+
'Browser speech candidate cleanup was rejected after configuration rejection.',
|
|
2535
|
+
new AggregateError(
|
|
2536
|
+
[failure,cleanupError],
|
|
2537
|
+
'Browser speech configuration and candidate cleanup were both rejected.'
|
|
2538
|
+
)
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
throw failure;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
async #configureBrowserSpeechOperation(normalized,controller,generation,operationId){
|
|
2545
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2546
|
+
const previousRecord=this.#browserSpeechConfigurationRecord;
|
|
2547
|
+
if(previousRecord&&!this.#browserSpeechRecordIsActive(previousRecord)){
|
|
2548
|
+
throw this.#browserSpeechProviderRouteOwnershipError(
|
|
2549
|
+
'The prior browser speech provider or route ownership changed before replacement.'
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
const configuration=this.#browserSpeechConfiguration(
|
|
2553
|
+
normalized,
|
|
2554
|
+
previousRecord
|
|
2555
|
+
);
|
|
2556
|
+
this.#publishBrowserSpeechEvent(
|
|
2557
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.configurationStarted,
|
|
2558
|
+
normalized,
|
|
2559
|
+
operationId,
|
|
2560
|
+
this.#browserSpeechConfigurationRecord
|
|
2561
|
+
?AI_BROWSER_SPEECH_REASONS.configurationReplaced
|
|
2562
|
+
:AI_BROWSER_SPEECH_REASONS.configurationAdded
|
|
2563
|
+
);
|
|
2564
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2565
|
+
|
|
2566
|
+
let module;
|
|
2567
|
+
try{
|
|
2568
|
+
module=await this.#browserSpeechModule();
|
|
2569
|
+
}catch(error){
|
|
2570
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2571
|
+
throw aiBrowserSpeechError(
|
|
2572
|
+
AI_BROWSER_SPEECH_ERROR_CODES.moduleImportRejected,
|
|
2573
|
+
AI_BROWSER_SPEECH_REASONS.moduleImportRejected,
|
|
2574
|
+
'The browser speech SDK module could not be imported.',
|
|
2575
|
+
error
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2579
|
+
this.#assertBrowserSpeechGraphs(normalized,module);
|
|
2580
|
+
|
|
2581
|
+
let store;
|
|
2582
|
+
try{
|
|
2583
|
+
store=module.createDbopfsSpeechArtifactStore({
|
|
2584
|
+
dbopfs:normalized.dbopfs,
|
|
2585
|
+
...(normalized.tableName?{tableName:normalized.tableName}:{})
|
|
2586
|
+
});
|
|
2587
|
+
}catch(error){
|
|
2588
|
+
throw aiBrowserSpeechError(
|
|
2589
|
+
AI_BROWSER_SPEECH_ERROR_CODES.artifactStoreConstructionRejected,
|
|
2590
|
+
AI_BROWSER_SPEECH_REASONS.artifactStoreConstructionRejected,
|
|
2591
|
+
'The browser speech artifact store could not be constructed.',
|
|
2592
|
+
error
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
const candidateProviders={
|
|
2597
|
+
stt:previousRecord?.providers.stt??null,
|
|
2598
|
+
tts:previousRecord?.providers.tts??null
|
|
2599
|
+
};
|
|
2600
|
+
for(const role of normalized.roles)candidateProviders[role]=null;
|
|
2601
|
+
let providers=null;
|
|
2602
|
+
let prepared=null;
|
|
2603
|
+
try{
|
|
2604
|
+
for(const role of normalized.roles){
|
|
2605
|
+
const factory=role==='stt'
|
|
2606
|
+
?module.createBrowserWhisperProvider
|
|
2607
|
+
:module.createBrowserKokoroProvider;
|
|
2608
|
+
const configured=normalized[role];
|
|
2609
|
+
candidateProviders[role]=factory({
|
|
2610
|
+
id:configured.providerId,
|
|
2611
|
+
...(configured.graph
|
|
2612
|
+
?{
|
|
2613
|
+
graph:configured.graph,
|
|
2614
|
+
security:configured.security
|
|
2615
|
+
}
|
|
2616
|
+
:{
|
|
2617
|
+
model:configured.model,
|
|
2618
|
+
runtime:configured.runtime,
|
|
2619
|
+
...(Object.hasOwn(configured,'security')
|
|
2620
|
+
?{security:configured.security}
|
|
2621
|
+
:{})
|
|
2622
|
+
}),
|
|
2623
|
+
store,
|
|
2624
|
+
offline:configured.offline
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
providers=Object.freeze({...candidateProviders});
|
|
2628
|
+
prepared=this.#browserSpeechRoutes(
|
|
2629
|
+
normalized,
|
|
2630
|
+
providers,
|
|
2631
|
+
previousRecord
|
|
2632
|
+
);
|
|
2633
|
+
}catch(error){
|
|
2634
|
+
const failure=error?.code===AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected
|
|
2635
|
+
?error
|
|
2636
|
+
:aiBrowserSpeechError(
|
|
2637
|
+
AI_BROWSER_SPEECH_ERROR_CODES.providerConstructionRejected,
|
|
2638
|
+
AI_BROWSER_SPEECH_REASONS.providerConstructionRejected,
|
|
2639
|
+
'The browser speech providers could not be constructed.',
|
|
2640
|
+
error
|
|
2641
|
+
);
|
|
2642
|
+
if(normalized.roles.some(role=>candidateProviders[role])){
|
|
2643
|
+
const currentRoutes=this.#currentSpeechRoutes();
|
|
2644
|
+
const partialRoutes={};
|
|
2645
|
+
for(const role of ['stt','tts']){
|
|
2646
|
+
const provider=candidateProviders[role];
|
|
2647
|
+
const changed=normalized.roles.includes(role);
|
|
2648
|
+
const selection=changed&&provider
|
|
2649
|
+
?Object.freeze({
|
|
2650
|
+
providerId:provider.id,
|
|
2651
|
+
modelId:normalized[role].graph?.model.id
|
|
2652
|
+
??normalized[role].model.id,
|
|
2653
|
+
localOnly:true
|
|
2654
|
+
})
|
|
2655
|
+
:null;
|
|
2656
|
+
partialRoutes[role]=changed
|
|
2657
|
+
?Object.freeze({default:selection,localOnly:selection})
|
|
2658
|
+
:previousRecord?.managedRoles.includes(role)
|
|
2659
|
+
?previousRecord.routes[role]
|
|
2660
|
+
:currentRoutes[role];
|
|
2661
|
+
}
|
|
2662
|
+
const partial=this.#browserSpeechCandidate(
|
|
2663
|
+
normalized,
|
|
2664
|
+
configuration,
|
|
2665
|
+
null,
|
|
2666
|
+
Object.freeze({...candidateProviders}),
|
|
2667
|
+
Object.freeze(partialRoutes),
|
|
2668
|
+
previousRecord
|
|
2669
|
+
);
|
|
2670
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2671
|
+
partial,
|
|
2672
|
+
failure
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
throw failure;
|
|
2676
|
+
}
|
|
2677
|
+
const descriptor=this.#browserSpeechDescriptor(
|
|
2678
|
+
normalized,
|
|
2679
|
+
prepared.catalogs,
|
|
2680
|
+
previousRecord
|
|
2681
|
+
);
|
|
2682
|
+
const candidate=this.#browserSpeechCandidate(
|
|
2683
|
+
normalized,
|
|
2684
|
+
configuration,
|
|
2685
|
+
descriptor,
|
|
2686
|
+
providers,
|
|
2687
|
+
prepared.routes,
|
|
2688
|
+
previousRecord
|
|
2689
|
+
);
|
|
2690
|
+
let replacementBoundary;
|
|
2691
|
+
try{
|
|
2692
|
+
replacementBoundary=this.#browserSpeechReplacementBoundary(
|
|
2693
|
+
previousRecord,
|
|
2694
|
+
normalized.roles
|
|
2695
|
+
);
|
|
2696
|
+
}catch(error){
|
|
2697
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2698
|
+
candidate,
|
|
2699
|
+
error
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
try{
|
|
2703
|
+
await this.#unloadSpeechProviderRolesForTransition(
|
|
2704
|
+
controller.signal,
|
|
2705
|
+
replacementBoundary.expectedProviders,
|
|
2706
|
+
normalized.roles
|
|
2707
|
+
);
|
|
2708
|
+
this.#assertBrowserSpeechOperation(controller,generation);
|
|
2709
|
+
}catch(error){
|
|
2710
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(candidate,error);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
let replacement;
|
|
2714
|
+
try{
|
|
2715
|
+
if(normalized.roles.length===2){
|
|
2716
|
+
replacement=this.#providerRuntime.replaceSpeechProviders({
|
|
2717
|
+
providers,
|
|
2718
|
+
routes:prepared.routes,
|
|
2719
|
+
expectedProviders:replacementBoundary.expectedProviders
|
|
2720
|
+
});
|
|
2721
|
+
}else{
|
|
2722
|
+
const role=normalized.roles[0];
|
|
2723
|
+
const roleReplacement=this.#providerRuntime.replaceSpeechProvider(
|
|
2724
|
+
role,
|
|
2725
|
+
{
|
|
2726
|
+
provider:providers[role],
|
|
2727
|
+
routes:prepared.routes[role],
|
|
2728
|
+
expectedProvider:replacementBoundary.expectedProviders[role]
|
|
2729
|
+
}
|
|
2730
|
+
);
|
|
2731
|
+
replacement=Object.freeze({
|
|
2732
|
+
routes:prepared.routes,
|
|
2733
|
+
unregisters:Object.freeze({
|
|
2734
|
+
stt:role==='stt'
|
|
2735
|
+
?roleReplacement.unregister
|
|
2736
|
+
:previousRecord?.unregisters.stt??null,
|
|
2737
|
+
tts:role==='tts'
|
|
2738
|
+
?roleReplacement.unregister
|
|
2739
|
+
:previousRecord?.unregisters.tts??null
|
|
2740
|
+
})
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
}catch(error){
|
|
2744
|
+
const commitFailure=aiBrowserSpeechError(
|
|
2745
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeCommitRejected,
|
|
2746
|
+
AI_BROWSER_SPEECH_REASONS.routeCommitRejected,
|
|
2747
|
+
'The browser speech provider and route replacement could not be committed.',
|
|
2748
|
+
error
|
|
2749
|
+
);
|
|
2750
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2751
|
+
candidate,
|
|
2752
|
+
commitFailure
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
const record=this.#browserSpeechRecordFromReplacement(
|
|
2757
|
+
candidate,
|
|
2758
|
+
replacement
|
|
2759
|
+
);
|
|
2760
|
+
for(const legacyRecord of replacementBoundary.legacyRecords){
|
|
2761
|
+
this.#browserSpeechRetiredLegacyRecords.add(legacyRecord);
|
|
2762
|
+
}
|
|
2763
|
+
if(previousRecord){
|
|
2764
|
+
this.#retireBrowserSpeechRegistration(
|
|
2765
|
+
previousRecord,
|
|
2766
|
+
normalized.roles
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
this.#browserSpeechConfigurationRecord=record;
|
|
2770
|
+
try{
|
|
2771
|
+
this.#applySpeechPreferenceTuple(
|
|
2772
|
+
this.#tupleFromSpeechProviderRoutes(replacement.routes)
|
|
2773
|
+
);
|
|
2774
|
+
}catch(error){
|
|
2775
|
+
const finalizationFailure=aiBrowserSpeechError(
|
|
2776
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeViewUpdateRejected,
|
|
2777
|
+
AI_BROWSER_SPEECH_REASONS.routeViewUpdateRejected,
|
|
2778
|
+
'The committed browser speech replacement could not update the AI speech route view.',
|
|
2779
|
+
error
|
|
2780
|
+
);
|
|
2781
|
+
if(!previousRecord
|
|
2782
|
+
||normalized.roles.some(
|
|
2783
|
+
role=>!previousRecord.managedRoles.includes(role)
|
|
2784
|
+
)){
|
|
2785
|
+
finalizationFailure.committed=true;
|
|
2786
|
+
throw finalizationFailure;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
let rollback;
|
|
2790
|
+
try{
|
|
2791
|
+
if(normalized.roles.length===2){
|
|
2792
|
+
rollback=this.#providerRuntime.replaceSpeechProviders({
|
|
2793
|
+
providers:previousRecord.providers,
|
|
2794
|
+
routes:previousRecord.routes,
|
|
2795
|
+
expectedProviders:record.providers
|
|
2796
|
+
});
|
|
2797
|
+
}else{
|
|
2798
|
+
const role=normalized.roles[0];
|
|
2799
|
+
const roleRollback=this.#providerRuntime.replaceSpeechProvider(
|
|
2800
|
+
role,
|
|
2801
|
+
{
|
|
2802
|
+
provider:previousRecord.providers[role],
|
|
2803
|
+
routes:previousRecord.routes[role],
|
|
2804
|
+
expectedProvider:record.providers[role]
|
|
2805
|
+
}
|
|
2806
|
+
);
|
|
2807
|
+
rollback=Object.freeze({
|
|
2808
|
+
routes:previousRecord.routes,
|
|
2809
|
+
unregisters:Object.freeze({
|
|
2810
|
+
stt:role==='stt'
|
|
2811
|
+
?roleRollback.unregister
|
|
2812
|
+
:previousRecord.unregisters.stt,
|
|
2813
|
+
tts:role==='tts'
|
|
2814
|
+
?roleRollback.unregister
|
|
2815
|
+
:previousRecord.unregisters.tts
|
|
2816
|
+
})
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}catch(rollbackError){
|
|
2820
|
+
const candidateCommitted=this.#browserSpeechRecordIsActive(record);
|
|
2821
|
+
if(!candidateCommitted){
|
|
2822
|
+
this.#browserSpeechConfigurationRecord=null;
|
|
2823
|
+
this.#browserSpeechRetiredRecords.add(record);
|
|
2824
|
+
}
|
|
2825
|
+
throw aiBrowserSpeechError(
|
|
2826
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeRollbackRejected,
|
|
2827
|
+
AI_BROWSER_SPEECH_REASONS.routeRollbackRejected,
|
|
2828
|
+
'The prior browser speech providers and routes could not be restored after AI speech route view rejection.',
|
|
2829
|
+
new AggregateError(
|
|
2830
|
+
[finalizationFailure,rollbackError],
|
|
2831
|
+
'Browser speech replacement finalization and rollback were both rejected.'
|
|
2832
|
+
),
|
|
2833
|
+
{committed:candidateCommitted}
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
this.#retireBrowserSpeechRegistration(record,normalized.roles);
|
|
2838
|
+
const restoredRecord=this.#browserSpeechRecordFromReplacement(
|
|
2839
|
+
previousRecord,
|
|
2840
|
+
rollback
|
|
2841
|
+
);
|
|
2842
|
+
this.#browserSpeechRetiredRecords.delete(previousRecord);
|
|
2843
|
+
this.#browserSpeechConfigurationRecord=restoredRecord;
|
|
2844
|
+
this.#applySpeechPreferenceTuple(
|
|
2845
|
+
this.#tupleFromSpeechProviderRoutes(rollback.routes)
|
|
2846
|
+
);
|
|
2847
|
+
return this.#throwAfterBrowserSpeechCandidateCleanup(
|
|
2848
|
+
record,
|
|
2849
|
+
finalizationFailure
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
this.#assertBrowserSpeechOperation(
|
|
2853
|
+
controller,
|
|
2854
|
+
generation,
|
|
2855
|
+
{committed:true}
|
|
2856
|
+
);
|
|
2857
|
+
await this.#cleanupRetiredLegacySpeechProviders({
|
|
2858
|
+
signal:controller.signal,
|
|
2859
|
+
committed:true
|
|
2860
|
+
});
|
|
2861
|
+
this.#assertBrowserSpeechOperation(
|
|
2862
|
+
controller,
|
|
2863
|
+
generation,
|
|
2864
|
+
{committed:true}
|
|
2865
|
+
);
|
|
2866
|
+
this.#publishBrowserSpeechEvent(
|
|
2867
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.configured,
|
|
2868
|
+
normalized,
|
|
2869
|
+
operationId,
|
|
2870
|
+
previousRecord
|
|
2871
|
+
?AI_BROWSER_SPEECH_REASONS.configurationReplaced
|
|
2872
|
+
:AI_BROWSER_SPEECH_REASONS.configurationAdded,
|
|
2873
|
+
{descriptor}
|
|
2874
|
+
);
|
|
2875
|
+
|
|
2876
|
+
for(const retiredRecord of [...this.#browserSpeechRetiredRecords]){
|
|
2877
|
+
try{
|
|
2878
|
+
await this.#cleanupBrowserSpeechRecord(
|
|
2879
|
+
retiredRecord,
|
|
2880
|
+
{signal:controller.signal,committed:true}
|
|
2881
|
+
);
|
|
2882
|
+
}catch(error){
|
|
2883
|
+
if(error?.committed===true)throw error;
|
|
2884
|
+
if(generation!==this.#browserSpeechGeneration
|
|
2885
|
+
||controller.signal.aborted){
|
|
2886
|
+
throw this.#browserSpeechAbortError(
|
|
2887
|
+
controller,
|
|
2888
|
+
generation,
|
|
2889
|
+
{committed:true}
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
throw error;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
this.#assertBrowserSpeechOperation(
|
|
2896
|
+
controller,
|
|
2897
|
+
generation,
|
|
2898
|
+
{committed:true}
|
|
1393
2899
|
);
|
|
1394
|
-
return
|
|
2900
|
+
return descriptor;
|
|
1395
2901
|
}
|
|
1396
2902
|
|
|
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
|
-
|
|
2903
|
+
configureBrowserSpeech(configuration,options={}){
|
|
2904
|
+
const normalized=normalizeBrowserSpeechConfiguration(configuration);
|
|
2905
|
+
const operation=normalizeBrowserSpeechOperationOptions(
|
|
2906
|
+
options,
|
|
2907
|
+
'AI.configureBrowserSpeech'
|
|
2908
|
+
);
|
|
2909
|
+
if(operation.signal?.aborted){
|
|
2910
|
+
return Promise.reject(aiBrowserSpeechError(
|
|
2911
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationCancelled,
|
|
2912
|
+
AI_BROWSER_SPEECH_REASONS.configurationCancelled,
|
|
2913
|
+
'The browser speech configuration was cancelled before admission.',
|
|
2914
|
+
operation.signal.reason,
|
|
2915
|
+
{name:'AbortError'}
|
|
2916
|
+
));
|
|
2917
|
+
}
|
|
2918
|
+
if(this.#browserSpeechConfigurationRecord
|
|
2919
|
+
&&normalized.roles.every(role=>
|
|
2920
|
+
this.#browserSpeechConfigurationRecord.configurationByRole[role]
|
|
2921
|
+
===configuration
|
|
2922
|
+
)
|
|
2923
|
+
&&this.#browserSpeechRecordIsActive(
|
|
2924
|
+
this.#browserSpeechConfigurationRecord
|
|
2925
|
+
)
|
|
2926
|
+
&&this.#browserSpeechRetiredRecords.size===0
|
|
2927
|
+
&&!this.#browserSpeechController){
|
|
2928
|
+
return Promise.resolve(this.#browserSpeechConfigurationRecord.descriptor);
|
|
2929
|
+
}
|
|
2930
|
+
const operationId=this.#browserSpeechOperationId('configure-browser-speech');
|
|
2931
|
+
const generation=++this.#browserSpeechGeneration;
|
|
2932
|
+
const superseded=aiBrowserSpeechError(
|
|
2933
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded,
|
|
2934
|
+
AI_BROWSER_SPEECH_REASONS.configurationReplaced,
|
|
2935
|
+
'The browser speech configuration was replaced by a newer configuration.',
|
|
2936
|
+
undefined,
|
|
2937
|
+
{name:'AbortError'}
|
|
2938
|
+
);
|
|
2939
|
+
this.#browserSpeechController?.abort(superseded);
|
|
2940
|
+
if(normalized.roles.includes('tts'))this.#invalidateSpeechControl();
|
|
2941
|
+
const controller=new AbortController();
|
|
2942
|
+
this.#browserSpeechController=controller;
|
|
2943
|
+
this.#browserSpeechControllerRoles=normalized.roles;
|
|
2944
|
+
const forwardAbort=function cancelBrowserSpeechConfiguration(){
|
|
2945
|
+
if(!controller.signal.aborted)controller.abort(operation.signal.reason);
|
|
2946
|
+
};
|
|
2947
|
+
if(operation.signal?.aborted)forwardAbort();
|
|
2948
|
+
else operation.signal?.addEventListener('abort',forwardAbort,{once:true});
|
|
2949
|
+
const runtime=this;
|
|
2950
|
+
const scheduled=this.#browserSpeechTransition.then(
|
|
2951
|
+
async function runBrowserSpeechConfiguration(){
|
|
2952
|
+
try{
|
|
2953
|
+
return await runtime.#configureBrowserSpeechOperation(
|
|
2954
|
+
normalized,
|
|
2955
|
+
controller,
|
|
2956
|
+
generation,
|
|
2957
|
+
operationId
|
|
2958
|
+
);
|
|
2959
|
+
}catch(error){
|
|
2960
|
+
const failure=error?.committed===true
|
|
2961
|
+
?error
|
|
2962
|
+
:(generation!==runtime.#browserSpeechGeneration
|
|
2963
|
+
||controller.signal.aborted)
|
|
2964
|
+
?runtime.#browserSpeechAbortError(controller,generation)
|
|
2965
|
+
:error;
|
|
2966
|
+
runtime.#publishBrowserSpeechEvent(
|
|
2967
|
+
failure.name==='AbortError'
|
|
2968
|
+
?AI_BROWSER_SPEECH_EVENT_TYPES.configurationCancelled
|
|
2969
|
+
:AI_BROWSER_SPEECH_EVENT_TYPES.configurationError,
|
|
2970
|
+
normalized,
|
|
2971
|
+
operationId,
|
|
2972
|
+
failure.reason
|
|
2973
|
+
||AI_BROWSER_SPEECH_REASONS.routeCommitRejected,
|
|
2974
|
+
{error:failure}
|
|
2975
|
+
);
|
|
2976
|
+
throw failure;
|
|
2977
|
+
}finally{
|
|
2978
|
+
operation.signal?.removeEventListener('abort',forwardAbort);
|
|
2979
|
+
if(runtime.#browserSpeechController===controller){
|
|
2980
|
+
runtime.#browserSpeechController=null;
|
|
2981
|
+
runtime.#browserSpeechControllerRoles=Object.freeze([]);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
);
|
|
2986
|
+
this.#browserSpeechTransition=scheduled.then(
|
|
2987
|
+
function completeBrowserSpeechConfigurationLane(){},
|
|
2988
|
+
function retainBrowserSpeechConfigurationFailure(){}
|
|
2989
|
+
);
|
|
2990
|
+
return scheduled;
|
|
1429
2991
|
}
|
|
1430
2992
|
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
2993
|
+
disposeBrowserSpeech(options={}){
|
|
2994
|
+
const operation=normalizeBrowserSpeechOperationOptions(
|
|
2995
|
+
options,
|
|
2996
|
+
'AI.disposeBrowserSpeech'
|
|
1435
2997
|
);
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2998
|
+
if(operation.signal?.aborted){
|
|
2999
|
+
return Promise.reject(aiBrowserSpeechError(
|
|
3000
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationCancelled,
|
|
3001
|
+
AI_BROWSER_SPEECH_REASONS.configurationCancelled,
|
|
3002
|
+
'Browser speech disposal was cancelled before admission.',
|
|
3003
|
+
operation.signal.reason,
|
|
3004
|
+
{name:'AbortError'}
|
|
3005
|
+
));
|
|
3006
|
+
}
|
|
3007
|
+
const operationId=this.#browserSpeechOperationId('dispose-browser-speech');
|
|
3008
|
+
const generation=++this.#browserSpeechGeneration;
|
|
3009
|
+
const superseded=aiBrowserSpeechError(
|
|
3010
|
+
AI_BROWSER_SPEECH_ERROR_CODES.configurationSuperseded,
|
|
3011
|
+
AI_BROWSER_SPEECH_REASONS.configurationDisposed,
|
|
3012
|
+
'The browser speech configuration was superseded by disposal.',
|
|
3013
|
+
undefined,
|
|
3014
|
+
{name:'AbortError'}
|
|
1439
3015
|
);
|
|
1440
|
-
|
|
1441
|
-
'tts'
|
|
1442
|
-
|
|
3016
|
+
const invalidatesSpeech=Boolean(
|
|
3017
|
+
this.#browserSpeechControllerRoles.includes('tts')
|
|
3018
|
+
||this.#browserSpeechConfigurationRecord?.managedRoles.includes('tts')
|
|
3019
|
+
||[...this.#browserSpeechRetiredRecords].some(record=>
|
|
3020
|
+
record.managedRoles.includes('tts')
|
|
3021
|
+
||record.candidateRoles.includes('tts')
|
|
3022
|
+
)
|
|
3023
|
+
||[...this.#browserSpeechRetiredLegacyRecords].some(
|
|
3024
|
+
record=>record.role==='tts'
|
|
3025
|
+
)
|
|
1443
3026
|
);
|
|
1444
|
-
this.#
|
|
1445
|
-
this
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
this.#
|
|
1449
|
-
|
|
1450
|
-
|
|
3027
|
+
this.#browserSpeechController?.abort(superseded);
|
|
3028
|
+
if(invalidatesSpeech)this.#invalidateSpeechControl();
|
|
3029
|
+
const controller=new AbortController();
|
|
3030
|
+
this.#browserSpeechController=controller;
|
|
3031
|
+
this.#browserSpeechControllerRoles=Object.freeze([]);
|
|
3032
|
+
const forwardAbort=function cancelBrowserSpeechDisposal(){
|
|
3033
|
+
if(!controller.signal.aborted)controller.abort(operation.signal.reason);
|
|
3034
|
+
};
|
|
3035
|
+
if(operation.signal?.aborted)forwardAbort();
|
|
3036
|
+
else operation.signal?.addEventListener('abort',forwardAbort,{once:true});
|
|
3037
|
+
const runtime=this;
|
|
3038
|
+
const scheduled=this.#browserSpeechTransition.then(
|
|
3039
|
+
async function runBrowserSpeechDisposal(){
|
|
3040
|
+
let normalized=null;
|
|
3041
|
+
let descriptor=null;
|
|
3042
|
+
let changed=false;
|
|
3043
|
+
let committed=false;
|
|
3044
|
+
try{
|
|
3045
|
+
runtime.#assertBrowserSpeechOperation(controller,generation);
|
|
3046
|
+
const activeRecord=runtime.#browserSpeechConfigurationRecord;
|
|
3047
|
+
const records=new Set(runtime.#browserSpeechRetiredRecords);
|
|
3048
|
+
if(activeRecord)records.add(activeRecord);
|
|
3049
|
+
const eventRecord=activeRecord||records.values().next().value||null;
|
|
3050
|
+
if(!eventRecord
|
|
3051
|
+
&&runtime.#browserSpeechRetiredLegacyRecords.size===0){
|
|
3052
|
+
return false;
|
|
3053
|
+
}
|
|
3054
|
+
if(eventRecord){
|
|
3055
|
+
normalized=normalizeBrowserSpeechConfiguration(
|
|
3056
|
+
eventRecord.configuration
|
|
3057
|
+
);
|
|
3058
|
+
descriptor=eventRecord.descriptor;
|
|
3059
|
+
}
|
|
3060
|
+
if(activeRecord){
|
|
3061
|
+
if(!runtime.#browserSpeechRecordIsActive(activeRecord)){
|
|
3062
|
+
throw runtime.#browserSpeechProviderRouteOwnershipError(
|
|
3063
|
+
'The configured browser speech provider or route ownership changed before disposal.'
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
await runtime.#unloadSpeechProviderRolesForTransition(
|
|
3067
|
+
controller.signal,
|
|
3068
|
+
activeRecord.providers,
|
|
3069
|
+
activeRecord.managedRoles
|
|
3070
|
+
);
|
|
3071
|
+
runtime.#assertBrowserSpeechOperation(controller,generation);
|
|
3072
|
+
let removed;
|
|
3073
|
+
if(activeRecord.managedRoles.length===2){
|
|
3074
|
+
removed=runtime.#providerRuntime.replaceSpeechProviders({
|
|
3075
|
+
providers:{stt:null,tts:null},
|
|
3076
|
+
routes:runtime.#emptySpeechRoutes(),
|
|
3077
|
+
expectedProviders:activeRecord.providers
|
|
3078
|
+
});
|
|
3079
|
+
}else{
|
|
3080
|
+
const role=activeRecord.managedRoles[0];
|
|
3081
|
+
const currentRoutes=runtime.#currentSpeechRoutes();
|
|
3082
|
+
const emptyRoleRoutes=Object.freeze({
|
|
3083
|
+
default:null,
|
|
3084
|
+
localOnly:null
|
|
3085
|
+
});
|
|
3086
|
+
runtime.#providerRuntime.replaceSpeechProvider(
|
|
3087
|
+
role,
|
|
3088
|
+
{
|
|
3089
|
+
provider:null,
|
|
3090
|
+
routes:emptyRoleRoutes,
|
|
3091
|
+
expectedProvider:activeRecord.providers[role]
|
|
3092
|
+
}
|
|
3093
|
+
);
|
|
3094
|
+
removed=Object.freeze({
|
|
3095
|
+
routes:Object.freeze({
|
|
3096
|
+
stt:role==='stt'
|
|
3097
|
+
?emptyRoleRoutes
|
|
3098
|
+
:currentRoutes.stt,
|
|
3099
|
+
tts:role==='tts'
|
|
3100
|
+
?emptyRoleRoutes
|
|
3101
|
+
:currentRoutes.tts
|
|
3102
|
+
})
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
runtime.#retireBrowserSpeechRegistration(
|
|
3106
|
+
activeRecord,
|
|
3107
|
+
activeRecord.managedRoles
|
|
3108
|
+
);
|
|
3109
|
+
runtime.#browserSpeechConfigurationRecord=null;
|
|
3110
|
+
changed=true;
|
|
3111
|
+
committed=true;
|
|
3112
|
+
try{
|
|
3113
|
+
runtime.#applySpeechPreferenceTuple(
|
|
3114
|
+
runtime.#tupleFromSpeechProviderRoutes(removed.routes)
|
|
3115
|
+
);
|
|
3116
|
+
}catch(error){
|
|
3117
|
+
throw aiBrowserSpeechError(
|
|
3118
|
+
AI_BROWSER_SPEECH_ERROR_CODES.routeViewUpdateRejected,
|
|
3119
|
+
AI_BROWSER_SPEECH_REASONS.routeViewUpdateRejected,
|
|
3120
|
+
'The committed browser speech removal could not update the AI speech route view.',
|
|
3121
|
+
error,
|
|
3122
|
+
{committed:true}
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
if(records.size)committed=true;
|
|
3128
|
+
for(const record of records){
|
|
3129
|
+
await runtime.#cleanupBrowserSpeechRecord(
|
|
3130
|
+
record,
|
|
3131
|
+
{signal:controller.signal,committed:true}
|
|
3132
|
+
);
|
|
3133
|
+
changed=true;
|
|
3134
|
+
}
|
|
3135
|
+
if(runtime.#browserSpeechRetiredLegacyRecords.size){
|
|
3136
|
+
await runtime.#cleanupRetiredLegacySpeechProviders({
|
|
3137
|
+
signal:controller.signal,
|
|
3138
|
+
committed:true
|
|
3139
|
+
});
|
|
3140
|
+
changed=true;
|
|
3141
|
+
}
|
|
3142
|
+
runtime.#assertBrowserSpeechOperation(
|
|
3143
|
+
controller,
|
|
3144
|
+
generation,
|
|
3145
|
+
{committed}
|
|
3146
|
+
);
|
|
3147
|
+
if(normalized){
|
|
3148
|
+
runtime.#publishBrowserSpeechEvent(
|
|
3149
|
+
AI_BROWSER_SPEECH_EVENT_TYPES.disposed,
|
|
3150
|
+
normalized,
|
|
3151
|
+
operationId,
|
|
3152
|
+
AI_BROWSER_SPEECH_REASONS.configurationDisposed,
|
|
3153
|
+
{descriptor}
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
return changed;
|
|
3157
|
+
}catch(error){
|
|
3158
|
+
const failure=error?.committed===true
|
|
3159
|
+
?error
|
|
3160
|
+
:(generation!==runtime.#browserSpeechGeneration
|
|
3161
|
+
||controller.signal.aborted)
|
|
3162
|
+
?runtime.#browserSpeechAbortError(
|
|
3163
|
+
controller,
|
|
3164
|
+
generation,
|
|
3165
|
+
{committed}
|
|
3166
|
+
)
|
|
3167
|
+
:error;
|
|
3168
|
+
if(normalized){
|
|
3169
|
+
runtime.#publishBrowserSpeechEvent(
|
|
3170
|
+
failure.name==='AbortError'
|
|
3171
|
+
?AI_BROWSER_SPEECH_EVENT_TYPES.configurationCancelled
|
|
3172
|
+
:AI_BROWSER_SPEECH_EVENT_TYPES.configurationError,
|
|
3173
|
+
normalized,
|
|
3174
|
+
operationId,
|
|
3175
|
+
failure.reason
|
|
3176
|
+
||AI_BROWSER_SPEECH_REASONS.providerDisposalRejected,
|
|
3177
|
+
{error:failure}
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
throw failure;
|
|
3181
|
+
}finally{
|
|
3182
|
+
operation.signal?.removeEventListener('abort',forwardAbort);
|
|
3183
|
+
if(runtime.#browserSpeechController===controller){
|
|
3184
|
+
runtime.#browserSpeechController=null;
|
|
3185
|
+
runtime.#browserSpeechControllerRoles=Object.freeze([]);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
1451
3189
|
);
|
|
1452
|
-
this.#
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
await this.#reconcileLegacySpeechReadiness();
|
|
1458
|
-
return configured;
|
|
3190
|
+
this.#browserSpeechTransition=scheduled.then(
|
|
3191
|
+
function completeBrowserSpeechDisposalLane(){},
|
|
3192
|
+
function retainBrowserSpeechDisposalFailure(){}
|
|
3193
|
+
);
|
|
3194
|
+
return scheduled;
|
|
1459
3195
|
}
|
|
1460
3196
|
|
|
1461
3197
|
async startProviders(options){
|
|
1462
3198
|
const normalized=normalizeAIStartupOptions(options);
|
|
1463
|
-
this
|
|
3199
|
+
const generation=++this.#speechControlGeneration;
|
|
3200
|
+
this.muted=true;
|
|
1464
3201
|
if(normalized.startMuted){
|
|
1465
3202
|
this.stopAudio();
|
|
1466
3203
|
}
|
|
1467
|
-
|
|
3204
|
+
const handle=await this.#providerRuntime.start(normalized);
|
|
3205
|
+
if(!normalized.startMuted){
|
|
3206
|
+
const runtime=this;
|
|
3207
|
+
handle.settled.then(
|
|
3208
|
+
function admitReadyStartupSpeech(){
|
|
3209
|
+
if(generation!==runtime.#speechControlGeneration)return;
|
|
3210
|
+
const status=runtime.#providerRuntime.status('tts');
|
|
3211
|
+
runtime.muted=!(status.state==='ready'&&status.loaded===true);
|
|
3212
|
+
},
|
|
3213
|
+
function retainFailClosedStartupSpeech(){
|
|
3214
|
+
if(generation===runtime.#speechControlGeneration){
|
|
3215
|
+
runtime.muted=true;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
return handle;
|
|
1468
3221
|
}
|
|
1469
3222
|
|
|
1470
3223
|
async setSpeechMuted(muted){
|
|
1471
3224
|
if(typeof muted!=='boolean'){
|
|
1472
3225
|
throw new TypeError('AI speech muted state must be a boolean.');
|
|
1473
3226
|
}
|
|
1474
|
-
this
|
|
3227
|
+
const generation=++this.#speechControlGeneration;
|
|
3228
|
+
this.muted=true;
|
|
1475
3229
|
if(muted){
|
|
1476
3230
|
this.stopAudio();
|
|
1477
3231
|
}
|
|
1478
3232
|
if(!this.#usesProviderRuntime('tts',this.ttsService)){
|
|
3233
|
+
if(generation===this.#speechControlGeneration)this.muted=muted;
|
|
1479
3234
|
return true;
|
|
1480
3235
|
}
|
|
1481
3236
|
await this.#providerRuntime.setSpeechMuted(muted);
|
|
3237
|
+
if(generation===this.#speechControlGeneration){
|
|
3238
|
+
const status=this.#providerRuntime.status('tts');
|
|
3239
|
+
this.muted=muted
|
|
3240
|
+
||status.state!=='ready'
|
|
3241
|
+
||status.loaded!==true;
|
|
3242
|
+
}
|
|
1482
3243
|
return true;
|
|
1483
3244
|
}
|
|
1484
3245
|
|
|
@@ -1624,7 +3385,11 @@ class AI {
|
|
|
1624
3385
|
throw normalizeAIRequestAbort(signal.reason);
|
|
1625
3386
|
}
|
|
1626
3387
|
if(!response||typeof response.audioBase64!=='string'){
|
|
1627
|
-
|
|
3388
|
+
const error=new TypeError(
|
|
3389
|
+
'The Arcane speech bridge returned invalid TTS audio.'
|
|
3390
|
+
);
|
|
3391
|
+
error.code='ARCANE_AI_TTS_NATIVE_AUDIO_INVALID';
|
|
3392
|
+
throw error;
|
|
1628
3393
|
}
|
|
1629
3394
|
return new Blob(
|
|
1630
3395
|
[this.#base64ToBytes(response.audioBase64)],
|
|
@@ -1637,28 +3402,41 @@ class AI {
|
|
|
1637
3402
|
}
|
|
1638
3403
|
|
|
1639
3404
|
await this.#assertAndroidSpeechBridge(this.ttsService);
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
3405
|
+
let response;
|
|
3406
|
+
try{
|
|
3407
|
+
response=await fetch(
|
|
3408
|
+
this.urlTTS,
|
|
3409
|
+
{
|
|
3410
|
+
method:'POST',
|
|
3411
|
+
credentials,
|
|
3412
|
+
headers:this.#ttsHeaders[this.ttsService],
|
|
3413
|
+
body:JSON.stringify({
|
|
3414
|
+
model,
|
|
3415
|
+
voice,
|
|
3416
|
+
input,
|
|
3417
|
+
speed,
|
|
3418
|
+
response_format:responseFormat
|
|
3419
|
+
}),
|
|
3420
|
+
signal
|
|
3421
|
+
}
|
|
3422
|
+
);
|
|
3423
|
+
}catch(error){
|
|
3424
|
+
if(isAIRequestAbort(error,signal)){
|
|
3425
|
+
throw normalizeAIRequestAbort(error);
|
|
1658
3426
|
}
|
|
1659
|
-
|
|
3427
|
+
throw legacyAIProviderError(
|
|
3428
|
+
'The configured TTS HTTP request failed.',
|
|
3429
|
+
'ARCANE_AI_TTS_HTTP_REQUEST_FAILED',
|
|
3430
|
+
error
|
|
3431
|
+
);
|
|
3432
|
+
}
|
|
1660
3433
|
if(!response.ok){
|
|
1661
|
-
|
|
3434
|
+
const error=legacyAIProviderError(
|
|
3435
|
+
`The configured TTS HTTP response was rejected with status ${response.status}.`,
|
|
3436
|
+
'ARCANE_AI_TTS_HTTP_RESPONSE_REJECTED'
|
|
3437
|
+
);
|
|
3438
|
+
error.status=response.status;
|
|
3439
|
+
throw error;
|
|
1662
3440
|
}
|
|
1663
3441
|
const contentType=response.headers.get('content-type')||this.audioType;
|
|
1664
3442
|
return new Blob([await response.arrayBuffer()],{type:contentType});
|
|
@@ -3085,34 +4863,17 @@ class AI {
|
|
|
3085
4863
|
}
|
|
3086
4864
|
|
|
3087
4865
|
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
4866
|
job.abortController=new AbortController();
|
|
3111
|
-
const
|
|
4867
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
4868
|
+
const voice=selection?this.#providerSpeechVoice():null;
|
|
4869
|
+
const response=await this.fetchTTS(
|
|
3112
4870
|
{
|
|
3113
|
-
model:this.modelTTS,
|
|
4871
|
+
model:selection?.modelId||this.modelTTS,
|
|
3114
4872
|
input:job.text,
|
|
3115
|
-
|
|
4873
|
+
...(voice?{voice}:{}),
|
|
4874
|
+
responseFormat:selection
|
|
4875
|
+
?this.#providerSpeechResponseFormat()
|
|
4876
|
+
:this.audioFormat,
|
|
3116
4877
|
speed:this.voiceSpeed
|
|
3117
4878
|
},
|
|
3118
4879
|
job.abortController.signal
|
|
@@ -3234,7 +4995,19 @@ class AI {
|
|
|
3234
4995
|
}
|
|
3235
4996
|
}
|
|
3236
4997
|
|
|
3237
|
-
|
|
4998
|
+
const error=new TypeError(
|
|
4999
|
+
'The selected TTS provider returned invalid playable audio.'
|
|
5000
|
+
);
|
|
5001
|
+
error.code='ARCANE_AI_TTS_PROVIDER_AUDIO_INVALID';
|
|
5002
|
+
throw error;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
async #normalizeProviderSpeechBlob(response){
|
|
5006
|
+
if(response instanceof Blob){
|
|
5007
|
+
return response;
|
|
5008
|
+
}
|
|
5009
|
+
const normalized=await this.#normalizeProviderSpeechAudio(response);
|
|
5010
|
+
return new Blob(normalized.chunks,{type:normalized.type||this.audioType});
|
|
3238
5011
|
}
|
|
3239
5012
|
|
|
3240
5013
|
#getSpeechAudioContext(){
|
|
@@ -3252,33 +5025,190 @@ class AI {
|
|
|
3252
5025
|
return this.audioContext;
|
|
3253
5026
|
}
|
|
3254
5027
|
|
|
5028
|
+
async fetchTTS(payload={},signal=null){
|
|
5029
|
+
this.#assertServiceConfigured(this.ttsService,'tts');
|
|
5030
|
+
if(!payload
|
|
5031
|
+
||typeof payload!=='object'
|
|
5032
|
+
||Array.isArray(payload)
|
|
5033
|
+
||![Object.prototype,null].includes(Object.getPrototypeOf(payload))){
|
|
5034
|
+
const error=new TypeError('AI.fetchTTS requires a speech request object.');
|
|
5035
|
+
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
5036
|
+
throw error;
|
|
5037
|
+
}
|
|
5038
|
+
const descriptors=Object.getOwnPropertyDescriptors(payload);
|
|
5039
|
+
const acceptedKeys=new Set([
|
|
5040
|
+
'model',
|
|
5041
|
+
'voice',
|
|
5042
|
+
'input',
|
|
5043
|
+
'responseFormat',
|
|
5044
|
+
'speed'
|
|
5045
|
+
]);
|
|
5046
|
+
for(const key of Reflect.ownKeys(descriptors)){
|
|
5047
|
+
if(typeof key==='symbol'
|
|
5048
|
+
||!acceptedKeys.has(key)
|
|
5049
|
+
||!Object.hasOwn(descriptors[key],'value')){
|
|
5050
|
+
const error=new TypeError(
|
|
5051
|
+
'AI.fetchTTS accepts only model, voice, input, responseFormat, and speed data properties.'
|
|
5052
|
+
);
|
|
5053
|
+
error.code='ARCANE_AI_TTS_REQUEST_INVALID';
|
|
5054
|
+
throw error;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
if(signal&&(
|
|
5058
|
+
typeof signal.aborted!=='boolean'
|
|
5059
|
+
||typeof signal.addEventListener!=='function'
|
|
5060
|
+
||typeof signal.removeEventListener!=='function'
|
|
5061
|
+
)){
|
|
5062
|
+
const error=new TypeError('AI.fetchTTS signal must be an AbortSignal.');
|
|
5063
|
+
error.code='ARCANE_AI_TTS_SIGNAL_INVALID';
|
|
5064
|
+
throw error;
|
|
5065
|
+
}
|
|
5066
|
+
if(signal?.aborted){
|
|
5067
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
const input=descriptors.input?.value;
|
|
5071
|
+
if(typeof input!=='string'||!input.trim()){
|
|
5072
|
+
const error=new TypeError('AI.fetchTTS input must be nonempty text.');
|
|
5073
|
+
error.code='ARCANE_AI_TTS_INPUT_INVALID';
|
|
5074
|
+
throw error;
|
|
5075
|
+
}
|
|
5076
|
+
const selection=this.#providerRuntime.selection('tts');
|
|
5077
|
+
const requestedModel=descriptors.model?.value;
|
|
5078
|
+
if(requestedModel!==undefined
|
|
5079
|
+
&&(typeof requestedModel!=='string'
|
|
5080
|
+
||requestedModel.trim()!==requestedModel
|
|
5081
|
+
||!requestedModel)){
|
|
5082
|
+
const error=new TypeError(
|
|
5083
|
+
'AI.fetchTTS model must be a nonempty trimmed string when provided.'
|
|
5084
|
+
);
|
|
5085
|
+
error.code='ARCANE_AI_TTS_MODEL_INVALID';
|
|
5086
|
+
throw error;
|
|
5087
|
+
}
|
|
5088
|
+
const model=requestedModel
|
|
5089
|
+
||selection?.modelId
|
|
5090
|
+
||this.modelTTS
|
|
5091
|
+
||'';
|
|
5092
|
+
if(!model){
|
|
5093
|
+
const error=new TypeError('AI.fetchTTS model must be selected explicitly.');
|
|
5094
|
+
error.code='ARCANE_AI_TTS_MODEL_REQUIRED';
|
|
5095
|
+
throw error;
|
|
5096
|
+
}
|
|
5097
|
+
if(selection&&model!==selection.modelId){
|
|
5098
|
+
const error=new TypeError(
|
|
5099
|
+
'AI.fetchTTS model must match the admitted TTS route.'
|
|
5100
|
+
);
|
|
5101
|
+
error.code='ARCANE_AI_TTS_MODEL_SELECTION_MISMATCH';
|
|
5102
|
+
throw error;
|
|
5103
|
+
}
|
|
5104
|
+
const requestedVoice=descriptors.voice?.value;
|
|
5105
|
+
if(requestedVoice!==undefined
|
|
5106
|
+
&&(typeof requestedVoice!=='string'
|
|
5107
|
+
||requestedVoice.trim()!==requestedVoice
|
|
5108
|
+
||!requestedVoice)){
|
|
5109
|
+
const error=new TypeError(
|
|
5110
|
+
'AI.fetchTTS voice must be a nonempty trimmed string when provided.'
|
|
5111
|
+
);
|
|
5112
|
+
error.code='ARCANE_AI_TTS_VOICE_INVALID';
|
|
5113
|
+
throw error;
|
|
5114
|
+
}
|
|
5115
|
+
const voice=requestedVoice
|
|
5116
|
+
?requestedVoice
|
|
5117
|
+
:selection
|
|
5118
|
+
?this.#providerSpeechVoice()
|
|
5119
|
+
:this.#legacySpeechDefaultVoice('tts',this.ttsService);
|
|
5120
|
+
if(!voice){
|
|
5121
|
+
const error=new TypeError(
|
|
5122
|
+
'AI.fetchTTS requires a caller- or model-catalog-admitted voice.'
|
|
5123
|
+
);
|
|
5124
|
+
error.code='ARCANE_AI_TTS_VOICE_REQUIRED';
|
|
5125
|
+
throw error;
|
|
5126
|
+
}
|
|
5127
|
+
const requestedResponseFormat=descriptors.responseFormat?.value;
|
|
5128
|
+
if(requestedResponseFormat!==undefined
|
|
5129
|
+
&&(typeof requestedResponseFormat!=='string'
|
|
5130
|
+
||requestedResponseFormat.trim()!==requestedResponseFormat
|
|
5131
|
+
||!requestedResponseFormat)){
|
|
5132
|
+
const error=new TypeError(
|
|
5133
|
+
'AI.fetchTTS responseFormat must be a nonempty trimmed string when provided.'
|
|
5134
|
+
);
|
|
5135
|
+
error.code='ARCANE_AI_TTS_RESPONSE_FORMAT_INVALID';
|
|
5136
|
+
throw error;
|
|
5137
|
+
}
|
|
5138
|
+
const responseFormat=requestedResponseFormat
|
|
5139
|
+
||(selection?this.#providerSpeechResponseFormat():this.audioFormat)
|
|
5140
|
+
||'';
|
|
5141
|
+
if(!responseFormat){
|
|
5142
|
+
const error=new TypeError('AI.fetchTTS responseFormat must be nonempty.');
|
|
5143
|
+
error.code='ARCANE_AI_TTS_RESPONSE_FORMAT_INVALID';
|
|
5144
|
+
throw error;
|
|
5145
|
+
}
|
|
5146
|
+
const speed=descriptors.speed
|
|
5147
|
+
?Number(descriptors.speed.value)
|
|
5148
|
+
:this.voiceSpeed;
|
|
5149
|
+
if(!Number.isFinite(speed)||speed<=0){
|
|
5150
|
+
const error=new RangeError('AI.fetchTTS speed must be a positive number.');
|
|
5151
|
+
error.code='ARCANE_AI_TTS_SPEED_INVALID';
|
|
5152
|
+
throw error;
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
if(selection){
|
|
5156
|
+
const response=await this.#providerRuntime.request(
|
|
5157
|
+
'tts',
|
|
5158
|
+
{
|
|
5159
|
+
operation:'synthesize',
|
|
5160
|
+
payload:{model,voice,input,responseFormat,speed},
|
|
5161
|
+
localOnly:false,
|
|
5162
|
+
signal
|
|
5163
|
+
}
|
|
5164
|
+
);
|
|
5165
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
5166
|
+
const audio=await this.#normalizeProviderSpeechBlob(response);
|
|
5167
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
5168
|
+
return audio;
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
return this.#requestLegacySpeechSynthesis(
|
|
5172
|
+
{model,voice,input,responseFormat,speed},
|
|
5173
|
+
signal
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
|
|
3255
5177
|
async fetchSTT(
|
|
3256
5178
|
audioFile,
|
|
3257
5179
|
responseHandler=(text='')=>{},
|
|
3258
5180
|
signal=null
|
|
3259
5181
|
){
|
|
3260
5182
|
this.#assertServiceConfigured(this.sttService,'stt');
|
|
5183
|
+
if(typeof responseHandler!=='function'){
|
|
5184
|
+
const error=new TypeError('AI.fetchSTT responseHandler must be a function.');
|
|
5185
|
+
error.code='ARCANE_AI_STT_RESPONSE_HANDLER_INVALID';
|
|
5186
|
+
throw error;
|
|
5187
|
+
}
|
|
3261
5188
|
if(signal&&(
|
|
3262
5189
|
typeof signal.aborted!=='boolean'
|
|
3263
5190
|
||typeof signal.addEventListener!=='function'
|
|
5191
|
+
||typeof signal.removeEventListener!=='function'
|
|
3264
5192
|
)){
|
|
3265
|
-
|
|
5193
|
+
const error=new TypeError('AI.fetchSTT signal must be an AbortSignal.');
|
|
5194
|
+
error.code='ARCANE_AI_STT_SIGNAL_INVALID';
|
|
5195
|
+
throw error;
|
|
3266
5196
|
}
|
|
3267
5197
|
if(signal?.aborted){
|
|
3268
|
-
throw normalizeAIRequestAbort();
|
|
5198
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
3269
5199
|
}
|
|
3270
5200
|
|
|
3271
5201
|
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
5202
|
const response=await this.#providerRuntime.request(
|
|
3276
5203
|
'stt',
|
|
3277
5204
|
{
|
|
3278
5205
|
operation:'transcribe',
|
|
3279
5206
|
payload:{
|
|
3280
5207
|
audio:audioFile,
|
|
3281
|
-
mimeType:
|
|
5208
|
+
mimeType:typeof Blob==='function'
|
|
5209
|
+
&&audioFile instanceof Blob
|
|
5210
|
+
?String(audioFile.type||'audio/webm')
|
|
5211
|
+
:'audio/webm',
|
|
3282
5212
|
model:this.#providerRuntime.selection('stt')?.modelId
|
|
3283
5213
|
},
|
|
3284
5214
|
localOnly:false,
|
|
@@ -3289,11 +5219,15 @@ class AI {
|
|
|
3289
5219
|
?response
|
|
3290
5220
|
:response?.text;
|
|
3291
5221
|
if(typeof text!=='string'){
|
|
3292
|
-
|
|
5222
|
+
const error=new TypeError(
|
|
3293
5223
|
'Arcane returned an invalid provider speech transcription.'
|
|
3294
5224
|
);
|
|
5225
|
+
error.code='ARCANE_AI_STT_PROVIDER_TRANSCRIPT_INVALID';
|
|
5226
|
+
throw error;
|
|
3295
5227
|
}
|
|
5228
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3296
5229
|
await responseHandler(text);
|
|
5230
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3297
5231
|
return text;
|
|
3298
5232
|
}
|
|
3299
5233
|
|
|
@@ -3305,7 +5239,9 @@ class AI {
|
|
|
3305
5239
|
},
|
|
3306
5240
|
signal
|
|
3307
5241
|
);
|
|
5242
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3308
5243
|
await responseHandler(text);
|
|
5244
|
+
if(signal?.aborted)throw normalizeAIRequestAbort(signal.reason);
|
|
3309
5245
|
return text;
|
|
3310
5246
|
}
|
|
3311
5247
|
|
|
@@ -3659,13 +5595,70 @@ class AI {
|
|
|
3659
5595
|
}
|
|
3660
5596
|
}
|
|
3661
5597
|
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
)
|
|
5598
|
+
installAIUserReadyRegistration();
|
|
5599
|
+
|
|
5600
|
+
function installAIUserReadyRegistration(){
|
|
5601
|
+
if(window.user?.ready){
|
|
5602
|
+
instantiateAI();
|
|
5603
|
+
return null;
|
|
5604
|
+
}
|
|
3666
5605
|
|
|
3667
|
-
|
|
3668
|
-
|
|
5606
|
+
const existingDescriptor=Object.getOwnPropertyDescriptor(
|
|
5607
|
+
globalThis,
|
|
5608
|
+
AI_USER_READY_REGISTRATION_KEY
|
|
5609
|
+
);
|
|
5610
|
+
if(existingDescriptor){
|
|
5611
|
+
const existing=Object.hasOwn(existingDescriptor,'value')
|
|
5612
|
+
?existingDescriptor.value
|
|
5613
|
+
:null;
|
|
5614
|
+
if(existing?.protocol!==AI_USER_READY_REGISTRATION_PROTOCOL
|
|
5615
|
+
||typeof existing.dispose!=='function'){
|
|
5616
|
+
throw aiInitializationError(
|
|
5617
|
+
AI_INITIALIZATION_ERROR_CODES.userReadyRegistrationCollision,
|
|
5618
|
+
AI_INITIALIZATION_REASONS.userReadyRegistrationCollision,
|
|
5619
|
+
'The AI user-readiness registration collides with an incompatible realm owner.'
|
|
5620
|
+
);
|
|
5621
|
+
}
|
|
5622
|
+
return existing;
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
let registration;
|
|
5626
|
+
let unsubscribe=null;
|
|
5627
|
+
function disposeAIUserReadyRegistration(){
|
|
5628
|
+
unsubscribe?.();
|
|
5629
|
+
unsubscribe=null;
|
|
5630
|
+
const descriptor=Object.getOwnPropertyDescriptor(
|
|
5631
|
+
globalThis,
|
|
5632
|
+
AI_USER_READY_REGISTRATION_KEY
|
|
5633
|
+
);
|
|
5634
|
+
if(descriptor?.value===registration){
|
|
5635
|
+
delete globalThis[AI_USER_READY_REGISTRATION_KEY];
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
registration=Object.freeze({
|
|
5639
|
+
protocol:AI_USER_READY_REGISTRATION_PROTOCOL,
|
|
5640
|
+
dispose:disposeAIUserReadyRegistration
|
|
5641
|
+
});
|
|
5642
|
+
unsubscribe=arcaneEvents.subscribe(
|
|
5643
|
+
'user-entity-loaded',
|
|
5644
|
+
function initializeAIFromCanonicalUser(event){
|
|
5645
|
+
if(event?.detail?.user&&event.detail.user!==window.user)return;
|
|
5646
|
+
if(!window.user?.ready)return;
|
|
5647
|
+
registration.dispose();
|
|
5648
|
+
instantiateAI(event);
|
|
5649
|
+
}
|
|
5650
|
+
);
|
|
5651
|
+
Object.defineProperty(
|
|
5652
|
+
globalThis,
|
|
5653
|
+
AI_USER_READY_REGISTRATION_KEY,
|
|
5654
|
+
{
|
|
5655
|
+
value:registration,
|
|
5656
|
+
configurable:true,
|
|
5657
|
+
enumerable:false,
|
|
5658
|
+
writable:false
|
|
5659
|
+
}
|
|
5660
|
+
);
|
|
5661
|
+
return registration;
|
|
3669
5662
|
}
|
|
3670
5663
|
|
|
3671
5664
|
function instantiateAI(event) {
|
|
@@ -3694,13 +5687,7 @@ function instantiateAI(event) {
|
|
|
3694
5687
|
|
|
3695
5688
|
window.ai.ready=true;
|
|
3696
5689
|
|
|
3697
|
-
|
|
3698
|
-
'ai-ready', {
|
|
3699
|
-
detail: { db: window.ai }
|
|
3700
|
-
}
|
|
3701
|
-
);
|
|
3702
|
-
|
|
3703
|
-
window.dispatchEvent(aiReady);
|
|
5690
|
+
window.ai[AI_PUBLISH_READY]();
|
|
3704
5691
|
|
|
3705
5692
|
}
|
|
3706
5693
|
}
|