arcane-os 0.45.0 → 0.47.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 +39 -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 +59 -4
- package/docs/reference/runtime-modules.md +78 -10
- package/docs/reference/sdk-api.md +57 -0
- package/package.json +2 -2
- package/runtime/arcane/components/file-manager.html +35 -3
- package/runtime/arcane/components/speech.html +141 -11
- package/runtime/arcane/components/voice-transcription.html +77 -8
- package/runtime/arcane/css/theme.css +41 -0
- package/runtime/arcane/modules/AI.js +15 -4
- package/runtime/arcane/modules/AIProviderRuntime.js +118 -0
- package/runtime/arcane/modules/MD.js +14 -0
- package/runtime/arcane/modules/MarkdownMedia.js +41 -13
- package/runtime/arcane/modules/PrintView.js +23 -3
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
}
|
|
192
192
|
);
|
|
193
193
|
const aiModuleImport = import('../modules/AI.js');
|
|
194
|
+
const providerRuntimeImport = import('../modules/AIProviderRuntime.js');
|
|
194
195
|
const {
|
|
195
196
|
requestAIRuntimeIntent,
|
|
196
197
|
subscribeAIRuntimeState
|
|
@@ -200,6 +201,8 @@
|
|
|
200
201
|
formatAIRuntimeProgress
|
|
201
202
|
} = await import('../modules/ComponentContracts.js');
|
|
202
203
|
const {getBrowserDeviceClass} = await browserDeviceImport;
|
|
204
|
+
const {getAIProviderRuntime} = await providerRuntimeImport;
|
|
205
|
+
const providerRuntime = getAIProviderRuntime();
|
|
203
206
|
|
|
204
207
|
const host = this;
|
|
205
208
|
const deviceClass = getBrowserDeviceClass();
|
|
@@ -237,6 +240,7 @@
|
|
|
237
240
|
let microphonePermissionStatus = null;
|
|
238
241
|
let microphonePermissionListener = null;
|
|
239
242
|
let captureSession = null;
|
|
243
|
+
let nativeCaptureSession = null;
|
|
240
244
|
let recordingRequested = false;
|
|
241
245
|
let recordingGeneration = 0;
|
|
242
246
|
let transcriptionGeneration = 0;
|
|
@@ -362,8 +366,8 @@
|
|
|
362
366
|
host.setMuted = setMuted;
|
|
363
367
|
host.availability = {
|
|
364
368
|
microphone: transcriptionEnabled && Boolean(
|
|
365
|
-
navigator.mediaDevices?.getUserMedia
|
|
366
|
-
|
|
369
|
+
(navigator.mediaDevices?.getUserMedia && globalThis.MediaRecorder)
|
|
370
|
+
|| globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition
|
|
367
371
|
),
|
|
368
372
|
stt: false,
|
|
369
373
|
tts: false
|
|
@@ -626,24 +630,25 @@
|
|
|
626
630
|
}
|
|
627
631
|
const transcriptionReady = sttRole.state === 'ready';
|
|
628
632
|
const microphoneReady = host.availability.microphone
|
|
629
|
-
&&
|
|
630
|
-
|
|
633
|
+
&& (providerRuntime.supportsTranscriptionCapture()
|
|
634
|
+
|| (is.function(globalThis.MediaRecorder)
|
|
635
|
+
&& is.function(navigator.mediaDevices?.getUserMedia)));
|
|
631
636
|
const activationVisible = sttActivationController.visible;
|
|
632
637
|
recordButton.hidden = activationVisible;
|
|
633
638
|
recordButton.disabled = !transcriptionReady
|
|
634
639
|
|| !microphoneReady
|
|
635
|
-
|| sttRole.busy
|
|
640
|
+
|| (sttRole.busy && !nativeCaptureSession)
|
|
636
641
|
|| transcriptionActive
|
|
637
642
|
|| destroyed;
|
|
638
643
|
stopButton.disabled = destroyed
|
|
639
644
|
|| !transcriptionReady
|
|
640
|
-
|| sttRole.busy
|
|
641
|
-
|| captureSession?.recorder.state !== 'recording';
|
|
645
|
+
|| (sttRole.busy && !nativeCaptureSession)
|
|
646
|
+
|| (!nativeCaptureSession && captureSession?.recorder.state !== 'recording');
|
|
642
647
|
recordButton.title = transcriptionButtonTitle();
|
|
643
648
|
recordButton.setAttribute('aria-label', recordButton.title);
|
|
644
649
|
recordButton.setAttribute(
|
|
645
650
|
'aria-pressed',
|
|
646
|
-
String(recordingRequested || Boolean(captureSession))
|
|
651
|
+
String(recordingRequested || Boolean(captureSession) || Boolean(nativeCaptureSession))
|
|
647
652
|
);
|
|
648
653
|
sttActivationButton.hidden = !activationVisible;
|
|
649
654
|
sttActivationButton.disabled = destroyed
|
|
@@ -1288,21 +1293,23 @@
|
|
|
1288
1293
|
}
|
|
1289
1294
|
}
|
|
1290
1295
|
);
|
|
1291
|
-
transcriptionOperationId = null;
|
|
1296
|
+
if (transcriptionOperationId === operationId) transcriptionOperationId = null;
|
|
1292
1297
|
return accepted;
|
|
1293
1298
|
}
|
|
1294
1299
|
|
|
1295
1300
|
async function record() {
|
|
1301
|
+
const liveCapture = providerRuntime.supportsTranscriptionCapture();
|
|
1296
1302
|
if (
|
|
1297
1303
|
!transcriptionEnabled
|
|
1298
1304
|
|| destroyed
|
|
1299
1305
|
|| sttRole.state !== 'ready'
|
|
1300
1306
|
|| sttRole.busy
|
|
1301
1307
|
|| !host.availability.microphone
|
|
1302
|
-
|| !is.function(globalThis.MediaRecorder)
|
|
1303
|
-
|
|
1308
|
+
|| (!liveCapture && (!is.function(globalThis.MediaRecorder)
|
|
1309
|
+
|| !is.function(navigator.mediaDevices?.getUserMedia)))
|
|
1304
1310
|
|| recordingRequested
|
|
1305
1311
|
|| captureSession
|
|
1312
|
+
|| nativeCaptureSession
|
|
1306
1313
|
|| transcriptionActive
|
|
1307
1314
|
) {
|
|
1308
1315
|
resetRecordPress();
|
|
@@ -1322,6 +1329,8 @@
|
|
|
1322
1329
|
renderControls();
|
|
1323
1330
|
renderStatus();
|
|
1324
1331
|
|
|
1332
|
+
if (liveCapture) return startNativeRecognition(generation, operationId);
|
|
1333
|
+
|
|
1325
1334
|
let stream;
|
|
1326
1335
|
try {
|
|
1327
1336
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
@@ -1461,6 +1470,111 @@
|
|
|
1461
1470
|
}
|
|
1462
1471
|
}
|
|
1463
1472
|
|
|
1473
|
+
function startNativeRecognition(generation, operationId) {
|
|
1474
|
+
const session = {
|
|
1475
|
+
generation,
|
|
1476
|
+
operationId,
|
|
1477
|
+
controller: new AbortController(),
|
|
1478
|
+
capture: null,
|
|
1479
|
+
text: [],
|
|
1480
|
+
interim: '',
|
|
1481
|
+
cancelled: false,
|
|
1482
|
+
error: null
|
|
1483
|
+
};
|
|
1484
|
+
nativeCaptureSession = session;
|
|
1485
|
+
function currentNativeCapture() {
|
|
1486
|
+
return !destroyed && !session.cancelled
|
|
1487
|
+
&& nativeCaptureSession === session
|
|
1488
|
+
&& generation === recordingGeneration;
|
|
1489
|
+
}
|
|
1490
|
+
function displayNativeRecognitionText() {
|
|
1491
|
+
if (!currentNativeCapture()) return;
|
|
1492
|
+
localStatus = {
|
|
1493
|
+
message: session.text.join('') + session.interim || 'Listening. Release to finish.',
|
|
1494
|
+
tone: '',
|
|
1495
|
+
owner: 'recording',
|
|
1496
|
+
generation
|
|
1497
|
+
};
|
|
1498
|
+
renderStatus();
|
|
1499
|
+
}
|
|
1500
|
+
async function finishNativeRecognition(error = null) {
|
|
1501
|
+
if (!currentNativeCapture()) return;
|
|
1502
|
+
nativeCaptureSession = null;
|
|
1503
|
+
recordingRequested = false;
|
|
1504
|
+
resetRecordPress();
|
|
1505
|
+
clearRecordingStatus(generation);
|
|
1506
|
+
if (error && error !== session.error) {
|
|
1507
|
+
session.error = error;
|
|
1508
|
+
reportTranscriptionError(error);
|
|
1509
|
+
}
|
|
1510
|
+
// Keep confirmed final words after a service/cleanup failure. Only
|
|
1511
|
+
// explicit cancellation or supersession abandons this submission.
|
|
1512
|
+
if (session.text.length && !destroyed && !session.cancelled
|
|
1513
|
+
&& generation === recordingGeneration) {
|
|
1514
|
+
transcriptionOperationId = operationId;
|
|
1515
|
+
await transcribe(session.text.join(''));
|
|
1516
|
+
} else if (transcriptionOperationId === operationId) {
|
|
1517
|
+
transcriptionOperationId = null;
|
|
1518
|
+
}
|
|
1519
|
+
renderControls();
|
|
1520
|
+
renderStatus();
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
session.capture = providerRuntime.createTranscriptionCapture(
|
|
1524
|
+
{
|
|
1525
|
+
continuous: true,
|
|
1526
|
+
onSegment: function retainFinalRecognition(segment) {
|
|
1527
|
+
if (!currentNativeCapture()) return;
|
|
1528
|
+
session.text.push(segment.text);
|
|
1529
|
+
displayNativeRecognitionText();
|
|
1530
|
+
},
|
|
1531
|
+
onInterim: function displayInterimRecognition(result) {
|
|
1532
|
+
if (!currentNativeCapture()) return;
|
|
1533
|
+
session.interim = result.text;
|
|
1534
|
+
displayNativeRecognitionText();
|
|
1535
|
+
},
|
|
1536
|
+
onError: function reportNativeRecognitionFailure(error) {
|
|
1537
|
+
if (!currentNativeCapture()) return;
|
|
1538
|
+
session.error = error;
|
|
1539
|
+
reportTranscriptionError(error);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
if (!session.capture) throw new Error('The selected live transcription service is unavailable.');
|
|
1544
|
+
session.capture.done.then(
|
|
1545
|
+
function nativeRecognitionCompleted() {
|
|
1546
|
+
return finishNativeRecognition();
|
|
1547
|
+
},
|
|
1548
|
+
finishNativeRecognition
|
|
1549
|
+
).catch(
|
|
1550
|
+
function reportNativeRecognitionSettlementFailure(error) {
|
|
1551
|
+
if (destroyed || generation !== recordingGeneration) return;
|
|
1552
|
+
reportTranscriptionError(error);
|
|
1553
|
+
}
|
|
1554
|
+
);
|
|
1555
|
+
// No awaited preparation precedes the browser's gesture-bound start.
|
|
1556
|
+
const starting = session.capture.start({signal: session.controller.signal});
|
|
1557
|
+
return Promise.resolve(starting).catch(
|
|
1558
|
+
function reportNativeRecognitionStartupFailure(error) {
|
|
1559
|
+
if (!currentNativeCapture()) return false;
|
|
1560
|
+
session.capture.cancel();
|
|
1561
|
+
nativeCaptureSession = null;
|
|
1562
|
+
recordingRequested = false;
|
|
1563
|
+
resetRecordPress();
|
|
1564
|
+
if (error !== session.error) reportTranscriptionError(error);
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
session.capture?.cancel();
|
|
1570
|
+
nativeCaptureSession = null;
|
|
1571
|
+
recordingRequested = false;
|
|
1572
|
+
resetRecordPress();
|
|
1573
|
+
if (error !== session.error) reportTranscriptionError(error);
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1464
1578
|
function stopAfterSilence() {
|
|
1465
1579
|
clearSilenceTimer();
|
|
1466
1580
|
stop();
|
|
@@ -1483,6 +1597,12 @@
|
|
|
1483
1597
|
}
|
|
1484
1598
|
|
|
1485
1599
|
function stop() {
|
|
1600
|
+
if (nativeCaptureSession) {
|
|
1601
|
+
recordingRequested = false;
|
|
1602
|
+
nativeCaptureSession.capture.stop();
|
|
1603
|
+
renderControls();
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1486
1606
|
const pendingCaptureStart = recordingRequested && !captureSession;
|
|
1487
1607
|
const generation = recordingGeneration;
|
|
1488
1608
|
const operationId = transcriptionOperationId;
|
|
@@ -1703,6 +1823,7 @@
|
|
|
1703
1823
|
function cancelSTTOperation(reason) {
|
|
1704
1824
|
const hadActiveOperation = recordingRequested
|
|
1705
1825
|
|| Boolean(captureSession)
|
|
1826
|
+
|| Boolean(nativeCaptureSession)
|
|
1706
1827
|
|| transcriptionActive;
|
|
1707
1828
|
recordingGeneration += 1;
|
|
1708
1829
|
transcriptionGeneration += 1;
|
|
@@ -1713,6 +1834,14 @@
|
|
|
1713
1834
|
clearSilenceTimer();
|
|
1714
1835
|
resetRecordPress();
|
|
1715
1836
|
|
|
1837
|
+
if (nativeCaptureSession) {
|
|
1838
|
+
const session = nativeCaptureSession;
|
|
1839
|
+
nativeCaptureSession = null;
|
|
1840
|
+
session.cancelled = true;
|
|
1841
|
+
session.controller.abort();
|
|
1842
|
+
session.capture?.cancel();
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1716
1845
|
if (captureSession) {
|
|
1717
1846
|
captureSession.cancelled = true;
|
|
1718
1847
|
if (captureSession.recorder.state !== 'inactive') {
|
|
@@ -1769,6 +1898,7 @@
|
|
|
1769
1898
|
|
|
1770
1899
|
function handlePageHide(event) {
|
|
1771
1900
|
if (event.persisted === true) {
|
|
1901
|
+
if (nativeCaptureSession) stop();
|
|
1772
1902
|
return;
|
|
1773
1903
|
}
|
|
1774
1904
|
destroy();
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
<p id="description" class="description" part="description"></p>
|
|
83
83
|
<section id="transcript" class="transcript" part="transcript"></section>
|
|
84
84
|
<div class="transcription-feedback">
|
|
85
|
+
<p id="interim" class="status" part="interim" aria-live="polite" hidden></p>
|
|
85
86
|
<p id="status" class="status" part="status" role="status" aria-live="polite"></p>
|
|
86
87
|
<progress
|
|
87
88
|
id="sttActivationProgress"
|
|
@@ -136,16 +137,20 @@
|
|
|
136
137
|
{default:MD},
|
|
137
138
|
{appendTranscription,createSTTActivationController,normalizeVoiceOptions},
|
|
138
139
|
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
139
|
-
{default:ContinuousVoiceCapture}
|
|
140
|
+
{default:ContinuousVoiceCapture},
|
|
141
|
+
{getAIProviderRuntime}
|
|
140
142
|
]=await Promise.all([
|
|
141
143
|
import('../modules/MD.js'),
|
|
142
144
|
import('../modules/ComponentContracts.js'),
|
|
143
145
|
import('../modules/AIRuntimeState.js'),
|
|
144
|
-
import('../modules/ContinuousVoiceCapture.js')
|
|
146
|
+
import('../modules/ContinuousVoiceCapture.js'),
|
|
147
|
+
import('../modules/AIProviderRuntime.js')
|
|
145
148
|
]);
|
|
149
|
+
const providerRuntime=getAIProviderRuntime();
|
|
146
150
|
const description=host.shadowRoot.querySelector('#description');
|
|
147
151
|
const transcriptElement=host.shadowRoot.querySelector('#transcript');
|
|
148
152
|
const status=host.shadowRoot.querySelector('#status');
|
|
153
|
+
const interimElement=host.shadowRoot.querySelector('#interim');
|
|
149
154
|
const startButton=host.shadowRoot.querySelector('#start');
|
|
150
155
|
const sttActivationButton=host.shadowRoot.querySelector('#sttActivation');
|
|
151
156
|
const sttActivationProgress = host.shadowRoot.querySelector('#sttActivationProgress');
|
|
@@ -385,6 +390,7 @@
|
|
|
385
390
|
flushing:false,
|
|
386
391
|
processing:'idle',
|
|
387
392
|
queue:[],
|
|
393
|
+
interim:'',
|
|
388
394
|
failure:null,
|
|
389
395
|
captureError:null,
|
|
390
396
|
worker:null,
|
|
@@ -396,15 +402,25 @@
|
|
|
396
402
|
if(!isCurrentContinuousSession(session)||!session.listening){
|
|
397
403
|
return false;
|
|
398
404
|
}
|
|
399
|
-
|
|
405
|
+
const captureOptions={
|
|
400
406
|
...options.capture,
|
|
401
407
|
onSegment:function capturedSegment(clip){
|
|
402
408
|
if(!isCurrentContinuousSession(session)){
|
|
403
409
|
return;
|
|
404
410
|
}
|
|
405
|
-
session.queue.push({
|
|
411
|
+
session.queue.push({
|
|
412
|
+
...clip,
|
|
413
|
+
stage:is.string(clip.text)?'append':'transcribe',
|
|
414
|
+
segment:is.string(clip.text)?clip.text:null
|
|
415
|
+
});
|
|
416
|
+
renderInterimRecognition(session);
|
|
406
417
|
processContinuousQueue(session);
|
|
407
418
|
},
|
|
419
|
+
onInterim:function displayInterimRecognition(result){
|
|
420
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
421
|
+
session.interim=result.text;
|
|
422
|
+
renderInterimRecognition(session);
|
|
423
|
+
},
|
|
408
424
|
onState:function captureStateChanged(nextState){
|
|
409
425
|
if(!isCurrentContinuousSession(session)){
|
|
410
426
|
return;
|
|
@@ -412,8 +428,11 @@
|
|
|
412
428
|
session.captureState=nextState;
|
|
413
429
|
if(nextState==='stopped'||nextState==='interrupted'){
|
|
414
430
|
session.listening=false;
|
|
431
|
+
session.interim='';
|
|
432
|
+
renderInterimRecognition(session);
|
|
415
433
|
}
|
|
416
434
|
renderContinuousState(session);
|
|
435
|
+
settleContinuousDrain(session);
|
|
417
436
|
},
|
|
418
437
|
onError:function captureFailed(error){
|
|
419
438
|
if(!isCurrentContinuousSession(session)){
|
|
@@ -425,7 +444,29 @@
|
|
|
425
444
|
reportContinuousError(session,'capture',error);
|
|
426
445
|
settleContinuousDrain(session);
|
|
427
446
|
}
|
|
428
|
-
}
|
|
447
|
+
};
|
|
448
|
+
session.capture=!options.transcribe
|
|
449
|
+
?providerRuntime.createTranscriptionCapture(captureOptions):null;
|
|
450
|
+
session.native=Boolean(session.capture);
|
|
451
|
+
if(!session.capture){
|
|
452
|
+
session.capture=new ContinuousVoiceCapture(captureOptions);
|
|
453
|
+
}else{
|
|
454
|
+
session.capture.done.then(function nativeRecognitionEnded(){
|
|
455
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
456
|
+
session.listening=false;
|
|
457
|
+
session.interim='';
|
|
458
|
+
renderInterimRecognition(session);
|
|
459
|
+
if(!session.captureError) session.captureState='stopped';
|
|
460
|
+
renderContinuousState(session);
|
|
461
|
+
settleContinuousDrain(session);
|
|
462
|
+
}).catch(function nativeRecognitionCleanupFailed(error){
|
|
463
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
464
|
+
session.captureError=error;
|
|
465
|
+
session.listening=false;
|
|
466
|
+
reportContinuousError(session,'capture',error);
|
|
467
|
+
settleContinuousDrain(session);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
429
470
|
const started=await session.capture.start({
|
|
430
471
|
mediaConstraints:options.mediaConstraints,
|
|
431
472
|
signal:session.controller.signal
|
|
@@ -467,6 +508,17 @@
|
|
|
467
508
|
};
|
|
468
509
|
}
|
|
469
510
|
|
|
511
|
+
function renderInterimRecognition(session){
|
|
512
|
+
if(!isCurrentContinuousSession(session)) return;
|
|
513
|
+
const pending=[];
|
|
514
|
+
for(const clip of session.queue){
|
|
515
|
+
if(clip.stage==='append') pending.push(clip.segment);
|
|
516
|
+
}
|
|
517
|
+
const text=pending.join(options.separator)+session.interim;
|
|
518
|
+
interimElement.textContent=text;
|
|
519
|
+
interimElement.hidden=text.length===0;
|
|
520
|
+
}
|
|
521
|
+
|
|
470
522
|
function renderContinuousState(session){
|
|
471
523
|
if(!isCurrentContinuousSession(session)){
|
|
472
524
|
return;
|
|
@@ -598,7 +650,10 @@
|
|
|
598
650
|
throw new TypeError('Transcription must return the complete text as a string.');
|
|
599
651
|
}
|
|
600
652
|
clip.segment=result;
|
|
601
|
-
|
|
653
|
+
clip.stage='append';
|
|
654
|
+
}
|
|
655
|
+
if(clip.stage==='append'){
|
|
656
|
+
if(clip.segment.length===0){
|
|
602
657
|
session.queue.shift();
|
|
603
658
|
const detail={sequence:clip.sequence,transcript};
|
|
604
659
|
dispatchVoiceEvent('voice-transcription-empty',detail,{
|
|
@@ -606,10 +661,11 @@
|
|
|
606
661
|
});
|
|
607
662
|
continue;
|
|
608
663
|
}
|
|
609
|
-
clip.transcript=appendTranscription(transcript,
|
|
664
|
+
clip.transcript=appendTranscription(transcript,clip.segment,options.separator);
|
|
610
665
|
clip.stage='save';
|
|
611
666
|
transcript=clip.transcript;
|
|
612
667
|
renderTranscript();
|
|
668
|
+
renderInterimRecognition(session);
|
|
613
669
|
}
|
|
614
670
|
if(options.persist){
|
|
615
671
|
const save=options.onSave||(is.function(host.save)?host.save.bind(host):null);
|
|
@@ -740,6 +796,8 @@
|
|
|
740
796
|
return false;
|
|
741
797
|
}
|
|
742
798
|
continuousSession=null;
|
|
799
|
+
interimElement.textContent='';
|
|
800
|
+
interimElement.hidden=true;
|
|
743
801
|
session.controller.abort();
|
|
744
802
|
session.requestController?.abort();
|
|
745
803
|
session.capture?.cancel();
|
|
@@ -766,7 +824,8 @@
|
|
|
766
824
|
}
|
|
767
825
|
|
|
768
826
|
async function startRecording(){
|
|
769
|
-
if(options.capture.mode==='continuous'
|
|
827
|
+
if(options.capture.mode==='continuous'
|
|
828
|
+
||(!options.transcribe&&providerRuntime.supportsTranscriptionCapture())){
|
|
770
829
|
return startContinuousRecording();
|
|
771
830
|
}
|
|
772
831
|
if(!canStartVoiceRecording(sttRole,state,destroyed)){
|
|
@@ -1366,6 +1425,16 @@
|
|
|
1366
1425
|
sttActivationController.synchronize(sttRole);
|
|
1367
1426
|
if(continuousSession){
|
|
1368
1427
|
const session=continuousSession;
|
|
1428
|
+
if(session.native&&session.listening
|
|
1429
|
+
&&(sttRole.state!=='ready'
|
|
1430
|
+
||previousRole.providerId!==sttRole.providerId
|
|
1431
|
+
||previousRole.modelId!==sttRole.modelId)){
|
|
1432
|
+
session.capture?.cancel();
|
|
1433
|
+
session.listening=false;
|
|
1434
|
+
session.captureState='interrupted';
|
|
1435
|
+
session.interim='';
|
|
1436
|
+
renderInterimRecognition(session);
|
|
1437
|
+
}
|
|
1369
1438
|
if(['completing','complete'].includes(state)){
|
|
1370
1439
|
renderState();
|
|
1371
1440
|
return;
|
|
@@ -327,3 +327,44 @@ body{
|
|
|
327
327
|
scroll-behavior:auto !important;
|
|
328
328
|
transition-duration:.01ms !important;
|
|
329
329
|
}
|
|
330
|
+
|
|
331
|
+
/* Paper uses the existing base light palette regardless of the screen skin.
|
|
332
|
+
* Print-only importance also overrides custom-theme inline tokens; no saved
|
|
333
|
+
* preference, screen layout, media paint, or scrolling state changes here.
|
|
334
|
+
*/
|
|
335
|
+
@media print {
|
|
336
|
+
@page {
|
|
337
|
+
margin: 15mm;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
:root,
|
|
341
|
+
body,
|
|
342
|
+
[data-arcane-palette] {
|
|
343
|
+
color-scheme: light !important;
|
|
344
|
+
--background: rgb(255, 255, 255) !important;
|
|
345
|
+
--modal-background: rgb(255, 255, 255) !important;
|
|
346
|
+
--text-color: rgb(23, 34, 56) !important;
|
|
347
|
+
--primary-color: rgb(23, 34, 56) !important;
|
|
348
|
+
--secondary-color: rgb(229, 233, 243) !important;
|
|
349
|
+
--accent-color: var(--focus-color) !important;
|
|
350
|
+
--button-text-color: rgb(255, 255, 255) !important;
|
|
351
|
+
--border-color: rgb(203, 211, 226) !important;
|
|
352
|
+
--focus-color: rgb(118, 87, 213) !important;
|
|
353
|
+
--muted-text-color: rgb(95, 107, 130) !important;
|
|
354
|
+
--success-color: rgb(39, 122, 75) !important;
|
|
355
|
+
--warning-color: rgb(154, 101, 0) !important;
|
|
356
|
+
--danger-color: rgb(165, 56, 56) !important;
|
|
357
|
+
--review-color: rgb(133, 85, 165) !important;
|
|
358
|
+
--shadow-color: rgba(23, 34, 56, .16) !important;
|
|
359
|
+
--platinum: rgb(102, 112, 133) !important;
|
|
360
|
+
--icon-filter: none !important;
|
|
361
|
+
--file-icon-filter: hue-rotate(8deg) saturate(.62) brightness(.86) !important;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
html,
|
|
365
|
+
body {
|
|
366
|
+
margin: 0;
|
|
367
|
+
background-color: var(--modal-background);
|
|
368
|
+
color: var(--text-color);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -2081,8 +2081,10 @@ class AI {
|
|
|
2081
2081
|
#assertDeviceSpeechConfiguration(configuration){
|
|
2082
2082
|
for(const routeName of ['default','localOnly']){
|
|
2083
2083
|
const selection=configuration?.stt?.[routeName];
|
|
2084
|
-
|
|
2085
|
-
|
|
2084
|
+
const liveCapture=selection
|
|
2085
|
+
&&this.#providerRuntime.supportsTranscriptionCapture(selection.providerId);
|
|
2086
|
+
if(selection&&selection.localOnly!==true&&!liveCapture){
|
|
2087
|
+
const error=new TypeError('Speech recognition requires an on-device or explicitly selected live-capture provider.');
|
|
2086
2088
|
error.code='AI_STT_DEVICE_ONLY';
|
|
2087
2089
|
throw error;
|
|
2088
2090
|
}
|
|
@@ -3430,8 +3432,9 @@ class AI {
|
|
|
3430
3432
|
if(provider!==null&&provider?.role!==role){
|
|
3431
3433
|
throw new TypeError('The speech provider must implement the selected role.');
|
|
3432
3434
|
}
|
|
3433
|
-
if(role==='stt'&&provider&&provider.localOnly!==true
|
|
3434
|
-
|
|
3435
|
+
if(role==='stt'&&provider&&provider.localOnly!==true
|
|
3436
|
+
&&!is.function(provider.createCapture)){
|
|
3437
|
+
const error=new TypeError('Speech recognition requires an on-device or explicitly selected live-capture provider.');
|
|
3435
3438
|
error.code='AI_STT_DEVICE_ONLY';
|
|
3436
3439
|
throw error;
|
|
3437
3440
|
}
|
|
@@ -6875,6 +6878,14 @@ class AI {
|
|
|
6875
6878
|
}
|
|
6876
6879
|
}
|
|
6877
6880
|
|
|
6881
|
+
supportsTranscriptionCapture(){
|
|
6882
|
+
return this.#providerRuntime.supportsTranscriptionCapture();
|
|
6883
|
+
}
|
|
6884
|
+
|
|
6885
|
+
createTranscriptionCapture(options={}){
|
|
6886
|
+
return this.#providerRuntime.createTranscriptionCapture(options);
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6878
6889
|
async fetchSTT(audioFile,signal=null){
|
|
6879
6890
|
const callId=this.#traceSpeech('fetchSTT.call',{audioFile,aborted:signal?.aborted});
|
|
6880
6891
|
try{
|
|
@@ -1366,6 +1366,9 @@ function validateProvider(provider) {
|
|
|
1366
1366
|
for (const method of PROVIDER_METHODS) {
|
|
1367
1367
|
record[method] = provider[method].bind(provider);
|
|
1368
1368
|
}
|
|
1369
|
+
if (provider.role === 'stt' && is.function(provider.createCapture)) {
|
|
1370
|
+
record.createCapture = provider.createCapture.bind(provider);
|
|
1371
|
+
}
|
|
1369
1372
|
const admitted = completeValue(record);
|
|
1370
1373
|
PROVIDER_RECORDS.set(provider, admitted);
|
|
1371
1374
|
return admitted;
|
|
@@ -4103,6 +4106,121 @@ export class AIProviderRuntime {
|
|
|
4103
4106
|
return this.#roleRequestAlias('llm', 'stream', payload, options);
|
|
4104
4107
|
}
|
|
4105
4108
|
|
|
4109
|
+
supportsTranscriptionCapture(providerId = null) {
|
|
4110
|
+
const slot = this.#slots.stt;
|
|
4111
|
+
const provider = providerId === null
|
|
4112
|
+
? this.#providerFor(slot)
|
|
4113
|
+
: this.#providers.get(providerKey('stt', providerId));
|
|
4114
|
+
return is.function(provider?.createCapture);
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
createTranscriptionCapture(options = {}) {
|
|
4118
|
+
this.#assertOpen();
|
|
4119
|
+
this.#assertNotConfiguring();
|
|
4120
|
+
const slot = this.#slots.stt;
|
|
4121
|
+
const provider = this.#providerFor(slot);
|
|
4122
|
+
if (!is.function(provider?.createCapture)) return null;
|
|
4123
|
+
|
|
4124
|
+
const runtime = this;
|
|
4125
|
+
const generation = slot.generation;
|
|
4126
|
+
const capture = provider.createCapture(options);
|
|
4127
|
+
const controller = new AbortController();
|
|
4128
|
+
let requestRecord = null;
|
|
4129
|
+
let detachSignal = null;
|
|
4130
|
+
let started = false;
|
|
4131
|
+
let closed = false;
|
|
4132
|
+
const done = Promise.resolve(capture.done).finally(
|
|
4133
|
+
function releaseTranscriptionCapture() {
|
|
4134
|
+
closed = true;
|
|
4135
|
+
detachSignal?.();
|
|
4136
|
+
if (!requestRecord) return;
|
|
4137
|
+
slot.activeRequests.delete(requestRecord.requestSequence);
|
|
4138
|
+
runtime.#drainRoleRequestQueue(slot);
|
|
4139
|
+
if (slot.generation !== generation || slot.unloadPromise) return;
|
|
4140
|
+
if (slot.activeRequests.size || slot.requestQueue.length) {
|
|
4141
|
+
runtime.#publishRoleRequestState(slot);
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
const status = validateProviderStatus(provider.status());
|
|
4145
|
+
slot.ready = status.state === 'ready' && status.loaded && !status.busy;
|
|
4146
|
+
publishAIRuntimeRoleState(
|
|
4147
|
+
'stt',
|
|
4148
|
+
roleRecord(
|
|
4149
|
+
'stt',
|
|
4150
|
+
slot.selection,
|
|
4151
|
+
{state: slot.ready ? 'ready' : 'unloaded', loaded: status.loaded}
|
|
4152
|
+
)
|
|
4153
|
+
);
|
|
4154
|
+
}
|
|
4155
|
+
);
|
|
4156
|
+
done.catch(
|
|
4157
|
+
function observeTranscriptionCaptureCleanupFailure(error) {
|
|
4158
|
+
arcaneLogging.error('Transcription capture cleanup failed.', error);
|
|
4159
|
+
}
|
|
4160
|
+
);
|
|
4161
|
+
// Capture has its own synchronous start boundary so native microphone
|
|
4162
|
+
// permission retains the originating user gesture. It is never queued.
|
|
4163
|
+
return {
|
|
4164
|
+
done,
|
|
4165
|
+
start({signal} = {}) {
|
|
4166
|
+
if (started || closed) return Promise.resolve(false);
|
|
4167
|
+
try {
|
|
4168
|
+
runtime.#assertOpen();
|
|
4169
|
+
runtime.#assertNotConfiguring();
|
|
4170
|
+
const status = validateProviderStatus(provider.status());
|
|
4171
|
+
if (slot.generation !== generation || !slot.ready
|
|
4172
|
+
|| slot.unloadPromise || slot.disposePromise
|
|
4173
|
+
|| slot.activeRequests.size || slot.requestQueue.length
|
|
4174
|
+
|| status.state !== 'ready' || !status.loaded || status.busy) {
|
|
4175
|
+
throw operationError(
|
|
4176
|
+
'The selected transcription capture provider is not ready.',
|
|
4177
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
assertAbortSignal(signal);
|
|
4181
|
+
started = true;
|
|
4182
|
+
detachSignal = runtime.#forwardAbort(signal, controller);
|
|
4183
|
+
const requestSequence = runtime.#nextRequestSequence(slot);
|
|
4184
|
+
requestRecord = {
|
|
4185
|
+
controller,
|
|
4186
|
+
requestSequence,
|
|
4187
|
+
operationId: runtime.#nextOperationId(slot, 'capture'),
|
|
4188
|
+
promise: done,
|
|
4189
|
+
cancel: function cancelOwnedTranscriptionCapture(reason) {
|
|
4190
|
+
controller.abort(reason);
|
|
4191
|
+
capture.cancel();
|
|
4192
|
+
return done;
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
slot.activeRequests.set(requestSequence, requestRecord);
|
|
4196
|
+
runtime.#publishRoleRequestState(slot);
|
|
4197
|
+
const starting = capture.start({signal: controller.signal});
|
|
4198
|
+
return Promise.resolve(starting).catch(
|
|
4199
|
+
function releaseFailedTranscriptionCaptureStart(error) {
|
|
4200
|
+
capture.cancel();
|
|
4201
|
+
throw error;
|
|
4202
|
+
}
|
|
4203
|
+
);
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
capture.cancel();
|
|
4206
|
+
throw error;
|
|
4207
|
+
}
|
|
4208
|
+
},
|
|
4209
|
+
stop() {
|
|
4210
|
+
capture.stop();
|
|
4211
|
+
return done;
|
|
4212
|
+
},
|
|
4213
|
+
cancel() {
|
|
4214
|
+
controller.abort();
|
|
4215
|
+
capture.cancel();
|
|
4216
|
+
},
|
|
4217
|
+
destroy() {
|
|
4218
|
+
controller.abort();
|
|
4219
|
+
capture.destroy();
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4106
4224
|
transcribe(payload, options = {}) {
|
|
4107
4225
|
arcaneLogging.debug('[Arcane speech runtime] transcribe', payload, options);
|
|
4108
4226
|
return this.#roleRequestAlias('stt', 'transcribe', payload, options);
|
|
@@ -6,6 +6,20 @@ const markdownOptions = {
|
|
|
6
6
|
async: false,
|
|
7
7
|
pedantic: false,
|
|
8
8
|
gfm: true,
|
|
9
|
+
tokenizer: {
|
|
10
|
+
html(src) {
|
|
11
|
+
const native = marked.Tokenizer.prototype.html.call(this, src);
|
|
12
|
+
if (native) return native;
|
|
13
|
+
// Quoted image values can contain blank lines that would otherwise
|
|
14
|
+
// split the tag into Markdown paragraphs before inline HTML parsing.
|
|
15
|
+
const image = /^ {0,3}<img(?:\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>(?=[ \t]*(?:\n|$))/i;
|
|
16
|
+
const match = image.exec(src);
|
|
17
|
+
if (!match) return;
|
|
18
|
+
const quotedValues = match[0].match(/"[^"]*"|'[^']*'/g) || [];
|
|
19
|
+
if (!quotedValues.some(function containsNewline(value) {return value.includes('\n');})) return;
|
|
20
|
+
return {type:'html', block:true, raw:match[0], text:match[0]};
|
|
21
|
+
}
|
|
22
|
+
},
|
|
9
23
|
renderer: {
|
|
10
24
|
link(href, title, text) {
|
|
11
25
|
const link = marked.Renderer.prototype.link.call(this, href, title, text);
|