arcane-os 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +20 -20
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +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 +143 -33
- package/docs/reference/runtime-components.md +192 -80
- package/docs/reference/runtime-modules.md +584 -70
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +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 +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +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 +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -146,12 +146,17 @@
|
|
|
146
146
|
<button id="muteButton" type="button" class="mute" aria-pressed="true" aria-label="Voice unavailable" disabled>Voice unavailable</button>
|
|
147
147
|
|
|
148
148
|
<script type="module">
|
|
149
|
+
const {
|
|
150
|
+
createArcaneEventSource,
|
|
151
|
+
projectArcaneDOMEvent
|
|
152
|
+
} = await import('arcane-os/event-manager');
|
|
153
|
+
|
|
149
154
|
void import('../modules/DBLS.js').catch(
|
|
150
155
|
function reportDBLSImportFailure(error) {
|
|
151
156
|
console.error('The shared database runtime could not load.');
|
|
152
157
|
}
|
|
153
158
|
);
|
|
154
|
-
const aiModuleImport = import('../modules/AI.js
|
|
159
|
+
const aiModuleImport = import('../modules/AI.js');
|
|
155
160
|
const {
|
|
156
161
|
requestAIRuntimeIntent,
|
|
157
162
|
subscribeAIRuntimeState
|
|
@@ -161,6 +166,23 @@
|
|
|
161
166
|
} = await import('../modules/ComponentContracts.js');
|
|
162
167
|
|
|
163
168
|
const host = this;
|
|
169
|
+
const events = createArcaneEventSource(
|
|
170
|
+
host,
|
|
171
|
+
{
|
|
172
|
+
source: 'arcane.component.speech',
|
|
173
|
+
eventTypes: [
|
|
174
|
+
'speech-ready',
|
|
175
|
+
'speech-transcription-complete',
|
|
176
|
+
'speech-transcription-error',
|
|
177
|
+
'speech-transcription-cancelled',
|
|
178
|
+
'speech-microphone-unavailable',
|
|
179
|
+
'speech-stt-activation-request',
|
|
180
|
+
'speech-stt-activation-error',
|
|
181
|
+
'speech-tts-lifecycle-error',
|
|
182
|
+
'speech-synthesis-error'
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
);
|
|
164
186
|
const shadowRoot = host.shadowRoot;
|
|
165
187
|
const recordButton = shadowRoot.querySelector('#recordButton');
|
|
166
188
|
const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
|
|
@@ -186,12 +208,99 @@
|
|
|
186
208
|
let keyboardRecording = false;
|
|
187
209
|
let localStatus = null;
|
|
188
210
|
let pendingTTSIntent = null;
|
|
211
|
+
let activeTTSIntent = null;
|
|
212
|
+
let pendingUnmute = false;
|
|
189
213
|
let ttsIntentGeneration = 0;
|
|
214
|
+
let eventOperationSequence = 0;
|
|
215
|
+
let transcriptionOperationId = null;
|
|
216
|
+
let ttsOperationId = null;
|
|
190
217
|
let destroyed = false;
|
|
191
218
|
|
|
219
|
+
function nextSpeechOperationId(kind) {
|
|
220
|
+
eventOperationSequence += 1;
|
|
221
|
+
return `${events.descriptor.instanceId}:${kind}:${eventOperationSequence}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function dispatchSpeechEvent(
|
|
225
|
+
type,
|
|
226
|
+
compatibilityDetail = {},
|
|
227
|
+
{
|
|
228
|
+
publicDetail = {},
|
|
229
|
+
operationId = null,
|
|
230
|
+
bubbles = false,
|
|
231
|
+
composed = false,
|
|
232
|
+
cancelable = false
|
|
233
|
+
} = {}
|
|
234
|
+
) {
|
|
235
|
+
if (destroyed && type !== 'speech-transcription-cancelled') {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const publication = events.dispatch(
|
|
239
|
+
type,
|
|
240
|
+
compatibilityDetail,
|
|
241
|
+
{
|
|
242
|
+
publicDetail,
|
|
243
|
+
operationId,
|
|
244
|
+
cancelable
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
if (!publication.accepted) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return projectArcaneDOMEvent(
|
|
251
|
+
host,
|
|
252
|
+
publication.occurrence,
|
|
253
|
+
{
|
|
254
|
+
bubbles,
|
|
255
|
+
composed,
|
|
256
|
+
cancelable
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function publicSpeechErrorFields(error, boundaryCode) {
|
|
262
|
+
let causeCode = '';
|
|
263
|
+
try {
|
|
264
|
+
causeCode = typeof error?.code === 'string'
|
|
265
|
+
? error.code.trim()
|
|
266
|
+
: '';
|
|
267
|
+
} catch {}
|
|
268
|
+
return Object.freeze(
|
|
269
|
+
{
|
|
270
|
+
code: boundaryCode,
|
|
271
|
+
...(causeCode && causeCode !== boundaryCode
|
|
272
|
+
? { causeCode }
|
|
273
|
+
: {})
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function microphoneFailureReason(error) {
|
|
279
|
+
if (error?.name === 'NotFoundError') {
|
|
280
|
+
return 'microphone-input-missing';
|
|
281
|
+
}
|
|
282
|
+
if (error?.name === 'NotReadableError') {
|
|
283
|
+
return 'microphone-input-unavailable';
|
|
284
|
+
}
|
|
285
|
+
if (['NotAllowedError', 'SecurityError'].includes(error?.name)) {
|
|
286
|
+
return 'microphone-capture-denied';
|
|
287
|
+
}
|
|
288
|
+
return 'microphone-capture-rejected';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function canonicalSTTCancellationReason(reason) {
|
|
292
|
+
if (reason === 'runtime-unready') {
|
|
293
|
+
return 'stt-role-unready';
|
|
294
|
+
}
|
|
295
|
+
if (reason === 'stt-provider-request-cancelled') {
|
|
296
|
+
return 'stt-provider-transcription-cancelled';
|
|
297
|
+
}
|
|
298
|
+
return String(reason || 'stt-transcription-cancelled');
|
|
299
|
+
}
|
|
300
|
+
|
|
192
301
|
const initialMutedWasHydrated = typeof host.initialMuted === 'boolean';
|
|
193
302
|
host.initialMuted = initialMutedWasHydrated ? host.initialMuted : true;
|
|
194
|
-
host.muted =
|
|
303
|
+
host.muted = true;
|
|
195
304
|
host.configure = configure;
|
|
196
305
|
host.destroy = destroy;
|
|
197
306
|
host.reportTTSError = reportTTSError;
|
|
@@ -213,20 +322,29 @@
|
|
|
213
322
|
{
|
|
214
323
|
host,
|
|
215
324
|
button: sttActivationButton,
|
|
216
|
-
onChange: renderSTTActivationState
|
|
325
|
+
onChange: renderSTTActivationState,
|
|
326
|
+
eventSource: events
|
|
217
327
|
}
|
|
218
328
|
);
|
|
219
329
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
recordButton.addEventListener('
|
|
224
|
-
recordButton.addEventListener('
|
|
225
|
-
recordButton.addEventListener('
|
|
226
|
-
recordButton.addEventListener('
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
330
|
+
const lifecycleListenerOptions = Object.freeze(
|
|
331
|
+
{ signal: runtimeStateAbortController.signal }
|
|
332
|
+
);
|
|
333
|
+
recordButton.addEventListener('pointerdown', handleRecordPointerDown, lifecycleListenerOptions);
|
|
334
|
+
recordButton.addEventListener('pointerup', handleRecordPointerUp, lifecycleListenerOptions);
|
|
335
|
+
recordButton.addEventListener('pointercancel', handleRecordPointerCancel, lifecycleListenerOptions);
|
|
336
|
+
recordButton.addEventListener('lostpointercapture', handleLostPointerCapture, lifecycleListenerOptions);
|
|
337
|
+
recordButton.addEventListener('keydown', handleRecordKeyDown, lifecycleListenerOptions);
|
|
338
|
+
recordButton.addEventListener('keyup', handleRecordKeyUp, lifecycleListenerOptions);
|
|
339
|
+
recordButton.addEventListener('blur', handleRecordBlur, lifecycleListenerOptions);
|
|
340
|
+
stopButton.addEventListener('click', stop, lifecycleListenerOptions);
|
|
341
|
+
muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
|
|
342
|
+
endButton.addEventListener('click', submitCompletionCommand, lifecycleListenerOptions);
|
|
343
|
+
window.addEventListener(
|
|
344
|
+
'pagehide',
|
|
345
|
+
destroy,
|
|
346
|
+
{ once: true, signal: runtimeStateAbortController.signal }
|
|
347
|
+
);
|
|
230
348
|
|
|
231
349
|
subscribeAIRuntimeState(
|
|
232
350
|
synchronizeAIRuntimeState,
|
|
@@ -234,27 +352,36 @@
|
|
|
234
352
|
signal: runtimeStateAbortController.signal
|
|
235
353
|
}
|
|
236
354
|
);
|
|
355
|
+
if (!host.initialMuted && ttsRole.state === 'ready') {
|
|
356
|
+
host.muted = false;
|
|
357
|
+
}
|
|
237
358
|
synchronizeAIMutedState();
|
|
238
359
|
renderControls();
|
|
239
360
|
renderStatus();
|
|
240
361
|
void observeMicrophonePermission();
|
|
241
362
|
void aiModuleImport.then(
|
|
242
|
-
|
|
363
|
+
function synchronizeMutedStateAfterAIImport() {
|
|
364
|
+
if (!destroyed) {
|
|
365
|
+
synchronizeAIMutedState();
|
|
366
|
+
}
|
|
367
|
+
},
|
|
243
368
|
function reportAIModuleImportFailure(error) {
|
|
369
|
+
if (destroyed) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
244
372
|
console.error('The shared AI runtime could not load.');
|
|
245
373
|
}
|
|
246
374
|
);
|
|
247
375
|
|
|
248
376
|
host.componentReady = true;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
377
|
+
dispatchSpeechEvent(
|
|
378
|
+
'speech-ready',
|
|
379
|
+
{ speech: host },
|
|
380
|
+
{
|
|
381
|
+
bubbles: true,
|
|
382
|
+
composed: true,
|
|
383
|
+
publicDetail: { ready: true }
|
|
384
|
+
}
|
|
258
385
|
);
|
|
259
386
|
|
|
260
387
|
function unavailableRole(role) {
|
|
@@ -282,6 +409,9 @@
|
|
|
282
409
|
}
|
|
283
410
|
|
|
284
411
|
function configure(options = {}) {
|
|
412
|
+
if (destroyed) {
|
|
413
|
+
return host;
|
|
414
|
+
}
|
|
285
415
|
if (Object.prototype.hasOwnProperty.call(options, 'initialMuted')) {
|
|
286
416
|
const configuredMuted = typeof options.initialMuted === 'boolean'
|
|
287
417
|
? options.initialMuted
|
|
@@ -302,6 +432,9 @@
|
|
|
302
432
|
}
|
|
303
433
|
|
|
304
434
|
function setAvailability(input = {}) {
|
|
435
|
+
if (destroyed) {
|
|
436
|
+
return { ...host.availability };
|
|
437
|
+
}
|
|
305
438
|
// Compatibility-only explicit adapter report. Canonical consumers use
|
|
306
439
|
// the sticky AIRuntimeState role records instead of this wrapper.
|
|
307
440
|
const previousSTTRole = sttRole;
|
|
@@ -330,6 +463,9 @@
|
|
|
330
463
|
}
|
|
331
464
|
|
|
332
465
|
function setMuted(muted) {
|
|
466
|
+
if (destroyed) {
|
|
467
|
+
return host;
|
|
468
|
+
}
|
|
333
469
|
if (typeof muted !== 'boolean') {
|
|
334
470
|
throw new TypeError('muted must be boolean');
|
|
335
471
|
}
|
|
@@ -343,11 +479,11 @@
|
|
|
343
479
|
}
|
|
344
480
|
|
|
345
481
|
function applyConfiguredMutedState(muted) {
|
|
346
|
-
host.muted = muted;
|
|
347
|
-
pendingTTSIntent = null;
|
|
348
482
|
if (muted) {
|
|
349
|
-
|
|
483
|
+
requestUserMute();
|
|
350
484
|
} else {
|
|
485
|
+
pendingUnmute = false;
|
|
486
|
+
host.muted = ttsRole.state !== 'ready';
|
|
351
487
|
synchronizeAIMutedState();
|
|
352
488
|
}
|
|
353
489
|
renderControls();
|
|
@@ -377,14 +513,18 @@
|
|
|
377
513
|
cancelSTTOperation('runtime-unready');
|
|
378
514
|
}
|
|
379
515
|
if (ttsBecameUnready) {
|
|
516
|
+
pendingUnmute = false;
|
|
380
517
|
host.muted = true;
|
|
381
518
|
stopTTSPlayback();
|
|
382
519
|
}
|
|
383
|
-
if (ttsRole.state === 'error' && !host.muted) {
|
|
520
|
+
if (ttsRole.state === 'error' && (pendingUnmute || !host.muted)) {
|
|
521
|
+
pendingUnmute = false;
|
|
384
522
|
host.muted = true;
|
|
385
523
|
stopTTSPlayback();
|
|
386
524
|
}
|
|
387
525
|
|
|
526
|
+
settleSuccessfulUnmute();
|
|
527
|
+
|
|
388
528
|
synchronizeAIMutedState();
|
|
389
529
|
renderControls();
|
|
390
530
|
renderStatus();
|
|
@@ -415,7 +555,9 @@
|
|
|
415
555
|
|
|
416
556
|
function renderControls() {
|
|
417
557
|
const transcriptionReady = sttRole.state === 'ready';
|
|
418
|
-
const microphoneReady = host.availability.microphone
|
|
558
|
+
const microphoneReady = host.availability.microphone
|
|
559
|
+
&& typeof globalThis.MediaRecorder === 'function'
|
|
560
|
+
&& typeof navigator.mediaDevices?.getUserMedia === 'function';
|
|
419
561
|
const activationVisible = sttActivationController.visible;
|
|
420
562
|
recordButton.hidden = activationVisible;
|
|
421
563
|
recordButton.disabled = !transcriptionReady
|
|
@@ -423,7 +565,10 @@
|
|
|
423
565
|
|| sttRole.busy
|
|
424
566
|
|| transcriptionActive
|
|
425
567
|
|| destroyed;
|
|
426
|
-
stopButton.disabled =
|
|
568
|
+
stopButton.disabled = destroyed
|
|
569
|
+
|| !transcriptionReady
|
|
570
|
+
|| sttRole.busy
|
|
571
|
+
|| captureSession?.recorder.state !== 'recording';
|
|
427
572
|
recordButton.title = transcriptionButtonTitle();
|
|
428
573
|
recordButton.setAttribute('aria-label', recordButton.title);
|
|
429
574
|
recordButton.setAttribute(
|
|
@@ -618,13 +763,18 @@
|
|
|
618
763
|
}
|
|
619
764
|
|
|
620
765
|
try {
|
|
621
|
-
|
|
766
|
+
const permissionStatus = await navigator.permissions.query(
|
|
622
767
|
{ name: 'microphone' }
|
|
623
768
|
);
|
|
769
|
+
if (destroyed) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
microphonePermissionStatus = permissionStatus;
|
|
624
773
|
microphonePermissionListener = synchronizeMicrophonePermission;
|
|
625
774
|
microphonePermissionStatus.addEventListener?.(
|
|
626
775
|
'change',
|
|
627
|
-
microphonePermissionListener
|
|
776
|
+
microphonePermissionListener,
|
|
777
|
+
{ signal: runtimeStateAbortController.signal }
|
|
628
778
|
);
|
|
629
779
|
synchronizeMicrophonePermission();
|
|
630
780
|
} catch (error) {
|
|
@@ -634,6 +784,9 @@
|
|
|
634
784
|
}
|
|
635
785
|
|
|
636
786
|
function synchronizeMicrophonePermission() {
|
|
787
|
+
if (destroyed) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
637
790
|
setAvailability(
|
|
638
791
|
{
|
|
639
792
|
microphone: microphonePermissionStatus?.state !== 'denied'
|
|
@@ -739,27 +892,49 @@
|
|
|
739
892
|
}
|
|
740
893
|
|
|
741
894
|
if (host.muted) {
|
|
742
|
-
requestUserUnmute();
|
|
895
|
+
void requestUserUnmute();
|
|
743
896
|
} else {
|
|
744
897
|
requestUserMute();
|
|
745
898
|
}
|
|
746
899
|
}
|
|
747
900
|
|
|
748
901
|
function requestUserMute() {
|
|
902
|
+
if (destroyed) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
if (host.muted && activeTTSIntent?.action === 'unload') {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
749
908
|
const wasMuted = host.muted;
|
|
909
|
+
pendingUnmute = false;
|
|
750
910
|
const shouldUnload = !wasMuted
|
|
751
911
|
|| pendingTTSIntent === 'load'
|
|
752
912
|
|| ['loading', 'ready', 'error'].includes(ttsRole.state);
|
|
753
913
|
host.muted = true;
|
|
754
914
|
stopTTSPlayback();
|
|
755
915
|
if (shouldUnload) {
|
|
756
|
-
requestTTSIntent('unload', 'user');
|
|
916
|
+
void requestTTSIntent('unload', 'user');
|
|
757
917
|
}
|
|
758
918
|
renderControls();
|
|
759
919
|
renderStatus();
|
|
760
920
|
}
|
|
761
921
|
|
|
762
|
-
function requestUserUnmute() {
|
|
922
|
+
async function requestUserUnmute() {
|
|
923
|
+
if (destroyed) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
if (!host.muted && ttsRole.state === 'ready') {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
if (activeTTSIntent?.action === 'load') {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (
|
|
933
|
+
pendingUnmute
|
|
934
|
+
&& (pendingTTSIntent === 'load' || ttsRole.state === 'loading')
|
|
935
|
+
) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
763
938
|
const providerSelected = typeof ttsRole.providerId === 'string'
|
|
764
939
|
&& ttsRole.providerId.length > 0
|
|
765
940
|
&& typeof ttsRole.modelId === 'string'
|
|
@@ -779,60 +954,93 @@
|
|
|
779
954
|
return false;
|
|
780
955
|
}
|
|
781
956
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
957
|
+
pendingUnmute = true;
|
|
958
|
+
const requested = await requestTTSIntent('load', 'user');
|
|
959
|
+
if (!requested || destroyed) {
|
|
960
|
+
pendingUnmute = false;
|
|
961
|
+
host.muted = true;
|
|
962
|
+
stopTTSPlayback();
|
|
963
|
+
renderControls();
|
|
964
|
+
renderStatus();
|
|
965
|
+
return false;
|
|
787
966
|
}
|
|
967
|
+
settleSuccessfulUnmute();
|
|
788
968
|
renderControls();
|
|
789
969
|
renderStatus();
|
|
970
|
+
return host.muted === false;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function settleSuccessfulUnmute() {
|
|
974
|
+
if (!pendingUnmute || ttsRole.state !== 'ready' || destroyed) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
pendingUnmute = false;
|
|
978
|
+
pendingTTSIntent = null;
|
|
979
|
+
host.muted = false;
|
|
980
|
+
synchronizeAIMutedState();
|
|
981
|
+
resumeTTSPlayback();
|
|
790
982
|
return true;
|
|
791
983
|
}
|
|
792
984
|
|
|
793
|
-
function requestTTSIntent(action, reason) {
|
|
985
|
+
async function requestTTSIntent(action, reason) {
|
|
986
|
+
if (destroyed) {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
if (activeTTSIntent?.action === action) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
794
992
|
const generation = ++ttsIntentGeneration;
|
|
993
|
+
const operationId = nextSpeechOperationId(`tts-${action}`);
|
|
994
|
+
const intent = Object.freeze({ action, generation, operationId });
|
|
995
|
+
activeTTSIntent = intent;
|
|
996
|
+
ttsOperationId = operationId;
|
|
795
997
|
pendingTTSIntent = action === 'load' && ttsRole.state === 'ready'
|
|
796
998
|
? null
|
|
797
999
|
: action;
|
|
798
|
-
let lifecycle
|
|
1000
|
+
let lifecycle;
|
|
799
1001
|
try {
|
|
800
1002
|
if (typeof globalThis.ai?.setSpeechMuted === 'function') {
|
|
801
1003
|
lifecycle = globalThis.ai.setSpeechMuted(action === 'unload');
|
|
1004
|
+
} else {
|
|
1005
|
+
lifecycle = requestAIRuntimeIntent(
|
|
1006
|
+
{
|
|
1007
|
+
role: 'tts',
|
|
1008
|
+
action,
|
|
1009
|
+
reason
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
802
1012
|
}
|
|
803
|
-
|
|
804
|
-
{
|
|
805
|
-
role: 'tts',
|
|
806
|
-
action,
|
|
807
|
-
reason
|
|
808
|
-
}
|
|
809
|
-
);
|
|
1013
|
+
await lifecycle;
|
|
810
1014
|
} catch (error) {
|
|
1015
|
+
if (destroyed || generation !== ttsIntentGeneration) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
811
1018
|
pendingTTSIntent = null;
|
|
812
1019
|
if (action === 'load') {
|
|
1020
|
+
pendingUnmute = false;
|
|
813
1021
|
host.muted = true;
|
|
814
1022
|
stopTTSPlayback();
|
|
815
1023
|
}
|
|
816
|
-
reportTTSLifecycleError(error, action);
|
|
1024
|
+
reportTTSLifecycleError(error, action, operationId);
|
|
817
1025
|
return false;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (destroyed || generation !== ttsIntentGeneration) {
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
pendingTTSIntent = null;
|
|
825
|
-
if (action === 'load') {
|
|
826
|
-
host.muted = true;
|
|
827
|
-
stopTTSPlayback();
|
|
828
|
-
}
|
|
829
|
-
reportTTSLifecycleError(error, action);
|
|
1026
|
+
} finally {
|
|
1027
|
+
if (activeTTSIntent === intent) {
|
|
1028
|
+
activeTTSIntent = null;
|
|
830
1029
|
}
|
|
831
|
-
|
|
1030
|
+
}
|
|
1031
|
+
if (destroyed || generation !== ttsIntentGeneration) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (action === 'load') {
|
|
1035
|
+
settleSuccessfulUnmute();
|
|
1036
|
+
}
|
|
832
1037
|
return true;
|
|
833
1038
|
}
|
|
834
1039
|
|
|
835
|
-
function reportTTSLifecycleError(error, action) {
|
|
1040
|
+
function reportTTSLifecycleError(error, action, operationId = null) {
|
|
1041
|
+
if (destroyed) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
836
1044
|
const message = visibleErrorMessage(
|
|
837
1045
|
error,
|
|
838
1046
|
`The voice ${action} request failed.`
|
|
@@ -841,47 +1049,80 @@
|
|
|
841
1049
|
message,
|
|
842
1050
|
tone: 'error'
|
|
843
1051
|
};
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1052
|
+
const errorFields = publicSpeechErrorFields(
|
|
1053
|
+
error,
|
|
1054
|
+
`ARCANE_SPEECH_TTS_${action.toUpperCase()}_REJECTED`
|
|
1055
|
+
);
|
|
1056
|
+
const reason = `tts-${action}-rejected`;
|
|
1057
|
+
dispatchSpeechEvent(
|
|
1058
|
+
'speech-tts-lifecycle-error',
|
|
1059
|
+
{
|
|
1060
|
+
action,
|
|
1061
|
+
...errorFields,
|
|
1062
|
+
error,
|
|
1063
|
+
message,
|
|
1064
|
+
reason
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
bubbles: true,
|
|
1068
|
+
composed: true,
|
|
1069
|
+
operationId: operationId || ttsOperationId
|
|
1070
|
+
|| nextSpeechOperationId(`tts-${action}`),
|
|
1071
|
+
publicDetail: {
|
|
1072
|
+
action,
|
|
1073
|
+
...errorFields,
|
|
1074
|
+
reason
|
|
855
1075
|
}
|
|
856
|
-
|
|
1076
|
+
}
|
|
857
1077
|
);
|
|
858
1078
|
renderControls();
|
|
859
1079
|
renderStatus();
|
|
1080
|
+
return false;
|
|
860
1081
|
}
|
|
861
1082
|
|
|
862
|
-
function reportTTSError(error) {
|
|
1083
|
+
function reportTTSError(error, boundary = 'synthesis') {
|
|
1084
|
+
if (destroyed) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
const playbackRejected = boundary === 'playback';
|
|
863
1088
|
const message = visibleErrorMessage(
|
|
864
1089
|
error,
|
|
865
|
-
|
|
1090
|
+
playbackRejected
|
|
1091
|
+
? 'Speech playback failed.'
|
|
1092
|
+
: 'Speech synthesis failed.'
|
|
866
1093
|
);
|
|
867
1094
|
host.muted = true;
|
|
868
1095
|
stopTTSPlayback();
|
|
869
1096
|
localStatus = {
|
|
870
|
-
message:
|
|
1097
|
+
message: `${playbackRejected ? 'Speech playback' : 'Speech synthesis'} failed: ${message}`,
|
|
871
1098
|
tone: 'error'
|
|
872
1099
|
};
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1100
|
+
const errorFields = publicSpeechErrorFields(
|
|
1101
|
+
error,
|
|
1102
|
+
playbackRejected
|
|
1103
|
+
? 'ARCANE_SPEECH_TTS_PLAYBACK_RESUME_REJECTED'
|
|
1104
|
+
: 'ARCANE_SPEECH_TTS_SYNTHESIS_REQUEST_REJECTED'
|
|
1105
|
+
);
|
|
1106
|
+
const reason = playbackRejected
|
|
1107
|
+
? 'tts-playback-resume-rejected'
|
|
1108
|
+
: 'tts-synthesis-rejected';
|
|
1109
|
+
dispatchSpeechEvent(
|
|
1110
|
+
'speech-synthesis-error',
|
|
1111
|
+
{
|
|
1112
|
+
...errorFields,
|
|
1113
|
+
error,
|
|
1114
|
+
message,
|
|
1115
|
+
reason
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
bubbles: true,
|
|
1119
|
+
composed: true,
|
|
1120
|
+
operationId: nextSpeechOperationId('synthesis'),
|
|
1121
|
+
publicDetail: {
|
|
1122
|
+
...errorFields,
|
|
1123
|
+
reason
|
|
883
1124
|
}
|
|
884
|
-
|
|
1125
|
+
}
|
|
885
1126
|
);
|
|
886
1127
|
renderControls();
|
|
887
1128
|
renderStatus();
|
|
@@ -915,20 +1156,26 @@
|
|
|
915
1156
|
|
|
916
1157
|
void Promise.resolve(aiRuntime.resumeAudio()).catch(
|
|
917
1158
|
function reportTTSResumeFailure(error) {
|
|
918
|
-
reportTTSError(error);
|
|
1159
|
+
reportTTSError(error, 'playback');
|
|
919
1160
|
}
|
|
920
1161
|
);
|
|
921
1162
|
}
|
|
922
1163
|
|
|
923
1164
|
async function transcribe(transcription = '') {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1165
|
+
const operationId = transcriptionOperationId
|
|
1166
|
+
|| nextSpeechOperationId('transcription');
|
|
1167
|
+
const accepted = dispatchSpeechEvent(
|
|
1168
|
+
'speech-transcription-complete',
|
|
1169
|
+
{ text: transcription },
|
|
1170
|
+
{
|
|
1171
|
+
operationId,
|
|
1172
|
+
publicDetail: {
|
|
1173
|
+
characterCount: transcription.length
|
|
929
1174
|
}
|
|
930
|
-
|
|
1175
|
+
}
|
|
931
1176
|
);
|
|
1177
|
+
transcriptionOperationId = null;
|
|
1178
|
+
return accepted;
|
|
932
1179
|
}
|
|
933
1180
|
|
|
934
1181
|
async function record() {
|
|
@@ -937,6 +1184,8 @@
|
|
|
937
1184
|
|| sttRole.state !== 'ready'
|
|
938
1185
|
|| sttRole.busy
|
|
939
1186
|
|| !host.availability.microphone
|
|
1187
|
+
|| typeof globalThis.MediaRecorder !== 'function'
|
|
1188
|
+
|| typeof navigator.mediaDevices?.getUserMedia !== 'function'
|
|
940
1189
|
|| recordingRequested
|
|
941
1190
|
|| captureSession
|
|
942
1191
|
|| transcriptionActive
|
|
@@ -946,6 +1195,7 @@
|
|
|
946
1195
|
}
|
|
947
1196
|
|
|
948
1197
|
const generation = ++recordingGeneration;
|
|
1198
|
+
transcriptionOperationId = nextSpeechOperationId('transcription');
|
|
949
1199
|
recordingRequested = true;
|
|
950
1200
|
localStatus = {
|
|
951
1201
|
message: 'Recording. Release to transcribe.',
|
|
@@ -969,16 +1219,23 @@
|
|
|
969
1219
|
message: 'Microphone access is unavailable. Text chat is still available.',
|
|
970
1220
|
tone: 'error'
|
|
971
1221
|
};
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1222
|
+
dispatchSpeechEvent(
|
|
1223
|
+
'speech-microphone-unavailable',
|
|
1224
|
+
{ reason: error.name },
|
|
1225
|
+
{
|
|
1226
|
+
bubbles: true,
|
|
1227
|
+
composed: true,
|
|
1228
|
+
operationId: transcriptionOperationId,
|
|
1229
|
+
publicDetail: {
|
|
1230
|
+
...publicSpeechErrorFields(
|
|
1231
|
+
error,
|
|
1232
|
+
'ARCANE_SPEECH_MICROPHONE_CAPTURE_REJECTED'
|
|
1233
|
+
),
|
|
1234
|
+
reason: microphoneFailureReason(error)
|
|
979
1235
|
}
|
|
980
|
-
|
|
1236
|
+
}
|
|
981
1237
|
);
|
|
1238
|
+
transcriptionOperationId = null;
|
|
982
1239
|
} else {
|
|
983
1240
|
reportTranscriptionError(error);
|
|
984
1241
|
}
|
|
@@ -991,6 +1248,7 @@
|
|
|
991
1248
|
!recordingRequested
|
|
992
1249
|
|| generation !== recordingGeneration
|
|
993
1250
|
|| sttRole.state !== 'ready'
|
|
1251
|
+
|| sttRole.busy
|
|
994
1252
|
|| destroyed
|
|
995
1253
|
) {
|
|
996
1254
|
stopMediaStream(stream);
|
|
@@ -1097,6 +1355,7 @@
|
|
|
1097
1355
|
session.cancelled
|
|
1098
1356
|
|| session.generation !== recordingGeneration
|
|
1099
1357
|
|| sttRole.state !== 'ready'
|
|
1358
|
+
|| sttRole.busy
|
|
1100
1359
|
|| destroyed
|
|
1101
1360
|
) {
|
|
1102
1361
|
renderControls();
|
|
@@ -1118,6 +1377,8 @@
|
|
|
1118
1377
|
|
|
1119
1378
|
async function transcribeAudio(audioFile) {
|
|
1120
1379
|
const generation = ++transcriptionGeneration;
|
|
1380
|
+
transcriptionOperationId = transcriptionOperationId
|
|
1381
|
+
|| nextSpeechOperationId('transcription');
|
|
1121
1382
|
const controller = new AbortController();
|
|
1122
1383
|
transcriptionAbortController?.abort();
|
|
1123
1384
|
transcriptionAbortController = controller;
|
|
@@ -1131,8 +1392,19 @@
|
|
|
1131
1392
|
|
|
1132
1393
|
try {
|
|
1133
1394
|
const aiRuntime = globalThis.ai;
|
|
1134
|
-
if (sttRole.state !== 'ready' ||
|
|
1135
|
-
|
|
1395
|
+
if (sttRole.state !== 'ready' || sttRole.busy) {
|
|
1396
|
+
reportTranscriptionCancellation(
|
|
1397
|
+
sttRole.busy ? 'stt-role-busy' : 'runtime-unready',
|
|
1398
|
+
sttRole.busy
|
|
1399
|
+
? 'Transcription stopped because the selected service is busy.'
|
|
1400
|
+
: 'Transcription stopped because the selected service became unavailable.'
|
|
1401
|
+
);
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
if (typeof aiRuntime?.fetchSTT !== 'function') {
|
|
1405
|
+
const error = new Error('The selected transcription service is unavailable.');
|
|
1406
|
+
error.code = 'ARCANE_SPEECH_STT_RUNTIME_METHOD_UNAVAILABLE';
|
|
1407
|
+
throw error;
|
|
1136
1408
|
}
|
|
1137
1409
|
|
|
1138
1410
|
const transcription = await aiRuntime.fetchSTT(
|
|
@@ -1156,7 +1428,14 @@
|
|
|
1156
1428
|
return true;
|
|
1157
1429
|
} catch (error) {
|
|
1158
1430
|
if (generation === transcriptionGeneration && !destroyed) {
|
|
1159
|
-
|
|
1431
|
+
if (isTranscriptionCancellation(error, controller)) {
|
|
1432
|
+
reportTranscriptionCancellation(
|
|
1433
|
+
'stt-provider-request-cancelled',
|
|
1434
|
+
'The selected transcription request was cancelled.'
|
|
1435
|
+
);
|
|
1436
|
+
} else {
|
|
1437
|
+
reportTranscriptionError(error);
|
|
1438
|
+
}
|
|
1160
1439
|
}
|
|
1161
1440
|
return false;
|
|
1162
1441
|
} finally {
|
|
@@ -1180,23 +1459,72 @@
|
|
|
1180
1459
|
message: `Transcription failed: ${message}`,
|
|
1181
1460
|
tone: 'error'
|
|
1182
1461
|
};
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1462
|
+
const operationId = transcriptionOperationId
|
|
1463
|
+
|| nextSpeechOperationId('transcription');
|
|
1464
|
+
const errorFields = publicSpeechErrorFields(
|
|
1465
|
+
error,
|
|
1466
|
+
'ARCANE_SPEECH_STT_TRANSCRIPTION_REQUEST_REJECTED'
|
|
1467
|
+
);
|
|
1468
|
+
const reason = 'stt-transcription-rejected';
|
|
1469
|
+
dispatchSpeechEvent(
|
|
1470
|
+
'speech-transcription-error',
|
|
1471
|
+
{
|
|
1472
|
+
...errorFields,
|
|
1473
|
+
error,
|
|
1474
|
+
message,
|
|
1475
|
+
reason
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
bubbles: true,
|
|
1479
|
+
composed: true,
|
|
1480
|
+
operationId,
|
|
1481
|
+
publicDetail: {
|
|
1482
|
+
...errorFields,
|
|
1483
|
+
reason
|
|
1193
1484
|
}
|
|
1194
|
-
|
|
1485
|
+
}
|
|
1195
1486
|
);
|
|
1487
|
+
transcriptionOperationId = null;
|
|
1196
1488
|
renderControls();
|
|
1197
1489
|
renderStatus();
|
|
1198
1490
|
}
|
|
1199
1491
|
|
|
1492
|
+
function isTranscriptionCancellation(error, controller) {
|
|
1493
|
+
return controller.signal.aborted
|
|
1494
|
+
|| error?.name === 'AbortError'
|
|
1495
|
+
|| [
|
|
1496
|
+
'ARCANE_AI_REQUEST_ABORTED',
|
|
1497
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
1498
|
+
].includes(error?.code);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function reportTranscriptionCancellation(reason, message) {
|
|
1502
|
+
const operationId = transcriptionOperationId
|
|
1503
|
+
|| nextSpeechOperationId('transcription');
|
|
1504
|
+
localStatus = {
|
|
1505
|
+
message,
|
|
1506
|
+
tone: ''
|
|
1507
|
+
};
|
|
1508
|
+
const accepted = dispatchSpeechEvent(
|
|
1509
|
+
'speech-transcription-cancelled',
|
|
1510
|
+
{ reason },
|
|
1511
|
+
{
|
|
1512
|
+
bubbles: true,
|
|
1513
|
+
composed: true,
|
|
1514
|
+
operationId,
|
|
1515
|
+
publicDetail: {
|
|
1516
|
+
reason: canonicalSTTCancellationReason(reason)
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
transcriptionOperationId = null;
|
|
1521
|
+
if (!destroyed) {
|
|
1522
|
+
renderControls();
|
|
1523
|
+
renderStatus();
|
|
1524
|
+
}
|
|
1525
|
+
return accepted;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1200
1528
|
function cancelSTTOperation(reason) {
|
|
1201
1529
|
const hadActiveOperation = recordingRequested
|
|
1202
1530
|
|| Boolean(captureSession)
|
|
@@ -1221,21 +1549,14 @@
|
|
|
1221
1549
|
}
|
|
1222
1550
|
|
|
1223
1551
|
if (hadActiveOperation) {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
new CustomEvent(
|
|
1230
|
-
'speech-transcription-cancelled',
|
|
1231
|
-
{
|
|
1232
|
-
bubbles: true,
|
|
1233
|
-
composed: true,
|
|
1234
|
-
detail: { reason }
|
|
1235
|
-
}
|
|
1236
|
-
)
|
|
1552
|
+
reportTranscriptionCancellation(
|
|
1553
|
+
String(reason || 'stt-transcription-cancelled'),
|
|
1554
|
+
reason === 'component-destroyed'
|
|
1555
|
+
? 'Transcription stopped because the speech component was destroyed.'
|
|
1556
|
+
: 'Transcription stopped because the selected service became unavailable.'
|
|
1237
1557
|
);
|
|
1238
1558
|
}
|
|
1559
|
+
transcriptionOperationId = null;
|
|
1239
1560
|
}
|
|
1240
1561
|
|
|
1241
1562
|
function releaseCaptureSession(session) {
|
|
@@ -1286,6 +1607,7 @@
|
|
|
1286
1607
|
);
|
|
1287
1608
|
cancelSTTOperation('component-destroyed');
|
|
1288
1609
|
stopTTSPlayback();
|
|
1610
|
+
events.dispose();
|
|
1289
1611
|
host.componentReady = false;
|
|
1290
1612
|
renderControls();
|
|
1291
1613
|
renderStatus();
|