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
|
@@ -19,6 +19,10 @@ const ROLE_OPERATION = completeValue({ stt: "transcribe", tts: "synthesize" });
|
|
|
19
19
|
const STT_SAMPLE_RATE = 16_000;
|
|
20
20
|
const TTS_SAMPLE_RATE = 24_000;
|
|
21
21
|
const TTS_RESPONSE_FORMAT = "wav";
|
|
22
|
+
const TTS_EXECUTION_DEVICES = new Set(["auto", "webgpu", "wasm"]);
|
|
23
|
+
const DEFAULT_TTS_EXECUTION_DEVICE = "auto";
|
|
24
|
+
const DEFAULT_TTS_MAX_CONCURRENT_REQUESTS = 2;
|
|
25
|
+
const MAX_TTS_CONCURRENT_REQUESTS = 4;
|
|
22
26
|
const ROLE_REQUEST_REASON = completeValue({
|
|
23
27
|
stt: "stt-transcription-cancelled",
|
|
24
28
|
tts: "tts-synthesis-cancelled",
|
|
@@ -141,6 +145,62 @@ function requiredIdentifier(value, label) {
|
|
|
141
145
|
return value.trim();
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
function normalizeSpeechExecution(role, execution) {
|
|
149
|
+
if (role === "stt") {
|
|
150
|
+
if (execution !== undefined) {
|
|
151
|
+
throw new TypeError("Browser Whisper does not accept an execution option.");
|
|
152
|
+
}
|
|
153
|
+
return completeValue({ device: "wasm", maxConcurrentRequests: 1 });
|
|
154
|
+
}
|
|
155
|
+
if (execution === undefined) {
|
|
156
|
+
return completeValue({
|
|
157
|
+
device: DEFAULT_TTS_EXECUTION_DEVICE,
|
|
158
|
+
maxConcurrentRequests: DEFAULT_TTS_MAX_CONCURRENT_REQUESTS,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (!execution || typeof execution !== "object" || Array.isArray(execution)) {
|
|
162
|
+
throw new TypeError("Browser Kokoro execution must be a plain data record.");
|
|
163
|
+
}
|
|
164
|
+
const prototype = Object.getPrototypeOf(execution);
|
|
165
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
166
|
+
throw new TypeError("Browser Kokoro execution must be a plain data record.");
|
|
167
|
+
}
|
|
168
|
+
const descriptors = Object.getOwnPropertyDescriptors(execution);
|
|
169
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
170
|
+
if (
|
|
171
|
+
(key !== "device" && key !== "maxConcurrentRequests")
|
|
172
|
+
|| !Object.hasOwn(descriptors[key], "value")
|
|
173
|
+
) {
|
|
174
|
+
throw new TypeError("Browser Kokoro execution contains an unsupported or accessor field.");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const device = Object.hasOwn(descriptors, "device")
|
|
178
|
+
? descriptors.device.value
|
|
179
|
+
: DEFAULT_TTS_EXECUTION_DEVICE;
|
|
180
|
+
const maxConcurrentRequests = Object.hasOwn(descriptors, "maxConcurrentRequests")
|
|
181
|
+
? descriptors.maxConcurrentRequests.value
|
|
182
|
+
: DEFAULT_TTS_MAX_CONCURRENT_REQUESTS;
|
|
183
|
+
if (!TTS_EXECUTION_DEVICES.has(device)) {
|
|
184
|
+
throw new TypeError('Browser Kokoro execution.device must be "auto", "webgpu", or "wasm".');
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
!Number.isSafeInteger(maxConcurrentRequests)
|
|
188
|
+
|| maxConcurrentRequests < 1
|
|
189
|
+
|| maxConcurrentRequests > MAX_TTS_CONCURRENT_REQUESTS
|
|
190
|
+
) {
|
|
191
|
+
throw new TypeError("Browser Kokoro execution.maxConcurrentRequests must be a safe integer from 1 through 4.");
|
|
192
|
+
}
|
|
193
|
+
return completeValue({ device, maxConcurrentRequests });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function navigatorHasWebGpu() {
|
|
197
|
+
try {
|
|
198
|
+
return Boolean(globalThis.navigator?.gpu);
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
144
204
|
function createProviderAuthority({
|
|
145
205
|
providerId,
|
|
146
206
|
role,
|
|
@@ -150,7 +210,7 @@ function createProviderAuthority({
|
|
|
150
210
|
}) {
|
|
151
211
|
if (graph !== undefined) {
|
|
152
212
|
if (model !== undefined || runtime !== undefined) {
|
|
153
|
-
throw new TypeError("Browser speech graph is mutually exclusive with
|
|
213
|
+
throw new TypeError("Browser speech graph is mutually exclusive with model and runtime descriptors.");
|
|
154
214
|
}
|
|
155
215
|
if (!isBrowserSpeechArtifactGraph(graph)) {
|
|
156
216
|
throw new TypeError("Browser speech graph must be created by createBrowserSpeechArtifactGraph().");
|
|
@@ -324,6 +384,7 @@ function publicStatus({
|
|
|
324
384
|
cache,
|
|
325
385
|
lifecycleReason,
|
|
326
386
|
activeOperation,
|
|
387
|
+
execution,
|
|
327
388
|
secureIntent,
|
|
328
389
|
warnings,
|
|
329
390
|
}) {
|
|
@@ -340,6 +401,7 @@ function publicStatus({
|
|
|
340
401
|
generation,
|
|
341
402
|
errorCode,
|
|
342
403
|
cache,
|
|
404
|
+
...(execution ? { execution } : {}),
|
|
343
405
|
...(secureIntent ? { security: secureIntent } : {}),
|
|
344
406
|
warnings,
|
|
345
407
|
});
|
|
@@ -1040,6 +1102,7 @@ function createBrowserSpeechProvider({
|
|
|
1040
1102
|
graph,
|
|
1041
1103
|
model,
|
|
1042
1104
|
runtime,
|
|
1105
|
+
execution,
|
|
1043
1106
|
appSecurity,
|
|
1044
1107
|
security,
|
|
1045
1108
|
store,
|
|
@@ -1062,6 +1125,7 @@ function createBrowserSpeechProvider({
|
|
|
1062
1125
|
model,
|
|
1063
1126
|
runtime,
|
|
1064
1127
|
});
|
|
1128
|
+
const speechExecution = normalizeSpeechExecution(role, execution);
|
|
1065
1129
|
const defaultSecureIntent = reportedSecureIntent(appSecurity, security);
|
|
1066
1130
|
const operation = ROLE_OPERATION[role];
|
|
1067
1131
|
const speech = role === "stt"
|
|
@@ -1097,7 +1161,8 @@ function createBrowserSpeechProvider({
|
|
|
1097
1161
|
let loadOperation = null;
|
|
1098
1162
|
let unloadOperation = null;
|
|
1099
1163
|
let disposeOperation = null;
|
|
1100
|
-
|
|
1164
|
+
const requestOperations = new Set();
|
|
1165
|
+
let selectedDevice = null;
|
|
1101
1166
|
let lastWarnings = NO_PROVIDER_WARNINGS;
|
|
1102
1167
|
let secureIntent = defaultSecureIntent;
|
|
1103
1168
|
|
|
@@ -1110,45 +1175,186 @@ function createBrowserSpeechProvider({
|
|
|
1110
1175
|
id: providerId,
|
|
1111
1176
|
authority,
|
|
1112
1177
|
state,
|
|
1113
|
-
busy:
|
|
1178
|
+
busy: requestOperations.size > 0,
|
|
1114
1179
|
generation,
|
|
1115
1180
|
errorCode,
|
|
1116
1181
|
cache,
|
|
1117
1182
|
lifecycleReason,
|
|
1118
1183
|
activeOperation,
|
|
1184
|
+
execution: role === "tts"
|
|
1185
|
+
? completeValue({
|
|
1186
|
+
requestedDevice: speechExecution.device,
|
|
1187
|
+
selectedDevice,
|
|
1188
|
+
maxConcurrentRequests: speechExecution.maxConcurrentRequests,
|
|
1189
|
+
activeRequestCount: requestOperations.size,
|
|
1190
|
+
})
|
|
1191
|
+
: null,
|
|
1119
1192
|
secureIntent,
|
|
1120
1193
|
warnings: providerWarnings(runtimeWarnings),
|
|
1121
1194
|
});
|
|
1122
1195
|
}
|
|
1123
1196
|
|
|
1124
|
-
function
|
|
1125
|
-
if (!
|
|
1126
|
-
|
|
1127
|
-
|
|
1197
|
+
function releasePreparation(preparation) {
|
|
1198
|
+
if (!preparation || preparation.released) return;
|
|
1199
|
+
preparation.released = true;
|
|
1200
|
+
preparation.prepared.release();
|
|
1128
1201
|
}
|
|
1129
1202
|
|
|
1130
|
-
async function
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
undefined,
|
|
1137
|
-
`${role}-worker-client-authority-mismatch`,
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1203
|
+
async function terminatePool(
|
|
1204
|
+
pool,
|
|
1205
|
+
reason,
|
|
1206
|
+
{ intentional = true, releasePrepared = true } = {},
|
|
1207
|
+
) {
|
|
1208
|
+
if (!pool) return;
|
|
1140
1209
|
const trustedReason = trustedWorkerFailure(reason, role);
|
|
1210
|
+
let terminationFailure = null;
|
|
1141
1211
|
try {
|
|
1142
|
-
|
|
1212
|
+
if (!pool.terminationPromise) {
|
|
1213
|
+
pool.terminating = true;
|
|
1214
|
+
pool.terminationPromise = (async function terminateSpeechWorkerPool() {
|
|
1215
|
+
const terminations = [];
|
|
1216
|
+
for (const slot of pool.slots) {
|
|
1217
|
+
if (!isSpeechWorkerClient(slot.client)) {
|
|
1218
|
+
terminations.push(Promise.reject(providerError(
|
|
1219
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
1220
|
+
"The browser speech Worker client is not SDK-owned.",
|
|
1221
|
+
undefined,
|
|
1222
|
+
`${role}-worker-client-authority-mismatch`,
|
|
1223
|
+
)));
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
terminations.push(slot.client.terminate(trustedReason, { intentional }));
|
|
1227
|
+
}
|
|
1228
|
+
const settlements = await Promise.allSettled(terminations);
|
|
1229
|
+
for (const settlement of settlements) {
|
|
1230
|
+
if (settlement.status === "rejected") throw settlement.reason;
|
|
1231
|
+
}
|
|
1232
|
+
})();
|
|
1233
|
+
}
|
|
1234
|
+
await pool.terminationPromise;
|
|
1143
1235
|
} catch (error) {
|
|
1144
|
-
|
|
1236
|
+
terminationFailure = trustedWorkerFailure(error, role);
|
|
1145
1237
|
} finally {
|
|
1238
|
+
if (releasePrepared) {
|
|
1239
|
+
try {
|
|
1240
|
+
releasePreparation(pool.preparation);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
terminationFailure = trustedWorkerFailure(error, role);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (terminationFailure) throw terminationFailure;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function handlePoolTermination(pool, slot, { reason, intentional }) {
|
|
1250
|
+
slot.terminated = true;
|
|
1251
|
+
if (pool.terminating) return;
|
|
1252
|
+
const trustedReason = trustedWorkerFailure(reason, role);
|
|
1253
|
+
pool.failure = trustedReason;
|
|
1254
|
+
const isActivePool = active === pool;
|
|
1255
|
+
const terminationGeneration = generation;
|
|
1256
|
+
if (isActivePool) {
|
|
1257
|
+
active = null;
|
|
1258
|
+
state = intentional ? "unloaded" : "error";
|
|
1259
|
+
errorCode = intentional ? null : workerFailureCode(trustedReason);
|
|
1260
|
+
lifecycleReason = intentional
|
|
1261
|
+
? `${role}-worker-terminated`
|
|
1262
|
+
: trustedReason.reason ?? (errorCode === "ARCANE_AI_WORKER_MESSAGE_ERROR"
|
|
1263
|
+
|| errorCode === "ARCANE_AI_WORKER_MESSAGE_REJECTED"
|
|
1264
|
+
? `${role}-worker-message-rejected`
|
|
1265
|
+
: errorCode === "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH"
|
|
1266
|
+
? `${role}-worker-protocol-mismatch`
|
|
1267
|
+
: `${role}-worker-crashed`);
|
|
1268
|
+
activeOperation = null;
|
|
1269
|
+
selectedDevice = null;
|
|
1270
|
+
}
|
|
1271
|
+
void terminatePool(pool, trustedReason, {
|
|
1272
|
+
releasePrepared: isActivePool,
|
|
1273
|
+
}).then(
|
|
1274
|
+
function completeUnexpectedPoolTermination() {
|
|
1275
|
+
if (active === pool) active = null;
|
|
1276
|
+
},
|
|
1277
|
+
function reportUnexpectedPoolTerminationFailure(error) {
|
|
1278
|
+
if (active === pool) active = null;
|
|
1279
|
+
if (
|
|
1280
|
+
isActivePool
|
|
1281
|
+
&& generation === terminationGeneration
|
|
1282
|
+
&& state !== "disposed"
|
|
1283
|
+
&& state !== "unloading"
|
|
1284
|
+
) {
|
|
1285
|
+
const failure = trustedWorkerFailure(error, role);
|
|
1286
|
+
state = "error";
|
|
1287
|
+
errorCode = failure.code;
|
|
1288
|
+
lifecycleReason = failure.reason;
|
|
1289
|
+
activeOperation = null;
|
|
1290
|
+
selectedDevice = null;
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function createWorkerPool(preparation, device, warnings) {
|
|
1297
|
+
const pool = {
|
|
1298
|
+
preparation,
|
|
1299
|
+
device,
|
|
1300
|
+
warnings,
|
|
1301
|
+
slots: [],
|
|
1302
|
+
failure: null,
|
|
1303
|
+
terminating: false,
|
|
1304
|
+
terminationPromise: null,
|
|
1305
|
+
};
|
|
1306
|
+
for (let index = 0; index < speechExecution.maxConcurrentRequests; index += 1) {
|
|
1307
|
+
const slot = {
|
|
1308
|
+
client: null,
|
|
1309
|
+
requestOperation: null,
|
|
1310
|
+
terminated: false,
|
|
1311
|
+
};
|
|
1312
|
+
slot.client = createSpeechWorkerClient({
|
|
1313
|
+
role,
|
|
1314
|
+
onTermination: function handleSpeechWorkerTermination(termination) {
|
|
1315
|
+
handlePoolTermination(pool, slot, termination);
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
pool.slots.push(slot);
|
|
1319
|
+
}
|
|
1320
|
+
return pool;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function loadWorkerPool(preparation, device, warnings, signal) {
|
|
1324
|
+
const pool = createWorkerPool(preparation, device, warnings);
|
|
1325
|
+
const configuration = completeValue({
|
|
1326
|
+
role,
|
|
1327
|
+
runtime: preparation.prepared.runtime,
|
|
1328
|
+
model: preparation.prepared.model,
|
|
1329
|
+
execution: completeValue({ device }),
|
|
1330
|
+
...(authority.graph ? {
|
|
1331
|
+
artifactGraphProtocol: authority.graph.protocol,
|
|
1332
|
+
} : {}),
|
|
1333
|
+
});
|
|
1334
|
+
let failure = pool.failure;
|
|
1335
|
+
if (!failure) {
|
|
1146
1336
|
try {
|
|
1147
|
-
|
|
1337
|
+
await pool.slots[0].client.request("load", { configuration }, { signal });
|
|
1148
1338
|
} catch (error) {
|
|
1149
|
-
|
|
1339
|
+
failure = error;
|
|
1150
1340
|
}
|
|
1151
1341
|
}
|
|
1342
|
+
if (!failure) {
|
|
1343
|
+
const remainingLoads = [];
|
|
1344
|
+
for (const slot of pool.slots.slice(1)) {
|
|
1345
|
+
remainingLoads.push(slot.client.request("load", { configuration }, { signal }));
|
|
1346
|
+
}
|
|
1347
|
+
const settlements = await Promise.allSettled(remainingLoads);
|
|
1348
|
+
failure = pool.failure;
|
|
1349
|
+
for (const settlement of settlements) {
|
|
1350
|
+
if (!failure && settlement.status === "rejected") failure = settlement.reason;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (failure) {
|
|
1354
|
+
await terminatePool(pool, failure, { releasePrepared: false });
|
|
1355
|
+
throw failure;
|
|
1356
|
+
}
|
|
1357
|
+
return pool;
|
|
1152
1358
|
}
|
|
1153
1359
|
|
|
1154
1360
|
const provider = {
|
|
@@ -1156,6 +1362,7 @@ function createBrowserSpeechProvider({
|
|
|
1156
1362
|
role,
|
|
1157
1363
|
id: providerId,
|
|
1158
1364
|
localOnly: true,
|
|
1365
|
+
maxConcurrentRequests: speechExecution.maxConcurrentRequests,
|
|
1159
1366
|
|
|
1160
1367
|
catalog() {
|
|
1161
1368
|
return completeValue([catalogEntry]);
|
|
@@ -1282,27 +1489,37 @@ function createBrowserSpeechProvider({
|
|
|
1282
1489
|
warnings: NO_PROVIDER_WARNINGS,
|
|
1283
1490
|
observers: new Set(),
|
|
1284
1491
|
settled: false,
|
|
1285
|
-
abort
|
|
1492
|
+
abort(
|
|
1286
1493
|
reason = `${role}-load-cancelled`,
|
|
1287
1494
|
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
1288
|
-
)
|
|
1495
|
+
) {
|
|
1496
|
+
linked.abort(reason, code);
|
|
1497
|
+
},
|
|
1289
1498
|
};
|
|
1290
1499
|
lastWarnings = NO_PROVIDER_WARNINGS;
|
|
1291
|
-
|
|
1500
|
+
selectedDevice = null;
|
|
1501
|
+
const promise = Promise.resolve().then(async function loadBrowserSpeechProviderPool() {
|
|
1292
1502
|
let prepared = null;
|
|
1293
|
-
let
|
|
1503
|
+
let preparation = null;
|
|
1504
|
+
let pool = null;
|
|
1294
1505
|
try {
|
|
1295
1506
|
prepared = await store.prepare(authority.graph ?? authority, {
|
|
1296
1507
|
signal: linked.controller.signal,
|
|
1297
1508
|
offline,
|
|
1298
1509
|
});
|
|
1510
|
+
preparation = {
|
|
1511
|
+
prepared,
|
|
1512
|
+
released: false,
|
|
1513
|
+
};
|
|
1299
1514
|
record.warnings = Array.isArray(prepared.warnings)
|
|
1300
1515
|
? completeValue([...prepared.warnings])
|
|
1301
1516
|
: NO_PROVIDER_WARNINGS;
|
|
1302
1517
|
lastWarnings = record.warnings;
|
|
1303
1518
|
throwIfAborted(
|
|
1304
1519
|
linked.controller.signal,
|
|
1305
|
-
()
|
|
1520
|
+
function browserSpeechLoadAbortReason() {
|
|
1521
|
+
return linked.reason(`${role}-load-cancelled`);
|
|
1522
|
+
},
|
|
1306
1523
|
);
|
|
1307
1524
|
if (operationGeneration !== generation) {
|
|
1308
1525
|
throw providerError(
|
|
@@ -1312,68 +1529,40 @@ function createBrowserSpeechProvider({
|
|
|
1312
1529
|
`${role}-load-superseded-by-unload`,
|
|
1313
1530
|
);
|
|
1314
1531
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
} catch (error) {
|
|
1343
|
-
const releaseFailure = trustedWorkerFailure(error, role);
|
|
1344
|
-
if (state !== "disposed" && state !== "unloading") {
|
|
1345
|
-
state = "error";
|
|
1346
|
-
errorCode = releaseFailure.code;
|
|
1347
|
-
lifecycleReason = releaseFailure.reason;
|
|
1348
|
-
activeOperation = null;
|
|
1349
|
-
}
|
|
1350
|
-
throw releaseFailure;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
},
|
|
1354
|
-
});
|
|
1355
|
-
if (!isSpeechWorkerClient(slot.client)) {
|
|
1356
|
-
throw providerError(
|
|
1357
|
-
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
1358
|
-
"The browser speech Worker client is not SDK-owned.",
|
|
1359
|
-
undefined,
|
|
1360
|
-
`${role}-worker-client-authority-mismatch`,
|
|
1532
|
+
const primaryDevice = role === "stt"
|
|
1533
|
+
? "wasm"
|
|
1534
|
+
: speechExecution.device === "auto"
|
|
1535
|
+
? navigatorHasWebGpu() ? "webgpu" : "wasm"
|
|
1536
|
+
: speechExecution.device;
|
|
1537
|
+
try {
|
|
1538
|
+
pool = await loadWorkerPool(
|
|
1539
|
+
preparation,
|
|
1540
|
+
primaryDevice,
|
|
1541
|
+
record.warnings,
|
|
1542
|
+
linked.controller.signal,
|
|
1543
|
+
);
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
if (
|
|
1546
|
+
role !== "tts"
|
|
1547
|
+
|| speechExecution.device !== "auto"
|
|
1548
|
+
|| primaryDevice !== "webgpu"
|
|
1549
|
+
|| linked.controller.signal.aborted
|
|
1550
|
+
|| operationGeneration !== generation
|
|
1551
|
+
) {
|
|
1552
|
+
throw error;
|
|
1553
|
+
}
|
|
1554
|
+
pool = await loadWorkerPool(
|
|
1555
|
+
preparation,
|
|
1556
|
+
"wasm",
|
|
1557
|
+
record.warnings,
|
|
1558
|
+
linked.controller.signal,
|
|
1361
1559
|
);
|
|
1362
1560
|
}
|
|
1363
|
-
const configuration = completeValue({
|
|
1364
|
-
role,
|
|
1365
|
-
runtime: prepared.runtime,
|
|
1366
|
-
model: prepared.model,
|
|
1367
|
-
...(authority.graph ? {
|
|
1368
|
-
artifactGraphProtocol: authority.graph.protocol,
|
|
1369
|
-
} : {}),
|
|
1370
|
-
});
|
|
1371
|
-
await slot.client.request("load", { configuration }, {
|
|
1372
|
-
signal: linked.controller.signal,
|
|
1373
|
-
});
|
|
1374
1561
|
throwIfAborted(
|
|
1375
1562
|
linked.controller.signal,
|
|
1376
|
-
()
|
|
1563
|
+
function browserSpeechLoadedPoolAbortReason() {
|
|
1564
|
+
return linked.reason(`${role}-load-cancelled`);
|
|
1565
|
+
},
|
|
1377
1566
|
);
|
|
1378
1567
|
if (operationGeneration !== generation) {
|
|
1379
1568
|
throw providerError(
|
|
@@ -1383,7 +1572,8 @@ function createBrowserSpeechProvider({
|
|
|
1383
1572
|
`${role}-load-superseded-by-unload`,
|
|
1384
1573
|
);
|
|
1385
1574
|
}
|
|
1386
|
-
active =
|
|
1575
|
+
active = pool;
|
|
1576
|
+
selectedDevice = pool.device;
|
|
1387
1577
|
cache = prepared.cache;
|
|
1388
1578
|
state = "ready";
|
|
1389
1579
|
lifecycleReason = `${role}-load-completed`;
|
|
@@ -1394,13 +1584,16 @@ function createBrowserSpeechProvider({
|
|
|
1394
1584
|
? linkedAbortFailure(linked, `${role}-load-cancelled`)
|
|
1395
1585
|
: trustedLoadFailure(error, role);
|
|
1396
1586
|
try {
|
|
1397
|
-
if (
|
|
1398
|
-
|
|
1587
|
+
if (pool) {
|
|
1588
|
+
await terminatePool(pool, failure, { releasePrepared: false });
|
|
1589
|
+
}
|
|
1590
|
+
releasePreparation(preparation);
|
|
1399
1591
|
} catch (cleanupError) {
|
|
1400
|
-
failure =
|
|
1592
|
+
failure = pool
|
|
1401
1593
|
? trustedWorkerFailure(cleanupError, role)
|
|
1402
1594
|
: trustedLoadFailure(cleanupError, role);
|
|
1403
1595
|
}
|
|
1596
|
+
selectedDevice = null;
|
|
1404
1597
|
if (operationGeneration === generation && state !== "unloading" && state !== "disposed") {
|
|
1405
1598
|
state = failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1406
1599
|
|| failure?.code === "ARCANE_AI_OPERATION_SUPERSEDED"
|
|
@@ -1465,7 +1658,15 @@ function createBrowserSpeechProvider({
|
|
|
1465
1658
|
`${role}-provider-request-not-ready`,
|
|
1466
1659
|
);
|
|
1467
1660
|
}
|
|
1468
|
-
|
|
1661
|
+
const pool = active;
|
|
1662
|
+
let slot = null;
|
|
1663
|
+
for (const candidate of pool.slots) {
|
|
1664
|
+
if (!candidate.terminated && !candidate.requestOperation) {
|
|
1665
|
+
slot = candidate;
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
if (!slot) {
|
|
1469
1670
|
throw providerError(
|
|
1470
1671
|
"ARCANE_AI_PROVIDER_BUSY",
|
|
1471
1672
|
"The browser speech provider is already processing a request.",
|
|
@@ -1483,32 +1684,44 @@ function createBrowserSpeechProvider({
|
|
|
1483
1684
|
linked.release();
|
|
1484
1685
|
throw linkedAbortFailure(linked, ROLE_REQUEST_REASON[role]);
|
|
1485
1686
|
}
|
|
1486
|
-
const slot = active;
|
|
1487
1687
|
const requestGeneration = generation;
|
|
1488
|
-
let workerRequestStarted = false;
|
|
1489
1688
|
activeOperation = role === "stt"
|
|
1490
1689
|
? "stt-provider-transcription"
|
|
1491
1690
|
: "tts-provider-synthesis";
|
|
1492
1691
|
lifecycleReason = role === "stt"
|
|
1493
1692
|
? "stt-transcription-started"
|
|
1494
1693
|
: "tts-synthesis-started";
|
|
1495
|
-
const
|
|
1694
|
+
const requestRecord = {
|
|
1695
|
+
promise: null,
|
|
1696
|
+
slot,
|
|
1697
|
+
abort(
|
|
1698
|
+
reason = ROLE_REQUEST_REASON[role],
|
|
1699
|
+
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
1700
|
+
) {
|
|
1701
|
+
linked.abort(reason, code);
|
|
1702
|
+
},
|
|
1703
|
+
};
|
|
1704
|
+
slot.requestOperation = requestRecord;
|
|
1705
|
+
requestOperations.add(requestRecord);
|
|
1706
|
+
function browserSpeechRequestAbortReason() {
|
|
1707
|
+
return linked.reason(ROLE_REQUEST_REASON[role]);
|
|
1708
|
+
}
|
|
1709
|
+
const promise = Promise.resolve().then(async function runBrowserSpeechProviderRequest() {
|
|
1496
1710
|
const normalized = await normalizeRequestPayload(
|
|
1497
1711
|
role,
|
|
1498
1712
|
context.payload,
|
|
1499
1713
|
authority,
|
|
1500
1714
|
linked.controller.signal,
|
|
1501
|
-
|
|
1715
|
+
browserSpeechRequestAbortReason,
|
|
1502
1716
|
);
|
|
1503
1717
|
throwIfAborted(
|
|
1504
1718
|
linked.controller.signal,
|
|
1505
|
-
|
|
1719
|
+
browserSpeechRequestAbortReason,
|
|
1506
1720
|
);
|
|
1507
|
-
workerRequestStarted = true;
|
|
1508
1721
|
const result = await slot.client.request("use", normalized.payload, {
|
|
1509
1722
|
signal: linked.controller.signal,
|
|
1510
1723
|
});
|
|
1511
|
-
if (requestGeneration !== generation || active !==
|
|
1724
|
+
if (requestGeneration !== generation || active !== pool || state !== "ready") {
|
|
1512
1725
|
throw providerError(
|
|
1513
1726
|
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1514
1727
|
"The browser speech result was superseded.",
|
|
@@ -1521,48 +1734,36 @@ function createBrowserSpeechProvider({
|
|
|
1521
1734
|
return role === "tts" && normalized.shared
|
|
1522
1735
|
? encodeSharedSynthesisResult(result, authority)
|
|
1523
1736
|
: result;
|
|
1524
|
-
})();
|
|
1525
|
-
requestOperation = completeValue({
|
|
1526
|
-
promise,
|
|
1527
|
-
abort: (
|
|
1528
|
-
reason = ROLE_REQUEST_REASON[role],
|
|
1529
|
-
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
1530
|
-
) => linked.abort(reason, code),
|
|
1531
1737
|
});
|
|
1738
|
+
requestRecord.promise = promise;
|
|
1532
1739
|
try {
|
|
1533
1740
|
const result = await promise;
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1741
|
+
if (requestGeneration === generation && state === "ready") {
|
|
1742
|
+
lifecycleReason = role === "stt"
|
|
1743
|
+
? "stt-transcription-completed"
|
|
1744
|
+
: "tts-synthesis-completed";
|
|
1745
|
+
}
|
|
1537
1746
|
return result;
|
|
1538
1747
|
} catch (error) {
|
|
1539
|
-
|
|
1748
|
+
const failure = linked.controller.signal.aborted
|
|
1540
1749
|
&& (error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1541
1750
|
|| linked.code() === "ARCANE_AI_OPERATION_SUPERSEDED")
|
|
1542
1751
|
? linkedAbortFailure(linked, ROLE_REQUEST_REASON[role])
|
|
1543
1752
|
: trustedRequestFailure(error, role);
|
|
1544
|
-
if (
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
failure = trustedWorkerFailure(cleanupError, role);
|
|
1552
|
-
}
|
|
1553
|
-
state = "unloaded";
|
|
1753
|
+
if (requestGeneration === generation && state !== "unloading" && state !== "disposed") {
|
|
1754
|
+
lifecycleReason = failure?.reason
|
|
1755
|
+
?? (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1756
|
+
? ROLE_REQUEST_REASON[role]
|
|
1757
|
+
: role === "stt"
|
|
1758
|
+
? "stt-transcription-engine-operation-rejected"
|
|
1759
|
+
: "tts-synthesis-engine-operation-rejected");
|
|
1554
1760
|
}
|
|
1555
|
-
lifecycleReason = failure?.reason
|
|
1556
|
-
?? (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1557
|
-
? ROLE_REQUEST_REASON[role]
|
|
1558
|
-
: role === "stt"
|
|
1559
|
-
? "stt-transcription-engine-operation-rejected"
|
|
1560
|
-
: "tts-synthesis-engine-operation-rejected");
|
|
1561
1761
|
throw failure;
|
|
1562
1762
|
} finally {
|
|
1563
1763
|
linked.release();
|
|
1564
|
-
if (requestOperation
|
|
1565
|
-
|
|
1764
|
+
if (slot.requestOperation === requestRecord) slot.requestOperation = null;
|
|
1765
|
+
requestOperations.delete(requestRecord);
|
|
1766
|
+
if (requestOperations.size === 0 && state === "ready") activeOperation = null;
|
|
1566
1767
|
}
|
|
1567
1768
|
},
|
|
1568
1769
|
|
|
@@ -1594,47 +1795,56 @@ function createBrowserSpeechProvider({
|
|
|
1594
1795
|
errorCode = null;
|
|
1595
1796
|
activeOperation = `${role}-provider-unload`;
|
|
1596
1797
|
const capturedLoad = loadOperation?.promise ?? null;
|
|
1597
|
-
const
|
|
1798
|
+
const capturedRequests = [];
|
|
1799
|
+
for (const requestRecord of requestOperations) {
|
|
1800
|
+
if (requestRecord.promise) capturedRequests.push(requestRecord.promise);
|
|
1801
|
+
}
|
|
1598
1802
|
if (loadOperation) {
|
|
1599
1803
|
lifecycleReason = `${role}-load-superseded-by-unload`;
|
|
1600
1804
|
loadOperation.abort(
|
|
1601
1805
|
`${role}-load-superseded-by-unload`,
|
|
1602
1806
|
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1603
1807
|
);
|
|
1604
|
-
}
|
|
1808
|
+
}
|
|
1809
|
+
if (requestOperations.size > 0) {
|
|
1605
1810
|
lifecycleReason = role === "stt"
|
|
1606
1811
|
? "stt-transcription-superseded-by-unload"
|
|
1607
1812
|
: "tts-synthesis-superseded-by-unload";
|
|
1608
|
-
|
|
1609
|
-
|
|
1813
|
+
for (const requestRecord of requestOperations) {
|
|
1814
|
+
requestRecord.abort(lifecycleReason, "ARCANE_AI_OPERATION_SUPERSEDED");
|
|
1815
|
+
}
|
|
1816
|
+
} else if (!loadOperation) {
|
|
1610
1817
|
lifecycleReason = `${role}-unload-started`;
|
|
1611
1818
|
}
|
|
1612
|
-
const promise = (async ()
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const slot = active;
|
|
1819
|
+
const promise = Promise.resolve().then(async function unloadBrowserSpeechProviderPool() {
|
|
1820
|
+
const pendingOperations = [...capturedRequests];
|
|
1821
|
+
if (capturedLoad) pendingOperations.unshift(capturedLoad);
|
|
1822
|
+
await Promise.allSettled(pendingOperations);
|
|
1823
|
+
const pool = active;
|
|
1618
1824
|
active = null;
|
|
1619
|
-
await
|
|
1825
|
+
await terminatePool(pool, providerError(
|
|
1620
1826
|
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1621
1827
|
"The browser speech Worker was terminated by unload().",
|
|
1622
1828
|
undefined,
|
|
1623
1829
|
`${role}-worker-terminated-by-unload`,
|
|
1624
1830
|
));
|
|
1831
|
+
selectedDevice = null;
|
|
1625
1832
|
cache = null;
|
|
1626
1833
|
state = "unloaded";
|
|
1627
1834
|
lifecycleReason = `${role}-unload-completed`;
|
|
1628
1835
|
activeOperation = null;
|
|
1629
1836
|
return status();
|
|
1630
|
-
})();
|
|
1631
|
-
const tracked = promise.then((value) => {
|
|
1632
|
-
if (unloadOperation === tracked) unloadOperation = null;
|
|
1633
|
-
return value;
|
|
1634
|
-
}, (error) => {
|
|
1635
|
-
if (unloadOperation === tracked) unloadOperation = null;
|
|
1636
|
-
throw error;
|
|
1637
1837
|
});
|
|
1838
|
+
const tracked = promise.then(
|
|
1839
|
+
function completeBrowserSpeechProviderUnload(value) {
|
|
1840
|
+
if (unloadOperation === tracked) unloadOperation = null;
|
|
1841
|
+
return value;
|
|
1842
|
+
},
|
|
1843
|
+
function rejectBrowserSpeechProviderUnload(error) {
|
|
1844
|
+
if (unloadOperation === tracked) unloadOperation = null;
|
|
1845
|
+
throw error;
|
|
1846
|
+
},
|
|
1847
|
+
);
|
|
1638
1848
|
unloadOperation = tracked;
|
|
1639
1849
|
return awaitAbortableSpeechOperation(
|
|
1640
1850
|
tracked,
|