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.
Files changed (58) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +69 -7
  4. package/docs/guides/native-whisper-openvino.md +334 -0
  5. package/docs/reference/README.md +6 -0
  6. package/docs/reference/arcane-ollama.md +1 -1
  7. package/docs/reference/codex-app-server.md +269 -0
  8. package/docs/reference/core-client.md +71 -4
  9. package/docs/reference/core-development.md +157 -0
  10. package/docs/reference/core-native-packaging.md +48 -6
  11. package/docs/reference/core-repositories.md +258 -0
  12. package/docs/reference/inventory/package-api.json +199 -3
  13. package/docs/reference/inventory/runtime-modules.json +3 -2
  14. package/docs/reference/local-ai.md +77 -4
  15. package/docs/reference/local-image-flux.md +1 -1
  16. package/docs/reference/local-image-generation.md +11 -0
  17. package/docs/reference/local-image-sdxl.md +139 -0
  18. package/docs/reference/local-whisper.md +219 -0
  19. package/docs/reference/mcp-stdio.md +263 -0
  20. package/docs/reference/native-speech.md +51 -2
  21. package/docs/reference/runtime-components.md +18 -1
  22. package/docs/reference/runtime-modules.md +26 -4
  23. package/docs/reference/sdk-api.md +412 -2
  24. package/package.json +9 -2
  25. package/runtime/arcane/components/speech.html +15 -3
  26. package/runtime/arcane/components/voice-transcription.html +19 -3
  27. package/runtime/arcane/modules/AI.js +406 -40
  28. package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
  29. package/runtime/arcane/modules/AIRuntimeState.js +15 -2
  30. package/runtime/arcane/modules/ComponentContracts.js +3 -0
  31. package/runtime/arcane/modules/Ollama.js +1 -1
  32. package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
  33. package/schemas/arcane-app.schema.json +60 -2
  34. package/schemas/arcane-package.schema.json +60 -2
  35. package/src/codex/app-server.mjs +845 -0
  36. package/src/core/development.mjs +457 -0
  37. package/src/core/repositories.mjs +375 -0
  38. package/src/core/services/local-ai.mjs +80 -10
  39. package/src/core/services/speech.mjs +1 -0
  40. package/src/dev-server.mjs +3 -2
  41. package/src/index.mjs +1 -0
  42. package/src/local-ai/config.mjs +7 -3
  43. package/src/local-ai/dev.mjs +85 -401
  44. package/src/local-ai/image-library.mjs +9 -0
  45. package/src/local-ai/image-models/sdxl.mjs +18 -0
  46. package/src/local-ai/install.mjs +26 -7
  47. package/src/local-ai/whisper/build.mjs +48 -0
  48. package/src/local-ai/whisper/config.mjs +89 -0
  49. package/src/local-ai/whisper/index.mjs +572 -0
  50. package/src/local-ai/whisper/install.mjs +332 -0
  51. package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
  52. package/src/local-ai/whisper/native/NOTICE +18 -0
  53. package/src/local-ai/whisper/native/README.md +104 -0
  54. package/src/local-ai/whisper/native/main.cpp +520 -0
  55. package/src/local-ai/whisper/openvino-build.mjs +138 -0
  56. package/src/mcp/stdio-server.mjs +579 -0
  57. package/src/native/portable-layout.mjs +39 -7
  58. 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
- return globalThis.ai.fetchSTT(file,signal);
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(previousRole.state==='ready'&&sttRole.state!=='ready'){
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
- &&!busy
1378
- &&!runtime.#builtInLLMCapability(providerId)){
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 Promise.resolve(
1494
- runtime.#requestBuiltInLLMChat(
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
- ).finally(releaseBuiltInLLMRequest);
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
- return runtime.#requestBuiltInLLMStream(
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
- if(state==='ready'
1621
- &&!busy
1622
- &&!runtime.#builtInSpeechCapability(role,providerId)){
1623
- state='unloaded';
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:state==='ready',
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
- if(context.signal?.aborted){
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
- throw normalizeAIRequestAbort(context.signal.reason);
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
- if(runtime.#builtInSpeechCapability(role,selection.providerId)){
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);