arcane-os 0.2.0 → 0.2.2
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 +35 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +331 -25
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +79 -13
- package/docs/reference/ai/browser-speech.md +336 -0
- package/docs/reference/ai/browser-wasm.md +207 -82
- package/docs/reference/availability-and-normalization.md +30 -4
- package/docs/reference/behavioral-testing.md +4 -1
- package/docs/reference/cli.md +29 -10
- package/docs/reference/core/arcane-ai-contracts.md +43 -9
- package/docs/reference/inventory/package-api.json +110 -14
- package/docs/reference/inventory/runtime-components.json +19 -6
- package/docs/reference/inventory/runtime-modules.json +113 -9
- package/docs/reference/protocols.md +260 -38
- package/docs/reference/runtime-components.md +108 -15
- package/docs/reference/runtime-modules.md +422 -8
- package/docs/reference/sdk-api.md +626 -85
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +19 -19
- package/runtime/arcane/components/chat.html +288 -52
- package/runtime/arcane/components/speech.html +339 -15
- package/runtime/arcane/modules/AI.js +1245 -136
- package/runtime/arcane/modules/AIProviderRuntime.js +299 -30
- package/runtime/arcane/modules/AIRuntimeState.js +23 -4
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/runtime/arcane/modules/LocalAIReadinessController.js +1 -1
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
text-align: center;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
#sttActivationButton {
|
|
67
|
+
grid-column: 1;
|
|
68
|
+
max-width: 8rem;
|
|
69
|
+
background: var(--arcane-action, var(--primary-color));
|
|
70
|
+
color: var(--arcane-action-text, var(--button-text-color, var(--text-color)));
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
#recordButton:active,
|
|
67
74
|
#recordButton.pressed {
|
|
68
75
|
opacity: .82;
|
|
@@ -108,7 +115,8 @@
|
|
|
108
115
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
#recordButton
|
|
118
|
+
#recordButton,
|
|
119
|
+
#sttActivationButton {
|
|
112
120
|
grid-row: 1 / span 2;
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -131,6 +139,7 @@
|
|
|
131
139
|
|
|
132
140
|
<p class="hidden">Will auto send after X seconds of silence set in code.</p>
|
|
133
141
|
<button id="recordButton" type="button" aria-label="Transcription unavailable" disabled>Hold to talk</button>
|
|
142
|
+
<button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
|
|
134
143
|
<button id="stopButton" type="button" class="hidden" disabled>Send</button>
|
|
135
144
|
<p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
|
|
136
145
|
<button id="endButton" type="button" class="end">End</button>
|
|
@@ -151,6 +160,7 @@
|
|
|
151
160
|
const host = this;
|
|
152
161
|
const shadowRoot = host.shadowRoot;
|
|
153
162
|
const recordButton = shadowRoot.querySelector('#recordButton');
|
|
163
|
+
const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
|
|
154
164
|
const stopButton = shadowRoot.querySelector('#stopButton');
|
|
155
165
|
const endButton = shadowRoot.querySelector('#endButton');
|
|
156
166
|
const muteButton = shadowRoot.querySelector('#muteButton');
|
|
@@ -167,11 +177,13 @@
|
|
|
167
177
|
let recordingGeneration = 0;
|
|
168
178
|
let transcriptionGeneration = 0;
|
|
169
179
|
let transcriptionActive = false;
|
|
180
|
+
let transcriptionAbortController = null;
|
|
170
181
|
let silenceTimer = null;
|
|
171
182
|
let activePointerId = null;
|
|
172
183
|
let keyboardRecording = false;
|
|
173
184
|
let localStatus = null;
|
|
174
185
|
let pendingTTSIntent = null;
|
|
186
|
+
let ttsIntentGeneration = 0;
|
|
175
187
|
let destroyed = false;
|
|
176
188
|
|
|
177
189
|
const initialMutedWasHydrated = typeof host.initialMuted === 'boolean';
|
|
@@ -180,6 +192,10 @@
|
|
|
180
192
|
host.configure = configure;
|
|
181
193
|
host.destroy = destroy;
|
|
182
194
|
host.reportTTSError = reportTTSError;
|
|
195
|
+
host.requestSTTActivation = host.requestSTTActivation
|
|
196
|
+
|| function requestSTTActivation(intent) {
|
|
197
|
+
return requestAIRuntimeIntent(intent);
|
|
198
|
+
};
|
|
183
199
|
host.setAvailability = setAvailability;
|
|
184
200
|
host.setMuted = setMuted;
|
|
185
201
|
host.availability = {
|
|
@@ -190,6 +206,13 @@
|
|
|
190
206
|
stt: false,
|
|
191
207
|
tts: false
|
|
192
208
|
};
|
|
209
|
+
const sttActivationController = createSTTActivationController(
|
|
210
|
+
{
|
|
211
|
+
host,
|
|
212
|
+
button: sttActivationButton,
|
|
213
|
+
onChange: renderSTTActivationState
|
|
214
|
+
}
|
|
215
|
+
);
|
|
193
216
|
|
|
194
217
|
recordButton.addEventListener('pointerdown', handleRecordPointerDown);
|
|
195
218
|
recordButton.addEventListener('pointerup', handleRecordPointerUp);
|
|
@@ -231,6 +254,191 @@
|
|
|
231
254
|
)
|
|
232
255
|
);
|
|
233
256
|
|
|
257
|
+
function createSTTActivationController({
|
|
258
|
+
host,
|
|
259
|
+
button,
|
|
260
|
+
onChange,
|
|
261
|
+
EventClass = CustomEvent
|
|
262
|
+
}) {
|
|
263
|
+
let role = null;
|
|
264
|
+
let requestPending = false;
|
|
265
|
+
let requestError = '';
|
|
266
|
+
let requestGeneration = 0;
|
|
267
|
+
let destroyed = false;
|
|
268
|
+
|
|
269
|
+
function visibleError(error, fallback) {
|
|
270
|
+
const message = typeof error?.message === 'string'
|
|
271
|
+
? error.message.trim()
|
|
272
|
+
: '';
|
|
273
|
+
return (message || fallback).slice(0, 240);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function cancellation(error) {
|
|
277
|
+
return error?.name === 'AbortError'
|
|
278
|
+
|| [
|
|
279
|
+
'ARCANE_AI_REQUEST_ABORTED',
|
|
280
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
281
|
+
]
|
|
282
|
+
.includes(error?.code);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function selected() {
|
|
286
|
+
return typeof role?.providerId === 'string'
|
|
287
|
+
&& role.providerId.length > 0
|
|
288
|
+
&& typeof role?.modelId === 'string'
|
|
289
|
+
&& role.modelId.length > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function action() {
|
|
293
|
+
if (!selected()) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (role.state === 'loading') {
|
|
297
|
+
return 'unload';
|
|
298
|
+
}
|
|
299
|
+
if (!role.busy && ['unloaded', 'error'].includes(role.state)) {
|
|
300
|
+
return 'load';
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function request(nextAction) {
|
|
306
|
+
if (destroyed || requestPending || action() !== nextAction) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
const generation = ++requestGeneration;
|
|
310
|
+
requestError = '';
|
|
311
|
+
const intent = Object.freeze(
|
|
312
|
+
{
|
|
313
|
+
role: 'stt',
|
|
314
|
+
action: nextAction,
|
|
315
|
+
reason: 'user'
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
const activationRequest = Object.freeze({intent, state: role});
|
|
319
|
+
const activationEvent = new EventClass(
|
|
320
|
+
'speech-stt-activation-request',
|
|
321
|
+
{
|
|
322
|
+
bubbles: true,
|
|
323
|
+
composed: true,
|
|
324
|
+
cancelable: true,
|
|
325
|
+
detail: activationRequest
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
requestPending = true;
|
|
329
|
+
const accepted = host.dispatchEvent(activationEvent);
|
|
330
|
+
if (!accepted
|
|
331
|
+
|| destroyed
|
|
332
|
+
|| generation !== requestGeneration
|
|
333
|
+
|| action() !== nextAction) {
|
|
334
|
+
if (!destroyed && generation === requestGeneration) {
|
|
335
|
+
requestPending = false;
|
|
336
|
+
onChange();
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
onChange();
|
|
341
|
+
try {
|
|
342
|
+
await host.requestSTTActivation(intent);
|
|
343
|
+
if (destroyed || generation !== requestGeneration) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (destroyed || generation !== requestGeneration) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
if (nextAction === 'load'
|
|
352
|
+
&& cancellation(error)
|
|
353
|
+
&& ['unloaded', 'unloading'].includes(role?.state)) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const message = visibleError(
|
|
357
|
+
error,
|
|
358
|
+
`The transcription ${nextAction} request failed.`
|
|
359
|
+
);
|
|
360
|
+
requestError = message;
|
|
361
|
+
host.dispatchEvent(
|
|
362
|
+
new EventClass(
|
|
363
|
+
'speech-stt-activation-error',
|
|
364
|
+
{
|
|
365
|
+
bubbles: true,
|
|
366
|
+
composed: true,
|
|
367
|
+
detail: Object.freeze(
|
|
368
|
+
{
|
|
369
|
+
request: activationRequest,
|
|
370
|
+
error,
|
|
371
|
+
message
|
|
372
|
+
}
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
return false;
|
|
378
|
+
} finally {
|
|
379
|
+
if (!destroyed && generation === requestGeneration) {
|
|
380
|
+
requestPending = false;
|
|
381
|
+
onChange();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function activateSelectedSTT() {
|
|
387
|
+
const nextAction = action();
|
|
388
|
+
if (nextAction) {
|
|
389
|
+
void request(nextAction);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function synchronize(nextRole) {
|
|
394
|
+
if (destroyed) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (role?.state !== nextRole.state
|
|
398
|
+
|| role?.providerId !== nextRole.providerId
|
|
399
|
+
|| role?.modelId !== nextRole.modelId
|
|
400
|
+
|| role?.loaded !== nextRole.loaded
|
|
401
|
+
|| role?.busy !== nextRole.busy
|
|
402
|
+
|| role?.operationId !== nextRole.operationId) {
|
|
403
|
+
requestGeneration += 1;
|
|
404
|
+
requestError = '';
|
|
405
|
+
requestPending = false;
|
|
406
|
+
}
|
|
407
|
+
role = nextRole;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function destroy() {
|
|
411
|
+
if (destroyed) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
destroyed = true;
|
|
415
|
+
requestGeneration += 1;
|
|
416
|
+
requestPending = false;
|
|
417
|
+
button.removeEventListener('click', activateSelectedSTT);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
button.addEventListener('click', activateSelectedSTT);
|
|
421
|
+
return Object.freeze(
|
|
422
|
+
{
|
|
423
|
+
get action() {
|
|
424
|
+
return action();
|
|
425
|
+
},
|
|
426
|
+
get error() {
|
|
427
|
+
return requestError;
|
|
428
|
+
},
|
|
429
|
+
get pending() {
|
|
430
|
+
return requestPending;
|
|
431
|
+
},
|
|
432
|
+
get selected() {
|
|
433
|
+
return selected();
|
|
434
|
+
},
|
|
435
|
+
request,
|
|
436
|
+
synchronize,
|
|
437
|
+
destroy
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
234
442
|
function unavailableRole(role) {
|
|
235
443
|
return Object.freeze(
|
|
236
444
|
{
|
|
@@ -248,14 +456,11 @@
|
|
|
248
456
|
);
|
|
249
457
|
}
|
|
250
458
|
|
|
251
|
-
function
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
loaded: ready
|
|
257
|
-
}
|
|
258
|
-
);
|
|
459
|
+
function selectedRole(role) {
|
|
460
|
+
return typeof role?.providerId === 'string'
|
|
461
|
+
&& role.providerId.length > 0
|
|
462
|
+
&& typeof role?.modelId === 'string'
|
|
463
|
+
&& role.modelId.length > 0;
|
|
259
464
|
}
|
|
260
465
|
|
|
261
466
|
function configure(options = {}) {
|
|
@@ -288,11 +493,15 @@
|
|
|
288
493
|
if (Object.prototype.hasOwnProperty.call(input, 'microphone')) {
|
|
289
494
|
host.availability.microphone = Boolean(input.microphone);
|
|
290
495
|
}
|
|
291
|
-
if (Object.prototype.hasOwnProperty.call(input, 'stt')
|
|
292
|
-
|
|
496
|
+
if (Object.prototype.hasOwnProperty.call(input, 'stt')
|
|
497
|
+
&& !Boolean(input.stt)
|
|
498
|
+
&& !selectedRole(sttRole)) {
|
|
499
|
+
sttRole = unavailableRole('stt');
|
|
293
500
|
}
|
|
294
|
-
if (Object.prototype.hasOwnProperty.call(input, 'tts')
|
|
295
|
-
|
|
501
|
+
if (Object.prototype.hasOwnProperty.call(input, 'tts')
|
|
502
|
+
&& !Boolean(input.tts)
|
|
503
|
+
&& !selectedRole(ttsRole)) {
|
|
504
|
+
ttsRole = unavailableRole('tts');
|
|
296
505
|
}
|
|
297
506
|
|
|
298
507
|
if (microphoneWasAvailable && !host.availability.microphone) {
|
|
@@ -337,6 +546,7 @@
|
|
|
337
546
|
}
|
|
338
547
|
|
|
339
548
|
function applyRoleTransitions(previousSTTRole, previousTTSRole) {
|
|
549
|
+
sttActivationController.synchronize(sttRole);
|
|
340
550
|
const sttBecameUnready = previousSTTRole.state === 'ready'
|
|
341
551
|
&& sttRole.state !== 'ready';
|
|
342
552
|
const ttsBecameUnready = previousTTSRole.state === 'ready'
|
|
@@ -362,6 +572,11 @@
|
|
|
362
572
|
renderStatus();
|
|
363
573
|
}
|
|
364
574
|
|
|
575
|
+
function renderSTTActivationState() {
|
|
576
|
+
renderControls();
|
|
577
|
+
renderStatus();
|
|
578
|
+
}
|
|
579
|
+
|
|
365
580
|
function clearSettledTTSIntent() {
|
|
366
581
|
if (!pendingTTSIntent) {
|
|
367
582
|
return;
|
|
@@ -383,6 +598,11 @@
|
|
|
383
598
|
function renderControls() {
|
|
384
599
|
const transcriptionReady = sttRole.state === 'ready';
|
|
385
600
|
const microphoneReady = host.availability.microphone;
|
|
601
|
+
const activationVisible = sttActivationController.selected
|
|
602
|
+
&& sttRole.state !== 'ready'
|
|
603
|
+
&& ['unloaded', 'loading', 'unloading', 'error']
|
|
604
|
+
.includes(sttRole.state);
|
|
605
|
+
recordButton.hidden = activationVisible;
|
|
386
606
|
recordButton.disabled = !transcriptionReady
|
|
387
607
|
|| !microphoneReady
|
|
388
608
|
|| sttRole.busy
|
|
@@ -395,9 +615,62 @@
|
|
|
395
615
|
'aria-pressed',
|
|
396
616
|
String(recordingRequested || Boolean(captureSession))
|
|
397
617
|
);
|
|
618
|
+
sttActivationButton.hidden = !activationVisible;
|
|
619
|
+
sttActivationButton.disabled = destroyed
|
|
620
|
+
|| sttActivationController.pending
|
|
621
|
+
|| sttRole.state === 'unloading'
|
|
622
|
+
|| sttActivationController.action === null;
|
|
623
|
+
sttActivationButton.textContent = transcriptionActivationLabel();
|
|
624
|
+
sttActivationButton.title = transcriptionActivationTitle();
|
|
625
|
+
sttActivationButton.setAttribute(
|
|
626
|
+
'aria-label',
|
|
627
|
+
sttActivationButton.title
|
|
628
|
+
);
|
|
629
|
+
sttActivationButton.setAttribute(
|
|
630
|
+
'aria-busy',
|
|
631
|
+
String(
|
|
632
|
+
sttActivationController.pending
|
|
633
|
+
|| ['loading', 'unloading'].includes(sttRole.state)
|
|
634
|
+
)
|
|
635
|
+
);
|
|
398
636
|
renderTTSControl();
|
|
399
637
|
}
|
|
400
638
|
|
|
639
|
+
function transcriptionActivationLabel() {
|
|
640
|
+
if (sttActivationController.pending) {
|
|
641
|
+
return 'Requesting…';
|
|
642
|
+
}
|
|
643
|
+
if (sttRole.state === 'loading') {
|
|
644
|
+
return 'Cancel loading';
|
|
645
|
+
}
|
|
646
|
+
if (sttRole.state === 'unloading') {
|
|
647
|
+
return 'Canceling…';
|
|
648
|
+
}
|
|
649
|
+
if (sttRole.state === 'error') {
|
|
650
|
+
return 'Try again';
|
|
651
|
+
}
|
|
652
|
+
return 'Start transcription';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function transcriptionActivationTitle() {
|
|
656
|
+
if (sttActivationController.pending) {
|
|
657
|
+
return 'Requesting transcription activation.';
|
|
658
|
+
}
|
|
659
|
+
if (sttRole.state === 'loading') {
|
|
660
|
+
return 'Cancel loading the selected transcription service.';
|
|
661
|
+
}
|
|
662
|
+
if (sttRole.state === 'unloading') {
|
|
663
|
+
return 'The selected transcription service is being released.';
|
|
664
|
+
}
|
|
665
|
+
if (sttRole.state === 'error') {
|
|
666
|
+
return sttActivationController.error || visibleErrorMessage(
|
|
667
|
+
sttRole.error,
|
|
668
|
+
'Retry loading the selected transcription service.'
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return 'Start the selected transcription service.';
|
|
672
|
+
}
|
|
673
|
+
|
|
401
674
|
function transcriptionButtonTitle() {
|
|
402
675
|
if (destroyed || sttRole.state === 'disposed') {
|
|
403
676
|
return 'Transcription is unavailable.';
|
|
@@ -492,15 +765,23 @@
|
|
|
492
765
|
'aria-busy',
|
|
493
766
|
String(
|
|
494
767
|
sttRole.state === 'loading'
|
|
768
|
+
|| sttRole.state === 'unloading'
|
|
495
769
|
|| ttsRole.state === 'loading'
|
|
496
770
|
|| sttRole.busy
|
|
497
771
|
|| ttsRole.busy
|
|
772
|
+
|| sttActivationController.pending
|
|
498
773
|
|| transcriptionActive
|
|
499
774
|
)
|
|
500
775
|
);
|
|
501
776
|
}
|
|
502
777
|
|
|
503
778
|
function describeSTTRole() {
|
|
779
|
+
if (sttActivationController.error) {
|
|
780
|
+
return `Transcription activation error: ${sttActivationController.error}.`;
|
|
781
|
+
}
|
|
782
|
+
if (sttActivationController.pending) {
|
|
783
|
+
return 'Transcription activation requested.';
|
|
784
|
+
}
|
|
504
785
|
if (sttRole.state === 'ready') {
|
|
505
786
|
if (!host.availability.microphone) {
|
|
506
787
|
return 'Microphone unavailable.';
|
|
@@ -508,7 +789,10 @@
|
|
|
508
789
|
return sttRole.busy ? 'Transcription busy.' : 'Transcription ready.';
|
|
509
790
|
}
|
|
510
791
|
if (sttRole.state === 'loading') {
|
|
511
|
-
return `Transcription ${formatRoleProgress(sttRole.progress, 'loading')}.`;
|
|
792
|
+
return `Transcription ${formatRoleProgress(sttRole.progress, 'loading')}; Cancel is available.`;
|
|
793
|
+
}
|
|
794
|
+
if (sttRole.state === 'unloading') {
|
|
795
|
+
return `Transcription ${formatRoleProgress(sttRole.progress, 'releasing')}.`;
|
|
512
796
|
}
|
|
513
797
|
if (sttRole.state === 'error') {
|
|
514
798
|
return `Transcription error: ${visibleErrorMessage(sttRole.error, 'Unknown error')}.`;
|
|
@@ -516,6 +800,9 @@
|
|
|
516
800
|
if (sttRole.state === 'disposed') {
|
|
517
801
|
return 'Transcription disposed.';
|
|
518
802
|
}
|
|
803
|
+
if (sttRole.state === 'unloaded' && sttActivationController.selected) {
|
|
804
|
+
return 'Transcription selected and waiting to load.';
|
|
805
|
+
}
|
|
519
806
|
return 'Transcription unavailable.';
|
|
520
807
|
}
|
|
521
808
|
|
|
@@ -748,10 +1035,15 @@
|
|
|
748
1035
|
}
|
|
749
1036
|
|
|
750
1037
|
function requestTTSIntent(action, reason) {
|
|
1038
|
+
const generation = ++ttsIntentGeneration;
|
|
751
1039
|
pendingTTSIntent = action === 'load' && ttsRole.state === 'ready'
|
|
752
1040
|
? null
|
|
753
1041
|
: action;
|
|
1042
|
+
let lifecycle = null;
|
|
754
1043
|
try {
|
|
1044
|
+
if (typeof globalThis.ai?.setSpeechMuted === 'function') {
|
|
1045
|
+
lifecycle = globalThis.ai.setSpeechMuted(action === 'unload');
|
|
1046
|
+
}
|
|
755
1047
|
requestAIRuntimeIntent(
|
|
756
1048
|
{
|
|
757
1049
|
role: 'tts',
|
|
@@ -761,8 +1053,27 @@
|
|
|
761
1053
|
);
|
|
762
1054
|
} catch (error) {
|
|
763
1055
|
pendingTTSIntent = null;
|
|
1056
|
+
if (action === 'load') {
|
|
1057
|
+
host.muted = true;
|
|
1058
|
+
stopTTSPlayback();
|
|
1059
|
+
}
|
|
764
1060
|
reportTTSLifecycleError(error, action);
|
|
1061
|
+
return false;
|
|
765
1062
|
}
|
|
1063
|
+
void Promise.resolve(lifecycle).catch(
|
|
1064
|
+
function reportCurrentTTSSpeechLifecycleFailure(error) {
|
|
1065
|
+
if (destroyed || generation !== ttsIntentGeneration) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
pendingTTSIntent = null;
|
|
1069
|
+
if (action === 'load') {
|
|
1070
|
+
host.muted = true;
|
|
1071
|
+
stopTTSPlayback();
|
|
1072
|
+
}
|
|
1073
|
+
reportTTSLifecycleError(error, action);
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
return true;
|
|
766
1077
|
}
|
|
767
1078
|
|
|
768
1079
|
function reportTTSLifecycleError(error, action) {
|
|
@@ -1051,6 +1362,9 @@
|
|
|
1051
1362
|
|
|
1052
1363
|
async function transcribeAudio(audioFile) {
|
|
1053
1364
|
const generation = ++transcriptionGeneration;
|
|
1365
|
+
const controller = new AbortController();
|
|
1366
|
+
transcriptionAbortController?.abort();
|
|
1367
|
+
transcriptionAbortController = controller;
|
|
1054
1368
|
transcriptionActive = true;
|
|
1055
1369
|
localStatus = {
|
|
1056
1370
|
message: 'Transcribing speech.',
|
|
@@ -1065,7 +1379,11 @@
|
|
|
1065
1379
|
throw new Error('The selected transcription service is unavailable.');
|
|
1066
1380
|
}
|
|
1067
1381
|
|
|
1068
|
-
const transcription = await aiRuntime.fetchSTT(
|
|
1382
|
+
const transcription = await aiRuntime.fetchSTT(
|
|
1383
|
+
audioFile,
|
|
1384
|
+
undefined,
|
|
1385
|
+
controller.signal
|
|
1386
|
+
);
|
|
1069
1387
|
if (
|
|
1070
1388
|
generation !== transcriptionGeneration
|
|
1071
1389
|
|| sttRole.state !== 'ready'
|
|
@@ -1086,6 +1404,9 @@
|
|
|
1086
1404
|
}
|
|
1087
1405
|
return false;
|
|
1088
1406
|
} finally {
|
|
1407
|
+
if (transcriptionAbortController === controller) {
|
|
1408
|
+
transcriptionAbortController = null;
|
|
1409
|
+
}
|
|
1089
1410
|
if (generation === transcriptionGeneration) {
|
|
1090
1411
|
transcriptionActive = false;
|
|
1091
1412
|
renderControls();
|
|
@@ -1126,6 +1447,8 @@
|
|
|
1126
1447
|
|| transcriptionActive;
|
|
1127
1448
|
recordingGeneration += 1;
|
|
1128
1449
|
transcriptionGeneration += 1;
|
|
1450
|
+
transcriptionAbortController?.abort();
|
|
1451
|
+
transcriptionAbortController = null;
|
|
1129
1452
|
recordingRequested = false;
|
|
1130
1453
|
transcriptionActive = false;
|
|
1131
1454
|
clearSilenceTimer();
|
|
@@ -1199,6 +1522,7 @@
|
|
|
1199
1522
|
|
|
1200
1523
|
destroyed = true;
|
|
1201
1524
|
runtimeStateAbortController.abort();
|
|
1525
|
+
sttActivationController.destroy();
|
|
1202
1526
|
window.removeEventListener('pagehide', destroy);
|
|
1203
1527
|
microphonePermissionStatus?.removeEventListener?.(
|
|
1204
1528
|
'change',
|