arcane-os 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -146,18 +146,43 @@
|
|
|
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
|
|
158
163
|
} = await import('../modules/AIRuntimeState.js');
|
|
164
|
+
const {
|
|
165
|
+
createSTTActivationController
|
|
166
|
+
} = await import('../modules/ComponentContracts.js');
|
|
159
167
|
|
|
160
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
|
+
);
|
|
161
186
|
const shadowRoot = host.shadowRoot;
|
|
162
187
|
const recordButton = shadowRoot.querySelector('#recordButton');
|
|
163
188
|
const sttActivationButton = shadowRoot.querySelector('#sttActivationButton');
|
|
@@ -183,12 +208,99 @@
|
|
|
183
208
|
let keyboardRecording = false;
|
|
184
209
|
let localStatus = null;
|
|
185
210
|
let pendingTTSIntent = null;
|
|
211
|
+
let activeTTSIntent = null;
|
|
212
|
+
let pendingUnmute = false;
|
|
186
213
|
let ttsIntentGeneration = 0;
|
|
214
|
+
let eventOperationSequence = 0;
|
|
215
|
+
let transcriptionOperationId = null;
|
|
216
|
+
let ttsOperationId = null;
|
|
187
217
|
let destroyed = false;
|
|
188
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
|
+
|
|
189
301
|
const initialMutedWasHydrated = typeof host.initialMuted === 'boolean';
|
|
190
302
|
host.initialMuted = initialMutedWasHydrated ? host.initialMuted : true;
|
|
191
|
-
host.muted =
|
|
303
|
+
host.muted = true;
|
|
192
304
|
host.configure = configure;
|
|
193
305
|
host.destroy = destroy;
|
|
194
306
|
host.reportTTSError = reportTTSError;
|
|
@@ -210,20 +322,29 @@
|
|
|
210
322
|
{
|
|
211
323
|
host,
|
|
212
324
|
button: sttActivationButton,
|
|
213
|
-
onChange: renderSTTActivationState
|
|
325
|
+
onChange: renderSTTActivationState,
|
|
326
|
+
eventSource: events
|
|
214
327
|
}
|
|
215
328
|
);
|
|
216
329
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
recordButton.addEventListener('
|
|
221
|
-
recordButton.addEventListener('
|
|
222
|
-
recordButton.addEventListener('
|
|
223
|
-
recordButton.addEventListener('
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
);
|
|
227
348
|
|
|
228
349
|
subscribeAIRuntimeState(
|
|
229
350
|
synchronizeAIRuntimeState,
|
|
@@ -231,213 +352,37 @@
|
|
|
231
352
|
signal: runtimeStateAbortController.signal
|
|
232
353
|
}
|
|
233
354
|
);
|
|
355
|
+
if (!host.initialMuted && ttsRole.state === 'ready') {
|
|
356
|
+
host.muted = false;
|
|
357
|
+
}
|
|
234
358
|
synchronizeAIMutedState();
|
|
235
359
|
renderControls();
|
|
236
360
|
renderStatus();
|
|
237
361
|
void observeMicrophonePermission();
|
|
238
362
|
void aiModuleImport.then(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
host.componentReady = true;
|
|
246
|
-
host.dispatchEvent(
|
|
247
|
-
new CustomEvent(
|
|
248
|
-
'speech-ready',
|
|
249
|
-
{
|
|
250
|
-
bubbles: true,
|
|
251
|
-
composed: true,
|
|
252
|
-
detail: { speech: host }
|
|
253
|
-
}
|
|
254
|
-
)
|
|
255
|
-
);
|
|
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;
|
|
363
|
+
function synchronizeMutedStateAfterAIImport() {
|
|
364
|
+
if (!destroyed) {
|
|
365
|
+
synchronizeAIMutedState();
|
|
308
366
|
}
|
|
309
|
-
|
|
310
|
-
|
|
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) {
|
|
367
|
+
},
|
|
368
|
+
function reportAIModuleImportFailure(error) {
|
|
394
369
|
if (destroyed) {
|
|
395
370
|
return;
|
|
396
371
|
}
|
|
397
|
-
|
|
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;
|
|
372
|
+
console.error('The shared AI runtime could not load.');
|
|
408
373
|
}
|
|
374
|
+
);
|
|
409
375
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
376
|
+
host.componentReady = true;
|
|
377
|
+
dispatchSpeechEvent(
|
|
378
|
+
'speech-ready',
|
|
379
|
+
{ speech: host },
|
|
380
|
+
{
|
|
381
|
+
bubbles: true,
|
|
382
|
+
composed: true,
|
|
383
|
+
publicDetail: { ready: true }
|
|
418
384
|
}
|
|
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
|
-
}
|
|
385
|
+
);
|
|
441
386
|
|
|
442
387
|
function unavailableRole(role) {
|
|
443
388
|
return Object.freeze(
|
|
@@ -464,6 +409,9 @@
|
|
|
464
409
|
}
|
|
465
410
|
|
|
466
411
|
function configure(options = {}) {
|
|
412
|
+
if (destroyed) {
|
|
413
|
+
return host;
|
|
414
|
+
}
|
|
467
415
|
if (Object.prototype.hasOwnProperty.call(options, 'initialMuted')) {
|
|
468
416
|
const configuredMuted = typeof options.initialMuted === 'boolean'
|
|
469
417
|
? options.initialMuted
|
|
@@ -484,6 +432,9 @@
|
|
|
484
432
|
}
|
|
485
433
|
|
|
486
434
|
function setAvailability(input = {}) {
|
|
435
|
+
if (destroyed) {
|
|
436
|
+
return { ...host.availability };
|
|
437
|
+
}
|
|
487
438
|
// Compatibility-only explicit adapter report. Canonical consumers use
|
|
488
439
|
// the sticky AIRuntimeState role records instead of this wrapper.
|
|
489
440
|
const previousSTTRole = sttRole;
|
|
@@ -512,6 +463,9 @@
|
|
|
512
463
|
}
|
|
513
464
|
|
|
514
465
|
function setMuted(muted) {
|
|
466
|
+
if (destroyed) {
|
|
467
|
+
return host;
|
|
468
|
+
}
|
|
515
469
|
if (typeof muted !== 'boolean') {
|
|
516
470
|
throw new TypeError('muted must be boolean');
|
|
517
471
|
}
|
|
@@ -525,11 +479,11 @@
|
|
|
525
479
|
}
|
|
526
480
|
|
|
527
481
|
function applyConfiguredMutedState(muted) {
|
|
528
|
-
host.muted = muted;
|
|
529
|
-
pendingTTSIntent = null;
|
|
530
482
|
if (muted) {
|
|
531
|
-
|
|
483
|
+
requestUserMute();
|
|
532
484
|
} else {
|
|
485
|
+
pendingUnmute = false;
|
|
486
|
+
host.muted = ttsRole.state !== 'ready';
|
|
533
487
|
synchronizeAIMutedState();
|
|
534
488
|
}
|
|
535
489
|
renderControls();
|
|
@@ -559,14 +513,18 @@
|
|
|
559
513
|
cancelSTTOperation('runtime-unready');
|
|
560
514
|
}
|
|
561
515
|
if (ttsBecameUnready) {
|
|
516
|
+
pendingUnmute = false;
|
|
562
517
|
host.muted = true;
|
|
563
518
|
stopTTSPlayback();
|
|
564
519
|
}
|
|
565
|
-
if (ttsRole.state === 'error' && !host.muted) {
|
|
520
|
+
if (ttsRole.state === 'error' && (pendingUnmute || !host.muted)) {
|
|
521
|
+
pendingUnmute = false;
|
|
566
522
|
host.muted = true;
|
|
567
523
|
stopTTSPlayback();
|
|
568
524
|
}
|
|
569
525
|
|
|
526
|
+
settleSuccessfulUnmute();
|
|
527
|
+
|
|
570
528
|
synchronizeAIMutedState();
|
|
571
529
|
renderControls();
|
|
572
530
|
renderStatus();
|
|
@@ -597,18 +555,20 @@
|
|
|
597
555
|
|
|
598
556
|
function renderControls() {
|
|
599
557
|
const transcriptionReady = sttRole.state === 'ready';
|
|
600
|
-
const microphoneReady = host.availability.microphone
|
|
601
|
-
|
|
602
|
-
&&
|
|
603
|
-
|
|
604
|
-
.includes(sttRole.state);
|
|
558
|
+
const microphoneReady = host.availability.microphone
|
|
559
|
+
&& typeof globalThis.MediaRecorder === 'function'
|
|
560
|
+
&& typeof navigator.mediaDevices?.getUserMedia === 'function';
|
|
561
|
+
const activationVisible = sttActivationController.visible;
|
|
605
562
|
recordButton.hidden = activationVisible;
|
|
606
563
|
recordButton.disabled = !transcriptionReady
|
|
607
564
|
|| !microphoneReady
|
|
608
565
|
|| sttRole.busy
|
|
609
566
|
|| transcriptionActive
|
|
610
567
|
|| destroyed;
|
|
611
|
-
stopButton.disabled =
|
|
568
|
+
stopButton.disabled = destroyed
|
|
569
|
+
|| !transcriptionReady
|
|
570
|
+
|| sttRole.busy
|
|
571
|
+
|| captureSession?.recorder.state !== 'recording';
|
|
612
572
|
recordButton.title = transcriptionButtonTitle();
|
|
613
573
|
recordButton.setAttribute('aria-label', recordButton.title);
|
|
614
574
|
recordButton.setAttribute(
|
|
@@ -620,8 +580,8 @@
|
|
|
620
580
|
|| sttActivationController.pending
|
|
621
581
|
|| sttRole.state === 'unloading'
|
|
622
582
|
|| sttActivationController.action === null;
|
|
623
|
-
sttActivationButton.textContent =
|
|
624
|
-
sttActivationButton.title =
|
|
583
|
+
sttActivationButton.textContent = sttActivationController.label;
|
|
584
|
+
sttActivationButton.title = sttActivationController.title;
|
|
625
585
|
sttActivationButton.setAttribute(
|
|
626
586
|
'aria-label',
|
|
627
587
|
sttActivationButton.title
|
|
@@ -636,41 +596,6 @@
|
|
|
636
596
|
renderTTSControl();
|
|
637
597
|
}
|
|
638
598
|
|
|
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
|
-
|
|
674
599
|
function transcriptionButtonTitle() {
|
|
675
600
|
if (destroyed || sttRole.state === 'disposed') {
|
|
676
601
|
return 'Transcription is unavailable.';
|
|
@@ -776,34 +701,10 @@
|
|
|
776
701
|
}
|
|
777
702
|
|
|
778
703
|
function describeSTTRole() {
|
|
779
|
-
if (
|
|
780
|
-
return
|
|
781
|
-
}
|
|
782
|
-
if (sttActivationController.pending) {
|
|
783
|
-
return 'Transcription activation requested.';
|
|
784
|
-
}
|
|
785
|
-
if (sttRole.state === 'ready') {
|
|
786
|
-
if (!host.availability.microphone) {
|
|
787
|
-
return 'Microphone unavailable.';
|
|
788
|
-
}
|
|
789
|
-
return sttRole.busy ? 'Transcription busy.' : 'Transcription ready.';
|
|
790
|
-
}
|
|
791
|
-
if (sttRole.state === 'loading') {
|
|
792
|
-
return `Transcription ${formatRoleProgress(sttRole.progress, 'loading')}; Cancel is available.`;
|
|
704
|
+
if (sttRole.state === 'ready' && !host.availability.microphone) {
|
|
705
|
+
return 'Microphone unavailable.';
|
|
793
706
|
}
|
|
794
|
-
|
|
795
|
-
return `Transcription ${formatRoleProgress(sttRole.progress, 'releasing')}.`;
|
|
796
|
-
}
|
|
797
|
-
if (sttRole.state === 'error') {
|
|
798
|
-
return `Transcription error: ${visibleErrorMessage(sttRole.error, 'Unknown error')}.`;
|
|
799
|
-
}
|
|
800
|
-
if (sttRole.state === 'disposed') {
|
|
801
|
-
return 'Transcription disposed.';
|
|
802
|
-
}
|
|
803
|
-
if (sttRole.state === 'unloaded' && sttActivationController.selected) {
|
|
804
|
-
return 'Transcription selected and waiting to load.';
|
|
805
|
-
}
|
|
806
|
-
return 'Transcription unavailable.';
|
|
707
|
+
return sttActivationController.status;
|
|
807
708
|
}
|
|
808
709
|
|
|
809
710
|
function describeTTSRole() {
|
|
@@ -862,13 +763,18 @@
|
|
|
862
763
|
}
|
|
863
764
|
|
|
864
765
|
try {
|
|
865
|
-
|
|
766
|
+
const permissionStatus = await navigator.permissions.query(
|
|
866
767
|
{ name: 'microphone' }
|
|
867
768
|
);
|
|
769
|
+
if (destroyed) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
microphonePermissionStatus = permissionStatus;
|
|
868
773
|
microphonePermissionListener = synchronizeMicrophonePermission;
|
|
869
774
|
microphonePermissionStatus.addEventListener?.(
|
|
870
775
|
'change',
|
|
871
|
-
microphonePermissionListener
|
|
776
|
+
microphonePermissionListener,
|
|
777
|
+
{ signal: runtimeStateAbortController.signal }
|
|
872
778
|
);
|
|
873
779
|
synchronizeMicrophonePermission();
|
|
874
780
|
} catch (error) {
|
|
@@ -878,6 +784,9 @@
|
|
|
878
784
|
}
|
|
879
785
|
|
|
880
786
|
function synchronizeMicrophonePermission() {
|
|
787
|
+
if (destroyed) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
881
790
|
setAvailability(
|
|
882
791
|
{
|
|
883
792
|
microphone: microphonePermissionStatus?.state !== 'denied'
|
|
@@ -983,27 +892,49 @@
|
|
|
983
892
|
}
|
|
984
893
|
|
|
985
894
|
if (host.muted) {
|
|
986
|
-
requestUserUnmute();
|
|
895
|
+
void requestUserUnmute();
|
|
987
896
|
} else {
|
|
988
897
|
requestUserMute();
|
|
989
898
|
}
|
|
990
899
|
}
|
|
991
900
|
|
|
992
901
|
function requestUserMute() {
|
|
902
|
+
if (destroyed) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
if (host.muted && activeTTSIntent?.action === 'unload') {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
993
908
|
const wasMuted = host.muted;
|
|
909
|
+
pendingUnmute = false;
|
|
994
910
|
const shouldUnload = !wasMuted
|
|
995
911
|
|| pendingTTSIntent === 'load'
|
|
996
912
|
|| ['loading', 'ready', 'error'].includes(ttsRole.state);
|
|
997
913
|
host.muted = true;
|
|
998
914
|
stopTTSPlayback();
|
|
999
915
|
if (shouldUnload) {
|
|
1000
|
-
requestTTSIntent('unload', 'user');
|
|
916
|
+
void requestTTSIntent('unload', 'user');
|
|
1001
917
|
}
|
|
1002
918
|
renderControls();
|
|
1003
919
|
renderStatus();
|
|
1004
920
|
}
|
|
1005
921
|
|
|
1006
|
-
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
|
+
}
|
|
1007
938
|
const providerSelected = typeof ttsRole.providerId === 'string'
|
|
1008
939
|
&& ttsRole.providerId.length > 0
|
|
1009
940
|
&& typeof ttsRole.modelId === 'string'
|
|
@@ -1023,60 +954,93 @@
|
|
|
1023
954
|
return false;
|
|
1024
955
|
}
|
|
1025
956
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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;
|
|
1031
966
|
}
|
|
967
|
+
settleSuccessfulUnmute();
|
|
1032
968
|
renderControls();
|
|
1033
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();
|
|
1034
982
|
return true;
|
|
1035
983
|
}
|
|
1036
984
|
|
|
1037
|
-
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
|
+
}
|
|
1038
992
|
const generation = ++ttsIntentGeneration;
|
|
993
|
+
const operationId = nextSpeechOperationId(`tts-${action}`);
|
|
994
|
+
const intent = Object.freeze({ action, generation, operationId });
|
|
995
|
+
activeTTSIntent = intent;
|
|
996
|
+
ttsOperationId = operationId;
|
|
1039
997
|
pendingTTSIntent = action === 'load' && ttsRole.state === 'ready'
|
|
1040
998
|
? null
|
|
1041
999
|
: action;
|
|
1042
|
-
let lifecycle
|
|
1000
|
+
let lifecycle;
|
|
1043
1001
|
try {
|
|
1044
1002
|
if (typeof globalThis.ai?.setSpeechMuted === 'function') {
|
|
1045
1003
|
lifecycle = globalThis.ai.setSpeechMuted(action === 'unload');
|
|
1004
|
+
} else {
|
|
1005
|
+
lifecycle = requestAIRuntimeIntent(
|
|
1006
|
+
{
|
|
1007
|
+
role: 'tts',
|
|
1008
|
+
action,
|
|
1009
|
+
reason
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1046
1012
|
}
|
|
1047
|
-
|
|
1048
|
-
{
|
|
1049
|
-
role: 'tts',
|
|
1050
|
-
action,
|
|
1051
|
-
reason
|
|
1052
|
-
}
|
|
1053
|
-
);
|
|
1013
|
+
await lifecycle;
|
|
1054
1014
|
} catch (error) {
|
|
1015
|
+
if (destroyed || generation !== ttsIntentGeneration) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1055
1018
|
pendingTTSIntent = null;
|
|
1056
1019
|
if (action === 'load') {
|
|
1020
|
+
pendingUnmute = false;
|
|
1057
1021
|
host.muted = true;
|
|
1058
1022
|
stopTTSPlayback();
|
|
1059
1023
|
}
|
|
1060
|
-
reportTTSLifecycleError(error, action);
|
|
1024
|
+
reportTTSLifecycleError(error, action, operationId);
|
|
1061
1025
|
return false;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
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);
|
|
1026
|
+
} finally {
|
|
1027
|
+
if (activeTTSIntent === intent) {
|
|
1028
|
+
activeTTSIntent = null;
|
|
1074
1029
|
}
|
|
1075
|
-
|
|
1030
|
+
}
|
|
1031
|
+
if (destroyed || generation !== ttsIntentGeneration) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (action === 'load') {
|
|
1035
|
+
settleSuccessfulUnmute();
|
|
1036
|
+
}
|
|
1076
1037
|
return true;
|
|
1077
1038
|
}
|
|
1078
1039
|
|
|
1079
|
-
function reportTTSLifecycleError(error, action) {
|
|
1040
|
+
function reportTTSLifecycleError(error, action, operationId = null) {
|
|
1041
|
+
if (destroyed) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1080
1044
|
const message = visibleErrorMessage(
|
|
1081
1045
|
error,
|
|
1082
1046
|
`The voice ${action} request failed.`
|
|
@@ -1085,47 +1049,80 @@
|
|
|
1085
1049
|
message,
|
|
1086
1050
|
tone: 'error'
|
|
1087
1051
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
|
1099
1075
|
}
|
|
1100
|
-
|
|
1076
|
+
}
|
|
1101
1077
|
);
|
|
1102
1078
|
renderControls();
|
|
1103
1079
|
renderStatus();
|
|
1080
|
+
return false;
|
|
1104
1081
|
}
|
|
1105
1082
|
|
|
1106
|
-
function reportTTSError(error) {
|
|
1083
|
+
function reportTTSError(error, boundary = 'synthesis') {
|
|
1084
|
+
if (destroyed) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
const playbackRejected = boundary === 'playback';
|
|
1107
1088
|
const message = visibleErrorMessage(
|
|
1108
1089
|
error,
|
|
1109
|
-
|
|
1090
|
+
playbackRejected
|
|
1091
|
+
? 'Speech playback failed.'
|
|
1092
|
+
: 'Speech synthesis failed.'
|
|
1110
1093
|
);
|
|
1111
1094
|
host.muted = true;
|
|
1112
1095
|
stopTTSPlayback();
|
|
1113
1096
|
localStatus = {
|
|
1114
|
-
message:
|
|
1097
|
+
message: `${playbackRejected ? 'Speech playback' : 'Speech synthesis'} failed: ${message}`,
|
|
1115
1098
|
tone: 'error'
|
|
1116
1099
|
};
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
|
1127
1124
|
}
|
|
1128
|
-
|
|
1125
|
+
}
|
|
1129
1126
|
);
|
|
1130
1127
|
renderControls();
|
|
1131
1128
|
renderStatus();
|
|
@@ -1159,20 +1156,26 @@
|
|
|
1159
1156
|
|
|
1160
1157
|
void Promise.resolve(aiRuntime.resumeAudio()).catch(
|
|
1161
1158
|
function reportTTSResumeFailure(error) {
|
|
1162
|
-
reportTTSError(error);
|
|
1159
|
+
reportTTSError(error, 'playback');
|
|
1163
1160
|
}
|
|
1164
1161
|
);
|
|
1165
1162
|
}
|
|
1166
1163
|
|
|
1167
1164
|
async function transcribe(transcription = '') {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
|
1173
1174
|
}
|
|
1174
|
-
|
|
1175
|
+
}
|
|
1175
1176
|
);
|
|
1177
|
+
transcriptionOperationId = null;
|
|
1178
|
+
return accepted;
|
|
1176
1179
|
}
|
|
1177
1180
|
|
|
1178
1181
|
async function record() {
|
|
@@ -1181,6 +1184,8 @@
|
|
|
1181
1184
|
|| sttRole.state !== 'ready'
|
|
1182
1185
|
|| sttRole.busy
|
|
1183
1186
|
|| !host.availability.microphone
|
|
1187
|
+
|| typeof globalThis.MediaRecorder !== 'function'
|
|
1188
|
+
|| typeof navigator.mediaDevices?.getUserMedia !== 'function'
|
|
1184
1189
|
|| recordingRequested
|
|
1185
1190
|
|| captureSession
|
|
1186
1191
|
|| transcriptionActive
|
|
@@ -1190,6 +1195,7 @@
|
|
|
1190
1195
|
}
|
|
1191
1196
|
|
|
1192
1197
|
const generation = ++recordingGeneration;
|
|
1198
|
+
transcriptionOperationId = nextSpeechOperationId('transcription');
|
|
1193
1199
|
recordingRequested = true;
|
|
1194
1200
|
localStatus = {
|
|
1195
1201
|
message: 'Recording. Release to transcribe.',
|
|
@@ -1213,16 +1219,23 @@
|
|
|
1213
1219
|
message: 'Microphone access is unavailable. Text chat is still available.',
|
|
1214
1220
|
tone: 'error'
|
|
1215
1221
|
};
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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)
|
|
1223
1235
|
}
|
|
1224
|
-
|
|
1236
|
+
}
|
|
1225
1237
|
);
|
|
1238
|
+
transcriptionOperationId = null;
|
|
1226
1239
|
} else {
|
|
1227
1240
|
reportTranscriptionError(error);
|
|
1228
1241
|
}
|
|
@@ -1235,6 +1248,7 @@
|
|
|
1235
1248
|
!recordingRequested
|
|
1236
1249
|
|| generation !== recordingGeneration
|
|
1237
1250
|
|| sttRole.state !== 'ready'
|
|
1251
|
+
|| sttRole.busy
|
|
1238
1252
|
|| destroyed
|
|
1239
1253
|
) {
|
|
1240
1254
|
stopMediaStream(stream);
|
|
@@ -1341,6 +1355,7 @@
|
|
|
1341
1355
|
session.cancelled
|
|
1342
1356
|
|| session.generation !== recordingGeneration
|
|
1343
1357
|
|| sttRole.state !== 'ready'
|
|
1358
|
+
|| sttRole.busy
|
|
1344
1359
|
|| destroyed
|
|
1345
1360
|
) {
|
|
1346
1361
|
renderControls();
|
|
@@ -1362,6 +1377,8 @@
|
|
|
1362
1377
|
|
|
1363
1378
|
async function transcribeAudio(audioFile) {
|
|
1364
1379
|
const generation = ++transcriptionGeneration;
|
|
1380
|
+
transcriptionOperationId = transcriptionOperationId
|
|
1381
|
+
|| nextSpeechOperationId('transcription');
|
|
1365
1382
|
const controller = new AbortController();
|
|
1366
1383
|
transcriptionAbortController?.abort();
|
|
1367
1384
|
transcriptionAbortController = controller;
|
|
@@ -1375,8 +1392,19 @@
|
|
|
1375
1392
|
|
|
1376
1393
|
try {
|
|
1377
1394
|
const aiRuntime = globalThis.ai;
|
|
1378
|
-
if (sttRole.state !== 'ready' ||
|
|
1379
|
-
|
|
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;
|
|
1380
1408
|
}
|
|
1381
1409
|
|
|
1382
1410
|
const transcription = await aiRuntime.fetchSTT(
|
|
@@ -1400,7 +1428,14 @@
|
|
|
1400
1428
|
return true;
|
|
1401
1429
|
} catch (error) {
|
|
1402
1430
|
if (generation === transcriptionGeneration && !destroyed) {
|
|
1403
|
-
|
|
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
|
+
}
|
|
1404
1439
|
}
|
|
1405
1440
|
return false;
|
|
1406
1441
|
} finally {
|
|
@@ -1424,23 +1459,72 @@
|
|
|
1424
1459
|
message: `Transcription failed: ${message}`,
|
|
1425
1460
|
tone: 'error'
|
|
1426
1461
|
};
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
|
1437
1484
|
}
|
|
1438
|
-
|
|
1485
|
+
}
|
|
1439
1486
|
);
|
|
1487
|
+
transcriptionOperationId = null;
|
|
1440
1488
|
renderControls();
|
|
1441
1489
|
renderStatus();
|
|
1442
1490
|
}
|
|
1443
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
|
+
|
|
1444
1528
|
function cancelSTTOperation(reason) {
|
|
1445
1529
|
const hadActiveOperation = recordingRequested
|
|
1446
1530
|
|| Boolean(captureSession)
|
|
@@ -1465,21 +1549,14 @@
|
|
|
1465
1549
|
}
|
|
1466
1550
|
|
|
1467
1551
|
if (hadActiveOperation) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
new CustomEvent(
|
|
1474
|
-
'speech-transcription-cancelled',
|
|
1475
|
-
{
|
|
1476
|
-
bubbles: true,
|
|
1477
|
-
composed: true,
|
|
1478
|
-
detail: { reason }
|
|
1479
|
-
}
|
|
1480
|
-
)
|
|
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.'
|
|
1481
1557
|
);
|
|
1482
1558
|
}
|
|
1559
|
+
transcriptionOperationId = null;
|
|
1483
1560
|
}
|
|
1484
1561
|
|
|
1485
1562
|
function releaseCaptureSession(session) {
|
|
@@ -1530,6 +1607,7 @@
|
|
|
1530
1607
|
);
|
|
1531
1608
|
cancelSTTOperation('component-destroyed');
|
|
1532
1609
|
stopTTSPlayback();
|
|
1610
|
+
events.dispose();
|
|
1533
1611
|
host.componentReady = false;
|
|
1534
1612
|
renderControls();
|
|
1535
1613
|
renderStatus();
|