arcane-os 0.63.0 → 0.65.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 +48 -0
- package/README.md +1 -1
- package/browser-runtime/core/client.mjs +69 -7
- package/docs/guides/native-whisper-openvino.md +334 -0
- package/docs/reference/README.md +6 -0
- package/docs/reference/arcane-ollama.md +1 -1
- package/docs/reference/codex-app-server.md +269 -0
- package/docs/reference/core-client.md +71 -4
- package/docs/reference/core-development.md +157 -0
- package/docs/reference/core-native-packaging.md +48 -6
- package/docs/reference/core-repositories.md +258 -0
- package/docs/reference/inventory/package-api.json +199 -3
- package/docs/reference/inventory/runtime-modules.json +3 -2
- package/docs/reference/local-ai.md +77 -4
- package/docs/reference/local-image-flux.md +1 -1
- package/docs/reference/local-image-generation.md +11 -0
- package/docs/reference/local-image-sdxl.md +139 -0
- package/docs/reference/local-whisper.md +219 -0
- package/docs/reference/mcp-stdio.md +263 -0
- package/docs/reference/native-speech.md +51 -2
- package/docs/reference/runtime-components.md +18 -1
- package/docs/reference/runtime-modules.md +26 -4
- package/docs/reference/sdk-api.md +412 -2
- package/package.json +9 -2
- package/runtime/arcane/components/speech.html +15 -3
- package/runtime/arcane/components/voice-transcription.html +19 -3
- package/runtime/arcane/modules/AI.js +406 -40
- package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
- package/runtime/arcane/modules/AIRuntimeState.js +15 -2
- package/runtime/arcane/modules/ComponentContracts.js +3 -0
- package/runtime/arcane/modules/Ollama.js +1 -1
- package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
- package/schemas/arcane-app.schema.json +60 -2
- package/schemas/arcane-package.schema.json +60 -2
- package/src/codex/app-server.mjs +845 -0
- package/src/core/development.mjs +457 -0
- package/src/core/repositories.mjs +375 -0
- package/src/core/services/local-ai.mjs +80 -10
- package/src/core/services/speech.mjs +1 -0
- package/src/dev-server.mjs +3 -2
- package/src/index.mjs +1 -0
- package/src/local-ai/config.mjs +7 -3
- package/src/local-ai/dev.mjs +85 -401
- package/src/local-ai/image-library.mjs +9 -0
- package/src/local-ai/image-models/sdxl.mjs +18 -0
- package/src/local-ai/install.mjs +26 -7
- package/src/local-ai/whisper/build.mjs +48 -0
- package/src/local-ai/whisper/config.mjs +89 -0
- package/src/local-ai/whisper/index.mjs +572 -0
- package/src/local-ai/whisper/install.mjs +332 -0
- package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
- package/src/local-ai/whisper/native/NOTICE +18 -0
- package/src/local-ai/whisper/native/README.md +104 -0
- package/src/local-ai/whisper/native/main.cpp +520 -0
- package/src/local-ai/whisper/openvino-build.mjs +138 -0
- package/src/mcp/stdio-server.mjs +579 -0
- package/src/native/portable-layout.mjs +39 -7
- package/src/toolchain.mjs +19 -8
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
const [
|
|
136
136
|
{default:MD},
|
|
137
137
|
{appendTranscription,createSTTActivationController,normalizeVoiceOptions},
|
|
138
|
-
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
138
|
+
{continuesAIRuntimeOperation,requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
139
139
|
{default:ContinuousVoiceCapture},
|
|
140
140
|
{getAIProviderRuntime}
|
|
141
141
|
]=await Promise.all([
|
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
let recorder=null;
|
|
166
166
|
let state='idle';
|
|
167
167
|
let stateMessage='';
|
|
168
|
+
let transcriptionRuntimeOperationId=null;
|
|
168
169
|
let sttRole=unavailableRole('stt');
|
|
169
170
|
let transcript='';
|
|
170
171
|
let sessionGeneration=0;
|
|
@@ -1121,7 +1122,17 @@
|
|
|
1121
1122
|
error.code='ARCANE_VOICE_TRANSCRIPTION_STT_RUNTIME_METHOD_UNAVAILABLE';
|
|
1122
1123
|
throw error;
|
|
1123
1124
|
}
|
|
1124
|
-
|
|
1125
|
+
const previousOperationId=providerRuntime.status('stt').operationId;
|
|
1126
|
+
const pending=globalThis.ai.fetchSTT(file,signal);
|
|
1127
|
+
const activeRole=providerRuntime.status('stt');
|
|
1128
|
+
const operationId=activeRole.busy&&activeRole.operationId!==previousOperationId
|
|
1129
|
+
?activeRole.operationId:null;
|
|
1130
|
+
transcriptionRuntimeOperationId=operationId;
|
|
1131
|
+
try{
|
|
1132
|
+
return await pending;
|
|
1133
|
+
}finally{
|
|
1134
|
+
if(transcriptionRuntimeOperationId===operationId)transcriptionRuntimeOperationId=null;
|
|
1135
|
+
}
|
|
1125
1136
|
}
|
|
1126
1137
|
|
|
1127
1138
|
function isTranscriptionCancellation(error,controller){
|
|
@@ -1417,6 +1428,9 @@
|
|
|
1417
1428
|
function synchronizeAIRuntimeState(snapshot){
|
|
1418
1429
|
const previousRole=sttRole;
|
|
1419
1430
|
sttRole=snapshot.roles.stt;
|
|
1431
|
+
const continuingTranscription=continuesAIRuntimeOperation(
|
|
1432
|
+
previousRole,sttRole,transcriptionRuntimeOperationId
|
|
1433
|
+
);
|
|
1420
1434
|
if(sttRole.state==='error'&&previousRole.state!=='error'){
|
|
1421
1435
|
arcaneLogging.error('Arcane transcription runtime failed.',sttRole.error);
|
|
1422
1436
|
}
|
|
@@ -1438,6 +1452,7 @@
|
|
|
1438
1452
|
return;
|
|
1439
1453
|
}
|
|
1440
1454
|
if(session.processing==='transcribing'
|
|
1455
|
+
&&!continuingTranscription
|
|
1441
1456
|
&&(sttRole.state!=='ready'
|
|
1442
1457
|
||previousRole.providerId!==sttRole.providerId
|
|
1443
1458
|
||previousRole.modelId!==sttRole.modelId)){
|
|
@@ -1447,7 +1462,8 @@
|
|
|
1447
1462
|
renderContinuousState(session);
|
|
1448
1463
|
return;
|
|
1449
1464
|
}
|
|
1450
|
-
if(
|
|
1465
|
+
if(['ready','recovering'].includes(previousRole.state)
|
|
1466
|
+
&&sttRole.state!=='ready'&&!continuingTranscription){
|
|
1451
1467
|
if(cancelSTTOperation('runtime-unready')){
|
|
1452
1468
|
return;
|
|
1453
1469
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
AI_PROVIDER_PROTOCOL,
|
|
16
16
|
getAIProviderRuntime
|
|
17
17
|
} from './AIProviderRuntime.js';
|
|
18
|
-
import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
18
|
+
import {normalizeOllamaModelIdentifier,sameOllamaModelIdentifier} from './OllamaModelIdentifier.js';
|
|
19
19
|
import {arcaneLogging} from 'arcane-os/logging';
|
|
20
20
|
import {
|
|
21
21
|
fetchHTTPResponse,
|
|
@@ -1371,11 +1371,115 @@ class AI {
|
|
|
1371
1371
|
const localOnly=providerId==='OLLAMA';
|
|
1372
1372
|
let state='unloaded';
|
|
1373
1373
|
let busy=false;
|
|
1374
|
+
let nativeWatch=null;
|
|
1375
|
+
|
|
1376
|
+
function nativeOllamaReady(){
|
|
1377
|
+
if(!nativeWatch
|
|
1378
|
+
||nativeWatch.ollama!==runtime.#nativeOllama()
|
|
1379
|
+
||nativeWatch.modelId!==runtime.model
|
|
1380
|
+
||nativeWatch.error){
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
const snapshot=nativeWatch.snapshot;
|
|
1384
|
+
return snapshot?.available===true
|
|
1385
|
+
&&(snapshot.models??[]).some(function selectedResident(model){
|
|
1386
|
+
return sameOllamaModelIdentifier(model.id??model.model??model.name,nativeWatch.modelId)
|
|
1387
|
+
&&model.loaded===true;
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function stopNativeOllamaWatch(){
|
|
1392
|
+
const previous=nativeWatch;
|
|
1393
|
+
nativeWatch=null;
|
|
1394
|
+
previous?.unsubscribe?.();
|
|
1395
|
+
return previous;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function observeNativeOllama(watch,snapshot,error=null){
|
|
1399
|
+
if(nativeWatch!==watch){
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
watch.snapshot=snapshot;
|
|
1403
|
+
watch.error=error;
|
|
1404
|
+
if(state==='ready'&&!nativeOllamaReady()){
|
|
1405
|
+
state='unloaded';
|
|
1406
|
+
runtime.#retainBuiltInLLMReadiness(
|
|
1407
|
+
runtime.#reconcileBuiltInLLMReadiness()
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async function inspectNativeOllama(watch,signal){
|
|
1413
|
+
if(signal?.aborted){
|
|
1414
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1415
|
+
}
|
|
1416
|
+
const revision=watch.revision;
|
|
1417
|
+
try{
|
|
1418
|
+
const running=await watch.ollama.running({signal});
|
|
1419
|
+
if(signal?.aborted){
|
|
1420
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
1421
|
+
}
|
|
1422
|
+
if(nativeWatch===watch&&watch.revision===revision){
|
|
1423
|
+
observeNativeOllama(watch,{
|
|
1424
|
+
available:true,
|
|
1425
|
+
models:(running.models??[]).map(function residentModel(model){
|
|
1426
|
+
return {...model,loaded:true};
|
|
1427
|
+
})
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}catch(error){
|
|
1431
|
+
if(!signal?.aborted&&watch.revision===revision){
|
|
1432
|
+
observeNativeOllama(watch,null,error);
|
|
1433
|
+
}
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
if(nativeWatch!==watch||!nativeOllamaReady()){
|
|
1437
|
+
throw watch.error??aiProviderError(
|
|
1438
|
+
'The selected Ollama model is not loaded.',
|
|
1439
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function loadNativeOllama(context){
|
|
1445
|
+
stopNativeOllamaWatch();
|
|
1446
|
+
const ollama=runtime.#nativeOllama();
|
|
1447
|
+
if(!is.function(ollama?.generate)||!is.function(ollama?.running)){
|
|
1448
|
+
throw aiProviderError(
|
|
1449
|
+
'The native Ollama API does not expose model loading and resident status.',
|
|
1450
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
const watch={
|
|
1454
|
+
ollama,modelId:context.selection.modelId,
|
|
1455
|
+
snapshot:null,error:null,revision:0,unsubscribe:null
|
|
1456
|
+
};
|
|
1457
|
+
nativeWatch=watch;
|
|
1458
|
+
if(is.function(globalThis.Arcane?.events?.on)){
|
|
1459
|
+
watch.unsubscribe=globalThis.Arcane.events.on(
|
|
1460
|
+
'localai.state',
|
|
1461
|
+
function observeNativeOllamaState(snapshot){
|
|
1462
|
+
watch.revision+=1;
|
|
1463
|
+
const engine=snapshot.ollama??snapshot.runtimes?.find(function ollamaEngine(record){
|
|
1464
|
+
return record.id==='ollama';
|
|
1465
|
+
});
|
|
1466
|
+
observeNativeOllama(watch,engine??null,engine?.error??null);
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
// Empty generation is Ollama's public preload operation. Residency
|
|
1471
|
+
// is established separately; a successful response alone is insufficient.
|
|
1472
|
+
await ollama.generate(
|
|
1473
|
+
{model:watch.modelId,prompt:'',stream:false},
|
|
1474
|
+
{signal:context.signal}
|
|
1475
|
+
);
|
|
1476
|
+
await inspectNativeOllama(watch,context.signal);
|
|
1477
|
+
}
|
|
1374
1478
|
|
|
1375
1479
|
function statusBuiltInLLMProvider(){
|
|
1376
1480
|
if(state==='ready'
|
|
1377
|
-
&&!
|
|
1378
|
-
|
|
1481
|
+
&&((localOnly&&!nativeOllamaReady())
|
|
1482
|
+
||(!busy&&!runtime.#builtInLLMCapability(providerId)))){
|
|
1379
1483
|
state='unloaded';
|
|
1380
1484
|
}
|
|
1381
1485
|
return completeValue({
|
|
@@ -1423,7 +1527,7 @@ class AI {
|
|
|
1423
1527
|
return runtime.#builtInLLMInspection(providerId,selection);
|
|
1424
1528
|
},
|
|
1425
1529
|
status:statusBuiltInLLMProvider,
|
|
1426
|
-
load:function loadBuiltInLLMProvider(context={}){
|
|
1530
|
+
load:async function loadBuiltInLLMProvider(context={}){
|
|
1427
1531
|
if(context.signal?.aborted){
|
|
1428
1532
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1429
1533
|
}
|
|
@@ -1447,7 +1551,7 @@ class AI {
|
|
|
1447
1551
|
const inspection=assertBuiltInLLMSelection(context.selection);
|
|
1448
1552
|
state='loading';
|
|
1449
1553
|
context.progress({
|
|
1450
|
-
phase:'capability',
|
|
1554
|
+
phase:localOnly?'loading':'capability',
|
|
1451
1555
|
completed:0,
|
|
1452
1556
|
total:1,
|
|
1453
1557
|
unit:'items',
|
|
@@ -1457,9 +1561,18 @@ class AI {
|
|
|
1457
1561
|
state='unloaded';
|
|
1458
1562
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1459
1563
|
}
|
|
1564
|
+
if(localOnly){
|
|
1565
|
+
try{
|
|
1566
|
+
await loadNativeOllama(context);
|
|
1567
|
+
}catch(error){
|
|
1568
|
+
state='unloaded';
|
|
1569
|
+
stopNativeOllamaWatch();
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1460
1573
|
state='ready';
|
|
1461
1574
|
context.progress({
|
|
1462
|
-
phase:'capability',
|
|
1575
|
+
phase:localOnly?'loading':'capability',
|
|
1463
1576
|
completed:1,
|
|
1464
1577
|
total:1,
|
|
1465
1578
|
unit:'items',
|
|
@@ -1489,24 +1602,39 @@ class AI {
|
|
|
1489
1602
|
);
|
|
1490
1603
|
}
|
|
1491
1604
|
busy=true;
|
|
1605
|
+
const watch=nativeWatch;
|
|
1492
1606
|
if(context.operation==='chat'){
|
|
1493
|
-
return
|
|
1494
|
-
|
|
1607
|
+
return (async function requestLoadedBuiltInLLM(){
|
|
1608
|
+
if(localOnly){
|
|
1609
|
+
await inspectNativeOllama(watch,context.signal);
|
|
1610
|
+
}
|
|
1611
|
+
const result=await runtime.#requestBuiltInLLMChat(
|
|
1495
1612
|
context.payload,
|
|
1496
1613
|
context.signal,
|
|
1497
1614
|
controls.onRetry
|
|
1498
|
-
)
|
|
1499
|
-
|
|
1615
|
+
);
|
|
1616
|
+
if(localOnly){
|
|
1617
|
+
await inspectNativeOllama(watch,context.signal);
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1620
|
+
})().finally(releaseBuiltInLLMRequest);
|
|
1500
1621
|
}
|
|
1501
1622
|
if(context.operation==='stream'){
|
|
1502
1623
|
const handle=createBuiltInAIStreamBridge(
|
|
1503
|
-
function executeBuiltInLLMProviderStream(bridge){
|
|
1504
|
-
|
|
1624
|
+
async function executeBuiltInLLMProviderStream(bridge){
|
|
1625
|
+
if(localOnly){
|
|
1626
|
+
await inspectNativeOllama(watch,bridge.signal);
|
|
1627
|
+
}
|
|
1628
|
+
const result=await runtime.#requestBuiltInLLMStream(
|
|
1505
1629
|
context.payload,
|
|
1506
1630
|
bridge,
|
|
1507
1631
|
controls.observeToolText,
|
|
1508
1632
|
controls.onRetry
|
|
1509
1633
|
);
|
|
1634
|
+
if(localOnly){
|
|
1635
|
+
await inspectNativeOllama(watch,bridge.signal);
|
|
1636
|
+
}
|
|
1637
|
+
return result;
|
|
1510
1638
|
},
|
|
1511
1639
|
context.signal
|
|
1512
1640
|
);
|
|
@@ -1522,18 +1650,27 @@ class AI {
|
|
|
1522
1650
|
'ARCANE_AI_PROVIDER_RUNTIME_INVALID'
|
|
1523
1651
|
);
|
|
1524
1652
|
},
|
|
1525
|
-
unload:function unloadBuiltInLLMProvider(context={}){
|
|
1653
|
+
unload:async function unloadBuiltInLLMProvider(context={}){
|
|
1526
1654
|
if(context.signal?.aborted){
|
|
1527
1655
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1528
1656
|
}
|
|
1657
|
+
const watch=stopNativeOllamaWatch();
|
|
1658
|
+
const release=localOnly&&state==='ready';
|
|
1529
1659
|
state='unloaded';
|
|
1530
1660
|
busy=false;
|
|
1661
|
+
if(release&&watch){
|
|
1662
|
+
await watch.ollama.generate(
|
|
1663
|
+
{model:watch.modelId,prompt:'',keep_alive:0,stream:false},
|
|
1664
|
+
{signal:context.signal}
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1531
1667
|
return statusBuiltInLLMProvider();
|
|
1532
1668
|
},
|
|
1533
1669
|
dispose:function disposeBuiltInLLMProvider(context={}){
|
|
1534
1670
|
if(context.signal?.aborted){
|
|
1535
1671
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1536
1672
|
}
|
|
1673
|
+
stopNativeOllamaWatch();
|
|
1537
1674
|
state='disposed';
|
|
1538
1675
|
busy=false;
|
|
1539
1676
|
return statusBuiltInLLMProvider();
|
|
@@ -1615,17 +1752,205 @@ class AI {
|
|
|
1615
1752
|
const expectedOperation=role==='stt'?'transcribe':'synthesize';
|
|
1616
1753
|
let state='unloaded';
|
|
1617
1754
|
let busy=false;
|
|
1755
|
+
let nativeWatch=null;
|
|
1756
|
+
let nativeRequest=null;
|
|
1757
|
+
|
|
1758
|
+
function continuingNativeSpeechRequest(){
|
|
1759
|
+
const selected=nativeWatch?.snapshot?.roles?.[role];
|
|
1760
|
+
return role==='stt'&&busy&&nativeRequest?.requestId
|
|
1761
|
+
&&nativeRequest.watch===nativeWatch&&!nativeRequest.signal?.aborted
|
|
1762
|
+
&&nativeWatch.speech===runtime.#nativeSpeech(providerId,role)
|
|
1763
|
+
&&nativeWatch.modelId===runtime.#builtInSpeechModel(role)
|
|
1764
|
+
&&nativeWatch.snapshot.status==='ok'&&!nativeWatch.snapshot.closed
|
|
1765
|
+
&&selected?.modelId===nativeWatch.modelId
|
|
1766
|
+
&&selected.providerId===nativeWatch.engineId
|
|
1767
|
+
&&selected.requestId===nativeRequest.requestId
|
|
1768
|
+
&&selected.loaded===false
|
|
1769
|
+
&&((selected.state==='recovering'&&selected.busy===true)
|
|
1770
|
+
||selected.state==='error');
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function nativeSpeechReady(){
|
|
1774
|
+
if(!nativeWatch
|
|
1775
|
+
||nativeWatch.speech!==runtime.#nativeSpeech(providerId,role)
|
|
1776
|
+
||nativeWatch.modelId!==runtime.#builtInSpeechModel(role)){
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
const snapshot=nativeWatch.snapshot;
|
|
1780
|
+
const selected=snapshot?.roles?.[role];
|
|
1781
|
+
if(selected){
|
|
1782
|
+
return selected.modelId===nativeWatch.modelId
|
|
1783
|
+
&&selected.providerId===nativeWatch.engineId
|
|
1784
|
+
&&selected.loaded===true
|
|
1785
|
+
&&selected.available===true
|
|
1786
|
+
&&['ready','running'].includes(selected.state);
|
|
1787
|
+
}
|
|
1788
|
+
// Older fixed-model hosts expose independent health, not engine lifecycle.
|
|
1789
|
+
return snapshot?.status==='ok'
|
|
1790
|
+
&&nativeWatch.modelId===(role==='stt'?'whisper-small':'kokoro')
|
|
1791
|
+
&&snapshot[role==='stt'?'sttEngine':'ttsEngine']
|
|
1792
|
+
===(role==='stt'?'whisper.cpp':'kokoro-onnx')
|
|
1793
|
+
&&snapshot[role==='stt'?'transcriptionAvailable':'synthesisAvailable']===true;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function nativeSpeechReadinessError(){
|
|
1797
|
+
const snapshot=nativeWatch?.snapshot;
|
|
1798
|
+
if(nativeWatch?.error){
|
|
1799
|
+
return nativeWatch.error;
|
|
1800
|
+
}
|
|
1801
|
+
if(!snapshot){
|
|
1802
|
+
return aiProviderError(
|
|
1803
|
+
'The native speech API returned no readiness status.',
|
|
1804
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const selected=snapshot.roles?.[role];
|
|
1808
|
+
if(selected?.modelId&&selected.modelId!==nativeWatch.modelId){
|
|
1809
|
+
return aiProviderError(
|
|
1810
|
+
`The native ${role.toUpperCase()} model differs from the selected AI model.`,
|
|
1811
|
+
'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
if(selected?.providerId&&selected.providerId!==nativeWatch.engineId){
|
|
1815
|
+
return aiProviderError(
|
|
1816
|
+
`The native ${role.toUpperCase()} engine was replaced.`,
|
|
1817
|
+
'ARCANE_AI_MODEL_AUTHORITY_REQUIRED'
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
if(snapshot.closed===true
|
|
1821
|
+
||['closing','closed'].includes(snapshot.status)
|
|
1822
|
+
||['error','unavailable','closed','disposed'].includes(selected?.state)
|
|
1823
|
+
||(!selected&&!nativeSpeechReady())){
|
|
1824
|
+
return aiProviderError(
|
|
1825
|
+
`The native ${role.toUpperCase()} model is not ready.`,
|
|
1826
|
+
'ARCANE_AI_ROLE_NOT_READY',
|
|
1827
|
+
selected?.error
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function stopNativeSpeechWatch(){
|
|
1834
|
+
const previous=nativeWatch;
|
|
1835
|
+
nativeWatch=null;
|
|
1836
|
+
previous?.unsubscribe?.();
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function waitForNativeSpeech(context){
|
|
1840
|
+
stopNativeSpeechWatch();
|
|
1841
|
+
const watch={
|
|
1842
|
+
speech:runtime.#nativeSpeech(providerId,role),
|
|
1843
|
+
modelId:context.selection.modelId,
|
|
1844
|
+
engineId:null,
|
|
1845
|
+
snapshot:null,
|
|
1846
|
+
error:null,
|
|
1847
|
+
revision:0,
|
|
1848
|
+
unsubscribe:null
|
|
1849
|
+
};
|
|
1850
|
+
nativeWatch=watch;
|
|
1851
|
+
return new Promise(function waitForNativeSpeechState(resolve,reject){
|
|
1852
|
+
let settled=false;
|
|
1853
|
+
function finish(error){
|
|
1854
|
+
if(settled){
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
settled=true;
|
|
1858
|
+
context.signal?.removeEventListener('abort',cancel);
|
|
1859
|
+
if(error){
|
|
1860
|
+
reject(error);
|
|
1861
|
+
}else{
|
|
1862
|
+
resolve();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function cancel(){
|
|
1866
|
+
finish(normalizeAIRequestAbort(context.signal.reason));
|
|
1867
|
+
}
|
|
1868
|
+
function reportObservationFailure(error){
|
|
1869
|
+
finish(error);
|
|
1870
|
+
arcaneLogging.error('Native speech readiness observation failed.',error);
|
|
1871
|
+
}
|
|
1872
|
+
function observe(snapshot,error=null){
|
|
1873
|
+
if(nativeWatch!==watch){
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
watch.snapshot=snapshot;
|
|
1877
|
+
watch.error=error;
|
|
1878
|
+
watch.engineId??=snapshot?.roles?.[role]?.providerId??null;
|
|
1879
|
+
if(context.signal?.aborted&&!settled){
|
|
1880
|
+
cancel();
|
|
1881
|
+
}else if(nativeSpeechReady()){
|
|
1882
|
+
finish();
|
|
1883
|
+
nativeRequest?.refreshState?.();
|
|
1884
|
+
}else if(continuingNativeSpeechRequest()){
|
|
1885
|
+
nativeRequest.refreshState?.();
|
|
1886
|
+
}else{
|
|
1887
|
+
const failure=nativeSpeechReadinessError();
|
|
1888
|
+
if(failure){
|
|
1889
|
+
finish(failure);
|
|
1890
|
+
}
|
|
1891
|
+
if(state==='ready'){
|
|
1892
|
+
state='unloaded';
|
|
1893
|
+
runtime.#retainBuiltInSpeechReadiness(
|
|
1894
|
+
runtime.#reconcileBuiltInSpeechReadiness()
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
context.signal?.addEventListener('abort',cancel,{once:true});
|
|
1900
|
+
try{
|
|
1901
|
+
if(context.signal?.aborted){
|
|
1902
|
+
cancel();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
if(!is.function(watch.speech?.status)){
|
|
1906
|
+
throw aiProviderError(
|
|
1907
|
+
'The native speech API does not expose readiness status.',
|
|
1908
|
+
'AI_NATIVE_LOCAL_REQUIRED'
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
if(is.function(globalThis.Arcane?.events?.on)){
|
|
1912
|
+
watch.unsubscribe=globalThis.Arcane.events.on(
|
|
1913
|
+
'speech.state',
|
|
1914
|
+
function observeNativeSpeechState(snapshot){
|
|
1915
|
+
watch.revision+=1;
|
|
1916
|
+
try{
|
|
1917
|
+
observe(snapshot);
|
|
1918
|
+
}catch(error){
|
|
1919
|
+
reportObservationFailure(error);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
const revision=watch.revision;
|
|
1925
|
+
Promise.resolve(watch.speech.status()).then(
|
|
1926
|
+
function receiveNativeSpeechStatus(snapshot){
|
|
1927
|
+
if(watch.revision===revision){
|
|
1928
|
+
observe(snapshot);
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
function rejectNativeSpeechStatus(error){
|
|
1932
|
+
if(watch.revision===revision){
|
|
1933
|
+
observe(null,error);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
).catch(reportObservationFailure);
|
|
1937
|
+
}catch(error){
|
|
1938
|
+
finish(error);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1618
1942
|
|
|
1619
1943
|
function statusBuiltInSpeechProvider(){
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
state
|
|
1624
|
-
}
|
|
1944
|
+
const requestPending=Boolean(continuingNativeSpeechRequest());
|
|
1945
|
+
const selected=nativeWatch?.snapshot?.roles?.[role];
|
|
1946
|
+
const current=state==='ready'&&!nativeSpeechReady()
|
|
1947
|
+
?requestPending?selected.state:'unloaded':state;
|
|
1625
1948
|
return completeValue({
|
|
1626
|
-
state,
|
|
1627
|
-
loaded:
|
|
1628
|
-
busy
|
|
1949
|
+
state:current,
|
|
1950
|
+
loaded:current==='ready',
|
|
1951
|
+
busy,
|
|
1952
|
+
requestPending,
|
|
1953
|
+
...(current==='error'?{error:selected.error}:{})
|
|
1629
1954
|
});
|
|
1630
1955
|
}
|
|
1631
1956
|
|
|
@@ -1646,6 +1971,11 @@ class AI {
|
|
|
1646
1971
|
|
|
1647
1972
|
function releaseBuiltInSpeechRequest(){
|
|
1648
1973
|
busy=false;
|
|
1974
|
+
nativeRequest=null;
|
|
1975
|
+
if(state==='ready'&&!nativeSpeechReady()){
|
|
1976
|
+
state='unloaded';
|
|
1977
|
+
stopNativeSpeechWatch();
|
|
1978
|
+
}
|
|
1649
1979
|
}
|
|
1650
1980
|
|
|
1651
1981
|
return completeValue({
|
|
@@ -1676,7 +2006,7 @@ class AI {
|
|
|
1676
2006
|
return runtime.#builtInSpeechInspection(role,providerId,selection);
|
|
1677
2007
|
},
|
|
1678
2008
|
status:statusBuiltInSpeechProvider,
|
|
1679
|
-
load:function loadBuiltInSpeechProvider(context={}){
|
|
2009
|
+
load:async function loadBuiltInSpeechProvider(context={}){
|
|
1680
2010
|
if(context.signal?.aborted){
|
|
1681
2011
|
throw normalizeAIRequestAbort(context.signal.reason);
|
|
1682
2012
|
}
|
|
@@ -1706,22 +2036,35 @@ class AI {
|
|
|
1706
2036
|
unit:'items',
|
|
1707
2037
|
heartbeat:false
|
|
1708
2038
|
});
|
|
1709
|
-
|
|
2039
|
+
try{
|
|
2040
|
+
await waitForNativeSpeech(context);
|
|
2041
|
+
if(context.signal?.aborted){
|
|
2042
|
+
throw normalizeAIRequestAbort(context.signal.reason);
|
|
2043
|
+
}
|
|
2044
|
+
assertBuiltInSpeechSelection(context.selection);
|
|
2045
|
+
if(!nativeSpeechReady()){
|
|
2046
|
+
throw nativeSpeechReadinessError()??aiProviderError(
|
|
2047
|
+
`The native ${role.toUpperCase()} model lost readiness.`,
|
|
2048
|
+
'ARCANE_AI_ROLE_NOT_READY'
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
state='ready';
|
|
2052
|
+
context.progress({
|
|
2053
|
+
phase:'capability',
|
|
2054
|
+
completed:1,
|
|
2055
|
+
total:1,
|
|
2056
|
+
unit:'items',
|
|
2057
|
+
heartbeat:false
|
|
2058
|
+
});
|
|
2059
|
+
return completeValue({
|
|
2060
|
+
authority:inspection.authority,
|
|
2061
|
+
status:statusBuiltInSpeechProvider()
|
|
2062
|
+
});
|
|
2063
|
+
}catch(error){
|
|
1710
2064
|
state='unloaded';
|
|
1711
|
-
|
|
2065
|
+
stopNativeSpeechWatch();
|
|
2066
|
+
throw error;
|
|
1712
2067
|
}
|
|
1713
|
-
state='ready';
|
|
1714
|
-
context.progress({
|
|
1715
|
-
phase:'capability',
|
|
1716
|
-
completed:1,
|
|
1717
|
-
total:1,
|
|
1718
|
-
unit:'items',
|
|
1719
|
-
heartbeat:false
|
|
1720
|
-
});
|
|
1721
|
-
return completeValue({
|
|
1722
|
-
authority:inspection.authority,
|
|
1723
|
-
status:statusBuiltInSpeechProvider()
|
|
1724
|
-
});
|
|
1725
2068
|
},
|
|
1726
2069
|
request:function requestBuiltInSpeechProvider(context={}){
|
|
1727
2070
|
if(context.signal?.aborted){
|
|
@@ -1748,10 +2091,18 @@ class AI {
|
|
|
1748
2091
|
);
|
|
1749
2092
|
}
|
|
1750
2093
|
busy=true;
|
|
2094
|
+
const operation=role==='stt'?{
|
|
2095
|
+
requestId:null,watch:nativeWatch,signal:context.signal,
|
|
2096
|
+
refreshState:context.refreshState
|
|
2097
|
+
}:null;
|
|
2098
|
+
nativeRequest=operation;
|
|
1751
2099
|
const request=role==='stt'
|
|
1752
2100
|
?runtime.#requestBuiltInSpeechTranscription(
|
|
1753
2101
|
context.payload,
|
|
1754
|
-
context.signal
|
|
2102
|
+
context.signal,
|
|
2103
|
+
function ownNativeSpeechRequest({requestId}){
|
|
2104
|
+
if(nativeRequest===operation)operation.requestId=requestId;
|
|
2105
|
+
}
|
|
1755
2106
|
)
|
|
1756
2107
|
:runtime.#requestBuiltInSpeechSynthesis(
|
|
1757
2108
|
context.payload,
|
|
@@ -1770,6 +2121,7 @@ class AI {
|
|
|
1770
2121
|
);
|
|
1771
2122
|
}
|
|
1772
2123
|
state='unloaded';
|
|
2124
|
+
stopNativeSpeechWatch();
|
|
1773
2125
|
return statusBuiltInSpeechProvider();
|
|
1774
2126
|
},
|
|
1775
2127
|
dispose:function disposeBuiltInSpeechProvider(context={}){
|
|
@@ -1783,6 +2135,7 @@ class AI {
|
|
|
1783
2135
|
);
|
|
1784
2136
|
}
|
|
1785
2137
|
state='disposed';
|
|
2138
|
+
stopNativeSpeechWatch();
|
|
1786
2139
|
return statusBuiltInSpeechProvider();
|
|
1787
2140
|
}
|
|
1788
2141
|
});
|
|
@@ -1837,6 +2190,7 @@ class AI {
|
|
|
1837
2190
|
continue;
|
|
1838
2191
|
}
|
|
1839
2192
|
if(record.unregister()){
|
|
2193
|
+
record.provider.dispose();
|
|
1840
2194
|
this.#builtInSpeechProviders.delete(key);
|
|
1841
2195
|
}
|
|
1842
2196
|
}
|
|
@@ -1893,6 +2247,12 @@ class AI {
|
|
|
1893
2247
|
return Promise.resolve(this.#providerRuntime.status('llm'));
|
|
1894
2248
|
}
|
|
1895
2249
|
const status=this.#providerRuntime.status('llm');
|
|
2250
|
+
const provider=this.#builtInLLMProviders.get(selection.providerId)?.provider;
|
|
2251
|
+
if(selection.providerId==='OLLAMA'
|
|
2252
|
+
&&provider?.status().loaded!==true
|
|
2253
|
+
&&(status.loaded===true||status.busy===true)){
|
|
2254
|
+
return this.#providerRuntime.unload('llm');
|
|
2255
|
+
}
|
|
1896
2256
|
if(this.#builtInLLMCapability(selection.providerId)){
|
|
1897
2257
|
if(status.state==='ready'&&status.loaded===true){
|
|
1898
2258
|
return Promise.resolve(status);
|
|
@@ -1916,7 +2276,13 @@ class AI {
|
|
|
1916
2276
|
return runtime.#providerRuntime.status(role);
|
|
1917
2277
|
}
|
|
1918
2278
|
const status=runtime.#providerRuntime.status(role);
|
|
1919
|
-
|
|
2279
|
+
const record=runtime.#builtInSpeechProviders.get(
|
|
2280
|
+
runtime.#builtInSpeechProviderKey(role,selection.providerId)
|
|
2281
|
+
);
|
|
2282
|
+
const providerStatus=record?.provider.status();
|
|
2283
|
+
if(runtime.#builtInSpeechCapability(role,selection.providerId)
|
|
2284
|
+
&&(!(status.loaded===true||status.busy===true)
|
|
2285
|
+
||providerStatus?.loaded===true||providerStatus?.requestPending===true)){
|
|
1920
2286
|
return status;
|
|
1921
2287
|
}
|
|
1922
2288
|
if(status.loaded===true
|
|
@@ -4068,7 +4434,7 @@ class AI {
|
|
|
4068
4434
|
return null;
|
|
4069
4435
|
}
|
|
4070
4436
|
|
|
4071
|
-
async #requestBuiltInSpeechTranscription(payload={},signal=null){
|
|
4437
|
+
async #requestBuiltInSpeechTranscription(payload={},signal=null,onRequest=undefined){
|
|
4072
4438
|
const audio=payload?.audio;
|
|
4073
4439
|
if(!audio||!is.function(audio.arrayBuffer)){
|
|
4074
4440
|
throw new TypeError('Speech transcription requires an audio Blob or File.');
|
|
@@ -4090,7 +4456,7 @@ class AI {
|
|
|
4090
4456
|
audioBase64:this.#arrayBufferToBase64(audioBytes),
|
|
4091
4457
|
mimeType,
|
|
4092
4458
|
model
|
|
4093
|
-
},{signal});
|
|
4459
|
+
},{signal,onRequest});
|
|
4094
4460
|
}catch(error){
|
|
4095
4461
|
if(isAIRequestAbort(error,signal)){
|
|
4096
4462
|
throw normalizeAIRequestAbort(error);
|