arcane-os 0.77.0 → 0.79.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 +40 -0
- package/README.md +6 -1
- package/browser-runtime/ai/core-onnx.mjs +6 -2
- package/browser-runtime/ai/dbopfs-model-resources.mjs +1 -1
- package/browser-runtime/ai/speech-worker-runtime.mjs +79 -3
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/publishing.md +20 -3
- package/docs/reference/ai/browser-speech.md +59 -2
- package/docs/reference/execution-devices.md +157 -0
- package/docs/reference/inventory/package-api.json +39 -11
- package/docs/reference/inventory/runtime-modules.json +9 -8
- package/docs/reference/local-ai.md +104 -10
- package/docs/reference/native-decisions.md +65 -10
- package/docs/reference/runtime-modules.md +78 -11
- package/package.json +4 -2
- package/runtime/arcane/modules/AI.js +30 -7
- package/runtime/arcane/modules/DBOPFS.js +24 -6
- package/runtime/arcane/modules/PreparedSpeech.js +111 -6
- package/src/core/services/decisions.mjs +51 -15
- package/src/core/services/execution-devices.mjs +32 -0
- package/src/core/services/local-ai.mjs +12 -5
- package/src/local-ai/decisions.mjs +93 -15
- package/src/local-ai/dev.mjs +7 -3
- package/src/local-ai/execution-devices-windows.ps1 +833 -0
- package/src/local-ai/execution-devices.mjs +288 -0
- package/src/local-ai/onnx-worker.mjs +149 -19
- package/src/local-ai/onnx.mjs +87 -27
- package/src/native/portable-layout.mjs +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"./core/preferences": "./src/core/services/preferences.mjs",
|
|
70
70
|
"./core/diarization": "./src/diarization/service.mjs",
|
|
71
71
|
"./core/local-ai": "./src/core/services/local-ai.mjs",
|
|
72
|
+
"./core/execution-devices": "./src/core/services/execution-devices.mjs",
|
|
72
73
|
"./core/decisions": "./src/core/services/decisions.mjs",
|
|
73
74
|
"./core/image": "./src/core/services/image.mjs",
|
|
74
75
|
"./core/speech": "./src/core/services/speech.mjs",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"./diarization/build": "./src/diarization/build.mjs",
|
|
78
79
|
"./local-ai": "./src/local-ai/install.mjs",
|
|
79
80
|
"./local-ai/onnx": "./src/local-ai/onnx.mjs",
|
|
81
|
+
"./local-ai/execution-devices": "./src/local-ai/execution-devices.mjs",
|
|
80
82
|
"./local-ai/decisions": "./src/local-ai/decisions.mjs",
|
|
81
83
|
"./local-ai/image": "./src/local-ai/image-runtime.mjs",
|
|
82
84
|
"./local-ai/whisper": "./src/local-ai/whisper/index.mjs",
|
|
@@ -123,7 +125,7 @@
|
|
|
123
125
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
124
126
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
125
127
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
126
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
128
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/execution-devices.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
127
129
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
128
130
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/ai-audio-output.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
129
131
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
openAIResponseFormat
|
|
27
27
|
} from 'arcane-os/ai/twin-cloud';
|
|
28
28
|
import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
|
|
29
|
-
import {prepareSpeech} from './PreparedSpeech.js';
|
|
29
|
+
import {prepareSpeech, readPreparedSpeech} from './PreparedSpeech.js';
|
|
30
30
|
import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
|
|
31
31
|
|
|
32
32
|
const completeValue=(value)=>value;
|
|
@@ -6341,16 +6341,15 @@ class AI {
|
|
|
6341
6341
|
return result;
|
|
6342
6342
|
}
|
|
6343
6343
|
|
|
6344
|
-
|
|
6344
|
+
#preparedSpeechInput(parts, textFormat, operation) {
|
|
6345
6345
|
if(this.#providerSpeechModel()?.speech?.playback==='native'){
|
|
6346
6346
|
const error=new Error('Native browser speech has no audio file to prepare or store; use prepareTTSPlayback.');
|
|
6347
6347
|
error.code='ARCANE_AI_TTS_AUDIO_EXPORT_UNAVAILABLE';
|
|
6348
6348
|
throw error;
|
|
6349
6349
|
}
|
|
6350
6350
|
if(!is.array(parts)) {
|
|
6351
|
-
throw new TypeError(
|
|
6351
|
+
throw new TypeError(`${operation} parts must be an array.`);
|
|
6352
6352
|
}
|
|
6353
|
-
const runtime = this;
|
|
6354
6353
|
const selected = this.#providerRuntime.selection('tts');
|
|
6355
6354
|
const configuredSpeech = this.browserSpeechConfiguration?.tts;
|
|
6356
6355
|
const configuration = configuredSpeech
|
|
@@ -6367,12 +6366,9 @@ class AI {
|
|
|
6367
6366
|
responseFormat: selected ? this.#providerSpeechResponseFormat() : this.audioFormat,
|
|
6368
6367
|
configuration
|
|
6369
6368
|
};
|
|
6370
|
-
const browserGeneration = this.#browserSpeechGeneration;
|
|
6371
|
-
const controlGeneration = this.#speechControlGeneration;
|
|
6372
6369
|
const defaultVoice = selected
|
|
6373
6370
|
? this.#providerSpeechVoice()
|
|
6374
6371
|
: this.#builtInSpeechDefaultVoice('tts', this.ttsService);
|
|
6375
|
-
const responseFormat = selection.responseFormat;
|
|
6376
6372
|
const segmentation = this.ttsSegmentation;
|
|
6377
6373
|
const segments = [];
|
|
6378
6374
|
for(const part of parts) {
|
|
@@ -6398,6 +6394,33 @@ class AI {
|
|
|
6398
6394
|
);
|
|
6399
6395
|
}
|
|
6400
6396
|
}
|
|
6397
|
+
return {segments, selection, segmentation, selected};
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
async readPreparedTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6401
|
+
const {segments, selection, segmentation} = this.#preparedSpeechInput(parts, textFormat, 'AI.readPreparedTTS');
|
|
6402
|
+
this.#traceSpeech('readPreparedTTS.call', {parts, segments, selection, segmentation, identity, storage});
|
|
6403
|
+
return readPreparedSpeech(
|
|
6404
|
+
{
|
|
6405
|
+
owner: this,
|
|
6406
|
+
parts: segments,
|
|
6407
|
+
originalParts: parts,
|
|
6408
|
+
selection,
|
|
6409
|
+
segmentation,
|
|
6410
|
+
storage,
|
|
6411
|
+
identity,
|
|
6412
|
+
signal,
|
|
6413
|
+
onState
|
|
6414
|
+
}
|
|
6415
|
+
);
|
|
6416
|
+
}
|
|
6417
|
+
|
|
6418
|
+
prepareTTS({parts, textFormat, storage = null, identity = null, signal = null, onState = null} = {}) {
|
|
6419
|
+
const {segments, selection, segmentation, selected} = this.#preparedSpeechInput(parts, textFormat, 'AI.prepareTTS');
|
|
6420
|
+
const runtime = this;
|
|
6421
|
+
const browserGeneration = this.#browserSpeechGeneration;
|
|
6422
|
+
const controlGeneration = this.#speechControlGeneration;
|
|
6423
|
+
const responseFormat = selection.responseFormat;
|
|
6401
6424
|
this.#traceSpeech('prepareTTS.call', {parts, segments, selection, segmentation, identity, storage});
|
|
6402
6425
|
async function synthesizePreparedSpeech(part, requestSignal) {
|
|
6403
6426
|
function assertPreparedSpeechSelection() {
|
|
@@ -554,12 +554,13 @@ class DBOPFS {
|
|
|
554
554
|
|
|
555
555
|
/**
|
|
556
556
|
* Gets the directory handle for a table.
|
|
557
|
-
* Creates the table if it does not exist.
|
|
557
|
+
* Creates the table if it does not exist unless create is false.
|
|
558
558
|
*
|
|
559
559
|
* @param {string} tableName
|
|
560
|
+
* @param {{create?:boolean}} options A false create performs an existing-table lookup only.
|
|
560
561
|
* @returns {Promise<FileSystemDirectoryHandle>}
|
|
561
562
|
*/
|
|
562
|
-
async getTableHandle(tableName=''){
|
|
563
|
+
async getTableHandle(tableName='',{create=true}={}){
|
|
563
564
|
if(!this.ready){
|
|
564
565
|
await this.readyPromise;
|
|
565
566
|
}
|
|
@@ -582,6 +583,14 @@ class DBOPFS {
|
|
|
582
583
|
return existingHandle;
|
|
583
584
|
}
|
|
584
585
|
|
|
586
|
+
if(!create){
|
|
587
|
+
// A read-existing lookup must not wait for another caller to create the table.
|
|
588
|
+
return this.#db.getDirectoryHandle(
|
|
589
|
+
directoryName,
|
|
590
|
+
{create:false}
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
585
594
|
if(!this.#tableHandlePromises[registeredTableName]){
|
|
586
595
|
const handlePromise=this.#db.getDirectoryHandle(
|
|
587
596
|
directoryName,
|
|
@@ -736,10 +745,14 @@ class DBOPFS {
|
|
|
736
745
|
*
|
|
737
746
|
* @param {string} tableName
|
|
738
747
|
* @param {string} fileName
|
|
748
|
+
* @param {{createTable?:boolean}} options A false createTable preserves an absent table.
|
|
739
749
|
* @returns {Promise<File>}
|
|
740
750
|
*/
|
|
741
|
-
async readFile(tableName='',fileName=''){
|
|
742
|
-
const table=await this.getTableHandle(
|
|
751
|
+
async readFile(tableName='',fileName='',{createTable=true}={}){
|
|
752
|
+
const table=await this.getTableHandle(
|
|
753
|
+
tableName,
|
|
754
|
+
{create:createTable}
|
|
755
|
+
)
|
|
743
756
|
const handle=await table.getFileHandle(fileName,{create:false})
|
|
744
757
|
|
|
745
758
|
if(is.function(handle.getFile)){
|
|
@@ -818,14 +831,19 @@ class DBOPFS {
|
|
|
818
831
|
* @param {string} tableName
|
|
819
832
|
* @param {string} fileName
|
|
820
833
|
* @param {boolean} force
|
|
834
|
+
* @param {{createTable?:boolean}} options A false createTable preserves an absent table.
|
|
821
835
|
* @returns {Promise<*>}
|
|
822
836
|
*/
|
|
823
|
-
async get(tableName='',fileName='',force=false){
|
|
837
|
+
async get(tableName='',fileName='',force=false,{createTable=true}={}){
|
|
824
838
|
if(force||!this.#tables[tableName]?.[fileName]){
|
|
825
839
|
const cacheState=this.#fileCacheState(tableName,fileName);
|
|
826
840
|
const cacheRevision=cacheState.revision;
|
|
827
841
|
try{
|
|
828
|
-
const file=await this.readFile(
|
|
842
|
+
const file=await this.readFile(
|
|
843
|
+
tableName,
|
|
844
|
+
fileName,
|
|
845
|
+
{createTable}
|
|
846
|
+
)
|
|
829
847
|
const textContent=await file.text()
|
|
830
848
|
|
|
831
849
|
if(!this.#tables[tableName]){
|
|
@@ -111,8 +111,10 @@ function serializeStorage(storage,execute){
|
|
|
111
111
|
return operation;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async function readManifest(storage){
|
|
115
|
-
const stored=
|
|
114
|
+
async function readManifest(storage,cacheOnly=false){
|
|
115
|
+
const stored=cacheOnly
|
|
116
|
+
?await storage.db.get(storage.table,storage.fileName,true,{createTable:false})
|
|
117
|
+
:await storage.db.get(storage.table,storage.fileName,true);
|
|
116
118
|
if(stored===null||stored===undefined)return {version:1,entries:[]};
|
|
117
119
|
const manifest=copyJSON(stored);
|
|
118
120
|
if(manifest.version!==1||!is.array(manifest.entries)){
|
|
@@ -349,9 +351,10 @@ function finishPreparation(job,error){
|
|
|
349
351
|
job.finished.resolve();
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
+
function createJobState(input,storage,synthesize=null,previous=null){
|
|
355
|
+
return {
|
|
354
356
|
input,storage,synthesize,previous,
|
|
357
|
+
cacheOnly:false,
|
|
355
358
|
controller:new AbortController(),
|
|
356
359
|
interests:new Set(),
|
|
357
360
|
state:'queued',
|
|
@@ -367,6 +370,10 @@ function createJob(ownerState,input,storage,synthesize,previous){
|
|
|
367
370
|
return {...part,index,state:'queued',audioFile:null,contentType:null,error:null};
|
|
368
371
|
})
|
|
369
372
|
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createJob(ownerState,input,storage,synthesize,previous){
|
|
376
|
+
const job=createJobState(input,storage,synthesize,previous);
|
|
370
377
|
const admission=ownerState.admission.then(function admitOrderedPreparation(){
|
|
371
378
|
return admitPreparation(job);
|
|
372
379
|
});
|
|
@@ -416,7 +423,9 @@ function createHandle(job,signal,onState){
|
|
|
416
423
|
if(!interest.active)throw abortError();
|
|
417
424
|
if(!job.storage)return job.slots[index].blob;
|
|
418
425
|
const audio=job.record.segments[index];
|
|
419
|
-
const file=
|
|
426
|
+
const file=job.cacheOnly
|
|
427
|
+
?await job.storage.db.readFile(job.storage.table,audio.audioFile,{createTable:false})
|
|
428
|
+
:await job.storage.db.readFile(job.storage.table,audio.audioFile);
|
|
420
429
|
if(!interest.active)throw abortError();
|
|
421
430
|
return file.type===audio.contentType?file:new Blob([file],{type:audio.contentType});
|
|
422
431
|
},
|
|
@@ -482,4 +491,100 @@ function prepareSpeech({
|
|
|
482
491
|
return createHandle(job,signal,onState);
|
|
483
492
|
}
|
|
484
493
|
|
|
485
|
-
|
|
494
|
+
/** Read complete existing audio without synthesis, storage writes, or repair. */
|
|
495
|
+
async function readPreparedSpeech({
|
|
496
|
+
owner,
|
|
497
|
+
parts,
|
|
498
|
+
originalParts=parts,
|
|
499
|
+
selection=null,
|
|
500
|
+
segmentation=null,
|
|
501
|
+
storage=null,
|
|
502
|
+
identity=null,
|
|
503
|
+
signal=null,
|
|
504
|
+
onState=null
|
|
505
|
+
}={}){
|
|
506
|
+
if((!owner||!is.object(owner))&&!is.function(owner)){
|
|
507
|
+
throw new TypeError('Speech preparation requires its owning AI instance.');
|
|
508
|
+
}
|
|
509
|
+
if(!is.array(parts)||(onState!==null&&!is.function(onState))){
|
|
510
|
+
throw new TypeError('Cached speech requires ordered parts and an optional state callback.');
|
|
511
|
+
}
|
|
512
|
+
const input=copyJSON({parts,originalParts,selection,segmentation,identity});
|
|
513
|
+
const destination=normalizeStorage(storage);
|
|
514
|
+
if(signal?.aborted)throw abortError();
|
|
515
|
+
function completeCachedRead(job){
|
|
516
|
+
// The lookup signal owns only this read. Retained audio keeps its own
|
|
517
|
+
// explicit cancellation lifetime after a successful lookup returns.
|
|
518
|
+
const handle=createHandle(job,null,onState);
|
|
519
|
+
if(signal?.aborted){
|
|
520
|
+
handle.cancel();
|
|
521
|
+
throw abortError();
|
|
522
|
+
}
|
|
523
|
+
return handle;
|
|
524
|
+
}
|
|
525
|
+
if(!destination){
|
|
526
|
+
const jobs=owners.get(owner)?.jobs;
|
|
527
|
+
if(!jobs)return null;
|
|
528
|
+
for(const job of jobs){
|
|
529
|
+
if(job.storage||!job.done||job.failed||job.controller.signal.aborted
|
|
530
|
+
||!sameInput(job.input,input))continue;
|
|
531
|
+
if(!job.slots.every(function hasRetainedAudio(slot){
|
|
532
|
+
return slot.settled&&slot.blob instanceof Blob;
|
|
533
|
+
}))return null;
|
|
534
|
+
return completeCachedRead(job);
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const cancelled=deferred();
|
|
539
|
+
function cancelRead(){cancelled.reject(abortError());}
|
|
540
|
+
signal?.addEventListener('abort',cancelRead,{once:true});
|
|
541
|
+
try{
|
|
542
|
+
const reading=serializeStorage(destination,async function readCachedPreparation(){
|
|
543
|
+
if(signal?.aborted)throw abortError();
|
|
544
|
+
const manifest=await readManifest(destination,true);
|
|
545
|
+
if(signal?.aborted)throw abortError();
|
|
546
|
+
const record=manifest.entries.find(function findCachedPreparation(entry){
|
|
547
|
+
return sameInput(entry,input);
|
|
548
|
+
});
|
|
549
|
+
if(!record)return null;
|
|
550
|
+
if(!is.array(record.segments)||record.segments.length!==input.parts.length){
|
|
551
|
+
throw new TypeError('Stored speech preparation segments are unreadable.');
|
|
552
|
+
}
|
|
553
|
+
let complete=true;
|
|
554
|
+
for(const audio of record.segments){
|
|
555
|
+
if(!audio?.audioFile){
|
|
556
|
+
complete=false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if(!is.string(audio.contentType)){
|
|
560
|
+
throw new TypeError('Stored speech audio content type is unreadable.');
|
|
561
|
+
}
|
|
562
|
+
if(signal?.aborted)throw abortError();
|
|
563
|
+
try{
|
|
564
|
+
const file=await destination.db.readFile(destination.table,audio.audioFile,{createTable:false});
|
|
565
|
+
if(!(file instanceof Blob)){
|
|
566
|
+
throw new TypeError('Stored speech audio must be a Blob.');
|
|
567
|
+
}
|
|
568
|
+
}catch(error){
|
|
569
|
+
if(error?.name!=='NotFoundError')throw error;
|
|
570
|
+
complete=false;
|
|
571
|
+
}
|
|
572
|
+
if(signal?.aborted)throw abortError();
|
|
573
|
+
}
|
|
574
|
+
return complete?record:null;
|
|
575
|
+
});
|
|
576
|
+
const record=await Promise.race([reading,cancelled.promise]);
|
|
577
|
+
if(signal?.aborted)throw abortError();
|
|
578
|
+
if(!record)return null;
|
|
579
|
+
const job=createJobState(input,destination);
|
|
580
|
+
job.cacheOnly=true;
|
|
581
|
+
job.record=record;
|
|
582
|
+
for(let index=0;index<job.slots.length;index+=1)settleSegment(job,index);
|
|
583
|
+
finishPreparation(job);
|
|
584
|
+
return completeCachedRead(job);
|
|
585
|
+
}finally{
|
|
586
|
+
signal?.removeEventListener('abort',cancelRead);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export {prepareSpeech,readPreparedSpeech};
|
|
@@ -4,6 +4,10 @@ import {createArcaneEventSource} from '../../event-manager.mjs';
|
|
|
4
4
|
import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
5
5
|
import {encodeTensorMap} from '../../../browser-runtime/ai/onnx-tensors.mjs';
|
|
6
6
|
|
|
7
|
+
function sameExecutionTarget(left, right) {
|
|
8
|
+
return left === right || (left != null && right != null && left.deviceId !== undefined && left.deviceId === right.deviceId);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
/** App-owned selection; shared typed-decision execution, without a renderer. */
|
|
8
12
|
export function createNativeDecisionService(configuration = {}, {appRoot = process.cwd()} = {}) {
|
|
9
13
|
const events = createArcaneEventSource({}, {source: 'core-decisions', eventTypes: ['decisions.state']});
|
|
@@ -15,14 +19,22 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
15
19
|
let releasing;
|
|
16
20
|
let closing;
|
|
17
21
|
let unsubscribe;
|
|
22
|
+
let selectedTarget = configuration.executionTarget;
|
|
23
|
+
let loadingTarget;
|
|
24
|
+
let loadedTarget;
|
|
18
25
|
|
|
19
26
|
function current() {
|
|
20
|
-
|
|
27
|
+
const snapshot = model?.current() ?? {
|
|
21
28
|
family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
|
|
22
29
|
revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
|
|
23
30
|
state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
|
|
24
31
|
activeRequests: 0, progress: null, error: null, execution: null
|
|
25
32
|
};
|
|
33
|
+
const pending = Boolean(loading) && (!snapshot.loaded || !sameExecutionTarget(loadedTarget, loadingTarget));
|
|
34
|
+
return {
|
|
35
|
+
...snapshot, busy: snapshot.busy || pending,
|
|
36
|
+
pendingActivation: pending ? {executionTarget: loadingTarget} : snapshot.pendingActivation ?? null
|
|
37
|
+
};
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
function publish(value = current()) {
|
|
@@ -39,20 +51,24 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
39
51
|
|
|
40
52
|
function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
|
|
41
53
|
|
|
42
|
-
function load({signal} = {}) {
|
|
54
|
+
function load({signal, executionTarget: requestedTarget} = {}) {
|
|
43
55
|
operationSignal(signal).throwIfAborted();
|
|
44
56
|
if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
const
|
|
57
|
+
const target = requestedTarget === undefined ? selectedTarget : requestedTarget;
|
|
58
|
+
if (loading && !loadController.signal.aborted && sameExecutionTarget(loadingTarget, target)) return observeLoad(loading, loadController, signal);
|
|
59
|
+
if (!loading && model?.current().loaded && sameExecutionTarget(loadedTarget, target)) return Promise.resolve(current());
|
|
60
|
+
const previousLoad = loading;
|
|
61
|
+
const previousController = loadController;
|
|
62
|
+
const controller = new AbortController();
|
|
49
63
|
const selectedSignal = AbortSignal.any([lifetime.signal, controller.signal]);
|
|
50
64
|
const precedingRelease = releasing;
|
|
51
|
-
|
|
65
|
+
const task = Promise.resolve().then(async function loadSelectedModel() {
|
|
52
66
|
selectedSignal.throwIfAborted();
|
|
53
67
|
if (precedingRelease) await precedingRelease;
|
|
68
|
+
if (previousLoad) await Promise.allSettled([previousLoad]);
|
|
54
69
|
selectedSignal.throwIfAborted();
|
|
55
70
|
if (model) await model.dispose();
|
|
71
|
+
selectedSignal.throwIfAborted();
|
|
56
72
|
unsubscribe?.();
|
|
57
73
|
// Only the explicit load waits for its native dependencies. Merely
|
|
58
74
|
// registering this service does not download a model or delay UI.
|
|
@@ -65,23 +81,43 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
65
81
|
Object.entries(configuration.paths).map(function nativePath([name, value]) { return [name, path.resolve(appRoot, value)]; })
|
|
66
82
|
);
|
|
67
83
|
model = createNativeDecisionModel({
|
|
68
|
-
...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
|
|
84
|
+
...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets, executionTarget: target,
|
|
69
85
|
signal: lifetime.signal
|
|
70
86
|
});
|
|
71
|
-
unsubscribe = model.subscribe(publish);
|
|
72
|
-
|
|
87
|
+
unsubscribe = model.subscribe(function modelStateChanged() { publish(); });
|
|
88
|
+
const result = await model.load({signal: selectedSignal});
|
|
89
|
+
selectedSignal.throwIfAborted();
|
|
90
|
+
loadedTarget = target;
|
|
91
|
+
return result;
|
|
73
92
|
});
|
|
74
|
-
function settled() {
|
|
75
|
-
|
|
93
|
+
function settled() {
|
|
94
|
+
if (loading !== task) return;
|
|
95
|
+
loading = undefined;
|
|
96
|
+
loadController = undefined;
|
|
97
|
+
publish();
|
|
98
|
+
}
|
|
99
|
+
task.then(settled, settled).catch(function reportStateFailure(error) { console.error('Native decision state publication failed.', error); });
|
|
100
|
+
loading = task;
|
|
101
|
+
loadController = controller;
|
|
102
|
+
loadingTarget = target;
|
|
103
|
+
selectedTarget = target;
|
|
104
|
+
previousController?.abort(new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision activation request was replaced.'}));
|
|
76
105
|
publish();
|
|
77
|
-
return observeLoad(
|
|
106
|
+
return observeLoad(task, controller, signal);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
function observeLoad(task, controller, signal) {
|
|
81
110
|
function abort() { controller.abort(signal.reason); }
|
|
82
111
|
signal?.addEventListener('abort', abort, {once: true});
|
|
83
112
|
if (signal?.aborted) abort();
|
|
84
|
-
return task.
|
|
113
|
+
return task.then(function loadedActivation(result) {
|
|
114
|
+
controller.signal.throwIfAborted();
|
|
115
|
+
const snapshot = current();
|
|
116
|
+
if ((loading && loadController !== controller) || !snapshot.loaded || snapshot.execution !== result.execution) {
|
|
117
|
+
throw new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision activation was replaced.'});
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}).finally(function detachLoadCaller() { signal?.removeEventListener('abort', abort); });
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
async function evaluate(rows, {signal, runOptions} = {}) {
|
|
@@ -129,7 +165,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
|
|
|
129
165
|
start(serviceContext) { context = serviceContext; },
|
|
130
166
|
methods: {
|
|
131
167
|
'decisions.status': current,
|
|
132
|
-
'decisions.load': function loadRequest(parameters, request) { return load({signal: request.signal}); },
|
|
168
|
+
'decisions.load': function loadRequest(parameters, request) { return load({executionTarget: parameters?.executionTarget, signal: request.signal}); },
|
|
133
169
|
'decisions.evaluate': async function evaluateRequest({rows, runOptions}, request) {
|
|
134
170
|
const result = await evaluate(rows, {runOptions, signal: request.signal});
|
|
135
171
|
// Typed arrays retain their complete values through Core's
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {createExecutionDeviceCatalog} from '../../local-ai/execution-devices.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Independent of model-service startup. Core gives this service ownership of
|
|
5
|
+
* the shared catalog's disposal; model engines borrowing it do not dispose it.
|
|
6
|
+
*/
|
|
7
|
+
export function createExecutionDeviceService({catalog = createExecutionDeviceCatalog()} = {}) {
|
|
8
|
+
function devices({refresh = false} = {}, {signal} = {}) {
|
|
9
|
+
return catalog.devices(
|
|
10
|
+
{refresh, signal}
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveTarget({executionTarget = null, refresh = false} = {}, {signal} = {}) {
|
|
15
|
+
return catalog.resolveTarget(
|
|
16
|
+
{executionTarget, refresh, signal}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dispose() {
|
|
21
|
+
return catalog.dispose();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: 'execution-devices',
|
|
26
|
+
methods: {
|
|
27
|
+
'localai.devices': devices,
|
|
28
|
+
'localai.resolveTarget': resolveTarget
|
|
29
|
+
},
|
|
30
|
+
dispose
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -121,7 +121,7 @@ function collectCompletion(completion, chunk) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Core owns native processes; applications own runtime and model selection. */
|
|
124
|
-
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare} = {}) {
|
|
124
|
+
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare, executionDevices} = {}) {
|
|
125
125
|
const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
|
|
126
126
|
const lifetime = new AbortController();
|
|
127
127
|
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
@@ -173,7 +173,9 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
173
173
|
|
|
174
174
|
function startONNX() {
|
|
175
175
|
if (!onnxRecord?.modulePath) return;
|
|
176
|
-
onnx = createONNXRuntime(
|
|
176
|
+
onnx = createONNXRuntime(
|
|
177
|
+
{modulePath: onnxRecord.modulePath, signal: lifetimeSignal, onEvent, executionDevices}
|
|
178
|
+
);
|
|
177
179
|
stopONNXSubscription = onnx.subscribe(function onnxChanged() { publish(); });
|
|
178
180
|
}
|
|
179
181
|
|
|
@@ -630,9 +632,14 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
630
632
|
return snapshot();
|
|
631
633
|
},
|
|
632
634
|
'onnx.status': function currentONNX() { return onnxStatus(); },
|
|
633
|
-
'onnx.load': function loadONNX({id, model, sessionOptions}, request) {
|
|
634
|
-
return requireONNX().load(
|
|
635
|
-
|
|
635
|
+
'onnx.load': function loadONNX({id, model, sessionOptions, executionPreference, executionTarget}, request) {
|
|
636
|
+
return requireONNX().load(
|
|
637
|
+
{
|
|
638
|
+
id, model: path.resolve(appRoot ?? process.cwd(), model), sessionOptions,
|
|
639
|
+
executionPreference, executionTarget,
|
|
640
|
+
signal: AbortSignal.any([lifetimeSignal, request.signal])
|
|
641
|
+
}
|
|
642
|
+
);
|
|
636
643
|
},
|
|
637
644
|
'onnx.run': async function runONNX({id, feeds, fetches, runOptions}, request) {
|
|
638
645
|
const outputs = await requireONNX().run({id, feeds: decodeTensorMap(feeds),
|