arcane-os 0.5.7 → 0.5.9
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 +25 -0
- package/README.md +39 -27
- package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
- package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +32 -324
- package/browser-runtime/ai/speech-worker-client.mjs +51 -14
- package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
- package/browser-runtime/dependencies/strong-type/package.json +22 -23
- package/browser-runtime/event-manager.mjs +27 -41
- package/package.json +2 -3
- package/runtime/arcane/components/chat.html +1 -1
- package/runtime/arcane/components/speech.html +8 -38
- package/runtime/arcane/components/voice-transcription.html +1 -1
- package/runtime/arcane/css/theme.css +1 -1
- package/runtime/arcane/entities/Chat.js +12 -7
- package/runtime/arcane/modules/AI.js +569 -402
- package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
- package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
- package/runtime/arcane/modules/AIRuntimeState.js +2 -18
- package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
- package/runtime/arcane/modules/CalculatorEngine.js +0 -4
- package/runtime/arcane/modules/ChatRecords.js +169 -1
- package/runtime/arcane/modules/CommunicationHub.js +0 -19
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
- package/runtime/arcane/modules/Errors.js +6 -19
- package/runtime/arcane/modules/Mail.js +1 -2
- package/runtime/arcane/modules/MailTransport.mjs +1 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
- package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
- package/runtime/arcane/modules/ScreenCapture.js +2 -4
- package/runtime/arcane/modules/SpeechPlayback.js +0 -27
- package/runtime/arcane/modules/WaitForComponent.js +0 -1
- package/src/app-descriptor.mjs +1 -1
- package/src/doctor.mjs +1 -3
- package/src/event-manager.mjs +27 -41
- package/src/import-map.mjs +1 -1
- package/src/index.mjs +2 -9
- package/src/installed-sdk-runtime.mjs +1 -11
- package/src/mail-api.mjs +0 -1
- package/src/native-provider-loader.mjs +0 -9
- package/src/scaffold.mjs +9 -21
- package/src/toolchain.mjs +5 -28
- package/src/workspace-runtime.mjs +0 -4
- package/src/workspace.mjs +2 -23
|
@@ -10,9 +10,6 @@ import { arcaneEvents } from "../event-manager.mjs";
|
|
|
10
10
|
|
|
11
11
|
const completeValue = (value) => value;
|
|
12
12
|
|
|
13
|
-
// Compatibility export only. Ordinary model caching no longer creates or
|
|
14
|
-
// requires completion manifests, receipts, or byte identities.
|
|
15
|
-
const MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v4";
|
|
16
13
|
const BROWSER_MODEL_SOURCES = new WeakSet();
|
|
17
14
|
const BROWSER_MODEL_SOURCE_METADATA = new WeakMap();
|
|
18
15
|
const BROWSER_MODEL_SOURCE_TRANSPORTS = new WeakMap();
|
|
@@ -31,8 +28,6 @@ const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
|
|
|
31
28
|
const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
|
|
32
29
|
const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 4_096;
|
|
33
30
|
const MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 4_000_000;
|
|
34
|
-
const LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS = 16;
|
|
35
|
-
const LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 128_000_000;
|
|
36
31
|
const INTEL_VENDOR_ID = 0x8086;
|
|
37
32
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
38
33
|
let highPerformanceGpuNoticeShown = false;
|
|
@@ -132,39 +127,6 @@ function modelHttpRanges(total) {
|
|
|
132
127
|
);
|
|
133
128
|
}
|
|
134
129
|
|
|
135
|
-
function modelHttpSubranges(range) {
|
|
136
|
-
const length = range.end - range.start + 1;
|
|
137
|
-
const maximumParts = Math.floor(
|
|
138
|
-
MODEL_DOWNLOAD_MAX_RANGE_PARTS / LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
139
|
-
);
|
|
140
|
-
return completeValue(modelHttpRangesFor(
|
|
141
|
-
length,
|
|
142
|
-
maximumParts,
|
|
143
|
-
MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
144
|
-
).map((part) => completeValue({
|
|
145
|
-
start: range.start + part.start,
|
|
146
|
-
end: range.start + part.end,
|
|
147
|
-
total: range.total,
|
|
148
|
-
})));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function modelHttpRangePlans(total) {
|
|
152
|
-
const current = modelHttpRanges(total);
|
|
153
|
-
const legacy = modelHttpRangesFor(
|
|
154
|
-
total,
|
|
155
|
-
LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
156
|
-
LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
157
|
-
);
|
|
158
|
-
if (
|
|
159
|
-
current.length === legacy.length
|
|
160
|
-
&& current.every((range, index) => (
|
|
161
|
-
range.start === legacy[index].start
|
|
162
|
-
&& range.end === legacy[index].end
|
|
163
|
-
))
|
|
164
|
-
) return completeValue([current]);
|
|
165
|
-
return completeValue([current, legacy]);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
130
|
function progressClock() {
|
|
169
131
|
return typeof globalThis.performance?.now === "function"
|
|
170
132
|
? globalThis.performance.now()
|
|
@@ -208,7 +170,7 @@ function modelIdText(value) {
|
|
|
208
170
|
return id;
|
|
209
171
|
}
|
|
210
172
|
|
|
211
|
-
function descriptorFileName(value, url
|
|
173
|
+
function descriptorFileName(value, url) {
|
|
212
174
|
const suppliedName = value.name;
|
|
213
175
|
let name = suppliedName;
|
|
214
176
|
if (name === undefined) {
|
|
@@ -218,7 +180,6 @@ function descriptorFileName(value, url, fallbackName = null) {
|
|
|
218
180
|
} catch {
|
|
219
181
|
name = "";
|
|
220
182
|
}
|
|
221
|
-
if (!name && fallbackName) name = fallbackName;
|
|
222
183
|
}
|
|
223
184
|
name = requiredText(name, "file name");
|
|
224
185
|
if (
|
|
@@ -236,23 +197,19 @@ function descriptorFileName(value, url, fallbackName = null) {
|
|
|
236
197
|
return name;
|
|
237
198
|
}
|
|
238
199
|
|
|
239
|
-
function descriptorFile(value
|
|
200
|
+
function descriptorFile(value) {
|
|
240
201
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
241
202
|
throw new TypeError("Each browser model file descriptor must be an object.");
|
|
242
203
|
}
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
&& value.immutableUrl !== undefined
|
|
246
|
-
&& value.url !== value.immutableUrl
|
|
247
|
-
) {
|
|
248
|
-
throw new TypeError("Browser model file url and legacy immutableUrl must match when both are provided.");
|
|
204
|
+
if (Object.hasOwn(value, "immutableUrl")) {
|
|
205
|
+
throw new TypeError("Browser model file descriptors must use url.");
|
|
249
206
|
}
|
|
250
|
-
const url = modelSourceUrl(value.url
|
|
207
|
+
const url = modelSourceUrl(value.url);
|
|
251
208
|
if (!url) {
|
|
252
209
|
throw new TypeError("Browser model file url must be a valid absolute URL.");
|
|
253
210
|
}
|
|
254
211
|
const file = {
|
|
255
|
-
name: descriptorFileName(value, url
|
|
212
|
+
name: descriptorFileName(value, url),
|
|
256
213
|
url: url.href,
|
|
257
214
|
};
|
|
258
215
|
if (Number.isSafeInteger(value.bytes) && value.bytes > 0) {
|
|
@@ -266,24 +223,14 @@ function modelDescriptor(value) {
|
|
|
266
223
|
throw new TypeError("A browser model descriptor is required.");
|
|
267
224
|
}
|
|
268
225
|
const id = modelIdText(value.id);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let files;
|
|
276
|
-
let legacy = false;
|
|
277
|
-
if (hasFiles) {
|
|
278
|
-
if (!Array.isArray(value.files) || value.files.length === 0) {
|
|
279
|
-
throw new TypeError("Browser model files must be a nonempty ordered array.");
|
|
280
|
-
}
|
|
281
|
-
files = value.files.map((file) => descriptorFile(file));
|
|
282
|
-
} else {
|
|
283
|
-
legacy = true;
|
|
284
|
-
const safeId = id.replace(/[^a-z0-9._-]+/giu, "_");
|
|
285
|
-
files = [descriptorFile(value, { fallbackName: `${safeId}.gguf` })];
|
|
226
|
+
if (["url", "immutableUrl", "name", "bytes", "sha256", "licenseSpdx", "sourceRevision"]
|
|
227
|
+
.some((field) => Object.hasOwn(value, field))) {
|
|
228
|
+
throw new TypeError("Browser model descriptor files must be declared in files[].");
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(value.files) || value.files.length === 0) {
|
|
231
|
+
throw new TypeError("Browser model files must be a nonempty ordered array.");
|
|
286
232
|
}
|
|
233
|
+
let files = value.files.map((file) => descriptorFile(file));
|
|
287
234
|
const names = new Set();
|
|
288
235
|
const urls = new Set();
|
|
289
236
|
for (const file of files) {
|
|
@@ -303,16 +250,8 @@ function modelDescriptor(value) {
|
|
|
303
250
|
url: file.url,
|
|
304
251
|
...(file.bytes === undefined ? {} : { bytes: file.bytes }),
|
|
305
252
|
})));
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const [file] = files;
|
|
309
|
-
descriptor = { id, url: file.url };
|
|
310
|
-
if (file.bytes !== undefined) descriptor.bytes = file.bytes;
|
|
311
|
-
} else {
|
|
312
|
-
descriptor = { id, files: publicFiles };
|
|
313
|
-
}
|
|
314
|
-
descriptor = completeValue(descriptor);
|
|
315
|
-
MODEL_DESCRIPTOR_METADATA.set(descriptor, completeValue({ files, legacy }));
|
|
253
|
+
const descriptor = completeValue({ id, files: publicFiles });
|
|
254
|
+
MODEL_DESCRIPTOR_METADATA.set(descriptor, completeValue({ files }));
|
|
316
255
|
return descriptor;
|
|
317
256
|
}
|
|
318
257
|
|
|
@@ -394,7 +333,6 @@ function sourceMetadata(source) {
|
|
|
394
333
|
|
|
395
334
|
/**
|
|
396
335
|
* Creates a browser download authority for one caller-supplied model file set.
|
|
397
|
-
* Legacy one-file descriptors normalize to one ordered member.
|
|
398
336
|
* Security is an intent-only seam and does not change ordinary downloads.
|
|
399
337
|
*/
|
|
400
338
|
export function createBrowserModelSource(descriptor, {
|
|
@@ -470,15 +408,7 @@ export function createBrowserModelSource(descriptor, {
|
|
|
470
408
|
});
|
|
471
409
|
}
|
|
472
410
|
|
|
473
|
-
async function open(
|
|
474
|
-
let memberIndex = memberOrOptions;
|
|
475
|
-
if (!Number.isSafeInteger(memberOrOptions)) {
|
|
476
|
-
if (metadata.files.length !== 1) {
|
|
477
|
-
throw new TypeError("A browser model file index is required for a multi-file source.");
|
|
478
|
-
}
|
|
479
|
-
memberIndex = 0;
|
|
480
|
-
options = memberOrOptions ?? {};
|
|
481
|
-
}
|
|
411
|
+
async function open(memberIndex = 0, options = {}) {
|
|
482
412
|
const { signal } = options;
|
|
483
413
|
const download = await request(memberIndex, { signal });
|
|
484
414
|
if (!download.response?.ok) await rejectHttpResponse(download);
|
|
@@ -587,10 +517,6 @@ export function createBrowserModelSource(descriptor, {
|
|
|
587
517
|
descriptor: model,
|
|
588
518
|
open,
|
|
589
519
|
};
|
|
590
|
-
if (metadata.legacy) {
|
|
591
|
-
sourceRecord.name = metadata.files[0].name;
|
|
592
|
-
sourceRecord.immutableUrl = metadata.files[0].url;
|
|
593
|
-
}
|
|
594
520
|
const source = completeValue(sourceRecord);
|
|
595
521
|
BROWSER_MODEL_SOURCES.add(source);
|
|
596
522
|
BROWSER_MODEL_SOURCE_METADATA.set(source, metadata);
|
|
@@ -654,18 +580,14 @@ function injectiveStorageId(id) {
|
|
|
654
580
|
).join("");
|
|
655
581
|
}
|
|
656
582
|
|
|
657
|
-
function storageName(source
|
|
658
|
-
const safeId =
|
|
659
|
-
? source.id.replace(/[^a-z0-9._-]+/giu, "_")
|
|
660
|
-
: `id-${injectiveStorageId(source.id)}`;
|
|
583
|
+
function storageName(source) {
|
|
584
|
+
const safeId = `id-${injectiveStorageId(source.id)}`;
|
|
661
585
|
const models = sourceMetadata(source).files.map((file) => completeValue({
|
|
662
586
|
file,
|
|
663
587
|
name: `${safeId}--${file.name}`,
|
|
664
588
|
}));
|
|
665
589
|
return completeValue({
|
|
666
590
|
models: completeValue(models),
|
|
667
|
-
model: models[0].name,
|
|
668
|
-
manifest: `${safeId}.complete.json`,
|
|
669
591
|
});
|
|
670
592
|
}
|
|
671
593
|
|
|
@@ -973,7 +895,7 @@ export function createDbopfsModelStore({
|
|
|
973
895
|
} = {},
|
|
974
896
|
) {
|
|
975
897
|
const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
|
|
976
|
-
const ranges =
|
|
898
|
+
const ranges = rangePlanForInstall(total, signal);
|
|
977
899
|
const partFiles = new Array(ranges.length);
|
|
978
900
|
const pendingRangeIndexes = [];
|
|
979
901
|
for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
|
|
@@ -1161,7 +1083,6 @@ export function createDbopfsModelStore({
|
|
|
1161
1083
|
async function removeNames(names) {
|
|
1162
1084
|
const removed = [];
|
|
1163
1085
|
for (const entry of names.models) removed.push(await removeEntry(entry.name));
|
|
1164
|
-
removed.push(await removeEntry(names.manifest));
|
|
1165
1086
|
return removed.some(Boolean);
|
|
1166
1087
|
}
|
|
1167
1088
|
|
|
@@ -1176,74 +1097,9 @@ export function createDbopfsModelStore({
|
|
|
1176
1097
|
return names;
|
|
1177
1098
|
}
|
|
1178
1099
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
total
|
|
1182
|
-
signal,
|
|
1183
|
-
knownNames = undefined,
|
|
1184
|
-
) {
|
|
1185
|
-
const plans = modelHttpRangePlans(total);
|
|
1186
|
-
const existingNames = knownNames === undefined
|
|
1187
|
-
? await memberRangePartNames(modelName)
|
|
1188
|
-
: knownNames;
|
|
1189
|
-
const available = existingNames === null
|
|
1190
|
-
? null
|
|
1191
|
-
: existingNames instanceof Set
|
|
1192
|
-
? existingNames
|
|
1193
|
-
: new Set(existingNames);
|
|
1194
|
-
if (available?.size === 0) return plans[0];
|
|
1195
|
-
if (plans.length === 1) return plans[0];
|
|
1196
|
-
const completedParts = new Map();
|
|
1197
|
-
|
|
1198
|
-
async function completedPart(range) {
|
|
1199
|
-
throwIfAborted(signal, "install");
|
|
1200
|
-
const partName = rangePartName(modelName, range);
|
|
1201
|
-
if (available && !available.has(partName)) return null;
|
|
1202
|
-
if (completedParts.has(partName)) return completedParts.get(partName);
|
|
1203
|
-
const partFile = await file(partName);
|
|
1204
|
-
const expected = range.end - range.start + 1;
|
|
1205
|
-
const completed = partFile?.size === expected ? partFile : null;
|
|
1206
|
-
completedParts.set(partName, completed);
|
|
1207
|
-
return completed;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
const completedLegacyRanges = new Set();
|
|
1211
|
-
for (const range of plans[1]) {
|
|
1212
|
-
if (await completedPart(range)) completedLegacyRanges.add(range);
|
|
1213
|
-
}
|
|
1214
|
-
const hybrid = completeValue(plans[1].flatMap((range) => (
|
|
1215
|
-
completedLegacyRanges.has(range)
|
|
1216
|
-
? [range]
|
|
1217
|
-
: modelHttpSubranges(range)
|
|
1218
|
-
)));
|
|
1219
|
-
let selected = plans[0];
|
|
1220
|
-
let selectedBytes = 0;
|
|
1221
|
-
let selectedLastModified = 0;
|
|
1222
|
-
for (const ranges of [plans[0], hybrid]) {
|
|
1223
|
-
let completedBytes = 0;
|
|
1224
|
-
let lastModified = 0;
|
|
1225
|
-
for (const range of ranges) {
|
|
1226
|
-
const partFile = await completedPart(range);
|
|
1227
|
-
if (!partFile) continue;
|
|
1228
|
-
completedBytes += range.end - range.start + 1;
|
|
1229
|
-
if (Number.isFinite(partFile.lastModified)) {
|
|
1230
|
-
lastModified = Math.max(lastModified, partFile.lastModified);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
if (
|
|
1234
|
-
completedBytes > selectedBytes
|
|
1235
|
-
|| (
|
|
1236
|
-
completedBytes === selectedBytes
|
|
1237
|
-
&& completedBytes > 0
|
|
1238
|
-
&& lastModified > selectedLastModified
|
|
1239
|
-
)
|
|
1240
|
-
) {
|
|
1241
|
-
selected = ranges;
|
|
1242
|
-
selectedBytes = completedBytes;
|
|
1243
|
-
selectedLastModified = lastModified;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return selected;
|
|
1100
|
+
function rangePlanForInstall(total, signal) {
|
|
1101
|
+
throwIfAborted(signal, "install");
|
|
1102
|
+
return modelHttpRanges(total);
|
|
1247
1103
|
}
|
|
1248
1104
|
|
|
1249
1105
|
async function removeMemberRangeParts(modelName, {
|
|
@@ -1253,14 +1109,7 @@ export function createDbopfsModelStore({
|
|
|
1253
1109
|
const existingNames = await memberRangePartNames(modelName);
|
|
1254
1110
|
let names = existingNames;
|
|
1255
1111
|
if (names === null && Number.isSafeInteger(total) && total > 0) {
|
|
1256
|
-
|
|
1257
|
-
const hybridRanges = plans.length === 1
|
|
1258
|
-
? []
|
|
1259
|
-
: plans[1].flatMap((range) => modelHttpSubranges(range));
|
|
1260
|
-
names = [...new Set([
|
|
1261
|
-
...plans.flat(),
|
|
1262
|
-
...hybridRanges,
|
|
1263
|
-
].map((range) => rangePartName(modelName, range)))];
|
|
1112
|
+
names = modelHttpRanges(total).map((range) => rangePartName(modelName, range));
|
|
1264
1113
|
}
|
|
1265
1114
|
names ??= [];
|
|
1266
1115
|
const removed = [];
|
|
@@ -1356,120 +1205,11 @@ export function createDbopfsModelStore({
|
|
|
1356
1205
|
|
|
1357
1206
|
async function remove(source) {
|
|
1358
1207
|
const names = storageName(source);
|
|
1359
|
-
const legacyNames = storageName(source, { legacy: true });
|
|
1360
|
-
let legacyManifest = null;
|
|
1361
|
-
let removeLegacy = sourceMetadata(source).legacy;
|
|
1362
|
-
try {
|
|
1363
|
-
legacyManifest = await legacyManifestForSource(source, legacyNames);
|
|
1364
|
-
removeLegacy ||= Boolean(legacyManifest);
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
globalThis.console?.warn?.(
|
|
1367
|
-
"Arcane could not inspect the legacy browser model cache during removal.",
|
|
1368
|
-
error,
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
1208
|
let removed = await removeNames(names);
|
|
1372
1209
|
removed = await removeRangeParts(source, names) || removed;
|
|
1373
|
-
if (removeLegacy) {
|
|
1374
|
-
removed = await removeNames(
|
|
1375
|
-
legacyStorageNames(source, legacyNames, legacyManifest),
|
|
1376
|
-
) || removed;
|
|
1377
|
-
}
|
|
1378
1210
|
return removed;
|
|
1379
1211
|
}
|
|
1380
1212
|
|
|
1381
|
-
async function legacyManifestForSource(source, legacyNames) {
|
|
1382
|
-
const manifestFile = await file(legacyNames.manifest);
|
|
1383
|
-
if (!manifestFile) return null;
|
|
1384
|
-
let manifest;
|
|
1385
|
-
try {
|
|
1386
|
-
manifest = JSON.parse(await manifestFile.text());
|
|
1387
|
-
} catch {
|
|
1388
|
-
return null;
|
|
1389
|
-
}
|
|
1390
|
-
const model = manifest?.model;
|
|
1391
|
-
return manifest?.complete === true && model?.id === source.id ? manifest : null;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
function legacyStorageNames(source, names, manifest) {
|
|
1395
|
-
if (!manifest) return names;
|
|
1396
|
-
const safeId = source.id.replace(/[^a-z0-9._-]+/giu, "_");
|
|
1397
|
-
const storedNames = new Set(names.models.map((entry) => entry.name));
|
|
1398
|
-
const candidates = [];
|
|
1399
|
-
if (Array.isArray(manifest.model?.files)) candidates.push(...manifest.model.files);
|
|
1400
|
-
if (manifest.model && typeof manifest.model === "object") candidates.push(manifest.model);
|
|
1401
|
-
if (Array.isArray(manifest.files)) candidates.push(...manifest.files);
|
|
1402
|
-
for (const candidate of candidates) {
|
|
1403
|
-
if (!candidate || typeof candidate !== "object") continue;
|
|
1404
|
-
if (
|
|
1405
|
-
candidate.name === undefined
|
|
1406
|
-
&& candidate.url === undefined
|
|
1407
|
-
&& candidate.immutableUrl === undefined
|
|
1408
|
-
) continue;
|
|
1409
|
-
try {
|
|
1410
|
-
const url = modelSourceUrl(candidate.url ?? candidate.immutableUrl)
|
|
1411
|
-
?? new URL("https://arcane.invalid/model.gguf");
|
|
1412
|
-
const name = descriptorFileName(candidate, url);
|
|
1413
|
-
storedNames.add(`${safeId}--${name}`);
|
|
1414
|
-
} catch {
|
|
1415
|
-
// Keep cleanup limited to valid filenames attributable to this manifest.
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
return completeValue({
|
|
1419
|
-
...names,
|
|
1420
|
-
models: completeValue([...storedNames].map((name) => completeValue({ name }))),
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
async function legacyNamespaceMatchesSource(source, legacyNames) {
|
|
1425
|
-
const metadata = sourceMetadata(source);
|
|
1426
|
-
if (metadata.legacy) return true;
|
|
1427
|
-
if (metadata.files.length !== 1) return false;
|
|
1428
|
-
const manifest = await legacyManifestForSource(source, legacyNames);
|
|
1429
|
-
if (!manifest) return false;
|
|
1430
|
-
const model = manifest.model;
|
|
1431
|
-
const [member] = metadata.files;
|
|
1432
|
-
if (model.url === member.url) return true;
|
|
1433
|
-
if (model.name === member.name && model.immutableUrl === member.url) return true;
|
|
1434
|
-
return Array.isArray(model.files)
|
|
1435
|
-
&& model.files.length === 1
|
|
1436
|
-
&& model.files[0]?.name === member.name
|
|
1437
|
-
&& model.files[0]?.url === member.url;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
async function legacyCacheForSource(source, signal) {
|
|
1441
|
-
const names = storageName(source, { legacy: true });
|
|
1442
|
-
if (!await legacyNamespaceMatchesSource(source, names)) return null;
|
|
1443
|
-
const files = await storedModelFiles(names, signal);
|
|
1444
|
-
return files ? completeValue({ files, names }) : null;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
async function removeLegacyCacheAfterReplacement(source) {
|
|
1448
|
-
try {
|
|
1449
|
-
const names = storageName(source, { legacy: true });
|
|
1450
|
-
const manifest = await legacyManifestForSource(source, names);
|
|
1451
|
-
if (!sourceMetadata(source).legacy && !manifest) return;
|
|
1452
|
-
await removeNames(legacyStorageNames(source, names, manifest));
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
globalThis.console?.warn?.(
|
|
1455
|
-
"Arcane could not remove the superseded legacy browser model cache.",
|
|
1456
|
-
error,
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
async function removeIncompleteReplacement(source, names) {
|
|
1462
|
-
try {
|
|
1463
|
-
await removeNames(names);
|
|
1464
|
-
await removeRangeParts(source, names);
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
globalThis.console?.warn?.(
|
|
1467
|
-
"Arcane could not remove an incomplete duplicate browser model cache.",
|
|
1468
|
-
error,
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
1213
|
async function storagePolicy({ cached = false } = {}) {
|
|
1474
1214
|
return completeValue({
|
|
1475
1215
|
compatibility: "compatible",
|
|
@@ -1495,11 +1235,6 @@ export function createDbopfsModelStore({
|
|
|
1495
1235
|
return modelFiles;
|
|
1496
1236
|
}
|
|
1497
1237
|
|
|
1498
|
-
async function storedModelFiles(names, signal) {
|
|
1499
|
-
const modelFiles = await availableModelFiles(names, signal);
|
|
1500
|
-
return modelFiles.every(Boolean) ? modelFiles : null;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
1238
|
async function storedRangeCandidateForPlan(
|
|
1504
1239
|
modelName,
|
|
1505
1240
|
ranges,
|
|
@@ -1538,12 +1273,7 @@ export function createDbopfsModelStore({
|
|
|
1538
1273
|
signal,
|
|
1539
1274
|
availableNames = null,
|
|
1540
1275
|
) {
|
|
1541
|
-
const ranges =
|
|
1542
|
-
modelName,
|
|
1543
|
-
total,
|
|
1544
|
-
signal,
|
|
1545
|
-
availableNames,
|
|
1546
|
-
);
|
|
1276
|
+
const ranges = rangePlanForInstall(total, signal);
|
|
1547
1277
|
return storedRangeCandidateForPlan(
|
|
1548
1278
|
modelName,
|
|
1549
1279
|
ranges,
|
|
@@ -1624,20 +1354,10 @@ export function createDbopfsModelStore({
|
|
|
1624
1354
|
signal,
|
|
1625
1355
|
} = {}) {
|
|
1626
1356
|
const names = storageName(source);
|
|
1627
|
-
|
|
1628
|
-
if (modelFiles.every(Boolean)) {
|
|
1629
|
-
await removeLegacyCacheAfterReplacement(source);
|
|
1630
|
-
} else {
|
|
1631
|
-
const legacyCache = await legacyCacheForSource(source, signal);
|
|
1632
|
-
if (legacyCache) {
|
|
1633
|
-
await removeIncompleteReplacement(source, names);
|
|
1634
|
-
modelFiles = legacyCache.files;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1357
|
+
const modelFiles = await availableResumableModelFiles(source, names, signal);
|
|
1637
1358
|
if (!modelFiles.every(Boolean)) return null;
|
|
1638
1359
|
return completeValue({
|
|
1639
1360
|
files: completeValue(modelFiles),
|
|
1640
|
-
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1641
1361
|
});
|
|
1642
1362
|
}
|
|
1643
1363
|
|
|
@@ -1744,13 +1464,11 @@ export function createDbopfsModelStore({
|
|
|
1744
1464
|
}
|
|
1745
1465
|
}
|
|
1746
1466
|
if (failure !== null) throw failure;
|
|
1747
|
-
await removeLegacyCacheAfterReplacement(source);
|
|
1748
1467
|
progress.finish();
|
|
1749
1468
|
if (failure !== null) throw failure;
|
|
1750
1469
|
throwIfAborted(downloadSignal, "install");
|
|
1751
1470
|
return completeValue({
|
|
1752
1471
|
files: completeValue(modelFiles),
|
|
1753
|
-
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1754
1472
|
});
|
|
1755
1473
|
} catch (error) {
|
|
1756
1474
|
retainFailure(error);
|
|
@@ -3011,13 +2729,12 @@ function capabilityPolicy(
|
|
|
3011
2729
|
});
|
|
3012
2730
|
}
|
|
3013
2731
|
|
|
3014
|
-
function providerModelSources(
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
throw new TypeError("createBrowserWasmLlmProvider requires a nonempty sources array or legacy source.");
|
|
2732
|
+
function providerModelSources(sources) {
|
|
2733
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
2734
|
+
throw new TypeError("createBrowserWasmLlmProvider requires a nonempty sources array.");
|
|
3018
2735
|
}
|
|
3019
2736
|
const ids = new Set();
|
|
3020
|
-
for (const candidate of
|
|
2737
|
+
for (const candidate of sources) {
|
|
3021
2738
|
if (!BROWSER_MODEL_SOURCES.has(candidate)) {
|
|
3022
2739
|
throw new TypeError("Every browser-WASM model source must come from createBrowserModelSource().");
|
|
3023
2740
|
}
|
|
@@ -3026,27 +2743,20 @@ function providerModelSources(source, sources) {
|
|
|
3026
2743
|
}
|
|
3027
2744
|
ids.add(candidate.id);
|
|
3028
2745
|
}
|
|
3029
|
-
if (source !== undefined && !BROWSER_MODEL_SOURCES.has(source)) {
|
|
3030
|
-
throw new TypeError("The legacy default source must come from createBrowserModelSource().");
|
|
3031
|
-
}
|
|
3032
|
-
if (source !== undefined && !list.includes(source)) {
|
|
3033
|
-
throw new TypeError("The legacy default source must be one member of sources.");
|
|
3034
|
-
}
|
|
3035
2746
|
return completeValue({
|
|
3036
|
-
sources: completeValue(
|
|
3037
|
-
defaultSource:
|
|
2747
|
+
sources: completeValue(sources.slice()),
|
|
2748
|
+
defaultSource: sources[0],
|
|
3038
2749
|
});
|
|
3039
2750
|
}
|
|
3040
2751
|
|
|
3041
2752
|
export function createBrowserWasmLlmProvider({
|
|
3042
|
-
source,
|
|
3043
2753
|
sources,
|
|
3044
2754
|
store,
|
|
3045
2755
|
loadDefaults = {},
|
|
3046
2756
|
security,
|
|
3047
2757
|
logger = console,
|
|
3048
2758
|
} = {}) {
|
|
3049
|
-
const configuredModels = providerModelSources(
|
|
2759
|
+
const configuredModels = providerModelSources(sources);
|
|
3050
2760
|
const modelSources = configuredModels.sources;
|
|
3051
2761
|
const defaultSource = configuredModels.defaultSource;
|
|
3052
2762
|
if (!DBOPFS_MODEL_STORES.has(store)) {
|
|
@@ -3751,5 +3461,3 @@ export function adaptV1LlmProvider(provider) {
|
|
|
3751
3461
|
V1_LLM_PROVIDER_ADAPTERS.set(provider, adapted);
|
|
3752
3462
|
return adapted;
|
|
3753
3463
|
}
|
|
3754
|
-
|
|
3755
|
-
export { MODEL_MANIFEST_SCHEMA };
|
|
@@ -52,8 +52,9 @@ function validateWorker(worker) {
|
|
|
52
52
|
const WORKER_CLIENTS = new WeakSet();
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Owns exactly one role Worker.
|
|
56
|
-
*
|
|
55
|
+
* Owns exactly one role Worker. STT and lifecycle cancellation terminate that
|
|
56
|
+
* Worker. TTS use cancellation targets only the active request so sibling pool
|
|
57
|
+
* Workers remain available.
|
|
57
58
|
*/
|
|
58
59
|
class SpeechWorkerClient {
|
|
59
60
|
#role;
|
|
@@ -194,22 +195,54 @@ class SpeechWorkerClient {
|
|
|
194
195
|
}
|
|
195
196
|
const id = this.#nextId;
|
|
196
197
|
this.#nextId += 1;
|
|
197
|
-
|
|
198
|
+
const client = this;
|
|
199
|
+
return new Promise(function requestSpeechWorkerOperation(resolve, reject) {
|
|
198
200
|
let listening = true;
|
|
199
201
|
const cleanup = () => {
|
|
200
202
|
if (!listening) return;
|
|
201
203
|
listening = false;
|
|
202
204
|
signal?.removeEventListener?.("abort", onAbort);
|
|
203
205
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
function onAbort() {
|
|
207
|
+
const pending = client.#pending.get(id);
|
|
208
|
+
if (!pending) return;
|
|
209
|
+
if (client.#role === "tts" && op === "use") {
|
|
210
|
+
client.#pending.delete(id);
|
|
211
|
+
pending.cleanup();
|
|
212
|
+
pending.reject(abortError(signal, client.#role, op));
|
|
213
|
+
const cancelId = client.#nextId;
|
|
214
|
+
client.#nextId += 1;
|
|
215
|
+
try {
|
|
216
|
+
client.#transport.postMessage({
|
|
217
|
+
protocol: SPEECH_WORKER_PROTOCOL,
|
|
218
|
+
id: cancelId,
|
|
219
|
+
op: "cancel",
|
|
220
|
+
payload: { targetId: id },
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const failure = clientError(
|
|
224
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
225
|
+
"Unable to cancel an operation in the speech Worker.",
|
|
226
|
+
error,
|
|
227
|
+
`${operationSubject(client.#role, op)}-message-rejected`,
|
|
228
|
+
);
|
|
229
|
+
void client.terminate(failure, { intentional: false }).catch(
|
|
230
|
+
function ignoreCancellationTerminationFailure() {
|
|
231
|
+
return undefined;
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
void client.terminate(
|
|
238
|
+
abortError(signal, client.#role, op),
|
|
208
239
|
{ intentional: true },
|
|
209
|
-
).catch(()
|
|
210
|
-
|
|
240
|
+
).catch(function ignoreAbortTerminationFailure() {
|
|
241
|
+
return undefined;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
211
244
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
212
|
-
|
|
245
|
+
client.#pending.set(id, { resolve, reject, cleanup, op });
|
|
213
246
|
const message = {
|
|
214
247
|
protocol: SPEECH_WORKER_PROTOCOL,
|
|
215
248
|
id,
|
|
@@ -218,18 +251,22 @@ class SpeechWorkerClient {
|
|
|
218
251
|
};
|
|
219
252
|
const transfers = collectSpeechTransferables(payload);
|
|
220
253
|
try {
|
|
221
|
-
|
|
254
|
+
client.#transport.postMessage(message, transfers);
|
|
222
255
|
} catch (error) {
|
|
223
|
-
|
|
256
|
+
client.#pending.delete(id);
|
|
224
257
|
cleanup();
|
|
225
258
|
const failure = clientError(
|
|
226
259
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
227
260
|
"Unable to send an operation to the speech Worker.",
|
|
228
261
|
error,
|
|
229
|
-
`${operationSubject(
|
|
262
|
+
`${operationSubject(client.#role, op)}-message-rejected`,
|
|
230
263
|
);
|
|
231
264
|
reject(failure);
|
|
232
|
-
void
|
|
265
|
+
void client.terminate(failure, { intentional: false }).catch(
|
|
266
|
+
function ignoreRequestTerminationFailure() {
|
|
267
|
+
return undefined;
|
|
268
|
+
},
|
|
269
|
+
);
|
|
233
270
|
}
|
|
234
271
|
});
|
|
235
272
|
}
|