arcane-os 0.69.0 → 0.71.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 +28 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +46 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
- package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
- package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
- package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
- package/browser-runtime/ai/decision-runtime.mjs +17 -1
- package/browser-runtime/ai/decision-worker.mjs +21 -1
- package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
- package/browser-runtime/ai/speech-worker-client.mjs +68 -28
- package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
- package/browser-runtime/core/contracts.mjs +1 -1
- package/docs/reference/README.md +2 -0
- package/docs/reference/ai/browser-decisions.md +57 -3
- package/docs/reference/ai/browser-speech.md +45 -17
- package/docs/reference/ai/browser-wasm.md +28 -20
- package/docs/reference/core-native-packaging.md +13 -0
- package/docs/reference/core-runtime.md +9 -0
- package/docs/reference/core-shared-host.md +190 -0
- package/docs/reference/inventory/package-api.json +3 -3
- package/docs/reference/inventory/runtime-components.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +6 -6
- package/docs/reference/runtime-components.md +35 -1
- package/docs/reference/runtime-modules.md +97 -7
- package/docs/reference/sdk-api.md +69 -31
- package/package.json +2 -2
- package/runtime/arcane/components/theme-switcher.html +4 -4
- package/runtime/arcane/modules/AI.js +2 -18
- package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
- package/runtime/arcane/modules/DBOPFS.js +180 -26
- package/src/core/host.mjs +2 -0
- package/src/core/runtime.mjs +9 -4
- package/src/core/services/packaged-web.mjs +8 -1
- package/src/core/shared-host.mjs +500 -0
- package/src/native/portable-layout.mjs +66 -34
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Is from "../dependencies/strong-type/index.js";
|
|
2
|
+
import { createModelResourceClient, createModelResourceHost } from './model-resource-bridge.mjs';
|
|
2
3
|
|
|
3
4
|
const is = new Is(false);
|
|
4
5
|
|
|
@@ -922,14 +923,16 @@ function createOrdinaryRouteReader(
|
|
|
922
923
|
scope,
|
|
923
924
|
configuration,
|
|
924
925
|
originalFetch,
|
|
925
|
-
|
|
926
|
+
resourceClient,
|
|
927
|
+
getSignal,
|
|
928
|
+
getProgress,
|
|
929
|
+
sourceUrl,
|
|
926
930
|
) {
|
|
927
931
|
const routeTable = createOrdinaryRoutes(scope, configuration);
|
|
928
|
-
const nativeCaches = new Map();
|
|
929
932
|
const RequestConstructor = scope.Request ?? globalThis.Request;
|
|
930
933
|
|
|
931
934
|
function sourceBase(modulePath) {
|
|
932
|
-
return routeTable.filesByPath.get(modulePath)?.sourceUrl ??
|
|
935
|
+
return routeTable.filesByPath.get(modulePath)?.sourceUrl ?? sourceUrl;
|
|
933
936
|
}
|
|
934
937
|
|
|
935
938
|
function nativeInput(input, modulePath) {
|
|
@@ -951,52 +954,40 @@ function createOrdinaryRouteReader(
|
|
|
951
954
|
return originalFetch(mappedInput, init);
|
|
952
955
|
}
|
|
953
956
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
957
|
+
function fetchResource(input, init, modulePath) {
|
|
958
|
+
const options = resourceOptions(input, init);
|
|
959
|
+
const resolution = resolve(input, modulePath);
|
|
960
|
+
if (resolution) return responseFor(resolution, input, options);
|
|
961
|
+
const target = nativeInput(input, modulePath);
|
|
962
|
+
const url = ordinaryRequestUrl(target, scope, sourceBase(modulePath));
|
|
963
|
+
if (url?.startsWith('blob:') || url?.startsWith('data:')) {
|
|
964
|
+
return originalFetch(target, options);
|
|
958
965
|
}
|
|
959
|
-
return
|
|
966
|
+
return resourceClient.fetch(target, options);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function resourceOptions(input, init) {
|
|
970
|
+
const signal = getSignal();
|
|
971
|
+
const reportProgress = getProgress();
|
|
972
|
+
if (!signal && !reportProgress) return init;
|
|
973
|
+
const requestSignal = init?.signal ?? input?.signal;
|
|
974
|
+
return {
|
|
975
|
+
...init,
|
|
976
|
+
signal: signal && requestSignal && requestSignal !== signal
|
|
977
|
+
? AbortSignal.any([requestSignal, signal])
|
|
978
|
+
: signal ?? requestSignal,
|
|
979
|
+
onProgress: function reportResourceProgress(progress) {
|
|
980
|
+
reportProgress?.(progress);
|
|
981
|
+
if (init?.onProgress !== reportProgress) init?.onProgress?.(progress);
|
|
982
|
+
},
|
|
983
|
+
};
|
|
960
984
|
}
|
|
961
985
|
|
|
962
|
-
function
|
|
963
|
-
return
|
|
964
|
-
async match(input, options) {
|
|
965
|
-
const resolution = resolve(input, modulePath);
|
|
966
|
-
if (resolution) return responseFor(resolution, input);
|
|
967
|
-
const cache = await nativeCache(name);
|
|
968
|
-
return cache?.match(nativeInput(input, modulePath), options);
|
|
969
|
-
},
|
|
970
|
-
async put(input, response) {
|
|
971
|
-
const cache = await nativeCache(name);
|
|
972
|
-
if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
|
|
973
|
-
return cache.put(nativeInput(input, modulePath), response);
|
|
974
|
-
},
|
|
975
|
-
async add(input) {
|
|
976
|
-
const cache = await nativeCache(name);
|
|
977
|
-
if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
|
|
978
|
-
return cache.add(nativeInput(input, modulePath));
|
|
979
|
-
},
|
|
980
|
-
async addAll(inputs) {
|
|
981
|
-
const cache = await nativeCache(name);
|
|
982
|
-
if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
|
|
983
|
-
return cache.addAll(Array.from(inputs, (input) => nativeInput(input, modulePath)));
|
|
984
|
-
},
|
|
985
|
-
async delete(input, options) {
|
|
986
|
-
const cache = await nativeCache(name);
|
|
987
|
-
return cache ? cache.delete(nativeInput(input, modulePath), options) : false;
|
|
988
|
-
},
|
|
989
|
-
async keys(input, options) {
|
|
990
|
-
const cache = await nativeCache(name);
|
|
991
|
-
if (!cache) return [];
|
|
992
|
-
return input === undefined
|
|
993
|
-
? cache.keys()
|
|
994
|
-
: cache.keys(nativeInput(input, modulePath), options);
|
|
995
|
-
},
|
|
996
|
-
});
|
|
986
|
+
function fetchResourceRecord(input, init) {
|
|
987
|
+
return resourceClient.fetchResource(input, resourceOptions(input, init));
|
|
997
988
|
}
|
|
998
989
|
|
|
999
|
-
return completeValue({
|
|
990
|
+
return completeValue({ fetchResource, fetchResourceRecord, nativeInput, resolve, responseFor });
|
|
1000
991
|
}
|
|
1001
992
|
|
|
1002
993
|
function createOrdinaryArtifactModuleRouter(
|
|
@@ -1004,7 +995,6 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1004
995
|
configuration,
|
|
1005
996
|
role,
|
|
1006
997
|
reader,
|
|
1007
|
-
originalFetch,
|
|
1008
998
|
originalWorker,
|
|
1009
999
|
) {
|
|
1010
1000
|
const nestedWorkers = new Set();
|
|
@@ -1021,14 +1011,11 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1021
1011
|
},
|
|
1022
1012
|
|
|
1023
1013
|
fetch(modulePath, input, init) {
|
|
1024
|
-
|
|
1025
|
-
return resolution
|
|
1026
|
-
? reader.responseFor(resolution, input, init)
|
|
1027
|
-
: originalFetch(reader.nativeInput(input, modulePath), init);
|
|
1014
|
+
return reader.fetchResource(input, init, modulePath);
|
|
1028
1015
|
},
|
|
1029
1016
|
|
|
1030
|
-
openCache(
|
|
1031
|
-
|
|
1017
|
+
async openCache() {
|
|
1018
|
+
throw new Error('Browser CacheStorage is unavailable in this speech Worker; resources use DBOPFS through fetch.');
|
|
1032
1019
|
},
|
|
1033
1020
|
|
|
1034
1021
|
createWorker(modulePath, specifier, options = {}) {
|
|
@@ -1041,15 +1028,60 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1041
1028
|
);
|
|
1042
1029
|
}
|
|
1043
1030
|
const resolution = reader.resolve(specifier, modulePath);
|
|
1044
|
-
if (!resolution) {
|
|
1031
|
+
if (!resolution && options?.type !== 'module') {
|
|
1045
1032
|
return new originalWorker(reader.nativeInput(specifier, modulePath), options);
|
|
1046
1033
|
}
|
|
1047
1034
|
const workerOptions = options && is.object(options)
|
|
1048
1035
|
? { ...options, type: "module" }
|
|
1049
1036
|
: { type: "module" };
|
|
1050
1037
|
const worker = new originalWorker(nestedWorkerUrl(role), workerOptions);
|
|
1038
|
+
const resourceHost = createModelResourceHost({
|
|
1039
|
+
fetchResource: reader.fetchResourceRecord,
|
|
1040
|
+
send: function sendNestedSpeechResource(message) {
|
|
1041
|
+
worker.postMessage(message);
|
|
1042
|
+
},
|
|
1043
|
+
onError: function nestedResourceTransportFailed(error) {
|
|
1044
|
+
const failure = workerError(
|
|
1045
|
+
'ARCANE_AI_WORKER_MESSAGE_ERROR',
|
|
1046
|
+
'Unable to deliver a resource to the nested speech Worker.',
|
|
1047
|
+
error,
|
|
1048
|
+
'speech-module-worker-resource-message-rejected',
|
|
1049
|
+
);
|
|
1050
|
+
void terminateNestedSpeechWorker(failure);
|
|
1051
|
+
worker.dispatchEvent?.(nestedWorkerFailureEvent(scope, failure));
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
const terminateWorker = worker.terminate.bind(worker);
|
|
1055
|
+
let closing = null;
|
|
1056
|
+
function terminateNestedSpeechWorker(reason) {
|
|
1057
|
+
if (closing) return closing;
|
|
1058
|
+
closing = Promise.allSettled([
|
|
1059
|
+
Promise.resolve().then(function closeNestedResources() {
|
|
1060
|
+
return resourceHost.close(reason);
|
|
1061
|
+
}),
|
|
1062
|
+
Promise.resolve().then(function stopNestedWorker() {
|
|
1063
|
+
return terminateWorker();
|
|
1064
|
+
}),
|
|
1065
|
+
]).then(function finishNestedTermination(settlements) {
|
|
1066
|
+
const failures = [];
|
|
1067
|
+
for (const settlement of settlements) {
|
|
1068
|
+
if (settlement.status === 'rejected') failures.push(settlement.reason);
|
|
1069
|
+
}
|
|
1070
|
+
if (failures.length) throw new AggregateError(failures, 'Unable to terminate the nested speech Worker.', { cause: reason });
|
|
1071
|
+
});
|
|
1072
|
+
// Upstream Worker.terminate() is fire-and-forget; the parent joins it.
|
|
1073
|
+
closing.catch(function reportNestedTermination(error) {
|
|
1074
|
+
scope.console?.error?.('Speech Worker resource cleanup failed.', error);
|
|
1075
|
+
});
|
|
1076
|
+
return closing;
|
|
1077
|
+
}
|
|
1078
|
+
worker.terminate = terminateNestedSpeechWorker;
|
|
1051
1079
|
nestedWorkers.add(worker);
|
|
1052
1080
|
const onBootstrapMessage = (event) => {
|
|
1081
|
+
if (resourceHost.receive(event.data)) {
|
|
1082
|
+
event.stopImmediatePropagation?.();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1053
1085
|
if (event.data?.protocol !== NESTED_WORKER_PROTOCOL
|
|
1054
1086
|
|| event.data?.event !== "artifact-module-worker-bootstrap-rejected") return;
|
|
1055
1087
|
event.stopImmediatePropagation?.();
|
|
@@ -1070,11 +1102,11 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1070
1102
|
protocol: NESTED_WORKER_PROTOCOL,
|
|
1071
1103
|
op: "initialize-artifact-module-worker",
|
|
1072
1104
|
role,
|
|
1073
|
-
targetPath: resolution
|
|
1105
|
+
targetPath: resolution?.file.path,
|
|
1106
|
+
targetUrl: resolution ? undefined : reader.nativeInput(specifier, modulePath),
|
|
1074
1107
|
configuration,
|
|
1075
1108
|
});
|
|
1076
1109
|
} catch (error) {
|
|
1077
|
-
nestedWorkers.delete(worker);
|
|
1078
1110
|
worker.terminate();
|
|
1079
1111
|
throw workerError(
|
|
1080
1112
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
@@ -1088,24 +1120,28 @@ function createOrdinaryArtifactModuleRouter(
|
|
|
1088
1120
|
});
|
|
1089
1121
|
return completeValue({
|
|
1090
1122
|
router,
|
|
1091
|
-
|
|
1092
|
-
|
|
1123
|
+
async cleanup() {
|
|
1124
|
+
const terminations = [];
|
|
1093
1125
|
for (const worker of nestedWorkers) {
|
|
1094
|
-
|
|
1095
|
-
worker.terminate();
|
|
1096
|
-
} catch {
|
|
1097
|
-
// The owning speech Worker is already being torn down.
|
|
1098
|
-
}
|
|
1126
|
+
terminations.push(worker.terminate());
|
|
1099
1127
|
}
|
|
1100
1128
|
nestedWorkers.clear();
|
|
1129
|
+
const settlements = await Promise.allSettled(terminations);
|
|
1130
|
+
const failures = [];
|
|
1131
|
+
for (const settlement of settlements) {
|
|
1132
|
+
if (settlement.status === 'rejected') failures.push(settlement.reason);
|
|
1133
|
+
}
|
|
1134
|
+
if (failures.length) throw new AggregateError(failures, 'Unable to close nested speech Workers.');
|
|
1101
1135
|
},
|
|
1102
1136
|
});
|
|
1103
1137
|
}
|
|
1104
1138
|
|
|
1105
|
-
function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
|
|
1139
|
+
function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, sourceUrl = scope.location?.href) {
|
|
1106
1140
|
const originalFetch = scope.fetch?.bind(scope);
|
|
1107
1141
|
const originalWorker = scope.Worker;
|
|
1108
|
-
const
|
|
1142
|
+
const resourceClient = createModelResourceClient({ send });
|
|
1143
|
+
let requestSignal = null;
|
|
1144
|
+
let reportProgress = null;
|
|
1109
1145
|
if (!is.function(originalFetch)) {
|
|
1110
1146
|
throw workerError(
|
|
1111
1147
|
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
@@ -1118,19 +1154,63 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
|
|
|
1118
1154
|
scope,
|
|
1119
1155
|
configuration,
|
|
1120
1156
|
originalFetch,
|
|
1121
|
-
|
|
1157
|
+
resourceClient,
|
|
1158
|
+
function getRequestSignal() { return requestSignal; },
|
|
1159
|
+
function getResourceProgress() { return reportProgress; },
|
|
1160
|
+
sourceUrl,
|
|
1122
1161
|
);
|
|
1123
1162
|
const moduleRouter = createOrdinaryArtifactModuleRouter(
|
|
1124
1163
|
scope,
|
|
1125
1164
|
configuration,
|
|
1126
1165
|
role,
|
|
1127
1166
|
reader,
|
|
1128
|
-
originalFetch,
|
|
1129
1167
|
originalWorker,
|
|
1130
1168
|
);
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1169
|
+
const originalProperties = ['fetch', 'Worker', MODULE_ROUTER_NAME].map(
|
|
1170
|
+
function originalWorkerProperty(name) {
|
|
1171
|
+
return { name, descriptor: Object.getOwnPropertyDescriptor(scope, name) };
|
|
1172
|
+
},
|
|
1173
|
+
);
|
|
1174
|
+
const removedCacheProperties = [];
|
|
1175
|
+
function restoreEnvironment() {
|
|
1176
|
+
const failures = [];
|
|
1177
|
+
for (const { owner, descriptor } of removedCacheProperties.splice(0).reverse()) {
|
|
1178
|
+
try {
|
|
1179
|
+
Object.defineProperty(owner, 'caches', descriptor);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
failures.push(error);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
for (const { name, descriptor } of originalProperties) {
|
|
1185
|
+
try {
|
|
1186
|
+
if (descriptor) Object.defineProperty(scope, name, descriptor);
|
|
1187
|
+
else delete scope[name];
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
failures.push(error);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (failures.length) throw new AggregateError(failures, 'Unable to restore the speech Worker resource environment.');
|
|
1193
|
+
}
|
|
1194
|
+
function resourceFetch(input, init) {
|
|
1195
|
+
return reader.fetchResource(input, init, null);
|
|
1196
|
+
}
|
|
1133
1197
|
try {
|
|
1198
|
+
// Upstream checks `caches in self` before import. Remove only this Worker's
|
|
1199
|
+
// API descriptors so its normal fetch path reaches the parent DBOPFS owner.
|
|
1200
|
+
for (let owner = scope; owner; owner = Object.getPrototypeOf(owner)) {
|
|
1201
|
+
const descriptor = Object.getOwnPropertyDescriptor(owner, 'caches');
|
|
1202
|
+
if (!descriptor) continue;
|
|
1203
|
+
if (!delete owner.caches) {
|
|
1204
|
+
throw new Error('The speech Worker cannot disable its native CacheStorage API.');
|
|
1205
|
+
}
|
|
1206
|
+
removedCacheProperties.push({ owner, descriptor });
|
|
1207
|
+
}
|
|
1208
|
+
scope.fetch = resourceFetch;
|
|
1209
|
+
if (is.function(originalWorker)) {
|
|
1210
|
+
scope.Worker = function ResourceSpeechWorker(specifier, options) {
|
|
1211
|
+
return moduleRouter.router.createWorker(null, specifier, options);
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1134
1214
|
scope[MODULE_ROUTER_NAME] = moduleRouter.router;
|
|
1135
1215
|
if (scope[MODULE_ROUTER_NAME] !== moduleRouter.router) {
|
|
1136
1216
|
Object.defineProperty(scope, MODULE_ROUTER_NAME, {
|
|
@@ -1141,24 +1221,41 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
|
|
|
1141
1221
|
});
|
|
1142
1222
|
}
|
|
1143
1223
|
} catch (error) {
|
|
1144
|
-
|
|
1224
|
+
resourceClient.close(error);
|
|
1225
|
+
let cause = error;
|
|
1226
|
+
try {
|
|
1227
|
+
restoreEnvironment();
|
|
1228
|
+
} catch (restoreError) {
|
|
1229
|
+
cause = new AggregateError([error, restoreError], 'Speech Worker resource setup and restoration failed.');
|
|
1230
|
+
}
|
|
1145
1231
|
throw workerError(
|
|
1146
1232
|
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1147
1233
|
"The speech Worker cannot install the artifact module router.",
|
|
1148
|
-
|
|
1234
|
+
cause,
|
|
1149
1235
|
"speech-module-router-unavailable",
|
|
1150
1236
|
);
|
|
1151
1237
|
}
|
|
1152
1238
|
return completeValue({
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1239
|
+
receive: resourceClient.receive,
|
|
1240
|
+
setSignal(signal) { requestSignal = signal; },
|
|
1241
|
+
setProgress(report) { reportProgress = report; },
|
|
1242
|
+
async cleanup() {
|
|
1243
|
+
resourceClient.close(new Error('The speech Worker resource owner was unloaded.'));
|
|
1244
|
+
let cleanupFailure = null;
|
|
1156
1245
|
try {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1246
|
+
await moduleRouter.cleanup();
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
cleanupFailure = error;
|
|
1249
|
+
} finally {
|
|
1250
|
+
try {
|
|
1251
|
+
restoreEnvironment();
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
cleanupFailure = cleanupFailure
|
|
1254
|
+
? new AggregateError([cleanupFailure, error], 'Speech Worker resource cleanup and restoration failed.')
|
|
1255
|
+
: error;
|
|
1256
|
+
}
|
|
1161
1257
|
}
|
|
1258
|
+
if (cleanupFailure) throw cleanupFailure;
|
|
1162
1259
|
},
|
|
1163
1260
|
});
|
|
1164
1261
|
}
|
|
@@ -1243,7 +1340,7 @@ function configuredWasmPaths(configuration) {
|
|
|
1243
1340
|
return completeValue({ mjs: mjs.moduleUrl, wasm: wasm.moduleUrl });
|
|
1244
1341
|
}
|
|
1245
1342
|
|
|
1246
|
-
function configureRuntimeNamespace(namespace, configuration, role
|
|
1343
|
+
function configureRuntimeNamespace(namespace, configuration, role) {
|
|
1247
1344
|
const cleanup = [];
|
|
1248
1345
|
const restoreSettings = () => {
|
|
1249
1346
|
for (const restore of cleanup.splice(0).reverse()) {
|
|
@@ -1296,6 +1393,9 @@ function configureRuntimeNamespace(namespace, configuration, role, cache) {
|
|
|
1296
1393
|
);
|
|
1297
1394
|
}
|
|
1298
1395
|
try {
|
|
1396
|
+
for (const name of ['useBrowserCache', 'useCustomCache', 'useFSCache']) {
|
|
1397
|
+
if (name in env) assignSetting(env, name, false, cleanup);
|
|
1398
|
+
}
|
|
1299
1399
|
assignSetting(env, "allowLocalModels", false, cleanup, {
|
|
1300
1400
|
assignmentRejectedReason: "transformers-env-allow-local-models-assignment-rejected",
|
|
1301
1401
|
unavailableReason: "transformers-env-allow-local-models-unavailable",
|
|
@@ -1641,14 +1741,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1641
1741
|
configuration = validateConfiguration(request.payload?.configuration, role);
|
|
1642
1742
|
const entry = configuration.runtime.files.find((file) =>
|
|
1643
1743
|
file.path === configuration.runtime.entry);
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
environment = completeValue({
|
|
1648
|
-
cache: null,
|
|
1649
|
-
cleanup() {},
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1744
|
+
environment = installOrdinaryArtifactModuleRouter(scope, configuration, role, send);
|
|
1745
|
+
environment.setSignal(signal);
|
|
1746
|
+
environment.setProgress(publishProgress);
|
|
1652
1747
|
loadFailureReason = `${role}-worker-runtime-import-rejected`;
|
|
1653
1748
|
publishProgress(
|
|
1654
1749
|
{
|
|
@@ -1665,7 +1760,6 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1665
1760
|
namespace,
|
|
1666
1761
|
configuration,
|
|
1667
1762
|
role,
|
|
1668
|
-
environment.cache,
|
|
1669
1763
|
);
|
|
1670
1764
|
loadFailureReason = `${role}-worker-model-load-rejected`;
|
|
1671
1765
|
publishProgress(
|
|
@@ -1698,8 +1792,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1698
1792
|
restoreNamespace?.();
|
|
1699
1793
|
restoreNamespace = null;
|
|
1700
1794
|
try {
|
|
1701
|
-
environment?.cleanup();
|
|
1702
|
-
} catch {
|
|
1795
|
+
await environment?.cleanup();
|
|
1796
|
+
} catch (cleanupError) {
|
|
1797
|
+
scope.console?.error?.('Speech Worker resource cleanup failed after load rejection.', cleanupError);
|
|
1703
1798
|
// A rejected load is followed by provider-owned Worker termination.
|
|
1704
1799
|
}
|
|
1705
1800
|
environment = null;
|
|
@@ -1785,8 +1880,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1785
1880
|
restoreNamespace?.();
|
|
1786
1881
|
restoreNamespace = null;
|
|
1787
1882
|
try {
|
|
1788
|
-
environment?.cleanup();
|
|
1789
|
-
} catch {
|
|
1883
|
+
await environment?.cleanup();
|
|
1884
|
+
} catch (cleanupError) {
|
|
1885
|
+
scope.console?.error?.('Speech Worker resource cleanup failed during unload.', cleanupError);
|
|
1790
1886
|
// Worker termination remains the final cleanup boundary.
|
|
1791
1887
|
}
|
|
1792
1888
|
environment = null;
|
|
@@ -1833,6 +1929,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1833
1929
|
}
|
|
1834
1930
|
|
|
1835
1931
|
function handleMessage(request) {
|
|
1932
|
+
if (environment?.receive(request)) return Promise.resolve();
|
|
1836
1933
|
if (request?.protocol !== SPEECH_WORKER_PROTOCOL
|
|
1837
1934
|
|| !is.safeInteger(request.id)
|
|
1838
1935
|
|| request.id < 1) {
|
|
@@ -1871,7 +1968,14 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
1871
1968
|
const record = { controller, op, publicOperation };
|
|
1872
1969
|
const execute = tail.catch(() => undefined).then(() => {
|
|
1873
1970
|
throwIfAborted(controller.signal, operationReason(role, op, "cancelled"));
|
|
1874
|
-
|
|
1971
|
+
environment?.setSignal(controller.signal);
|
|
1972
|
+
environment?.setProgress(function publishResourceProgress(progress) {
|
|
1973
|
+
send({ protocol: SPEECH_WORKER_PROTOCOL, id: request.id, type: 'progress', progress });
|
|
1974
|
+
});
|
|
1975
|
+
return dispatch(request, controller.signal, op).finally(function releaseResourceSignal() {
|
|
1976
|
+
environment?.setSignal(null);
|
|
1977
|
+
environment?.setProgress(null);
|
|
1978
|
+
});
|
|
1875
1979
|
});
|
|
1876
1980
|
operations.set(request.id, record);
|
|
1877
1981
|
const operation = execute.finally(() => operations.delete(request.id));
|
|
@@ -1913,7 +2017,12 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
|
|
|
1913
2017
|
throw new TypeError('Speech artifact module Worker role must be "stt" or "tts".');
|
|
1914
2018
|
}
|
|
1915
2019
|
let initializing = false;
|
|
2020
|
+
let environment = null;
|
|
1916
2021
|
const queued = [];
|
|
2022
|
+
function receiveResource(event) {
|
|
2023
|
+
if (environment?.receive(event.data)) event.stopImmediatePropagation?.();
|
|
2024
|
+
}
|
|
2025
|
+
scope.addEventListener('message', receiveResource);
|
|
1917
2026
|
const bootstrap = (event) => {
|
|
1918
2027
|
if (initializing) {
|
|
1919
2028
|
queued.push(event);
|
|
@@ -1925,12 +2034,11 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
|
|
|
1925
2034
|
|| request.role !== role) return;
|
|
1926
2035
|
initializing = true;
|
|
1927
2036
|
void (async () => {
|
|
1928
|
-
let environment = null;
|
|
1929
2037
|
try {
|
|
1930
2038
|
const configuration = validateConfiguration(request.configuration, role);
|
|
1931
2039
|
const target = configuration.runtime.files.find((file) =>
|
|
1932
2040
|
file.path === request.targetPath);
|
|
1933
|
-
if (!target?.moduleUrl) {
|
|
2041
|
+
if (!target?.moduleUrl && !request.targetUrl) {
|
|
1934
2042
|
throw workerError(
|
|
1935
2043
|
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1936
2044
|
"The nested module Worker target was not materialized.",
|
|
@@ -1938,15 +2046,22 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
|
|
|
1938
2046
|
"artifact-graph-module-worker-target-not-materialized",
|
|
1939
2047
|
);
|
|
1940
2048
|
}
|
|
1941
|
-
environment = installOrdinaryArtifactModuleRouter(
|
|
1942
|
-
|
|
2049
|
+
environment = installOrdinaryArtifactModuleRouter(
|
|
2050
|
+
scope,
|
|
2051
|
+
configuration,
|
|
2052
|
+
role,
|
|
2053
|
+
function sendNestedResource(message) { scope.postMessage(message); },
|
|
2054
|
+
target?.sourceUrl ?? request.targetUrl,
|
|
2055
|
+
);
|
|
2056
|
+
await import(target?.moduleUrl ?? request.targetUrl);
|
|
1943
2057
|
scope.removeEventListener("message", bootstrap);
|
|
1944
2058
|
await new Promise((resolve) => queueMicrotask(resolve));
|
|
1945
2059
|
for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
|
|
1946
2060
|
} catch (error) {
|
|
1947
2061
|
try {
|
|
1948
|
-
environment?.cleanup();
|
|
1949
|
-
} catch {
|
|
2062
|
+
await environment?.cleanup();
|
|
2063
|
+
} catch (cleanupError) {
|
|
2064
|
+
scope.console?.error?.('Nested speech Worker resource cleanup failed.', cleanupError);
|
|
1950
2065
|
// Preserve the exact nested Worker bootstrap failure.
|
|
1951
2066
|
}
|
|
1952
2067
|
try {
|
|
@@ -5,7 +5,7 @@ export const CORE_READY_EVENTS=['transport.ready','core.ready'];
|
|
|
5
5
|
export const CORE_FRAME_CONTRACTS={
|
|
6
6
|
request:{type:'request',fields:['protocol','type','id','method','parameters','sentAt']},
|
|
7
7
|
response:{type:'response',fields:['protocol','type','id','ok'],success:['result'],failure:['error'],optional:['time']},
|
|
8
|
-
event:{type:'event',fields:['protocol','type','event','data'],optional:['time']},
|
|
8
|
+
event:{type:'event',fields:['protocol','type','event','data'],optional:['time','requestId']},
|
|
9
9
|
cancel:{type:'control',control:'request.cancel',fields:['protocol','type','control','requestId']},
|
|
10
10
|
cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']},
|
|
11
11
|
replay:{type:'control',control:'runtime.replay',fields:['protocol','type','control']}
|
package/docs/reference/README.md
CHANGED
|
@@ -52,6 +52,8 @@ alone does not make bare module names resolve in a browser.
|
|
|
52
52
|
| Present Light / Dark / System choices with complete labels | [Theme-switcher configuration](runtime-components.md#theme-switcherhtml) |
|
|
53
53
|
| Use optional Core preferences and appearance without masking real failures | [PreferenceStore](runtime-modules.md#preferencestorejs), [SystemAppearance](runtime-modules.md#systemappearancejs), and [Core lookup errors](core-runtime.md#state-and-frames) |
|
|
54
54
|
| Submit complete speech parts immediately and play them in order | [`SpeechPlayback`](runtime-modules.md#speechplaybackjs) and the [basic browser example](ai/browser-speech.md#play-a-complete-array-with-speechplayback) |
|
|
55
|
+
| Preserve exact text in an explicit complete synthesis call | [Exact complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input) and [managed AI narration](sdk-api.md#managed-ai-narration) |
|
|
56
|
+
| Select initial native window dimensions and resizing behavior | [Initial native window size](core-native-packaging.md#initial-native-window-size) and [Windows provider](sdk-api.md#createwindowsnativeprovider) |
|
|
55
57
|
| Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
|
|
56
58
|
| Connect a browser to one Core transport | [Core browser client](core-client.md) |
|
|
57
59
|
| Cancel one Core speech synthesis request without changing its payload | [Speech request lifetime](core-client.md#events-and-request-lifetime) and [Core facade](sdk-api.md#createcorefacade) |
|
|
@@ -70,9 +70,12 @@ it does not substitute a quantized graph or another model.
|
|
|
70
70
|
| `dtype` | Exact upstream precision selection; defaults to `fp16` for Laya and `fp32` for Julia. For Laya FP32, supply `dtype: 'fp32'`. Unsupported selections fail at the selected upstream loader rather than being replaced. |
|
|
71
71
|
| `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. |
|
|
72
72
|
| `runtime.wasmPaths` | Optional upstream ONNX WASM location passed to the selected runtime's public environment API. |
|
|
73
|
+
| `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK bridges the loader's actual resource requests to it. |
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
Without `store`, the runtime, tokenizer and weights use normal upstream loading
|
|
76
|
+
and caching. With `store`, the selected loader still owns filenames and model
|
|
77
|
+
selection while model resources persist through that existing SDK store. The
|
|
78
|
+
SDK does not redistribute upstream runtimes or models. Browser
|
|
76
79
|
network, cross-origin and backend availability still apply. WebGPU needs a
|
|
77
80
|
compatible browser/device context. Unsupported precision or backend errors
|
|
78
81
|
are returned, not replaced with a different model, backend or precision.
|
|
@@ -88,6 +91,57 @@ Julia selects `dtype: 'fp32'`, loading root `model.onnx` with the exact
|
|
|
88
91
|
through the public generic `PreTrainedModel` API, not a text-generation
|
|
89
92
|
pipeline. Existing Wllama, cloud, speech and chat APIs are unchanged.
|
|
90
93
|
|
|
94
|
+
## Shared DBOPFS model resources
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import {createDbopfsModelStore} from 'arcane-os/ai/browser-wasm';
|
|
98
|
+
import {createBrowserDecisionModel} from 'arcane-os/ai/browser-decisions';
|
|
99
|
+
|
|
100
|
+
// dbopfs is the application's existing Arcane DBOPFS instance.
|
|
101
|
+
const store = createDbopfsModelStore({dbopfs});
|
|
102
|
+
const decisions = createBrowserDecisionModel(
|
|
103
|
+
{
|
|
104
|
+
family: 'laya',
|
|
105
|
+
model: 'onnx-community/laya-typed-decisions-ONNX',
|
|
106
|
+
store
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
await decisions.load();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The same `store` may serve other SDK model clients. The application supplies no
|
|
113
|
+
tokenizer/configuration/graph manifest and performs no file routing. Inside the
|
|
114
|
+
dedicated decision Worker, Transformers.js's public `env.fetch` hook routes the
|
|
115
|
+
actual selected tokenizer, configuration, ONNX graph and external-data requests
|
|
116
|
+
to `store.fetchResource(input, options)`. Its competing browser, custom and
|
|
117
|
+
filesystem model caches are disabled only for this explicit stored mode. The
|
|
118
|
+
default upstream ONNX WASM binary and factory preloads use this same hook.
|
|
119
|
+
Native support-file fetches in the dedicated Worker also use the store. The
|
|
120
|
+
runtime module itself still loads from `runtime.moduleUrl` through normal module
|
|
121
|
+
loading. Native ESM imports remain browser-owned: an upstream custom factory
|
|
122
|
+
import outside its preload path is not claimed as a DBOPFS resource. No second
|
|
123
|
+
application cache or model substitution is introduced.
|
|
124
|
+
|
|
125
|
+
The store returns `{file, status, statusText, headers, url, redirected}`. The
|
|
126
|
+
Worker reconstructs the complete response with its actual HTTP status and
|
|
127
|
+
headers, so a missing optional upstream file remains a missing-file response.
|
|
128
|
+
New response bodies stream into ordered persistent shards, including resources
|
|
129
|
+
with unknown totals. Completed resources are reused by their semantic request
|
|
130
|
+
and URL selection. Interrupted closed shards remain available for HTTP Range
|
|
131
|
+
resume; a server returning a full response restarts that member without adding
|
|
132
|
+
the saved prefix twice. Independent resources remain concurrent, and repeated
|
|
133
|
+
requests for the same resource share its storage mutation owner. Existing saved
|
|
134
|
+
model formats are retained without a migration.
|
|
135
|
+
|
|
136
|
+
Resource progress uses `{phase: 'download', completed, total, unit: 'shards',
|
|
137
|
+
url}`. `completed` counts closed persistent shards; `total` is `null` until the
|
|
138
|
+
resource finishes. This progress carries no network-size, rate or ETA fields.
|
|
139
|
+
Cancellation aborts the owned fetch and joins reader/writer cleanup before the
|
|
140
|
+
resource owner becomes available to a later request. Interrupted closed shards
|
|
141
|
+
are retained. `unload()` and `dispose()` still acknowledge their lifecycle
|
|
142
|
+
transition synchronously; outstanding `load()`/`evaluate()` calls settle after
|
|
143
|
+
their resource cleanup.
|
|
144
|
+
|
|
91
145
|
## Complete rows and results
|
|
92
146
|
|
|
93
147
|
`evaluate(rows, {signal})` accepts an array in caller order. Each row has
|
|
@@ -139,7 +193,7 @@ errors, including graph diagnostics when decoding fails, remain available in
|
|
|
139
193
|
|
|
140
194
|
Work is acknowledged synchronously before loading/inference waits. Progress
|
|
141
195
|
reports real semantic phases such as `waiting-for-model`, `loading-runtime`, `loading-tokenizer`,
|
|
142
|
-
`loading-model`, `ready`, `evaluating` and `complete`; it is not a synthetic
|
|
196
|
+
`loading-model`, `download`, `ready`, `evaluating` and `complete`; it is not a synthetic
|
|
143
197
|
percentage. Loading the model and tokenizer proceeds concurrently. Repeated
|
|
144
198
|
identical state text is tokenized once per batch. Independent requests are
|
|
145
199
|
accepted concurrently; the selected backend still owns its execution ordering.
|