arcane-os 0.35.0 → 0.36.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 +23 -0
- package/README.md +1 -1
- package/docs/reference/inventory/runtime-components.json +13 -6
- package/docs/reference/inventory/runtime-modules.json +28 -4
- package/docs/reference/runtime-components.md +202 -22
- package/docs/reference/runtime-modules.md +109 -1
- package/package.json +2 -2
- package/runtime/arcane/components/data-view.html +18 -2
- package/runtime/arcane/components/file-manager.html +512 -88
- package/runtime/arcane/components/voice-transcription.html +611 -55
- package/runtime/arcane/modules/ComponentContracts.js +32 -1
- package/runtime/arcane/modules/ContinuousVoiceCapture.js +446 -0
- package/runtime/arcane/modules/VoiceCaptureWorklet.js +194 -0
|
@@ -37,6 +37,20 @@
|
|
|
37
37
|
margin:0;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
.controls[data-simple='true'] #start{
|
|
41
|
+
flex:1 1 100%;
|
|
42
|
+
min-height:4rem;
|
|
43
|
+
font-size:1.25em;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.voice-transcription[data-simple='true']{
|
|
47
|
+
grid-template-rows:auto auto auto minmax(12em,1fr);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[hidden]{
|
|
51
|
+
display:none!important;
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
.recording{
|
|
41
55
|
color:var(--danger-color);
|
|
42
56
|
}
|
|
@@ -65,10 +79,10 @@
|
|
|
65
79
|
</style>
|
|
66
80
|
|
|
67
81
|
<section class="voice-transcription">
|
|
68
|
-
<p id="description" class="description"></p>
|
|
69
|
-
<section id="transcript" class="transcript"></section>
|
|
82
|
+
<p id="description" class="description" part="description"></p>
|
|
83
|
+
<section id="transcript" class="transcript" part="transcript"></section>
|
|
70
84
|
<div class="transcription-feedback">
|
|
71
|
-
<p id="status" class="status" role="status" aria-live="polite"></p>
|
|
85
|
+
<p id="status" class="status" part="status" role="status" aria-live="polite"></p>
|
|
72
86
|
<progress
|
|
73
87
|
id="sttActivationProgress"
|
|
74
88
|
class="stt-activation-progress"
|
|
@@ -76,11 +90,13 @@
|
|
|
76
90
|
hidden
|
|
77
91
|
></progress>
|
|
78
92
|
</div>
|
|
79
|
-
<section class="controls">
|
|
80
|
-
<button id="start" type="button" disabled></button>
|
|
93
|
+
<section class="controls" part="controls">
|
|
94
|
+
<button id="start" part="primary-action" type="button" disabled></button>
|
|
81
95
|
<button id="sttActivation" type="button" hidden disabled>Start transcription</button>
|
|
82
|
-
<button id="stop" type="button" disabled></button>
|
|
83
|
-
<button id="
|
|
96
|
+
<button id="stop" part="stop-action" type="button" disabled></button>
|
|
97
|
+
<button id="retry" part="retry-action" type="button" hidden></button>
|
|
98
|
+
<button id="cancel" part="cancel-action" type="button" hidden></button>
|
|
99
|
+
<button id="complete" part="complete-action" type="button" disabled></button>
|
|
84
100
|
</section>
|
|
85
101
|
</section>
|
|
86
102
|
|
|
@@ -105,6 +121,8 @@
|
|
|
105
121
|
'voice-transcription-ready',
|
|
106
122
|
'voice-transcription-state',
|
|
107
123
|
'voice-transcription-segment',
|
|
124
|
+
'voice-transcription-empty',
|
|
125
|
+
'voice-transcription-error',
|
|
108
126
|
'voice-transcription-change',
|
|
109
127
|
'voice-transcription-complete',
|
|
110
128
|
'speech-transcription-complete',
|
|
@@ -114,16 +132,17 @@
|
|
|
114
132
|
]
|
|
115
133
|
}
|
|
116
134
|
);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
appendTranscription,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
const [
|
|
136
|
+
{default:MD},
|
|
137
|
+
{appendTranscription,createSTTActivationController,normalizeVoiceOptions},
|
|
138
|
+
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
139
|
+
{default:ContinuousVoiceCapture}
|
|
140
|
+
]=await Promise.all([
|
|
141
|
+
import('../modules/MD.js?v=2'),
|
|
142
|
+
import('../modules/ComponentContracts.js'),
|
|
143
|
+
import('../modules/AIRuntimeState.js'),
|
|
144
|
+
import('../modules/ContinuousVoiceCapture.js')
|
|
145
|
+
]);
|
|
127
146
|
const description=host.shadowRoot.querySelector('#description');
|
|
128
147
|
const transcriptElement=host.shadowRoot.querySelector('#transcript');
|
|
129
148
|
const status=host.shadowRoot.querySelector('#status');
|
|
@@ -131,6 +150,10 @@
|
|
|
131
150
|
const sttActivationButton=host.shadowRoot.querySelector('#sttActivation');
|
|
132
151
|
const sttActivationProgress = host.shadowRoot.querySelector('#sttActivationProgress');
|
|
133
152
|
const stopButton=host.shadowRoot.querySelector('#stop');
|
|
153
|
+
const retryButton=host.shadowRoot.querySelector('#retry');
|
|
154
|
+
const cancelButton=host.shadowRoot.querySelector('#cancel');
|
|
155
|
+
const controlsElement=host.shadowRoot.querySelector('.controls');
|
|
156
|
+
const feedbackElement=host.shadowRoot.querySelector('.transcription-feedback');
|
|
134
157
|
const completeButton=host.shadowRoot.querySelector('#complete');
|
|
135
158
|
const runtimeStateAbortController=new AbortController();
|
|
136
159
|
|
|
@@ -146,6 +169,8 @@
|
|
|
146
169
|
let eventOperationSequence=0;
|
|
147
170
|
let voiceOperationId=null;
|
|
148
171
|
let destroyed=false;
|
|
172
|
+
let continuousSession=null;
|
|
173
|
+
let completionAbortController=null;
|
|
149
174
|
let options=normalizeVoiceOptions(optionsFromDataset());
|
|
150
175
|
|
|
151
176
|
function nextVoiceOperationId(kind){
|
|
@@ -216,6 +241,8 @@
|
|
|
216
241
|
host.reset=clear;
|
|
217
242
|
host.startRecording=startRecording;
|
|
218
243
|
host.stopRecording=stopRecording;
|
|
244
|
+
host.retryTranscription=retryTranscription;
|
|
245
|
+
host.cancelRecording=cancelRecording;
|
|
219
246
|
host.requestSTTActivation=host.requestSTTActivation
|
|
220
247
|
||function requestSTTActivation(intent){
|
|
221
248
|
return requestAIRuntimeIntent(intent);
|
|
@@ -233,8 +260,10 @@
|
|
|
233
260
|
const lifecycleListenerOptions=completeValue(
|
|
234
261
|
{signal:runtimeStateAbortController.signal}
|
|
235
262
|
);
|
|
236
|
-
startButton.addEventListener('click',
|
|
263
|
+
startButton.addEventListener('click',handlePrimaryClick,lifecycleListenerOptions);
|
|
237
264
|
stopButton.addEventListener('click',stopRecording,lifecycleListenerOptions);
|
|
265
|
+
retryButton.addEventListener('click',retryTranscription,lifecycleListenerOptions);
|
|
266
|
+
cancelButton.addEventListener('click',cancelRecording,lifecycleListenerOptions);
|
|
238
267
|
completeButton.addEventListener('click',completeStream,lifecycleListenerOptions);
|
|
239
268
|
window.addEventListener(
|
|
240
269
|
'pagehide',
|
|
@@ -327,7 +356,419 @@
|
|
|
327
356
|
return getOptions();
|
|
328
357
|
}
|
|
329
358
|
|
|
359
|
+
function handlePrimaryClick(){
|
|
360
|
+
if(options.controls==='simple'&&continuousSession?.listening){
|
|
361
|
+
return stopRecording();
|
|
362
|
+
}
|
|
363
|
+
return startRecording();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isCurrentContinuousSession(session){
|
|
367
|
+
return !destroyed&&continuousSession===session
|
|
368
|
+
&&session.generation===sessionGeneration
|
|
369
|
+
&&!session.controller.signal.aborted;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function startContinuousRecording(){
|
|
373
|
+
if(!canStartVoiceRecording(sttRole,state,destroyed)
|
|
374
|
+
||continuousSession?.queue.length){
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
releaseContinuousSession();
|
|
378
|
+
const session={
|
|
379
|
+
generation:++sessionGeneration,
|
|
380
|
+
controller:new AbortController(),
|
|
381
|
+
requestController:null,
|
|
382
|
+
capture:null,
|
|
383
|
+
captureState:'starting',
|
|
384
|
+
listening:true,
|
|
385
|
+
flushing:false,
|
|
386
|
+
processing:'idle',
|
|
387
|
+
queue:[],
|
|
388
|
+
failure:null,
|
|
389
|
+
captureError:null,
|
|
390
|
+
worker:null,
|
|
391
|
+
waiters:[]
|
|
392
|
+
};
|
|
393
|
+
continuousSession=session;
|
|
394
|
+
setState('starting',options.messages.requesting,{beginOperation:'transcription'});
|
|
395
|
+
try{
|
|
396
|
+
if(!isCurrentContinuousSession(session)||!session.listening){
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
session.capture=new ContinuousVoiceCapture({
|
|
400
|
+
...options.capture,
|
|
401
|
+
onSegment:function capturedSegment(clip){
|
|
402
|
+
if(!isCurrentContinuousSession(session)){
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
session.queue.push({...clip,stage:'transcribe',segment:null});
|
|
406
|
+
processContinuousQueue(session);
|
|
407
|
+
},
|
|
408
|
+
onState:function captureStateChanged(nextState){
|
|
409
|
+
if(!isCurrentContinuousSession(session)){
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
session.captureState=nextState;
|
|
413
|
+
if(nextState==='stopped'||nextState==='interrupted'){
|
|
414
|
+
session.listening=false;
|
|
415
|
+
}
|
|
416
|
+
renderContinuousState(session);
|
|
417
|
+
},
|
|
418
|
+
onError:function captureFailed(error){
|
|
419
|
+
if(!isCurrentContinuousSession(session)){
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
session.captureError=error;
|
|
423
|
+
session.captureState='interrupted';
|
|
424
|
+
session.listening=false;
|
|
425
|
+
reportContinuousError(session,'capture',error);
|
|
426
|
+
settleContinuousDrain(session);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
const started=await session.capture.start({
|
|
430
|
+
mediaConstraints:options.mediaConstraints,
|
|
431
|
+
signal:session.controller.signal
|
|
432
|
+
});
|
|
433
|
+
if(!isCurrentContinuousSession(session)){
|
|
434
|
+
session.capture.cancel();
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if(!started){
|
|
438
|
+
session.listening=false;
|
|
439
|
+
if(!session.captureError){
|
|
440
|
+
session.captureState='stopped';
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
renderContinuousState(session);
|
|
444
|
+
settleContinuousDrain(session);
|
|
445
|
+
return started;
|
|
446
|
+
}catch(error){
|
|
447
|
+
if(!isCurrentContinuousSession(session)){
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
session.listening=false;
|
|
451
|
+
session.captureError=error;
|
|
452
|
+
session.captureState='interrupted';
|
|
453
|
+
session.capture?.cancel();
|
|
454
|
+
reportContinuousError(session,'capture',error);
|
|
455
|
+
settleContinuousDrain(session);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function continuousDetails(session){
|
|
461
|
+
return {
|
|
462
|
+
capture:session.captureState,
|
|
463
|
+
processing:session.processing,
|
|
464
|
+
queued:session.queue.length,
|
|
465
|
+
errorPhase:session.failure?.phase||(session.captureError?'capture':null),
|
|
466
|
+
error:session.failure?.error||session.captureError||null
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function renderContinuousState(session){
|
|
471
|
+
if(!isCurrentContinuousSession(session)){
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const messages=[];
|
|
475
|
+
if(session.captureError){
|
|
476
|
+
messages.push(options.messages.interrupted);
|
|
477
|
+
}else if(session.listening){
|
|
478
|
+
messages.push(session.captureState==='starting'
|
|
479
|
+
?options.messages.requesting:options.messages.listening);
|
|
480
|
+
}else if(session.flushing||session.queue.length){
|
|
481
|
+
messages.push(options.messages.draining);
|
|
482
|
+
}
|
|
483
|
+
const processingMessages={
|
|
484
|
+
transcribing:options.messages.transcribing,
|
|
485
|
+
saving:options.messages.saving,
|
|
486
|
+
waiting:options.messages.waiting
|
|
487
|
+
};
|
|
488
|
+
if(session.failure){
|
|
489
|
+
messages.push(session.failure.phase==='save'
|
|
490
|
+
?options.messages.saveError:options.messages.transcribeError);
|
|
491
|
+
}else if(processingMessages[session.processing]){
|
|
492
|
+
messages.push(processingMessages[session.processing]);
|
|
493
|
+
}
|
|
494
|
+
const nextState=session.listening
|
|
495
|
+
?(session.captureState==='starting'?'starting':'recording')
|
|
496
|
+
:session.failure||session.captureError?'error'
|
|
497
|
+
:session.flushing||session.queue.length?'transcribing':'idle';
|
|
498
|
+
setState(nextState,messages.join(' ')||options.messages.ready);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function reportContinuousError(session,phase,error){
|
|
502
|
+
arcaneLogging.error(`Continuous voice ${phase} failed:`,error);
|
|
503
|
+
renderContinuousState(session);
|
|
504
|
+
if(!isCurrentContinuousSession(session)){
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const detail={phase,error,sequence:session.queue[0]?.sequence??null};
|
|
508
|
+
dispatchVoiceEvent('voice-transcription-error',detail,{
|
|
509
|
+
bubbles:true,composed:true,publicDetail:detail
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function waitForContinuousOperation(result,signal){
|
|
514
|
+
return new Promise(function observeContinuousOperation(resolve,reject){
|
|
515
|
+
function aborted(){
|
|
516
|
+
reject(signal.reason||new DOMException('Transcription canceled.','AbortError'));
|
|
517
|
+
}
|
|
518
|
+
if(signal.aborted){
|
|
519
|
+
aborted();
|
|
520
|
+
}else{
|
|
521
|
+
signal.addEventListener('abort',aborted,{once:true});
|
|
522
|
+
}
|
|
523
|
+
Promise.resolve(result).then(function completed(value){
|
|
524
|
+
signal.removeEventListener('abort',aborted);
|
|
525
|
+
resolve(value);
|
|
526
|
+
},function failed(error){
|
|
527
|
+
signal.removeEventListener('abort',aborted);
|
|
528
|
+
reject(error);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function processContinuousQueue(session){
|
|
534
|
+
if(!isCurrentContinuousSession(session)||session.worker||session.failure
|
|
535
|
+
||!session.queue.length){
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
// One ordered transcription/save consumer; microphone capture never waits for it.
|
|
539
|
+
const work=Promise.resolve().then(function beginOrderedDrain(){
|
|
540
|
+
return drainContinuousQueue(session);
|
|
541
|
+
});
|
|
542
|
+
session.worker=work;
|
|
543
|
+
work.then(function queueSettled(){
|
|
544
|
+
if(session.worker===work){
|
|
545
|
+
session.worker=null;
|
|
546
|
+
}
|
|
547
|
+
if(!isCurrentContinuousSession(session)){
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
renderContinuousState(session);
|
|
551
|
+
settleContinuousDrain(session);
|
|
552
|
+
if(session.queue.length&&!session.failure
|
|
553
|
+
&&sttRole.state==='ready'&&!sttRole.busy){
|
|
554
|
+
processContinuousQueue(session);
|
|
555
|
+
}
|
|
556
|
+
},function unexpectedQueueFailure(error){
|
|
557
|
+
if(session.worker===work){
|
|
558
|
+
session.worker=null;
|
|
559
|
+
}
|
|
560
|
+
if(isCurrentContinuousSession(session)){
|
|
561
|
+
session.failure={phase:session.queue[0]?.stage==='save'?'save':'transcribe',error};
|
|
562
|
+
session.processing='error';
|
|
563
|
+
reportContinuousError(session,session.failure.phase,error);
|
|
564
|
+
settleContinuousDrain(session);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function drainContinuousQueue(session){
|
|
570
|
+
while(isCurrentContinuousSession(session)&&session.queue.length&&!session.failure){
|
|
571
|
+
const clip=session.queue[0];
|
|
572
|
+
if(clip.stage==='transcribe'&&(sttRole.state!=='ready'||sttRole.busy)){
|
|
573
|
+
session.processing='waiting';
|
|
574
|
+
renderContinuousState(session);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const controller=new AbortController();
|
|
578
|
+
session.requestController=controller;
|
|
579
|
+
try{
|
|
580
|
+
if(clip.stage==='transcribe'){
|
|
581
|
+
session.processing='transcribing';
|
|
582
|
+
renderContinuousState(session);
|
|
583
|
+
if(!isCurrentContinuousSession(session)||controller.signal.aborted){
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const file=new File([clip.audio],`segment-${clip.sequence}.wav`,{type:clip.audio.type});
|
|
587
|
+
const result=await waitForContinuousOperation(transcribeAudio(file,{
|
|
588
|
+
audio:clip.audio,mimeType:clip.audio.type,transcript,
|
|
589
|
+
sequence:clip.sequence,signal:controller.signal
|
|
590
|
+
},controller.signal),controller.signal);
|
|
591
|
+
if(!isCurrentContinuousSession(session)){
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if(controller.signal.aborted){
|
|
595
|
+
throw controller.signal.reason||new DOMException('Transcription canceled.','AbortError');
|
|
596
|
+
}
|
|
597
|
+
if(!is.string(result)){
|
|
598
|
+
throw new TypeError('Transcription must return the complete text as a string.');
|
|
599
|
+
}
|
|
600
|
+
clip.segment=result;
|
|
601
|
+
if(result.length===0){
|
|
602
|
+
session.queue.shift();
|
|
603
|
+
const detail={sequence:clip.sequence,transcript};
|
|
604
|
+
dispatchVoiceEvent('voice-transcription-empty',detail,{
|
|
605
|
+
bubbles:true,composed:true,publicDetail:detail
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
clip.transcript=appendTranscription(transcript,result,options.separator);
|
|
610
|
+
clip.stage='save';
|
|
611
|
+
transcript=clip.transcript;
|
|
612
|
+
renderTranscript();
|
|
613
|
+
}
|
|
614
|
+
if(options.persist){
|
|
615
|
+
const save=options.onSave||(is.function(host.save)?host.save.bind(host):null);
|
|
616
|
+
if(!save){
|
|
617
|
+
throw new Error('Transcription saving is not configured.');
|
|
618
|
+
}
|
|
619
|
+
session.processing='saving';
|
|
620
|
+
renderContinuousState(session);
|
|
621
|
+
if(!isCurrentContinuousSession(session)||controller.signal.aborted){
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const result=await waitForContinuousOperation(save({
|
|
625
|
+
transcript:clip.transcript,segment:clip.segment,
|
|
626
|
+
sequence:clip.sequence,signal:controller.signal
|
|
627
|
+
}),controller.signal);
|
|
628
|
+
if(result===false){
|
|
629
|
+
throw new Error('The transcription save was not accepted.');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if(!isCurrentContinuousSession(session)){
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if(controller.signal.aborted){
|
|
636
|
+
throw controller.signal.reason||new DOMException('Transcription canceled.','AbortError');
|
|
637
|
+
}
|
|
638
|
+
session.queue.shift();
|
|
639
|
+
const detail={text:clip.segment,transcript:clip.transcript,sequence:clip.sequence};
|
|
640
|
+
dispatchVoiceEvent('speech-transcription-complete',detail,{
|
|
641
|
+
bubbles:true,composed:true,publicDetail:detail
|
|
642
|
+
});
|
|
643
|
+
if(!isCurrentContinuousSession(session)){
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
dispatchVoiceEvent('voice-transcription-segment',detail,{
|
|
647
|
+
bubbles:true,composed:true,publicDetail:detail
|
|
648
|
+
});
|
|
649
|
+
}catch(error){
|
|
650
|
+
if(!isCurrentContinuousSession(session)){
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
session.failure={phase:clip.stage==='save'?'save':'transcribe',error};
|
|
654
|
+
session.processing='error';
|
|
655
|
+
reportContinuousError(session,session.failure.phase,error);
|
|
656
|
+
return;
|
|
657
|
+
}finally{
|
|
658
|
+
if(session.requestController===controller){
|
|
659
|
+
session.requestController=null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
session.processing='idle';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function settleContinuousDrain(session){
|
|
667
|
+
if(session.listening||session.flushing||session.worker){
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if(session.queue.length&&!session.failure){
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const success=isCurrentContinuousSession(session)
|
|
674
|
+
&&!session.failure&&!session.captureError;
|
|
675
|
+
for(const resolve of session.waiters.splice(0)){
|
|
676
|
+
resolve(success);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function continuousDrain(session){
|
|
681
|
+
return new Promise(function awaitContinuousDrain(resolve){
|
|
682
|
+
if(!isCurrentContinuousSession(session)){
|
|
683
|
+
resolve(false);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
session.waiters.push(resolve);
|
|
687
|
+
settleContinuousDrain(session);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function stopContinuousRecording(session){
|
|
692
|
+
if(!isCurrentContinuousSession(session)){
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
if((completionAbortController||state==='complete')
|
|
696
|
+
&&!session.listening&&!session.flushing&&!session.queue.length){
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
if(session.listening){
|
|
700
|
+
session.listening=false;
|
|
701
|
+
session.flushing=true;
|
|
702
|
+
session.captureState='stopped';
|
|
703
|
+
// stop() releases microphone tracks before waiting for its final worklet message.
|
|
704
|
+
const stopping=session.capture?.stop();
|
|
705
|
+
renderContinuousState(session);
|
|
706
|
+
try{
|
|
707
|
+
await stopping;
|
|
708
|
+
}catch(error){
|
|
709
|
+
if(isCurrentContinuousSession(session)){
|
|
710
|
+
session.captureError=error;
|
|
711
|
+
reportContinuousError(session,'capture',error);
|
|
712
|
+
}
|
|
713
|
+
}finally{
|
|
714
|
+
session.flushing=false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if(!isCurrentContinuousSession(session)){
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
processContinuousQueue(session);
|
|
721
|
+
renderContinuousState(session);
|
|
722
|
+
return continuousDrain(session);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function retryTranscription(){
|
|
726
|
+
const session=continuousSession;
|
|
727
|
+
if(!session||!isCurrentContinuousSession(session)||!session.failure){
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
session.failure=null;
|
|
731
|
+
session.processing='idle';
|
|
732
|
+
processContinuousQueue(session);
|
|
733
|
+
renderContinuousState(session);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function releaseContinuousSession(){
|
|
738
|
+
const session=continuousSession;
|
|
739
|
+
if(!session){
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
continuousSession=null;
|
|
743
|
+
session.controller.abort();
|
|
744
|
+
session.requestController?.abort();
|
|
745
|
+
session.capture?.cancel();
|
|
746
|
+
session.queue=[];
|
|
747
|
+
for(const resolve of session.waiters.splice(0)){
|
|
748
|
+
resolve(false);
|
|
749
|
+
}
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function cancelRecording(){
|
|
754
|
+
if(destroyed){
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
const hadContinuous=releaseContinuousSession();
|
|
758
|
+
completionAbortController?.abort();
|
|
759
|
+
completionAbortController=null;
|
|
760
|
+
if(hadContinuous){
|
|
761
|
+
sessionGeneration+=1;
|
|
762
|
+
reportSTTCancellation('user-cancelled',options.messages.cancelled);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
return cancelSTTOperation('user-cancelled');
|
|
766
|
+
}
|
|
767
|
+
|
|
330
768
|
async function startRecording(){
|
|
769
|
+
if(options.capture.mode==='continuous'){
|
|
770
|
+
return startContinuousRecording();
|
|
771
|
+
}
|
|
331
772
|
if(!canStartVoiceRecording(sttRole,state,destroyed)){
|
|
332
773
|
return false;
|
|
333
774
|
}
|
|
@@ -433,6 +874,9 @@
|
|
|
433
874
|
}
|
|
434
875
|
|
|
435
876
|
function stopRecording(){
|
|
877
|
+
if(continuousSession){
|
|
878
|
+
return stopContinuousRecording(continuousSession);
|
|
879
|
+
}
|
|
436
880
|
if(destroyed||state!=='recording'||recorder?.state!=='recording'){
|
|
437
881
|
return false;
|
|
438
882
|
}
|
|
@@ -670,54 +1114,94 @@
|
|
|
670
1114
|
}
|
|
671
1115
|
|
|
672
1116
|
async function completeStream(){
|
|
673
|
-
if(destroyed||
|
|
1117
|
+
if(destroyed||completionAbortController||state==='complete'){
|
|
674
1118
|
return false;
|
|
675
1119
|
}
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
const complete=options.onComplete||(
|
|
679
|
-
is.function(host.complete)?host.complete.bind(host):null
|
|
680
|
-
);
|
|
681
|
-
setState('completing',options.messages.completing,{
|
|
682
|
-
beginOperation:'completion'
|
|
683
|
-
});
|
|
684
|
-
if(!isCurrentVoiceOperation(generation,'completing')){
|
|
1120
|
+
const session=continuousSession;
|
|
1121
|
+
if(!session&&(transcript.length===0||!['idle','error'].includes(state))){
|
|
685
1122
|
return false;
|
|
686
1123
|
}
|
|
1124
|
+
const generation=sessionGeneration;
|
|
1125
|
+
const completionController=new AbortController();
|
|
1126
|
+
// Reserve completion before stopping capture: drain callbacks may reenter.
|
|
1127
|
+
completionAbortController=completionController;
|
|
687
1128
|
try{
|
|
688
|
-
if(
|
|
689
|
-
await
|
|
1129
|
+
if(session){
|
|
1130
|
+
await stopContinuousRecording(session);
|
|
1131
|
+
if(!isCurrentContinuousSession(session)||session.queue.length){
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
690
1134
|
}
|
|
691
|
-
if(
|
|
1135
|
+
if(destroyed||completionController.signal.aborted
|
|
1136
|
+
||completionAbortController!==completionController
|
|
1137
|
+
||generation!==sessionGeneration||transcript.length===0
|
|
1138
|
+
||!['idle','error'].includes(state)){
|
|
692
1139
|
return false;
|
|
693
1140
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
{
|
|
698
|
-
bubbles:true,
|
|
699
|
-
composed:true,
|
|
700
|
-
publicDetail:{
|
|
701
|
-
transcript:completionTranscript
|
|
702
|
-
}
|
|
703
|
-
}
|
|
1141
|
+
const completionTranscript=transcript;
|
|
1142
|
+
const complete=options.onComplete||(
|
|
1143
|
+
is.function(host.complete)?host.complete.bind(host):null
|
|
704
1144
|
);
|
|
1145
|
+
setState('completing',options.messages.completing,{
|
|
1146
|
+
beginOperation:'completion'
|
|
1147
|
+
});
|
|
705
1148
|
if(!isCurrentVoiceOperation(generation,'completing')){
|
|
706
1149
|
return false;
|
|
707
1150
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1151
|
+
try{
|
|
1152
|
+
if(complete){
|
|
1153
|
+
const input={transcript:completionTranscript};
|
|
1154
|
+
if(session){
|
|
1155
|
+
input.signal=completionController.signal;
|
|
1156
|
+
}
|
|
1157
|
+
const result=await waitForContinuousOperation(
|
|
1158
|
+
complete(input),completionController.signal
|
|
1159
|
+
);
|
|
1160
|
+
if(result===false&&session){
|
|
1161
|
+
throw new Error('Transcription completion was not accepted.');
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if(!isCurrentVoiceOperation(generation,'completing')){
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
dispatchVoiceEvent(
|
|
1168
|
+
'voice-transcription-complete',
|
|
1169
|
+
{transcript:completionTranscript},
|
|
1170
|
+
{
|
|
1171
|
+
bubbles:true,
|
|
1172
|
+
composed:true,
|
|
1173
|
+
publicDetail:{
|
|
1174
|
+
transcript:completionTranscript
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
if(!isCurrentVoiceOperation(generation,'completing')){
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
setState('complete',options.messages.complete);
|
|
1182
|
+
return isCurrentVoiceOperation(generation,'complete');
|
|
1183
|
+
}catch(error){
|
|
1184
|
+
if(!isCurrentVoiceOperation(generation,'completing')){
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
arcaneLogging.error('Unable to complete transcription:',error);
|
|
1188
|
+
setState('error',options.messages.completeError);
|
|
712
1189
|
return false;
|
|
713
1190
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1191
|
+
}finally{
|
|
1192
|
+
if(completionAbortController===completionController){
|
|
1193
|
+
completionAbortController=null;
|
|
1194
|
+
}
|
|
717
1195
|
}
|
|
718
1196
|
}
|
|
719
1197
|
|
|
720
1198
|
function supersedeForTranscriptReplacement(){
|
|
1199
|
+
if(releaseContinuousSession()){
|
|
1200
|
+
sessionGeneration+=1;
|
|
1201
|
+
completionAbortController?.abort();
|
|
1202
|
+
completionAbortController=null;
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
721
1205
|
const hadActiveOperation=[
|
|
722
1206
|
'starting',
|
|
723
1207
|
'transcribing',
|
|
@@ -728,6 +1212,8 @@
|
|
|
728
1212
|
return false;
|
|
729
1213
|
}
|
|
730
1214
|
sessionGeneration+=1;
|
|
1215
|
+
completionAbortController?.abort();
|
|
1216
|
+
completionAbortController=null;
|
|
731
1217
|
transcriptionAbortController?.abort();
|
|
732
1218
|
transcriptionAbortController=null;
|
|
733
1219
|
releaseMicrophone();
|
|
@@ -735,6 +1221,12 @@
|
|
|
735
1221
|
}
|
|
736
1222
|
|
|
737
1223
|
function supersedeForConfigurationReplacement(){
|
|
1224
|
+
if(releaseContinuousSession()){
|
|
1225
|
+
sessionGeneration+=1;
|
|
1226
|
+
completionAbortController?.abort();
|
|
1227
|
+
completionAbortController=null;
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
738
1230
|
const hadActiveOperation=[
|
|
739
1231
|
'starting',
|
|
740
1232
|
'recording',
|
|
@@ -746,6 +1238,8 @@
|
|
|
746
1238
|
return false;
|
|
747
1239
|
}
|
|
748
1240
|
sessionGeneration+=1;
|
|
1241
|
+
completionAbortController?.abort();
|
|
1242
|
+
completionAbortController=null;
|
|
749
1243
|
transcriptionAbortController?.abort();
|
|
750
1244
|
transcriptionAbortController=null;
|
|
751
1245
|
releaseMicrophone();
|
|
@@ -784,9 +1278,11 @@
|
|
|
784
1278
|
}
|
|
785
1279
|
|
|
786
1280
|
function clear(){
|
|
787
|
-
if(destroyed||!['complete','error','idle'].includes(state)
|
|
1281
|
+
if(destroyed||!['complete','error','idle'].includes(state)
|
|
1282
|
+
||continuousSession?.queue.length){
|
|
788
1283
|
return false;
|
|
789
1284
|
}
|
|
1285
|
+
releaseContinuousSession();
|
|
790
1286
|
transcript='';
|
|
791
1287
|
renderTranscript();
|
|
792
1288
|
setState('idle',options.messages.ready);
|
|
@@ -813,6 +1309,17 @@
|
|
|
813
1309
|
startButton.innerText=options.labels.start;
|
|
814
1310
|
stopButton.innerText=options.labels.stop;
|
|
815
1311
|
completeButton.innerText=options.labels.complete;
|
|
1312
|
+
retryButton.innerText=options.labels.retry;
|
|
1313
|
+
cancelButton.innerText=options.labels.cancel;
|
|
1314
|
+
completeButton.hidden=!options.showComplete;
|
|
1315
|
+
controlsElement.dataset.simple=String(options.controls==='simple');
|
|
1316
|
+
host.shadowRoot.querySelector('.voice-transcription').dataset.simple=
|
|
1317
|
+
String(options.controls==='simple');
|
|
1318
|
+
if(options.controls==='simple'){
|
|
1319
|
+
description.after(controlsElement,feedbackElement,transcriptElement);
|
|
1320
|
+
}else{
|
|
1321
|
+
description.after(transcriptElement,feedbackElement,controlsElement);
|
|
1322
|
+
}
|
|
816
1323
|
}
|
|
817
1324
|
|
|
818
1325
|
function releaseMicrophone(){
|
|
@@ -857,6 +1364,22 @@
|
|
|
857
1364
|
arcaneLogging.error('Arcane transcription runtime failed.',sttRole.error);
|
|
858
1365
|
}
|
|
859
1366
|
sttActivationController.synchronize(sttRole);
|
|
1367
|
+
if(continuousSession){
|
|
1368
|
+
const session=continuousSession;
|
|
1369
|
+
if(['completing','complete'].includes(state)){
|
|
1370
|
+
renderState();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if(session.processing==='transcribing'
|
|
1374
|
+
&&(sttRole.state!=='ready'
|
|
1375
|
+
||previousRole.providerId!==sttRole.providerId
|
|
1376
|
+
||previousRole.modelId!==sttRole.modelId)){
|
|
1377
|
+
session.requestController?.abort();
|
|
1378
|
+
}
|
|
1379
|
+
processContinuousQueue(session);
|
|
1380
|
+
renderContinuousState(session);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
860
1383
|
if(previousRole.state==='ready'&&sttRole.state!=='ready'){
|
|
861
1384
|
if(cancelSTTOperation('runtime-unready')){
|
|
862
1385
|
return;
|
|
@@ -866,6 +1389,13 @@
|
|
|
866
1389
|
}
|
|
867
1390
|
|
|
868
1391
|
function cancelSTTOperation(reason){
|
|
1392
|
+
if(releaseContinuousSession()){
|
|
1393
|
+
sessionGeneration+=1;
|
|
1394
|
+
completionAbortController?.abort();
|
|
1395
|
+
completionAbortController=null;
|
|
1396
|
+
reportSTTCancellation(reason,options.messages.cancelled);
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
869
1399
|
const hadActiveOperation=['starting','recording','transcribing'].includes(state)
|
|
870
1400
|
||Boolean(mediaStream)
|
|
871
1401
|
||Boolean(recorder)
|
|
@@ -917,13 +1447,26 @@
|
|
|
917
1447
|
)
|
|
918
1448
|
)
|
|
919
1449
|
);
|
|
920
|
-
|
|
921
|
-
|
|
1450
|
+
const session=continuousSession;
|
|
1451
|
+
const continuousListening=Boolean(session?.listening);
|
|
1452
|
+
const simpleStop=options.controls==='simple'&&continuousListening;
|
|
1453
|
+
startButton.disabled=simpleStop?destroyed
|
|
1454
|
+
:!canStartVoiceRecording(sttRole,state,destroyed)||Boolean(session?.queue.length);
|
|
1455
|
+
startButton.innerText=simpleStop?options.labels.stop:options.labels.start;
|
|
1456
|
+
startButton.title=simpleStop?options.labels.stop:recordingButtonTitle();
|
|
922
1457
|
startButton.setAttribute('aria-label',startButton.title);
|
|
923
|
-
|
|
1458
|
+
startButton.setAttribute('aria-pressed',String(continuousListening));
|
|
1459
|
+
stopButton.hidden=options.controls==='simple'&&options.capture.mode==='continuous';
|
|
1460
|
+
stopButton.disabled=destroyed||(session?!continuousListening:state!=='recording');
|
|
1461
|
+
retryButton.hidden=!session?.failure;
|
|
1462
|
+
retryButton.disabled=destroyed;
|
|
1463
|
+
cancelButton.hidden=!session||session.listening
|
|
1464
|
+
||(!session.flushing&&!session.queue.length&&!completionAbortController);
|
|
1465
|
+
cancelButton.disabled=destroyed;
|
|
924
1466
|
completeButton.disabled=destroyed
|
|
925
1467
|
||transcript.length===0
|
|
926
|
-
||!['idle','error'].includes(state)
|
|
1468
|
+
||!['idle','error'].includes(state)
|
|
1469
|
+
||Boolean(session?.queue.length||session?.flushing||session?.listening);
|
|
927
1470
|
sttActivationButton.hidden=destroyed||!sttActivationController.visible;
|
|
928
1471
|
sttActivationButton.disabled=destroyed
|
|
929
1472
|
||sttActivationController.pending
|
|
@@ -945,13 +1488,15 @@
|
|
|
945
1488
|
{
|
|
946
1489
|
message:status.innerText,
|
|
947
1490
|
state,
|
|
948
|
-
stt:sttRole
|
|
1491
|
+
stt:sttRole,
|
|
1492
|
+
...(session?continuousDetails(session):{})
|
|
949
1493
|
}
|
|
950
1494
|
),
|
|
951
1495
|
{
|
|
952
1496
|
bubbles:true,
|
|
953
1497
|
composed:true,
|
|
954
|
-
publicDetail:{message:status.innerText,state,stt:sttRole
|
|
1498
|
+
publicDetail:{message:status.innerText,state,stt:sttRole,
|
|
1499
|
+
...(session?continuousDetails(session):{})}
|
|
955
1500
|
}
|
|
956
1501
|
);
|
|
957
1502
|
}
|
|
@@ -982,6 +1527,9 @@
|
|
|
982
1527
|
if(destroyed){
|
|
983
1528
|
return 'Transcription unavailable.';
|
|
984
1529
|
}
|
|
1530
|
+
if(continuousSession){
|
|
1531
|
+
return stateMessage;
|
|
1532
|
+
}
|
|
985
1533
|
return sttRole.state==='ready'
|
|
986
1534
|
&&!sttRole.busy
|
|
987
1535
|
&&!sttActivationController.pending
|
|
@@ -1010,12 +1558,18 @@
|
|
|
1010
1558
|
labels:{...options.labels},
|
|
1011
1559
|
messages:{...options.messages},
|
|
1012
1560
|
mediaConstraints:{...options.mediaConstraints},
|
|
1561
|
+
capture:{...options.capture},
|
|
1013
1562
|
mimeTypes:options.mimeTypes.slice()
|
|
1014
1563
|
};
|
|
1015
1564
|
}
|
|
1016
1565
|
|
|
1017
1566
|
function handlePageHide(event){
|
|
1018
1567
|
if(event.persisted===true){
|
|
1568
|
+
if(continuousSession?.listening){
|
|
1569
|
+
stopContinuousRecording(continuousSession).catch(function pageCaptureStopFailed(error){
|
|
1570
|
+
arcaneLogging.error('Unable to finish captured speech before page caching:',error);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1019
1573
|
return;
|
|
1020
1574
|
}
|
|
1021
1575
|
destroy();
|
|
@@ -1037,6 +1591,8 @@
|
|
|
1037
1591
|
return true;
|
|
1038
1592
|
}
|
|
1039
1593
|
destroyed=true;
|
|
1594
|
+
completionAbortController?.abort();
|
|
1595
|
+
completionAbortController=null;
|
|
1040
1596
|
runtimeStateAbortController.abort();
|
|
1041
1597
|
sttActivationController.destroy();
|
|
1042
1598
|
window.removeEventListener('pagehide',handlePageHide);
|