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.
@@ -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
- && globalThis.MediaRecorder
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
- && is.function(globalThis.MediaRecorder)
630
- && is.function(navigator.mediaDevices?.getUserMedia);
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
- messages.push(describeTTSRole());
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
- || !is.function(navigator.mediaDevices?.getUserMedia)
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
- session.capture=new ContinuousVoiceCapture({
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({...clip,stage:'transcribe',segment:null});
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
- if(result.length===0){
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,result,options.separator);
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
- transcriptElement.innerHTML=new MD(transcript).safeRendered;
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
- }