arcane-os 0.46.0 → 0.48.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 +42 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-speech-recognition.mjs +495 -0
- package/browser-runtime/ai/browser-speech.mjs +1 -0
- package/docs/reference/ai/browser-speech.md +80 -2
- package/docs/reference/availability-and-normalization.md +2 -1
- package/docs/reference/inventory/package-api.json +14 -0
- package/docs/reference/runtime-components.md +89 -14
- package/docs/reference/runtime-modules.md +55 -12
- package/docs/reference/sdk-api.md +57 -0
- package/package.json +1 -1
- package/runtime/arcane/components/chat.html +223 -2
- package/runtime/arcane/components/speech.html +263 -14
- package/runtime/arcane/components/voice-transcription.html +77 -10
- package/runtime/arcane/css/document-site.css +2 -35
- package/runtime/arcane/css/print.css +235 -0
- package/runtime/arcane/css/theme.css +2 -40
- package/runtime/arcane/modules/AI.js +15 -4
- package/runtime/arcane/modules/AIProviderRuntime.js +118 -0
- package/runtime/arcane/modules/PrintView.js +19 -55
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
user-select: none;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
button:focus-visible
|
|
37
|
+
button:focus-visible,
|
|
38
|
+
#recognitionLanguage:focus-visible {
|
|
38
39
|
outline: .2rem solid var(--arcane-focus, var(--focus-color));
|
|
39
40
|
outline-offset: .18rem;
|
|
40
41
|
}
|
|
@@ -85,6 +86,32 @@
|
|
|
85
86
|
min-width: 0;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
.recognition_language {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-wrap: wrap;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: .35rem .6rem;
|
|
94
|
+
margin-block-end: .35rem;
|
|
95
|
+
font-size: .86rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.recognition_language[hidden] {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#recognitionLanguage {
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
width: 10rem;
|
|
105
|
+
max-width: 100%;
|
|
106
|
+
min-height: 2.25rem;
|
|
107
|
+
padding: .35rem .5rem;
|
|
108
|
+
border: 1px solid var(--arcane-border, var(--border-color));
|
|
109
|
+
border-radius: var(--arcane-radius-small, .4rem);
|
|
110
|
+
background: var(--arcane-surface, var(--background-color));
|
|
111
|
+
color: var(--arcane-text, var(--text-color));
|
|
112
|
+
font: inherit;
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
.speech_status {
|
|
89
116
|
margin: 0;
|
|
90
117
|
color: var(--arcane-muted, var(--text-color));
|
|
@@ -162,6 +189,25 @@
|
|
|
162
189
|
<button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
|
|
163
190
|
<button id="stopButton" type="button" class="hidden" disabled>Send</button>
|
|
164
191
|
<div class="speech_feedback">
|
|
192
|
+
<label id="recognitionLanguageControl" class="recognition_language" hidden>
|
|
193
|
+
Recognition language
|
|
194
|
+
<input id="recognitionLanguage" type="text" list="recognitionLanguages" placeholder="Browser default" title="Recognition language tag, such as en-US or es-MX" autocomplete="off" spellcheck="false">
|
|
195
|
+
<datalist id="recognitionLanguages">
|
|
196
|
+
<option value="en-US">English (United States)</option>
|
|
197
|
+
<option value="en-GB">English (United Kingdom)</option>
|
|
198
|
+
<option value="es-MX">Spanish (Mexico)</option>
|
|
199
|
+
<option value="es-ES">Spanish (Spain)</option>
|
|
200
|
+
<option value="fr-FR">French</option>
|
|
201
|
+
<option value="de-DE">German</option>
|
|
202
|
+
<option value="it-IT">Italian</option>
|
|
203
|
+
<option value="pt-BR">Portuguese (Brazil)</option>
|
|
204
|
+
<option value="ja-JP">Japanese</option>
|
|
205
|
+
<option value="ko-KR">Korean</option>
|
|
206
|
+
<option value="zh-CN">Chinese (Mandarin, mainland China)</option>
|
|
207
|
+
<option value="hi-IN">Hindi</option>
|
|
208
|
+
<option value="ar-SA">Arabic (Saudi Arabia)</option>
|
|
209
|
+
</datalist>
|
|
210
|
+
</label>
|
|
165
211
|
<p id="speechStatus" class="speech_status" role="status" aria-live="polite"></p>
|
|
166
212
|
<progress
|
|
167
213
|
id="sttActivationProgress"
|
|
@@ -191,6 +237,7 @@
|
|
|
191
237
|
}
|
|
192
238
|
);
|
|
193
239
|
const aiModuleImport = import('../modules/AI.js');
|
|
240
|
+
const providerRuntimeImport = import('../modules/AIProviderRuntime.js');
|
|
194
241
|
const {
|
|
195
242
|
requestAIRuntimeIntent,
|
|
196
243
|
subscribeAIRuntimeState
|
|
@@ -200,6 +247,8 @@
|
|
|
200
247
|
formatAIRuntimeProgress
|
|
201
248
|
} = await import('../modules/ComponentContracts.js');
|
|
202
249
|
const {getBrowserDeviceClass} = await browserDeviceImport;
|
|
250
|
+
const {getAIProviderRuntime} = await providerRuntimeImport;
|
|
251
|
+
const providerRuntime = getAIProviderRuntime();
|
|
203
252
|
|
|
204
253
|
const host = this;
|
|
205
254
|
const deviceClass = getBrowserDeviceClass();
|
|
@@ -211,6 +260,7 @@
|
|
|
211
260
|
source: 'arcane.component.speech',
|
|
212
261
|
eventTypes: [
|
|
213
262
|
'speech-ready',
|
|
263
|
+
'speech-transcription-progress',
|
|
214
264
|
'speech-transcription-complete',
|
|
215
265
|
'speech-transcription-error',
|
|
216
266
|
'speech-transcription-cancelled',
|
|
@@ -229,6 +279,8 @@
|
|
|
229
279
|
const stopButton = shadowRoot.querySelector('#stopButton');
|
|
230
280
|
const muteButton = shadowRoot.querySelector('#muteButton');
|
|
231
281
|
const speechStatus = shadowRoot.querySelector('#speechStatus');
|
|
282
|
+
const recognitionLanguageControl = shadowRoot.querySelector('#recognitionLanguageControl');
|
|
283
|
+
const recognitionLanguageInput = shadowRoot.querySelector('#recognitionLanguage');
|
|
232
284
|
const silenceDuration = 0;
|
|
233
285
|
const runtimeStateAbortController = new AbortController();
|
|
234
286
|
|
|
@@ -237,6 +289,7 @@
|
|
|
237
289
|
let microphonePermissionStatus = null;
|
|
238
290
|
let microphonePermissionListener = null;
|
|
239
291
|
let captureSession = null;
|
|
292
|
+
let nativeCaptureSession = null;
|
|
240
293
|
let recordingRequested = false;
|
|
241
294
|
let recordingGeneration = 0;
|
|
242
295
|
let transcriptionGeneration = 0;
|
|
@@ -254,6 +307,18 @@
|
|
|
254
307
|
let transcriptionOperationId = null;
|
|
255
308
|
let ttsOperationId = null;
|
|
256
309
|
let destroyed = false;
|
|
310
|
+
let recognitionLanguage = is.string(host.recognitionLanguage)
|
|
311
|
+
? host.recognitionLanguage
|
|
312
|
+
: navigator.language || '';
|
|
313
|
+
Object.defineProperty(host, 'recognitionLanguage', {
|
|
314
|
+
configurable: true,
|
|
315
|
+
enumerable: true,
|
|
316
|
+
get: function getRecognitionLanguage() {
|
|
317
|
+
return recognitionLanguage;
|
|
318
|
+
},
|
|
319
|
+
set: setRecognitionLanguage
|
|
320
|
+
});
|
|
321
|
+
recognitionLanguageInput.value = recognitionLanguage;
|
|
257
322
|
|
|
258
323
|
function nextSpeechOperationId(kind) {
|
|
259
324
|
eventOperationSequence += 1;
|
|
@@ -345,6 +410,7 @@
|
|
|
345
410
|
|
|
346
411
|
const initialMutedWasHydrated = is.boolean(host.initialMuted);
|
|
347
412
|
host.initialMuted = initialMutedWasHydrated ? host.initialMuted : true;
|
|
413
|
+
host.displayTranscription = host.displayTranscription !== false;
|
|
348
414
|
host.muted = true;
|
|
349
415
|
host.configure = configure;
|
|
350
416
|
host.destroy = destroy;
|
|
@@ -362,8 +428,8 @@
|
|
|
362
428
|
host.setMuted = setMuted;
|
|
363
429
|
host.availability = {
|
|
364
430
|
microphone: transcriptionEnabled && Boolean(
|
|
365
|
-
navigator.mediaDevices?.getUserMedia
|
|
366
|
-
|
|
431
|
+
(navigator.mediaDevices?.getUserMedia && globalThis.MediaRecorder)
|
|
432
|
+
|| globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition
|
|
367
433
|
),
|
|
368
434
|
stt: false,
|
|
369
435
|
tts: false
|
|
@@ -392,6 +458,7 @@
|
|
|
392
458
|
recordButton.addEventListener('blur', handleRecordBlur, lifecycleListenerOptions);
|
|
393
459
|
stopButton.addEventListener('click', stop, lifecycleListenerOptions);
|
|
394
460
|
muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
|
|
461
|
+
recognitionLanguageInput.addEventListener('input', updateRecognitionLanguage, lifecycleListenerOptions);
|
|
395
462
|
window.addEventListener('ai-ready', synchronizeAIReadiness, lifecycleListenerOptions);
|
|
396
463
|
window.addEventListener(
|
|
397
464
|
'pagehide',
|
|
@@ -473,10 +540,28 @@
|
|
|
473
540
|
host.initialMuted = configuredMuted;
|
|
474
541
|
applyConfiguredMutedState(configuredMuted);
|
|
475
542
|
}
|
|
543
|
+
if (Object.prototype.hasOwnProperty.call(options, 'recognitionLanguage')) {
|
|
544
|
+
host.recognitionLanguage = options.recognitionLanguage;
|
|
545
|
+
}
|
|
546
|
+
if (Object.prototype.hasOwnProperty.call(options, 'displayTranscription')) {
|
|
547
|
+
host.displayTranscription = options.displayTranscription !== false;
|
|
548
|
+
}
|
|
476
549
|
|
|
477
550
|
return host;
|
|
478
551
|
}
|
|
479
552
|
|
|
553
|
+
function setRecognitionLanguage(language) {
|
|
554
|
+
if (!is.string(language)) {
|
|
555
|
+
throw new TypeError('Recognition language must be a BCP47 language tag string.');
|
|
556
|
+
}
|
|
557
|
+
recognitionLanguage = language;
|
|
558
|
+
recognitionLanguageInput.value = language;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function updateRecognitionLanguage() {
|
|
562
|
+
host.recognitionLanguage = recognitionLanguageInput.value;
|
|
563
|
+
}
|
|
564
|
+
|
|
480
565
|
function updateMicrophoneAvailability(available) {
|
|
481
566
|
if (destroyed) {
|
|
482
567
|
return;
|
|
@@ -613,6 +698,12 @@
|
|
|
613
698
|
}
|
|
614
699
|
|
|
615
700
|
function renderControls() {
|
|
701
|
+
recognitionLanguageControl.hidden = !transcriptionEnabled
|
|
702
|
+
|| !providerRuntime.supportsTranscriptionCapture();
|
|
703
|
+
recognitionLanguageInput.disabled = destroyed
|
|
704
|
+
|| recordingRequested
|
|
705
|
+
|| Boolean(nativeCaptureSession)
|
|
706
|
+
|| transcriptionActive;
|
|
616
707
|
if (!transcriptionEnabled) {
|
|
617
708
|
recordButton.hidden = true;
|
|
618
709
|
recordButton.disabled = true;
|
|
@@ -626,24 +717,25 @@
|
|
|
626
717
|
}
|
|
627
718
|
const transcriptionReady = sttRole.state === 'ready';
|
|
628
719
|
const microphoneReady = host.availability.microphone
|
|
629
|
-
&&
|
|
630
|
-
|
|
720
|
+
&& (providerRuntime.supportsTranscriptionCapture()
|
|
721
|
+
|| (is.function(globalThis.MediaRecorder)
|
|
722
|
+
&& is.function(navigator.mediaDevices?.getUserMedia)));
|
|
631
723
|
const activationVisible = sttActivationController.visible;
|
|
632
724
|
recordButton.hidden = activationVisible;
|
|
633
725
|
recordButton.disabled = !transcriptionReady
|
|
634
726
|
|| !microphoneReady
|
|
635
|
-
|| sttRole.busy
|
|
727
|
+
|| (sttRole.busy && !nativeCaptureSession)
|
|
636
728
|
|| transcriptionActive
|
|
637
729
|
|| destroyed;
|
|
638
730
|
stopButton.disabled = destroyed
|
|
639
731
|
|| !transcriptionReady
|
|
640
|
-
|| sttRole.busy
|
|
641
|
-
|| captureSession?.recorder.state !== 'recording';
|
|
732
|
+
|| (sttRole.busy && !nativeCaptureSession)
|
|
733
|
+
|| (!nativeCaptureSession && captureSession?.recorder.state !== 'recording');
|
|
642
734
|
recordButton.title = transcriptionButtonTitle();
|
|
643
735
|
recordButton.setAttribute('aria-label', recordButton.title);
|
|
644
736
|
recordButton.setAttribute(
|
|
645
737
|
'aria-pressed',
|
|
646
|
-
String(recordingRequested || Boolean(captureSession))
|
|
738
|
+
String(recordingRequested || Boolean(captureSession) || Boolean(nativeCaptureSession))
|
|
647
739
|
);
|
|
648
740
|
sttActivationButton.hidden = !activationVisible;
|
|
649
741
|
sttActivationButton.disabled = destroyed
|
|
@@ -759,10 +851,21 @@
|
|
|
759
851
|
describeSTTRole()
|
|
760
852
|
);
|
|
761
853
|
}
|
|
762
|
-
} else if (transcriptionEnabled
|
|
854
|
+
} else if (transcriptionEnabled
|
|
855
|
+
&& (recognitionLanguageControl.hidden
|
|
856
|
+
|| sttRole.state !== 'ready'
|
|
857
|
+
|| !host.availability.microphone
|
|
858
|
+
|| sttRole.busy
|
|
859
|
+
|| sttActivationController.pending)) {
|
|
763
860
|
messages.push(describeSTTRole());
|
|
764
861
|
}
|
|
765
|
-
|
|
862
|
+
if (recognitionLanguageControl.hidden
|
|
863
|
+
|| pendingTTSIntent
|
|
864
|
+
|| pendingUnmute
|
|
865
|
+
|| ['loading', 'unloading', 'error'].includes(ttsRole.state)
|
|
866
|
+
|| ttsRole.busy) {
|
|
867
|
+
messages.push(describeTTSRole());
|
|
868
|
+
}
|
|
766
869
|
if ((transcriptionEnabled && sttRole.state === 'error')
|
|
767
870
|
|| ttsRole.state === 'error') {
|
|
768
871
|
tone = 'error';
|
|
@@ -1288,21 +1391,23 @@
|
|
|
1288
1391
|
}
|
|
1289
1392
|
}
|
|
1290
1393
|
);
|
|
1291
|
-
transcriptionOperationId = null;
|
|
1394
|
+
if (transcriptionOperationId === operationId) transcriptionOperationId = null;
|
|
1292
1395
|
return accepted;
|
|
1293
1396
|
}
|
|
1294
1397
|
|
|
1295
1398
|
async function record() {
|
|
1399
|
+
const liveCapture = providerRuntime.supportsTranscriptionCapture();
|
|
1296
1400
|
if (
|
|
1297
1401
|
!transcriptionEnabled
|
|
1298
1402
|
|| destroyed
|
|
1299
1403
|
|| sttRole.state !== 'ready'
|
|
1300
1404
|
|| sttRole.busy
|
|
1301
1405
|
|| !host.availability.microphone
|
|
1302
|
-
|| !is.function(globalThis.MediaRecorder)
|
|
1303
|
-
|
|
1406
|
+
|| (!liveCapture && (!is.function(globalThis.MediaRecorder)
|
|
1407
|
+
|| !is.function(navigator.mediaDevices?.getUserMedia)))
|
|
1304
1408
|
|| recordingRequested
|
|
1305
1409
|
|| captureSession
|
|
1410
|
+
|| nativeCaptureSession
|
|
1306
1411
|
|| transcriptionActive
|
|
1307
1412
|
) {
|
|
1308
1413
|
resetRecordPress();
|
|
@@ -1322,6 +1427,8 @@
|
|
|
1322
1427
|
renderControls();
|
|
1323
1428
|
renderStatus();
|
|
1324
1429
|
|
|
1430
|
+
if (liveCapture) return startNativeRecognition(generation, operationId);
|
|
1431
|
+
|
|
1325
1432
|
let stream;
|
|
1326
1433
|
try {
|
|
1327
1434
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
@@ -1461,6 +1568,132 @@
|
|
|
1461
1568
|
}
|
|
1462
1569
|
}
|
|
1463
1570
|
|
|
1571
|
+
function startNativeRecognition(generation, operationId) {
|
|
1572
|
+
const session = {
|
|
1573
|
+
generation,
|
|
1574
|
+
operationId,
|
|
1575
|
+
controller: new AbortController(),
|
|
1576
|
+
capture: null,
|
|
1577
|
+
text: [],
|
|
1578
|
+
interim: '',
|
|
1579
|
+
cancelled: false,
|
|
1580
|
+
error: null
|
|
1581
|
+
};
|
|
1582
|
+
nativeCaptureSession = session;
|
|
1583
|
+
function currentNativeCapture() {
|
|
1584
|
+
return !destroyed && !session.cancelled
|
|
1585
|
+
&& nativeCaptureSession === session
|
|
1586
|
+
&& generation === recordingGeneration;
|
|
1587
|
+
}
|
|
1588
|
+
function displayNativeRecognitionText() {
|
|
1589
|
+
if (!currentNativeCapture()) return;
|
|
1590
|
+
localStatus = {
|
|
1591
|
+
message: (host.displayTranscription
|
|
1592
|
+
? session.text.join('') + session.interim
|
|
1593
|
+
: '') || 'Listening. Release to finish.',
|
|
1594
|
+
tone: '',
|
|
1595
|
+
owner: 'recording',
|
|
1596
|
+
generation
|
|
1597
|
+
};
|
|
1598
|
+
renderStatus();
|
|
1599
|
+
publishNativeRecognitionProgress();
|
|
1600
|
+
}
|
|
1601
|
+
function publishNativeRecognitionProgress() {
|
|
1602
|
+
const text = session.text.join('');
|
|
1603
|
+
dispatchSpeechEvent(
|
|
1604
|
+
'speech-transcription-progress',
|
|
1605
|
+
{text, interim: session.interim},
|
|
1606
|
+
{
|
|
1607
|
+
operationId,
|
|
1608
|
+
publicDetail: {text, interim: session.interim}
|
|
1609
|
+
}
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
async function finishNativeRecognition(error = null) {
|
|
1613
|
+
if (!currentNativeCapture()) return;
|
|
1614
|
+
if (session.interim) {
|
|
1615
|
+
session.interim = '';
|
|
1616
|
+
publishNativeRecognitionProgress();
|
|
1617
|
+
if (!currentNativeCapture()) return;
|
|
1618
|
+
}
|
|
1619
|
+
nativeCaptureSession = null;
|
|
1620
|
+
recordingRequested = false;
|
|
1621
|
+
resetRecordPress();
|
|
1622
|
+
clearRecordingStatus(generation);
|
|
1623
|
+
if (error && error !== session.error) {
|
|
1624
|
+
session.error = error;
|
|
1625
|
+
reportTranscriptionError(error);
|
|
1626
|
+
}
|
|
1627
|
+
// Keep confirmed final words after a service/cleanup failure. Only
|
|
1628
|
+
// explicit cancellation or supersession abandons this submission.
|
|
1629
|
+
if (session.text.length && !destroyed && !session.cancelled
|
|
1630
|
+
&& generation === recordingGeneration) {
|
|
1631
|
+
transcriptionOperationId = operationId;
|
|
1632
|
+
await transcribe(session.text.join(''));
|
|
1633
|
+
} else if (transcriptionOperationId === operationId) {
|
|
1634
|
+
transcriptionOperationId = null;
|
|
1635
|
+
}
|
|
1636
|
+
renderControls();
|
|
1637
|
+
renderStatus();
|
|
1638
|
+
}
|
|
1639
|
+
try {
|
|
1640
|
+
session.capture = providerRuntime.createTranscriptionCapture(
|
|
1641
|
+
{
|
|
1642
|
+
continuous: true,
|
|
1643
|
+
language: recognitionLanguage || undefined,
|
|
1644
|
+
onSegment: function retainFinalRecognition(segment) {
|
|
1645
|
+
if (!currentNativeCapture()) return;
|
|
1646
|
+
session.text.push(segment.text);
|
|
1647
|
+
session.interim = '';
|
|
1648
|
+
displayNativeRecognitionText();
|
|
1649
|
+
},
|
|
1650
|
+
onInterim: function displayInterimRecognition(result) {
|
|
1651
|
+
if (!currentNativeCapture()) return;
|
|
1652
|
+
session.interim = result.text;
|
|
1653
|
+
displayNativeRecognitionText();
|
|
1654
|
+
},
|
|
1655
|
+
onError: function reportNativeRecognitionFailure(error) {
|
|
1656
|
+
if (!currentNativeCapture()) return;
|
|
1657
|
+
session.error = error;
|
|
1658
|
+
reportTranscriptionError(error);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
);
|
|
1662
|
+
if (!session.capture) throw new Error('The selected live transcription service is unavailable.');
|
|
1663
|
+
session.capture.done.then(
|
|
1664
|
+
function nativeRecognitionCompleted() {
|
|
1665
|
+
return finishNativeRecognition();
|
|
1666
|
+
},
|
|
1667
|
+
finishNativeRecognition
|
|
1668
|
+
).catch(
|
|
1669
|
+
function reportNativeRecognitionSettlementFailure(error) {
|
|
1670
|
+
if (destroyed || generation !== recordingGeneration) return;
|
|
1671
|
+
reportTranscriptionError(error);
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
// No awaited preparation precedes the browser's gesture-bound start.
|
|
1675
|
+
const starting = session.capture.start({signal: session.controller.signal});
|
|
1676
|
+
return Promise.resolve(starting).catch(
|
|
1677
|
+
function reportNativeRecognitionStartupFailure(error) {
|
|
1678
|
+
if (!currentNativeCapture()) return false;
|
|
1679
|
+
session.capture.cancel();
|
|
1680
|
+
nativeCaptureSession = null;
|
|
1681
|
+
recordingRequested = false;
|
|
1682
|
+
resetRecordPress();
|
|
1683
|
+
if (error !== session.error) reportTranscriptionError(error);
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
);
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
session.capture?.cancel();
|
|
1689
|
+
nativeCaptureSession = null;
|
|
1690
|
+
recordingRequested = false;
|
|
1691
|
+
resetRecordPress();
|
|
1692
|
+
if (error !== session.error) reportTranscriptionError(error);
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1464
1697
|
function stopAfterSilence() {
|
|
1465
1698
|
clearSilenceTimer();
|
|
1466
1699
|
stop();
|
|
@@ -1483,6 +1716,12 @@
|
|
|
1483
1716
|
}
|
|
1484
1717
|
|
|
1485
1718
|
function stop() {
|
|
1719
|
+
if (nativeCaptureSession) {
|
|
1720
|
+
recordingRequested = false;
|
|
1721
|
+
nativeCaptureSession.capture.stop();
|
|
1722
|
+
renderControls();
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1486
1725
|
const pendingCaptureStart = recordingRequested && !captureSession;
|
|
1487
1726
|
const generation = recordingGeneration;
|
|
1488
1727
|
const operationId = transcriptionOperationId;
|
|
@@ -1703,6 +1942,7 @@
|
|
|
1703
1942
|
function cancelSTTOperation(reason) {
|
|
1704
1943
|
const hadActiveOperation = recordingRequested
|
|
1705
1944
|
|| Boolean(captureSession)
|
|
1945
|
+
|| Boolean(nativeCaptureSession)
|
|
1706
1946
|
|| transcriptionActive;
|
|
1707
1947
|
recordingGeneration += 1;
|
|
1708
1948
|
transcriptionGeneration += 1;
|
|
@@ -1713,6 +1953,14 @@
|
|
|
1713
1953
|
clearSilenceTimer();
|
|
1714
1954
|
resetRecordPress();
|
|
1715
1955
|
|
|
1956
|
+
if (nativeCaptureSession) {
|
|
1957
|
+
const session = nativeCaptureSession;
|
|
1958
|
+
nativeCaptureSession = null;
|
|
1959
|
+
session.cancelled = true;
|
|
1960
|
+
session.controller.abort();
|
|
1961
|
+
session.capture?.cancel();
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1716
1964
|
if (captureSession) {
|
|
1717
1965
|
captureSession.cancelled = true;
|
|
1718
1966
|
if (captureSession.recorder.state !== 'inactive') {
|
|
@@ -1769,6 +2017,7 @@
|
|
|
1769
2017
|
|
|
1770
2018
|
function handlePageHide(event) {
|
|
1771
2019
|
if (event.persisted === true) {
|
|
2020
|
+
if (nativeCaptureSession) stop();
|
|
1772
2021
|
return;
|
|
1773
2022
|
}
|
|
1774
2023
|
destroy();
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
|
|
81
81
|
<section class="voice-transcription">
|
|
82
82
|
<p id="description" class="description" part="description"></p>
|
|
83
|
-
<section id="transcript" class="transcript" part="transcript"></section>
|
|
83
|
+
<section id="transcript" class="transcript" part="transcript" aria-live="polite"></section>
|
|
84
84
|
<div class="transcription-feedback">
|
|
85
85
|
<p id="status" class="status" part="status" role="status" aria-live="polite"></p>
|
|
86
86
|
<progress
|
|
@@ -136,13 +136,16 @@
|
|
|
136
136
|
{default:MD},
|
|
137
137
|
{appendTranscription,createSTTActivationController,normalizeVoiceOptions},
|
|
138
138
|
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
139
|
-
{default:ContinuousVoiceCapture}
|
|
139
|
+
{default:ContinuousVoiceCapture},
|
|
140
|
+
{getAIProviderRuntime}
|
|
140
141
|
]=await Promise.all([
|
|
141
142
|
import('../modules/MD.js'),
|
|
142
143
|
import('../modules/ComponentContracts.js'),
|
|
143
144
|
import('../modules/AIRuntimeState.js'),
|
|
144
|
-
import('../modules/ContinuousVoiceCapture.js')
|
|
145
|
+
import('../modules/ContinuousVoiceCapture.js'),
|
|
146
|
+
import('../modules/AIProviderRuntime.js')
|
|
145
147
|
]);
|
|
148
|
+
const providerRuntime=getAIProviderRuntime();
|
|
146
149
|
const description=host.shadowRoot.querySelector('#description');
|
|
147
150
|
const transcriptElement=host.shadowRoot.querySelector('#transcript');
|
|
148
151
|
const status=host.shadowRoot.querySelector('#status');
|
|
@@ -385,6 +388,7 @@
|
|
|
385
388
|
flushing:false,
|
|
386
389
|
processing:'idle',
|
|
387
390
|
queue:[],
|
|
391
|
+
interim:'',
|
|
388
392
|
failure:null,
|
|
389
393
|
captureError:null,
|
|
390
394
|
worker:null,
|
|
@@ -396,15 +400,26 @@
|
|
|
396
400
|
if(!isCurrentContinuousSession(session)||!session.listening){
|
|
397
401
|
return false;
|
|
398
402
|
}
|
|
399
|
-
|
|
403
|
+
const captureOptions={
|
|
400
404
|
...options.capture,
|
|
401
405
|
onSegment:function capturedSegment(clip){
|
|
402
406
|
if(!isCurrentContinuousSession(session)){
|
|
403
407
|
return;
|
|
404
408
|
}
|
|
405
|
-
session.queue.push({
|
|
409
|
+
session.queue.push({
|
|
410
|
+
...clip,
|
|
411
|
+
stage:is.string(clip.text)?'append':'transcribe',
|
|
412
|
+
segment:is.string(clip.text)?clip.text:null
|
|
413
|
+
});
|
|
414
|
+
if(is.string(clip.text)) session.interim='';
|
|
415
|
+
renderTranscript();
|
|
406
416
|
processContinuousQueue(session);
|
|
407
417
|
},
|
|
418
|
+
onInterim:function displayInterimRecognition(result){
|
|
419
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
420
|
+
session.interim=result.text;
|
|
421
|
+
renderTranscript();
|
|
422
|
+
},
|
|
408
423
|
onState:function captureStateChanged(nextState){
|
|
409
424
|
if(!isCurrentContinuousSession(session)){
|
|
410
425
|
return;
|
|
@@ -412,8 +427,11 @@
|
|
|
412
427
|
session.captureState=nextState;
|
|
413
428
|
if(nextState==='stopped'||nextState==='interrupted'){
|
|
414
429
|
session.listening=false;
|
|
430
|
+
session.interim='';
|
|
431
|
+
renderTranscript();
|
|
415
432
|
}
|
|
416
433
|
renderContinuousState(session);
|
|
434
|
+
settleContinuousDrain(session);
|
|
417
435
|
},
|
|
418
436
|
onError:function captureFailed(error){
|
|
419
437
|
if(!isCurrentContinuousSession(session)){
|
|
@@ -425,7 +443,29 @@
|
|
|
425
443
|
reportContinuousError(session,'capture',error);
|
|
426
444
|
settleContinuousDrain(session);
|
|
427
445
|
}
|
|
428
|
-
}
|
|
446
|
+
};
|
|
447
|
+
session.capture=!options.transcribe
|
|
448
|
+
?providerRuntime.createTranscriptionCapture(captureOptions):null;
|
|
449
|
+
session.native=Boolean(session.capture);
|
|
450
|
+
if(!session.capture){
|
|
451
|
+
session.capture=new ContinuousVoiceCapture(captureOptions);
|
|
452
|
+
}else{
|
|
453
|
+
session.capture.done.then(function nativeRecognitionEnded(){
|
|
454
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
455
|
+
session.listening=false;
|
|
456
|
+
session.interim='';
|
|
457
|
+
renderTranscript();
|
|
458
|
+
if(!session.captureError) session.captureState='stopped';
|
|
459
|
+
renderContinuousState(session);
|
|
460
|
+
settleContinuousDrain(session);
|
|
461
|
+
}).catch(function nativeRecognitionCleanupFailed(error){
|
|
462
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
463
|
+
session.captureError=error;
|
|
464
|
+
session.listening=false;
|
|
465
|
+
reportContinuousError(session,'capture',error);
|
|
466
|
+
settleContinuousDrain(session);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
429
469
|
const started=await session.capture.start({
|
|
430
470
|
mediaConstraints:options.mediaConstraints,
|
|
431
471
|
signal:session.controller.signal
|
|
@@ -598,7 +638,10 @@
|
|
|
598
638
|
throw new TypeError('Transcription must return the complete text as a string.');
|
|
599
639
|
}
|
|
600
640
|
clip.segment=result;
|
|
601
|
-
|
|
641
|
+
clip.stage='append';
|
|
642
|
+
}
|
|
643
|
+
if(clip.stage==='append'){
|
|
644
|
+
if(clip.segment.length===0){
|
|
602
645
|
session.queue.shift();
|
|
603
646
|
const detail={sequence:clip.sequence,transcript};
|
|
604
647
|
dispatchVoiceEvent('voice-transcription-empty',detail,{
|
|
@@ -606,7 +649,7 @@
|
|
|
606
649
|
});
|
|
607
650
|
continue;
|
|
608
651
|
}
|
|
609
|
-
clip.transcript=appendTranscription(transcript,
|
|
652
|
+
clip.transcript=appendTranscription(transcript,clip.segment,options.separator);
|
|
610
653
|
clip.stage='save';
|
|
611
654
|
transcript=clip.transcript;
|
|
612
655
|
renderTranscript();
|
|
@@ -740,6 +783,7 @@
|
|
|
740
783
|
return false;
|
|
741
784
|
}
|
|
742
785
|
continuousSession=null;
|
|
786
|
+
renderTranscript();
|
|
743
787
|
session.controller.abort();
|
|
744
788
|
session.requestController?.abort();
|
|
745
789
|
session.capture?.cancel();
|
|
@@ -766,7 +810,8 @@
|
|
|
766
810
|
}
|
|
767
811
|
|
|
768
812
|
async function startRecording(){
|
|
769
|
-
if(options.capture.mode==='continuous'
|
|
813
|
+
if(options.capture.mode==='continuous'
|
|
814
|
+
||(!options.transcribe&&providerRuntime.supportsTranscriptionCapture())){
|
|
770
815
|
return startContinuousRecording();
|
|
771
816
|
}
|
|
772
817
|
if(!canStartVoiceRecording(sttRole,state,destroyed)){
|
|
@@ -1290,7 +1335,19 @@
|
|
|
1290
1335
|
}
|
|
1291
1336
|
|
|
1292
1337
|
function renderTranscript(){
|
|
1293
|
-
|
|
1338
|
+
// Display live recognition in the output without committing hypotheses
|
|
1339
|
+
// or letting an earlier save delay later finalized text.
|
|
1340
|
+
let output=transcript;
|
|
1341
|
+
const session=continuousSession;
|
|
1342
|
+
if(session&&isCurrentContinuousSession(session)){
|
|
1343
|
+
for(const clip of session.queue){
|
|
1344
|
+
if(clip.stage==='append'){
|
|
1345
|
+
output=appendTranscription(output,clip.segment,options.separator);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
output=appendTranscription(output,session.interim,options.separator);
|
|
1349
|
+
}
|
|
1350
|
+
transcriptElement.innerHTML=new MD(output).safeRendered;
|
|
1294
1351
|
transcriptElement.querySelectorAll('a').forEach(
|
|
1295
1352
|
link=>{
|
|
1296
1353
|
link.target='_blank';
|
|
@@ -1366,6 +1423,16 @@
|
|
|
1366
1423
|
sttActivationController.synchronize(sttRole);
|
|
1367
1424
|
if(continuousSession){
|
|
1368
1425
|
const session=continuousSession;
|
|
1426
|
+
if(session.native&&session.listening
|
|
1427
|
+
&&(sttRole.state!=='ready'
|
|
1428
|
+
||previousRole.providerId!==sttRole.providerId
|
|
1429
|
+
||previousRole.modelId!==sttRole.modelId)){
|
|
1430
|
+
session.capture?.cancel();
|
|
1431
|
+
session.listening=false;
|
|
1432
|
+
session.captureState='interrupted';
|
|
1433
|
+
session.interim='';
|
|
1434
|
+
renderTranscript();
|
|
1435
|
+
}
|
|
1369
1436
|
if(['completing','complete'].includes(state)){
|
|
1370
1437
|
renderState();
|
|
1371
1438
|
return;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import url('./print.css');
|
|
2
|
+
|
|
1
3
|
/* Reusable, progressively enhanced documentation-site layout.
|
|
2
4
|
* Load after Arcane theme.css and primitives.css.
|
|
3
5
|
*/
|
|
@@ -944,38 +946,3 @@ body.static-doc-site{
|
|
|
944
946
|
}
|
|
945
947
|
}
|
|
946
948
|
|
|
947
|
-
@media print{
|
|
948
|
-
.static-site-header,
|
|
949
|
-
.static-site-footer,
|
|
950
|
-
.static-section-nav,
|
|
951
|
-
.static-previous-next,
|
|
952
|
-
.static-related,
|
|
953
|
-
.static-document__source,
|
|
954
|
-
.skip-link{
|
|
955
|
-
display:none;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
.static-site-main,
|
|
959
|
-
.static-site-main--document{
|
|
960
|
-
max-inline-size:none;
|
|
961
|
-
padding:0;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
.static-document-layout{
|
|
965
|
-
display:block;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
.static-document{
|
|
969
|
-
inline-size:100%;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
.static-document__content table{
|
|
973
|
-
display:table;
|
|
974
|
-
overflow:visible;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
.static-document__content table :where(code,kbd){
|
|
978
|
-
overflow-wrap:anywhere;
|
|
979
|
-
word-break:break-word;
|
|
980
|
-
}
|
|
981
|
-
}
|