arcane-os 0.1.2 → 0.2.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 +17 -0
- package/NOTICE +5 -3
- package/README.md +73 -24
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
- package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
- package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +475 -0
- package/browser-runtime/ai/browser-speech.mjs +9 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
- package/browser-runtime/ai/browser-wasm.mjs +46 -1
- package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
- package/browser-runtime/ai/model-controller.mjs +138 -12
- package/browser-runtime/ai/speech-worker-client.mjs +207 -0
- package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
- package/browser-runtime/ai/wllama/index.mjs +389 -0
- package/docs/architecture.md +132 -22
- package/docs/reference/README.md +1 -1
- package/docs/reference/ai/browser-wasm.md +101 -42
- package/docs/reference/availability-and-normalization.md +19 -5
- package/docs/reference/behavioral-testing.md +18 -5
- package/docs/reference/cli.md +2 -2
- package/docs/reference/inventory/package-api.json +14 -14
- package/docs/reference/protocols.md +4 -4
- package/docs/reference/sdk-api.md +68 -38
- package/docs/work-amplification.md +8 -4
- package/package.json +7 -3
- package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
- package/runtime/arcane/components/chat.html +280 -62
- package/runtime/arcane/components/speech.html +1113 -265
- package/runtime/arcane/entities/Chat.js +246 -43
- package/runtime/arcane/modules/AI.js +713 -162
- package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
- package/runtime/arcane/modules/AIRuntimeState.js +872 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -0
- package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/cli/main.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/dev-server.mjs +244 -13
- package/src/import-map.mjs +59 -1
- package/src/packager/core.mjs +2 -2
- package/src/runtime.mjs +14 -4
- package/src/sdk-browser-runtime.mjs +28 -75
- package/src/templates/workspace-template.mjs +4 -4
- package/src/toolchain.mjs +3 -0
- package/src/workspace-runtime.mjs +1 -1
- package/src/workspace.mjs +1 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBrowserSpeechAuthority,
|
|
3
|
+
isBrowserSpeechAuthority,
|
|
4
|
+
isDbopfsSpeechArtifactStore,
|
|
5
|
+
} from "./browser-speech-artifacts.mjs";
|
|
6
|
+
import {
|
|
7
|
+
normalizeModelSecurity,
|
|
8
|
+
resolveModelSecurity,
|
|
9
|
+
sameModelSecurity,
|
|
10
|
+
} from "./model-controller.mjs";
|
|
11
|
+
import {
|
|
12
|
+
createSpeechWorkerClient,
|
|
13
|
+
isSpeechWorkerClient,
|
|
14
|
+
} from "./speech-worker-client.mjs";
|
|
15
|
+
|
|
16
|
+
const AI_PROVIDER_PROTOCOL = "arcane-ai-provider/2";
|
|
17
|
+
const ROLE_OPERATION = Object.freeze({ stt: "transcribe", tts: "synthesize" });
|
|
18
|
+
const WORKER_FAILURE_CODES = new Set([
|
|
19
|
+
"ARCANE_AI_WORKER_CRASHED",
|
|
20
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
21
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function providerError(code, message, cause) {
|
|
25
|
+
const error = cause === undefined
|
|
26
|
+
? new Error(message)
|
|
27
|
+
: new Error(message, { cause });
|
|
28
|
+
error.name = code === "ARCANE_AI_REQUEST_ABORTED"
|
|
29
|
+
? "AbortError"
|
|
30
|
+
: "ArcaneBrowserSpeechProviderError";
|
|
31
|
+
error.code = code;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function abortError(signal) {
|
|
36
|
+
return providerError(
|
|
37
|
+
"ARCANE_AI_REQUEST_ABORTED",
|
|
38
|
+
"The browser speech operation was cancelled.",
|
|
39
|
+
signal?.reason,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function throwIfAborted(signal) {
|
|
44
|
+
if (signal?.aborted) throw abortError(signal);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function workerFailureCode(error) {
|
|
48
|
+
return WORKER_FAILURE_CODES.has(error?.code)
|
|
49
|
+
? error.code
|
|
50
|
+
: "ARCANE_AI_WORKER_CRASHED";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requiredIdentifier(value, label) {
|
|
54
|
+
if (typeof value !== "string" || !value.trim() || value.trim().length > 128) {
|
|
55
|
+
throw new TypeError(`${label} must be a trimmed 1-128 character string.`);
|
|
56
|
+
}
|
|
57
|
+
return value.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function linkSignal(signal) {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const abort = () => controller.abort(signal?.reason);
|
|
63
|
+
if (signal?.aborted) abort();
|
|
64
|
+
else signal?.addEventListener?.("abort", abort, { once: true });
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
controller,
|
|
67
|
+
release: () => signal?.removeEventListener?.("abort", abort),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function publicStatus({ role, id, authority, state, busy, generation, errorCode, cache }) {
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
role,
|
|
74
|
+
providerId: id,
|
|
75
|
+
modelId: authority.modelId,
|
|
76
|
+
state,
|
|
77
|
+
loaded: state === "ready",
|
|
78
|
+
busy,
|
|
79
|
+
generation,
|
|
80
|
+
errorCode,
|
|
81
|
+
cache,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function cloneRequestPayload(role, payload, authority) {
|
|
86
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
87
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", "Speech request payload must be an object.");
|
|
88
|
+
}
|
|
89
|
+
if (role === "stt") {
|
|
90
|
+
if (!(payload.audio instanceof Float32Array) || payload.sampleRate !== 16_000) {
|
|
91
|
+
throw providerError(
|
|
92
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
93
|
+
"Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
audio: new Float32Array(payload.audio),
|
|
98
|
+
sampleRate: 16_000,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const text = typeof payload.text === "string" ? payload.text.trim() : "";
|
|
102
|
+
const voice = typeof (payload.voice ?? authority.defaultVoice) === "string"
|
|
103
|
+
? (payload.voice ?? authority.defaultVoice).trim()
|
|
104
|
+
: "";
|
|
105
|
+
const speed = payload.speed ?? 1;
|
|
106
|
+
if (!text || !voice || !Number.isFinite(speed) || speed <= 0 || speed > 4) {
|
|
107
|
+
throw providerError(
|
|
108
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
109
|
+
"Kokoro requires text, a voice, and speed greater than 0 and at most 4.",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze({ text, voice, speed });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createBrowserSpeechProvider({
|
|
116
|
+
role,
|
|
117
|
+
id,
|
|
118
|
+
localOnly = true,
|
|
119
|
+
model,
|
|
120
|
+
runtime,
|
|
121
|
+
appSecurity,
|
|
122
|
+
security,
|
|
123
|
+
store,
|
|
124
|
+
offline = false,
|
|
125
|
+
} = {}) {
|
|
126
|
+
const providerId = requiredIdentifier(id, "Browser speech provider id");
|
|
127
|
+
if (localOnly !== true) {
|
|
128
|
+
throw new TypeError("Browser speech providers are localOnly.");
|
|
129
|
+
}
|
|
130
|
+
if (typeof offline !== "boolean") {
|
|
131
|
+
throw new TypeError("Browser speech offline must be a boolean.");
|
|
132
|
+
}
|
|
133
|
+
if (!isDbopfsSpeechArtifactStore(store)) {
|
|
134
|
+
throw new TypeError("Browser speech providers require an SDK-created DBOPFS artifact store.");
|
|
135
|
+
}
|
|
136
|
+
const configuredAppSecurity = normalizeModelSecurity(
|
|
137
|
+
appSecurity,
|
|
138
|
+
"Browser speech app security",
|
|
139
|
+
);
|
|
140
|
+
const configuredProviderSecurity = normalizeModelSecurity(
|
|
141
|
+
security,
|
|
142
|
+
"Browser speech provider security",
|
|
143
|
+
);
|
|
144
|
+
const authority = createBrowserSpeechAuthority({
|
|
145
|
+
providerId,
|
|
146
|
+
role,
|
|
147
|
+
model,
|
|
148
|
+
runtime,
|
|
149
|
+
security: configuredProviderSecurity,
|
|
150
|
+
});
|
|
151
|
+
if (!isBrowserSpeechAuthority(authority)) {
|
|
152
|
+
throw new TypeError("Browser speech authority construction failed.");
|
|
153
|
+
}
|
|
154
|
+
const operation = ROLE_OPERATION[role];
|
|
155
|
+
const catalogEntry = Object.freeze({
|
|
156
|
+
id: authority.modelId,
|
|
157
|
+
providerId,
|
|
158
|
+
role,
|
|
159
|
+
localOnly: true,
|
|
160
|
+
repository: authority.repository,
|
|
161
|
+
revision: authority.revision,
|
|
162
|
+
runtime: authority.runtime,
|
|
163
|
+
files: authority.files,
|
|
164
|
+
});
|
|
165
|
+
let state = "unloaded";
|
|
166
|
+
let errorCode = null;
|
|
167
|
+
let cache = null;
|
|
168
|
+
let generation = 0;
|
|
169
|
+
let active = null;
|
|
170
|
+
let loadOperation = null;
|
|
171
|
+
let unloadOperation = null;
|
|
172
|
+
let disposeOperation = null;
|
|
173
|
+
let requestOperation = null;
|
|
174
|
+
|
|
175
|
+
function status() {
|
|
176
|
+
return publicStatus({
|
|
177
|
+
role,
|
|
178
|
+
id: providerId,
|
|
179
|
+
authority,
|
|
180
|
+
state,
|
|
181
|
+
busy: requestOperation !== null,
|
|
182
|
+
generation,
|
|
183
|
+
errorCode,
|
|
184
|
+
cache,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function releaseSlot(slot) {
|
|
189
|
+
if (!slot || slot.released) return;
|
|
190
|
+
slot.released = true;
|
|
191
|
+
slot.prepared.release();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function terminateSlot(slot, reason, { intentional = true } = {}) {
|
|
195
|
+
if (!slot) return;
|
|
196
|
+
if (!isSpeechWorkerClient(slot.client)) {
|
|
197
|
+
throw providerError(
|
|
198
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
199
|
+
"The browser speech Worker client is not SDK-owned.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
await slot.client.terminate(reason, { intentional });
|
|
203
|
+
releaseSlot(slot);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const provider = {
|
|
207
|
+
protocol: AI_PROVIDER_PROTOCOL,
|
|
208
|
+
role,
|
|
209
|
+
id: providerId,
|
|
210
|
+
localOnly: true,
|
|
211
|
+
|
|
212
|
+
catalog() {
|
|
213
|
+
return Object.freeze([catalogEntry]);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
inspect(selection, { signal } = {}) {
|
|
217
|
+
throwIfAborted(signal);
|
|
218
|
+
const available = selection?.providerId === providerId
|
|
219
|
+
&& selection?.modelId === authority.modelId
|
|
220
|
+
&& selection?.localOnly !== false;
|
|
221
|
+
if (!available) {
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
available: false,
|
|
224
|
+
code: "ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
225
|
+
message: "The selected browser speech model does not match this provider authority.",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return Object.freeze({ available: true, authority });
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
status,
|
|
232
|
+
|
|
233
|
+
load(context = {}) {
|
|
234
|
+
if (state === "disposed" || disposeOperation) {
|
|
235
|
+
return Promise.reject(providerError("ARCANE_AI_PROVIDER_DISPOSED", "The browser speech provider is disposed."));
|
|
236
|
+
}
|
|
237
|
+
if (unloadOperation || state === "unloading") {
|
|
238
|
+
return Promise.reject(providerError(
|
|
239
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
240
|
+
"The browser speech provider is unloading.",
|
|
241
|
+
));
|
|
242
|
+
}
|
|
243
|
+
if (typeof context.progress !== "function") {
|
|
244
|
+
return Promise.reject(new TypeError("Browser speech load progress must be a function."));
|
|
245
|
+
}
|
|
246
|
+
if (context.role !== role
|
|
247
|
+
|| context.selection?.providerId !== providerId
|
|
248
|
+
|| context.selection?.modelId !== authority.modelId
|
|
249
|
+
|| context.selection?.localOnly === false) {
|
|
250
|
+
return Promise.reject(providerError("ARCANE_AI_MODEL_AUTHORITY_REQUIRED", "Browser speech load selection changed."));
|
|
251
|
+
}
|
|
252
|
+
let effectiveSecurity;
|
|
253
|
+
try {
|
|
254
|
+
effectiveSecurity = resolveModelSecurity({
|
|
255
|
+
app: configuredAppSecurity,
|
|
256
|
+
binding: configuredProviderSecurity,
|
|
257
|
+
load: context.security,
|
|
258
|
+
});
|
|
259
|
+
throwIfAborted(context.signal);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return Promise.reject(error);
|
|
262
|
+
}
|
|
263
|
+
if (state === "ready" && active) {
|
|
264
|
+
if (sameModelSecurity(active.security, effectiveSecurity)) {
|
|
265
|
+
return Promise.resolve(status());
|
|
266
|
+
}
|
|
267
|
+
return provider.unload().then(() => provider.load(context));
|
|
268
|
+
}
|
|
269
|
+
if (loadOperation) {
|
|
270
|
+
if (sameModelSecurity(loadOperation.security, effectiveSecurity)) {
|
|
271
|
+
return loadOperation.promise;
|
|
272
|
+
}
|
|
273
|
+
loadOperation.abort();
|
|
274
|
+
return loadOperation.promise.catch(() => undefined).then(() => provider.load(context));
|
|
275
|
+
}
|
|
276
|
+
generation += 1;
|
|
277
|
+
const operationGeneration = generation;
|
|
278
|
+
const linked = linkSignal(context.signal);
|
|
279
|
+
state = "loading";
|
|
280
|
+
errorCode = null;
|
|
281
|
+
const promise = (async () => {
|
|
282
|
+
let prepared = null;
|
|
283
|
+
let slot = null;
|
|
284
|
+
try {
|
|
285
|
+
prepared = await store.prepare(authority, {
|
|
286
|
+
signal: linked.controller.signal,
|
|
287
|
+
onProgress: context.progress,
|
|
288
|
+
offline,
|
|
289
|
+
security: effectiveSecurity,
|
|
290
|
+
});
|
|
291
|
+
throwIfAborted(linked.controller.signal);
|
|
292
|
+
if (operationGeneration !== generation) {
|
|
293
|
+
throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "Browser speech loading was superseded.");
|
|
294
|
+
}
|
|
295
|
+
slot = {
|
|
296
|
+
prepared,
|
|
297
|
+
released: false,
|
|
298
|
+
client: null,
|
|
299
|
+
security: effectiveSecurity,
|
|
300
|
+
};
|
|
301
|
+
slot.client = createSpeechWorkerClient({
|
|
302
|
+
role,
|
|
303
|
+
onTermination({ reason, intentional }) {
|
|
304
|
+
if (active === slot) {
|
|
305
|
+
active = null;
|
|
306
|
+
if (state !== "disposed" && state !== "unloading") {
|
|
307
|
+
state = intentional ? "unloaded" : "error";
|
|
308
|
+
errorCode = intentional ? null : workerFailureCode(reason);
|
|
309
|
+
}
|
|
310
|
+
releaseSlot(slot);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
if (!isSpeechWorkerClient(slot.client)) {
|
|
315
|
+
throw providerError(
|
|
316
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
317
|
+
"The browser speech Worker client is not SDK-owned.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const configuration = Object.freeze({
|
|
321
|
+
role,
|
|
322
|
+
runtime: prepared.runtime,
|
|
323
|
+
model: prepared.model,
|
|
324
|
+
});
|
|
325
|
+
await slot.client.request("load", { configuration }, {
|
|
326
|
+
signal: linked.controller.signal,
|
|
327
|
+
progress: context.progress,
|
|
328
|
+
});
|
|
329
|
+
throwIfAborted(linked.controller.signal);
|
|
330
|
+
if (operationGeneration !== generation) {
|
|
331
|
+
throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "Browser speech loading was superseded.");
|
|
332
|
+
}
|
|
333
|
+
active = slot;
|
|
334
|
+
cache = prepared.cache;
|
|
335
|
+
state = "ready";
|
|
336
|
+
return status();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (slot) await terminateSlot(slot, error).catch(() => undefined);
|
|
339
|
+
else prepared?.release();
|
|
340
|
+
if (operationGeneration === generation && state !== "unloading" && state !== "disposed") {
|
|
341
|
+
state = error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
342
|
+
|| error?.code === "ARCANE_AI_OPERATION_SUPERSEDED"
|
|
343
|
+
? "unloaded"
|
|
344
|
+
: "error";
|
|
345
|
+
errorCode = error?.code ?? "ARCANE_AI_PROVIDER_LOAD_FAILED";
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
} finally {
|
|
349
|
+
linked.release();
|
|
350
|
+
if (loadOperation?.promise === promise) loadOperation = null;
|
|
351
|
+
}
|
|
352
|
+
})();
|
|
353
|
+
loadOperation = Object.freeze({
|
|
354
|
+
promise,
|
|
355
|
+
security: effectiveSecurity,
|
|
356
|
+
abort: () => linked.controller.abort(),
|
|
357
|
+
});
|
|
358
|
+
return promise;
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
async request(context = {}) {
|
|
362
|
+
if (context.role !== role || context.operation !== operation) {
|
|
363
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `Browser ${role} supports only ${operation}.`);
|
|
364
|
+
}
|
|
365
|
+
const externalSignal = context.signal ?? null;
|
|
366
|
+
throwIfAborted(externalSignal);
|
|
367
|
+
if (state !== "ready" || !active) {
|
|
368
|
+
throw providerError("ARCANE_AI_NOT_READY", "The browser speech provider is not ready.");
|
|
369
|
+
}
|
|
370
|
+
if (requestOperation) {
|
|
371
|
+
throw providerError("ARCANE_AI_PROVIDER_BUSY", "The browser speech provider is already processing a request.");
|
|
372
|
+
}
|
|
373
|
+
const payload = cloneRequestPayload(role, context.payload, {
|
|
374
|
+
defaultVoice: authority.defaultVoice,
|
|
375
|
+
});
|
|
376
|
+
throwIfAborted(externalSignal);
|
|
377
|
+
const linked = linkSignal(externalSignal);
|
|
378
|
+
if (linked.controller.signal.aborted) {
|
|
379
|
+
linked.release();
|
|
380
|
+
throw abortError(externalSignal);
|
|
381
|
+
}
|
|
382
|
+
const slot = active;
|
|
383
|
+
const requestGeneration = generation;
|
|
384
|
+
const promise = slot.client.request("use", payload, {
|
|
385
|
+
signal: linked.controller.signal,
|
|
386
|
+
});
|
|
387
|
+
requestOperation = Object.freeze({
|
|
388
|
+
promise,
|
|
389
|
+
abort: () => linked.controller.abort(),
|
|
390
|
+
});
|
|
391
|
+
try {
|
|
392
|
+
const result = await promise;
|
|
393
|
+
if (requestGeneration !== generation || active !== slot) {
|
|
394
|
+
throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "The browser speech result was superseded.");
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error?.code === "ARCANE_AI_REQUEST_ABORTED" && active === slot) {
|
|
399
|
+
active = null;
|
|
400
|
+
releaseSlot(slot);
|
|
401
|
+
state = "unloaded";
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
} finally {
|
|
405
|
+
linked.release();
|
|
406
|
+
if (requestOperation?.promise === promise) requestOperation = null;
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
unload() {
|
|
411
|
+
if (state === "disposed") return Promise.resolve(status());
|
|
412
|
+
if (unloadOperation) return unloadOperation;
|
|
413
|
+
generation += 1;
|
|
414
|
+
state = "unloading";
|
|
415
|
+
errorCode = null;
|
|
416
|
+
loadOperation?.abort();
|
|
417
|
+
requestOperation?.abort();
|
|
418
|
+
const promise = (async () => {
|
|
419
|
+
await Promise.allSettled([
|
|
420
|
+
loadOperation?.promise,
|
|
421
|
+
requestOperation?.promise,
|
|
422
|
+
].filter(Boolean));
|
|
423
|
+
const slot = active;
|
|
424
|
+
active = null;
|
|
425
|
+
await terminateSlot(slot, providerError(
|
|
426
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
427
|
+
"The browser speech Worker was terminated by unload().",
|
|
428
|
+
));
|
|
429
|
+
cache = null;
|
|
430
|
+
state = "unloaded";
|
|
431
|
+
return status();
|
|
432
|
+
})();
|
|
433
|
+
unloadOperation = promise.finally(() => {
|
|
434
|
+
if (unloadOperation === promise || unloadOperation === wrapped) unloadOperation = null;
|
|
435
|
+
});
|
|
436
|
+
const wrapped = unloadOperation;
|
|
437
|
+
return wrapped;
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
dispose() {
|
|
441
|
+
if (state === "disposed") return Promise.resolve(status());
|
|
442
|
+
if (disposeOperation) return disposeOperation;
|
|
443
|
+
const promise = (async () => {
|
|
444
|
+
await provider.unload();
|
|
445
|
+
state = "disposed";
|
|
446
|
+
return status();
|
|
447
|
+
})();
|
|
448
|
+
disposeOperation = promise.then((value) => {
|
|
449
|
+
disposeOperation = null;
|
|
450
|
+
return value;
|
|
451
|
+
}, (error) => {
|
|
452
|
+
disposeOperation = null;
|
|
453
|
+
throw error;
|
|
454
|
+
});
|
|
455
|
+
return disposeOperation;
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
return Object.freeze(provider);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function createBrowserWhisperProvider(options = {}) {
|
|
462
|
+
return createBrowserSpeechProvider({
|
|
463
|
+
...options,
|
|
464
|
+
role: "stt",
|
|
465
|
+
id: options.id ?? "arcane-browser-whisper",
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function createBrowserKokoroProvider(options = {}) {
|
|
470
|
+
return createBrowserSpeechProvider({
|
|
471
|
+
...options,
|
|
472
|
+
role: "tts",
|
|
473
|
+
id: options.id ?? "arcane-browser-kokoro",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BROWSER_SPEECH_ARTIFACT_PROTOCOL,
|
|
3
|
+
createBrowserSpeechAuthority,
|
|
4
|
+
createDbopfsSpeechArtifactStore,
|
|
5
|
+
} from "./browser-speech-artifacts.mjs";
|
|
6
|
+
export {
|
|
7
|
+
createBrowserKokoroProvider,
|
|
8
|
+
createBrowserWhisperProvider,
|
|
9
|
+
} from "./browser-speech-providers.mjs";
|