arcane-os 0.5.3 → 0.5.4
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 +12 -0
- package/README.md +19 -16
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +200 -26
- package/package.json +1 -1
- package/runtime/arcane/modules/AI.js +42 -50
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.4
|
|
4
|
+
|
|
5
|
+
- Reduced Browser-WASM HTTP Range parts from roughly 128 MB to roughly 4 MB
|
|
6
|
+
and raised the deterministic part ceiling, so a refresh re-fetches only the
|
|
7
|
+
small in-flight parts while every completed part remains reusable. An
|
|
8
|
+
incomplete 0.5.3 cache keeps its completed legacy parts and subdivides only
|
|
9
|
+
the missing legacy intervals, preserving existing progress during adoption.
|
|
10
|
+
- Kept all built-in audio processing on device: Whisper owns transcription and
|
|
11
|
+
Kokoro owns speech synthesis. Legacy OpenAI audio selections migrate to those
|
|
12
|
+
local routes, non-local speech configurations are rejected, and the retired
|
|
13
|
+
OpenAI audio credential and endpoints are no longer used.
|
|
14
|
+
|
|
3
15
|
## 0.5.3
|
|
4
16
|
|
|
5
17
|
- Preserved every independently completed Browser-WASM HTTP Range part and
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.5.
|
|
22
|
+
This checkout defines the `0.5.4` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -88,10 +88,10 @@ OpenAI-compatible chat-completion requests to
|
|
|
88
88
|
`globalThis.arcane.config.twinCloud.accessKey`. The established `ai.license`
|
|
89
89
|
property and internal `OPENAI` route identifier remain compatibility aliases;
|
|
90
90
|
applications should present the service and credential as **TWiN Cloud** and
|
|
91
|
-
**TWiN access key**. The TWiN key is used only for remote LLM chat.
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
91
|
+
**TWiN access key**. The TWiN key is used only for remote LLM chat. Audio stays
|
|
92
|
+
on device: Whisper (`LOCAL_SPEACH` / `whisper-small`) owns transcription and
|
|
93
|
+
Kokoro (`LOCAL_SPEACH` / `kokoro`) owns speech synthesis. Neither audio route
|
|
94
|
+
uses the TWiN key, and no OpenAI audio key is required.
|
|
95
95
|
|
|
96
96
|
## Browser-local AI
|
|
97
97
|
|
|
@@ -124,11 +124,14 @@ Optional hardening is inactive unless the caller explicitly selects
|
|
|
124
124
|
`secure:true`. The DBOPFS store downloads ordered members with one bounded
|
|
125
125
|
parallel transfer pool and returns them in descriptor order. Completed shards
|
|
126
126
|
survive an interrupted attempt and only missing shard or Range-part work is
|
|
127
|
-
fetched on retry. A
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
fetched on retry. A monolithic file uses up to `downloadConcurrency` active
|
|
128
|
+
workers (four by default) over deterministic, resumable HTTP range parts of
|
|
129
|
+
roughly 4 MB each, up to 4,096 parts, when the server confirms `206` responses
|
|
130
|
+
and the total comes from `Content-Range` or, when that header is not exposed,
|
|
131
|
+
optional declared `bytes`.
|
|
132
|
+
An incomplete cache written by 0.5.3 keeps its completed coarse parts and
|
|
133
|
+
subdivides only the missing intervals into the smaller current parts, so an SDK
|
|
134
|
+
update preserves existing progress while reducing later refresh loss.
|
|
132
135
|
When a followed redirect turns the first Range probe into `200`, the SDK probes
|
|
133
136
|
the final URL directly before reusing that original response as the single-fetch
|
|
134
137
|
fallback. Completed range parts survive restart and are presented to Wllama as
|
|
@@ -165,7 +168,7 @@ uses the same controller for automatic memory extraction.
|
|
|
165
168
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
166
169
|
|
|
167
170
|
```bash
|
|
168
|
-
npx arcane-os@0.5.
|
|
171
|
+
npx arcane-os@0.5.4 new my-app --path ./my-app --target portable --git
|
|
169
172
|
cd my-app
|
|
170
173
|
npm install
|
|
171
174
|
npm run check
|
|
@@ -176,7 +179,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
176
179
|
missing Arcane files:
|
|
177
180
|
|
|
178
181
|
```bash
|
|
179
|
-
npm install --save-dev --save-exact arcane-os@0.5.
|
|
182
|
+
npm install --save-dev --save-exact arcane-os@0.5.4
|
|
180
183
|
npm exec -- arcane init my-app --target portable
|
|
181
184
|
```
|
|
182
185
|
|
|
@@ -192,7 +195,7 @@ npm exec -- arcane-os targets
|
|
|
192
195
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
193
196
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
194
197
|
|
|
195
|
-
Use `npx arcane-os@0.5.
|
|
198
|
+
Use `npx arcane-os@0.5.4` for the initial bootstrap because it names this npm
|
|
196
199
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
197
200
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
198
201
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -213,7 +216,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
213
216
|
|
|
214
217
|
# From the generated app repository
|
|
215
218
|
cd ../local-app
|
|
216
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.
|
|
219
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.4.tgz
|
|
217
220
|
npm run check
|
|
218
221
|
npm ci
|
|
219
222
|
```
|
|
@@ -223,7 +226,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
223
226
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
224
227
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
225
228
|
runner also needs that tarball at the locked path. After publication, replace
|
|
226
|
-
the local declaration with the exact `arcane-os@0.5.
|
|
229
|
+
the local declaration with the exact `arcane-os@0.5.4` registry package and
|
|
227
230
|
commit the regenerated lock.
|
|
228
231
|
|
|
229
232
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -361,7 +364,7 @@ package installation, or assertions.
|
|
|
361
364
|
|
|
362
365
|
## Current target support
|
|
363
366
|
|
|
364
|
-
Version `0.5.
|
|
367
|
+
Version `0.5.4` exposes one browser target and five explicitly paired
|
|
365
368
|
native development targets: a non-runnable portable directory, a
|
|
366
369
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
367
370
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -29,8 +29,10 @@ const MODEL_LOAD_HEARTBEAT_MS = 5_000;
|
|
|
29
29
|
const DEFAULT_MODEL_DOWNLOAD_CONCURRENCY = 4;
|
|
30
30
|
const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
|
|
31
31
|
const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
|
|
32
|
-
const MODEL_DOWNLOAD_MAX_RANGE_PARTS =
|
|
33
|
-
const MODEL_DOWNLOAD_TARGET_RANGE_BYTES =
|
|
32
|
+
const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 4_096;
|
|
33
|
+
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;
|
|
34
36
|
const INTEL_VENDOR_ID = 0x8086;
|
|
35
37
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
36
38
|
let highPerformanceGpuNoticeShown = false;
|
|
@@ -103,10 +105,10 @@ function downloadConcurrencyValue(value) {
|
|
|
103
105
|
return value;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function
|
|
108
|
+
function modelHttpRangesFor(total, maximumParts, targetPartBytes) {
|
|
107
109
|
const count = Math.min(
|
|
108
|
-
|
|
109
|
-
Math.ceil(total /
|
|
110
|
+
maximumParts,
|
|
111
|
+
Math.ceil(total / targetPartBytes),
|
|
110
112
|
total,
|
|
111
113
|
);
|
|
112
114
|
const width = Math.floor(total / count);
|
|
@@ -122,6 +124,47 @@ function modelHttpRanges(total) {
|
|
|
122
124
|
return completeValue(ranges);
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function modelHttpRanges(total) {
|
|
128
|
+
return modelHttpRangesFor(
|
|
129
|
+
total,
|
|
130
|
+
MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
131
|
+
MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
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
|
+
|
|
125
168
|
function progressClock() {
|
|
126
169
|
return typeof globalThis.performance?.now === "function"
|
|
127
170
|
? globalThis.performance.now()
|
|
@@ -930,7 +973,7 @@ export function createDbopfsModelStore({
|
|
|
930
973
|
} = {},
|
|
931
974
|
) {
|
|
932
975
|
const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
|
|
933
|
-
const ranges =
|
|
976
|
+
const ranges = await rangePlanForInstall(name, total, signal);
|
|
934
977
|
const partFiles = new Array(ranges.length);
|
|
935
978
|
const pendingRangeIndexes = [];
|
|
936
979
|
for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
|
|
@@ -1003,16 +1046,31 @@ export function createDbopfsModelStore({
|
|
|
1003
1046
|
writable = null;
|
|
1004
1047
|
partFiles[rangeIndex] = await storedModelFile(partName);
|
|
1005
1048
|
} catch (error) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1049
|
+
let transferError = error;
|
|
1050
|
+
if (downloadSignal.aborted
|
|
1051
|
+
&& error?.code !== "ARCANE_AI_REQUEST_ABORTED") {
|
|
1052
|
+
try {
|
|
1053
|
+
throwIfAborted(downloadSignal, "install");
|
|
1054
|
+
} catch (abortError) {
|
|
1055
|
+
transferError = abortError;
|
|
1056
|
+
}
|
|
1057
|
+
} else if (!error?.code) {
|
|
1058
|
+
transferError = fail(
|
|
1059
|
+
"ARCANE_AI_MODEL_DOWNLOAD_FAILED",
|
|
1060
|
+
"The model download failed while reading an HTTP byte range.",
|
|
1061
|
+
error,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
retainFailure(transferError);
|
|
1065
|
+
await cancelOpenedDownload(opened, transferError);
|
|
1008
1066
|
try {
|
|
1009
|
-
await writable?.abort?.(
|
|
1067
|
+
await writable?.abort?.(transferError);
|
|
1010
1068
|
} catch {
|
|
1011
1069
|
// The range part is removed below even if its writable already closed.
|
|
1012
1070
|
}
|
|
1013
1071
|
await removeEntry(partName).catch(() => undefined);
|
|
1014
1072
|
progress?.discardBytes(received);
|
|
1015
|
-
throw
|
|
1073
|
+
throw transferError;
|
|
1016
1074
|
} finally {
|
|
1017
1075
|
progress?.endTransfer({ publishProgress: failure === null });
|
|
1018
1076
|
}
|
|
@@ -1118,16 +1176,93 @@ export function createDbopfsModelStore({
|
|
|
1118
1176
|
return names;
|
|
1119
1177
|
}
|
|
1120
1178
|
|
|
1179
|
+
async function rangePlanForInstall(
|
|
1180
|
+
modelName,
|
|
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;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1121
1249
|
async function removeMemberRangeParts(modelName, {
|
|
1122
1250
|
except = null,
|
|
1123
1251
|
total = null,
|
|
1124
1252
|
} = {}) {
|
|
1125
1253
|
const existingNames = await memberRangePartNames(modelName);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1254
|
+
let names = existingNames;
|
|
1255
|
+
if (names === null && Number.isSafeInteger(total) && total > 0) {
|
|
1256
|
+
const plans = modelHttpRangePlans(total);
|
|
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)))];
|
|
1264
|
+
}
|
|
1265
|
+
names ??= [];
|
|
1131
1266
|
const removed = [];
|
|
1132
1267
|
for (const name of names) {
|
|
1133
1268
|
if (except?.has(name)) continue;
|
|
@@ -1211,9 +1346,10 @@ export function createDbopfsModelStore({
|
|
|
1211
1346
|
for (let memberIndex = 0; memberIndex < members.length; memberIndex += 1) {
|
|
1212
1347
|
const total = members[memberIndex].bytes;
|
|
1213
1348
|
if (!Number.isSafeInteger(total) || total <= 0) continue;
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1349
|
+
removed.push(await removeMemberRangeParts(
|
|
1350
|
+
names.models[memberIndex].name,
|
|
1351
|
+
{ total },
|
|
1352
|
+
));
|
|
1217
1353
|
}
|
|
1218
1354
|
return removed.some(Boolean);
|
|
1219
1355
|
}
|
|
@@ -1364,12 +1500,18 @@ export function createDbopfsModelStore({
|
|
|
1364
1500
|
return modelFiles.every(Boolean) ? modelFiles : null;
|
|
1365
1501
|
}
|
|
1366
1502
|
|
|
1367
|
-
async function
|
|
1503
|
+
async function storedRangeCandidateForPlan(
|
|
1504
|
+
modelName,
|
|
1505
|
+
ranges,
|
|
1506
|
+
signal,
|
|
1507
|
+
availableNames = null,
|
|
1508
|
+
) {
|
|
1368
1509
|
const partFiles = [];
|
|
1369
1510
|
let lastModified = 0;
|
|
1370
|
-
for (const range of
|
|
1511
|
+
for (const range of ranges) {
|
|
1371
1512
|
throwIfAborted(signal, "install");
|
|
1372
1513
|
const partName = rangePartName(modelName, range);
|
|
1514
|
+
if (availableNames && !availableNames.has(partName)) return null;
|
|
1373
1515
|
const partFile = await file(partName);
|
|
1374
1516
|
if (!partFile) return null;
|
|
1375
1517
|
const expected = range.end - range.start + 1;
|
|
@@ -1385,26 +1527,53 @@ export function createDbopfsModelStore({
|
|
|
1385
1527
|
return completeValue({
|
|
1386
1528
|
file: new Blob(partFiles, { type: "application/octet-stream" }),
|
|
1387
1529
|
lastModified,
|
|
1388
|
-
|
|
1530
|
+
ranges,
|
|
1531
|
+
total: ranges[0].total,
|
|
1389
1532
|
});
|
|
1390
1533
|
}
|
|
1391
1534
|
|
|
1535
|
+
async function storedRangeCandidate(
|
|
1536
|
+
modelName,
|
|
1537
|
+
total,
|
|
1538
|
+
signal,
|
|
1539
|
+
availableNames = null,
|
|
1540
|
+
) {
|
|
1541
|
+
const ranges = await rangePlanForInstall(
|
|
1542
|
+
modelName,
|
|
1543
|
+
total,
|
|
1544
|
+
signal,
|
|
1545
|
+
availableNames,
|
|
1546
|
+
);
|
|
1547
|
+
return storedRangeCandidateForPlan(
|
|
1548
|
+
modelName,
|
|
1549
|
+
ranges,
|
|
1550
|
+
signal,
|
|
1551
|
+
availableNames,
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1392
1555
|
async function storedRangeMember(member, modelName, signal) {
|
|
1556
|
+
const names = await memberRangePartNames(modelName);
|
|
1557
|
+
const availableNames = names === null ? null : new Set(names);
|
|
1393
1558
|
const declaredTotal = Number.isSafeInteger(member.bytes) && member.bytes > 0
|
|
1394
1559
|
? member.bytes
|
|
1395
1560
|
: null;
|
|
1396
1561
|
if (declaredTotal !== null) {
|
|
1397
|
-
const declared = await storedRangeCandidate(
|
|
1562
|
+
const declared = await storedRangeCandidate(
|
|
1563
|
+
modelName,
|
|
1564
|
+
declaredTotal,
|
|
1565
|
+
signal,
|
|
1566
|
+
availableNames,
|
|
1567
|
+
);
|
|
1398
1568
|
if (declared) {
|
|
1399
1569
|
await removeStaleRangePartsAfterCompletion(
|
|
1400
1570
|
modelName,
|
|
1401
|
-
|
|
1571
|
+
declared.ranges,
|
|
1402
1572
|
);
|
|
1403
1573
|
return declared.file;
|
|
1404
1574
|
}
|
|
1405
1575
|
}
|
|
1406
1576
|
|
|
1407
|
-
const names = await memberRangePartNames(modelName);
|
|
1408
1577
|
if (names === null) return null;
|
|
1409
1578
|
const discoveredTotals = new Set();
|
|
1410
1579
|
for (const name of names) {
|
|
@@ -1414,7 +1583,12 @@ export function createDbopfsModelStore({
|
|
|
1414
1583
|
let selected = null;
|
|
1415
1584
|
const totals = [...discoveredTotals].sort((left, right) => right - left);
|
|
1416
1585
|
for (const total of totals) {
|
|
1417
|
-
const candidate = await storedRangeCandidate(
|
|
1586
|
+
const candidate = await storedRangeCandidate(
|
|
1587
|
+
modelName,
|
|
1588
|
+
total,
|
|
1589
|
+
signal,
|
|
1590
|
+
availableNames,
|
|
1591
|
+
);
|
|
1418
1592
|
if (
|
|
1419
1593
|
candidate
|
|
1420
1594
|
&& (
|
|
@@ -1426,7 +1600,7 @@ export function createDbopfsModelStore({
|
|
|
1426
1600
|
if (selected === null) return null;
|
|
1427
1601
|
await removeStaleRangePartsAfterCompletion(
|
|
1428
1602
|
modelName,
|
|
1429
|
-
|
|
1603
|
+
selected.ranges,
|
|
1430
1604
|
);
|
|
1431
1605
|
return selected.file;
|
|
1432
1606
|
}
|
package/package.json
CHANGED
|
@@ -922,12 +922,10 @@ class AI {
|
|
|
922
922
|
OPENAI: 'https://inference.do-ai.run/v1'
|
|
923
923
|
},
|
|
924
924
|
sttURL: {
|
|
925
|
-
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
926
|
-
OPENAI: 'https://api.openai.com/v1'
|
|
925
|
+
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
927
926
|
},
|
|
928
927
|
ttsURL: {
|
|
929
|
-
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
930
|
-
OPENAI: 'https://api.openai.com/v1'
|
|
928
|
+
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
931
929
|
},
|
|
932
930
|
}
|
|
933
931
|
|
|
@@ -936,12 +934,10 @@ class AI {
|
|
|
936
934
|
OPENAI: '/chat/completions'
|
|
937
935
|
},
|
|
938
936
|
stt: {
|
|
939
|
-
LOCAL_SPEACH: '/audio/transcriptions'
|
|
940
|
-
OPENAI: '/audio/transcriptions'
|
|
937
|
+
LOCAL_SPEACH: '/audio/transcriptions'
|
|
941
938
|
},
|
|
942
939
|
tts: {
|
|
943
|
-
LOCAL_SPEACH: '/audio/speech'
|
|
944
|
-
OPENAI: '/audio/speech'
|
|
940
|
+
LOCAL_SPEACH: '/audio/speech'
|
|
945
941
|
}
|
|
946
942
|
}
|
|
947
943
|
|
|
@@ -950,12 +946,10 @@ class AI {
|
|
|
950
946
|
}
|
|
951
947
|
|
|
952
948
|
#sttModels = {
|
|
953
|
-
OPENAI: 'whisper-1',
|
|
954
949
|
LOCAL_SPEACH: 'whisper-small'
|
|
955
950
|
}
|
|
956
951
|
|
|
957
952
|
#ttsModels = {
|
|
958
|
-
OPENAI: 'gpt-4o-mini-tts',
|
|
959
953
|
LOCAL_SPEACH: 'kokoro'
|
|
960
954
|
}
|
|
961
955
|
|
|
@@ -974,17 +968,8 @@ class AI {
|
|
|
974
968
|
};
|
|
975
969
|
}
|
|
976
970
|
|
|
977
|
-
get #legacyOpenAISpeechKey(){
|
|
978
|
-
const value=globalThis.arcane?.config?.openAI?.apiKey;
|
|
979
|
-
return typeof value==='string'?value:'';
|
|
980
|
-
}
|
|
981
|
-
|
|
982
971
|
get #ttsHeaders(){
|
|
983
972
|
return {
|
|
984
|
-
OPENAI: {
|
|
985
|
-
'Content-Type': 'application/json',
|
|
986
|
-
'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`
|
|
987
|
-
},
|
|
988
973
|
LOCAL_SPEACH: {
|
|
989
974
|
'Content-Type': 'application/json',
|
|
990
975
|
}
|
|
@@ -993,9 +978,6 @@ class AI {
|
|
|
993
978
|
|
|
994
979
|
get #sttHeaders(){
|
|
995
980
|
return {
|
|
996
|
-
OPENAI: {
|
|
997
|
-
'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`,
|
|
998
|
-
},
|
|
999
981
|
LOCAL_SPEACH: {}
|
|
1000
982
|
};
|
|
1001
983
|
}
|
|
@@ -1048,11 +1030,11 @@ class AI {
|
|
|
1048
1030
|
|
|
1049
1031
|
const preferences=[
|
|
1050
1032
|
llmService||'OPENAI',
|
|
1051
|
-
sttService||'
|
|
1052
|
-
ttsService||'
|
|
1033
|
+
sttService||'LOCAL_SPEACH',
|
|
1034
|
+
ttsService||'LOCAL_SPEACH',
|
|
1053
1035
|
model||'OPENAI',
|
|
1054
|
-
modelTTS||'
|
|
1055
|
-
modelSTT||'
|
|
1036
|
+
modelTTS||'LOCAL_SPEACH',
|
|
1037
|
+
modelSTT||'LOCAL_SPEACH'
|
|
1056
1038
|
];
|
|
1057
1039
|
this.setAI(
|
|
1058
1040
|
...preferences
|
|
@@ -1089,11 +1071,11 @@ class AI {
|
|
|
1089
1071
|
#stopOllamaReady=null;
|
|
1090
1072
|
#preferenceTuple=completeValue([
|
|
1091
1073
|
'OPENAI',
|
|
1074
|
+
'LOCAL_SPEACH',
|
|
1075
|
+
'LOCAL_SPEACH',
|
|
1092
1076
|
'OPENAI',
|
|
1093
|
-
'
|
|
1094
|
-
'
|
|
1095
|
-
'OPENAI',
|
|
1096
|
-
'OPENAI'
|
|
1077
|
+
'LOCAL_SPEACH',
|
|
1078
|
+
'LOCAL_SPEACH'
|
|
1097
1079
|
]);
|
|
1098
1080
|
|
|
1099
1081
|
get providerRuntime(){
|
|
@@ -1173,9 +1155,6 @@ class AI {
|
|
|
1173
1155
|
this.#retainLegacyLLMReadiness(
|
|
1174
1156
|
this.#reconcileLegacyLLMReadiness()
|
|
1175
1157
|
);
|
|
1176
|
-
this.#retainLegacySpeechReadiness(
|
|
1177
|
-
this.#reconcileLegacySpeechReadiness()
|
|
1178
|
-
);
|
|
1179
1158
|
return this.#license;
|
|
1180
1159
|
}
|
|
1181
1160
|
|
|
@@ -1427,12 +1406,6 @@ class AI {
|
|
|
1427
1406
|
if(role!=='tts'){
|
|
1428
1407
|
return null;
|
|
1429
1408
|
}
|
|
1430
|
-
if(providerId==='OPENAI'){
|
|
1431
|
-
const selected=typeof globalThis.window?.user?.AI_voice==='string'
|
|
1432
|
-
?globalThis.window.user.AI_voice.trim()
|
|
1433
|
-
:'';
|
|
1434
|
-
return selected||'alloy';
|
|
1435
|
-
}
|
|
1436
1409
|
if(providerId==='LOCAL_SPEACH'){
|
|
1437
1410
|
return 'af_heart';
|
|
1438
1411
|
}
|
|
@@ -1445,10 +1418,6 @@ class AI {
|
|
|
1445
1418
|
if(service!==providerId||!model){
|
|
1446
1419
|
return false;
|
|
1447
1420
|
}
|
|
1448
|
-
if(providerId==='OPENAI'){
|
|
1449
|
-
return Boolean(this.#legacyOpenAISpeechKey)
|
|
1450
|
-
&&typeof globalThis.fetch==='function';
|
|
1451
|
-
}
|
|
1452
1421
|
if(providerId==='LOCAL_SPEACH'){
|
|
1453
1422
|
return Boolean(this.#nativeSpeech(service,role));
|
|
1454
1423
|
}
|
|
@@ -1685,8 +1654,7 @@ class AI {
|
|
|
1685
1654
|
}
|
|
1686
1655
|
|
|
1687
1656
|
#ensureLegacySpeechProvider(role,providerId){
|
|
1688
|
-
if(!['stt','tts'].includes(role)
|
|
1689
|
-
||!['OPENAI','LOCAL_SPEACH'].includes(providerId)){
|
|
1657
|
+
if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
|
|
1690
1658
|
return false;
|
|
1691
1659
|
}
|
|
1692
1660
|
if(this.#providerRuntime.hasProvider(role,providerId)){
|
|
@@ -1875,11 +1843,7 @@ class AI {
|
|
|
1875
1843
|
throw error;
|
|
1876
1844
|
}
|
|
1877
1845
|
|
|
1878
|
-
if(service==='OPENAI'&&(
|
|
1879
|
-
role==='llm'
|
|
1880
|
-
?Boolean(this.twinKey)
|
|
1881
|
-
:Boolean(this.#legacyOpenAISpeechKey)
|
|
1882
|
-
)){
|
|
1846
|
+
if(service==='OPENAI'&&role==='llm'&&Boolean(this.twinKey)){
|
|
1883
1847
|
return true;
|
|
1884
1848
|
}
|
|
1885
1849
|
|
|
@@ -1947,6 +1911,10 @@ class AI {
|
|
|
1947
1911
|
}
|
|
1948
1912
|
return value;
|
|
1949
1913
|
});
|
|
1914
|
+
next[1]='LOCAL_SPEACH';
|
|
1915
|
+
next[2]='LOCAL_SPEACH';
|
|
1916
|
+
next[4]='LOCAL_SPEACH';
|
|
1917
|
+
next[5]='LOCAL_SPEACH';
|
|
1950
1918
|
return completeValue(next);
|
|
1951
1919
|
}
|
|
1952
1920
|
|
|
@@ -1961,6 +1929,26 @@ class AI {
|
|
|
1961
1929
|
}
|
|
1962
1930
|
}
|
|
1963
1931
|
|
|
1932
|
+
#assertDeviceSpeechConfiguration(configuration){
|
|
1933
|
+
for(const role of ['stt','tts']){
|
|
1934
|
+
for(const routeName of ['default','localOnly']){
|
|
1935
|
+
const selection=configuration?.[role]?.[routeName];
|
|
1936
|
+
if(selection&&selection.localOnly!==true){
|
|
1937
|
+
const operation=role==='stt'
|
|
1938
|
+
?'Speech recognition'
|
|
1939
|
+
:'Speech synthesis';
|
|
1940
|
+
const error=new TypeError(
|
|
1941
|
+
`${operation} supports on-device providers only.`
|
|
1942
|
+
);
|
|
1943
|
+
error.code=role==='stt'
|
|
1944
|
+
?'AI_STT_DEVICE_ONLY'
|
|
1945
|
+
:'AI_TTS_DEVICE_ONLY';
|
|
1946
|
+
throw error;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1964
1952
|
#normalizedLLMModel(service,model){
|
|
1965
1953
|
if(service==='OLLAMA'){
|
|
1966
1954
|
const mappedModel=model==='OPENAI'?null:this.#models[model];
|
|
@@ -2187,6 +2175,7 @@ class AI {
|
|
|
2187
2175
|
|
|
2188
2176
|
configureProviders(selections){
|
|
2189
2177
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2178
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2190
2179
|
this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
|
|
2191
2180
|
this.#ensureLegacyLLMProvider(
|
|
2192
2181
|
prepared.llm.default?.providerId
|
|
@@ -2221,6 +2210,7 @@ class AI {
|
|
|
2221
2210
|
|
|
2222
2211
|
configureSpeechProviders(selections){
|
|
2223
2212
|
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
2213
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2224
2214
|
this.#assertSynchronousBrowserSpeechSupersession(
|
|
2225
2215
|
'AI.configureSpeechProviders'
|
|
2226
2216
|
);
|
|
@@ -2287,6 +2277,7 @@ class AI {
|
|
|
2287
2277
|
|
|
2288
2278
|
async transitionProviders(selections){
|
|
2289
2279
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2280
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2290
2281
|
await this.#supersedeBrowserSpeechForRouteChange();
|
|
2291
2282
|
this.#ensureLegacyLLMProvider(
|
|
2292
2283
|
prepared.llm.default?.providerId
|
|
@@ -2318,6 +2309,7 @@ class AI {
|
|
|
2318
2309
|
|
|
2319
2310
|
async transitionSpeechProviders(selections){
|
|
2320
2311
|
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
2312
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2321
2313
|
await this.#supersedeBrowserSpeechForRouteChange();
|
|
2322
2314
|
this.#invalidateSpeechControl();
|
|
2323
2315
|
await this.#unloadSpeechProviderRolesForTransition();
|