arcane-os 0.2.2 → 0.3.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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createBrowserSpeechAuthority,
|
|
3
|
+
isBrowserSpeechArtifactGraph,
|
|
4
|
+
isBrowserSpeechArtifactError,
|
|
3
5
|
isBrowserSpeechAuthority,
|
|
4
6
|
isDbopfsSpeechArtifactStore,
|
|
5
7
|
} from "./browser-speech-artifacts.mjs";
|
|
@@ -11,20 +13,29 @@ import {
|
|
|
11
13
|
import {
|
|
12
14
|
createSpeechWorkerClient,
|
|
13
15
|
isSpeechWorkerClient,
|
|
16
|
+
isSpeechWorkerClientError,
|
|
14
17
|
} from "./speech-worker-client.mjs";
|
|
15
18
|
|
|
16
19
|
const AI_PROVIDER_PROTOCOL = "arcane-ai-provider/2";
|
|
20
|
+
const AI_MODEL_AUTHORITY_PROTOCOL = "arcane-ai-model-authority/1";
|
|
17
21
|
const ROLE_OPERATION = Object.freeze({ stt: "transcribe", tts: "synthesize" });
|
|
18
22
|
const STT_SAMPLE_RATE = 16_000;
|
|
19
23
|
const TTS_SAMPLE_RATE = 24_000;
|
|
20
24
|
const TTS_RESPONSE_FORMAT = "wav";
|
|
25
|
+
const ROLE_REQUEST_REASON = Object.freeze({
|
|
26
|
+
stt: "stt-transcription-cancelled",
|
|
27
|
+
tts: "tts-synthesis-cancelled",
|
|
28
|
+
});
|
|
29
|
+
const UNMAPPED_PROVIDER_REASON = "browser-speech-provider-error-reason-unmapped";
|
|
21
30
|
const WORKER_FAILURE_CODES = new Set([
|
|
22
31
|
"ARCANE_AI_WORKER_CRASHED",
|
|
23
32
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
33
|
+
"ARCANE_AI_WORKER_MESSAGE_REJECTED",
|
|
24
34
|
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
25
35
|
]);
|
|
36
|
+
const PROVIDER_ERRORS = new WeakSet();
|
|
26
37
|
|
|
27
|
-
function providerError(code, message, cause) {
|
|
38
|
+
function providerError(code, message, cause, reason) {
|
|
28
39
|
const error = cause === undefined
|
|
29
40
|
? new Error(message)
|
|
30
41
|
: new Error(message, { cause });
|
|
@@ -32,19 +43,83 @@ function providerError(code, message, cause) {
|
|
|
32
43
|
? "AbortError"
|
|
33
44
|
: "ArcaneBrowserSpeechProviderError";
|
|
34
45
|
error.code = code;
|
|
46
|
+
error.reason = typeof reason === "string" && reason
|
|
47
|
+
? reason
|
|
48
|
+
: UNMAPPED_PROVIDER_REASON;
|
|
49
|
+
PROVIDER_ERRORS.add(error);
|
|
35
50
|
return error;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
function
|
|
53
|
+
function isBrowserSpeechProviderError(value) {
|
|
54
|
+
return typeof value === "object"
|
|
55
|
+
&& value !== null
|
|
56
|
+
&& PROVIDER_ERRORS.has(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isTrustedSpeechError(value) {
|
|
60
|
+
return isBrowserSpeechProviderError(value)
|
|
61
|
+
|| isBrowserSpeechArtifactError(value)
|
|
62
|
+
|| isSpeechWorkerClientError(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function trustedLoadFailure(error, role) {
|
|
66
|
+
if (isTrustedSpeechError(error)) return error;
|
|
67
|
+
return providerError(
|
|
68
|
+
"ARCANE_AI_PROVIDER_LOAD_FAILED",
|
|
69
|
+
`The browser ${role} provider load was rejected.`,
|
|
70
|
+
error,
|
|
71
|
+
`${role}-provider-load-rejected`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function trustedRequestFailure(error, role) {
|
|
76
|
+
if (isTrustedSpeechError(error)) return error;
|
|
77
|
+
return providerError(
|
|
78
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
79
|
+
`The browser ${role} engine operation was rejected.`,
|
|
80
|
+
error,
|
|
81
|
+
role === "stt"
|
|
82
|
+
? "stt-transcription-engine-operation-rejected"
|
|
83
|
+
: "tts-synthesis-engine-operation-rejected",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function trustedWorkerFailure(error, role) {
|
|
88
|
+
if (isTrustedSpeechError(error)) return error;
|
|
89
|
+
return providerError(
|
|
90
|
+
"ARCANE_AI_WORKER_MESSAGE_REJECTED",
|
|
91
|
+
`The browser ${role} Worker returned an untrusted error envelope.`,
|
|
92
|
+
error,
|
|
93
|
+
`${role}-worker-error-envelope-rejected`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function trustedLifecycleFailure(error, role, operation) {
|
|
98
|
+
if (isTrustedSpeechError(error)) return error;
|
|
99
|
+
return providerError(
|
|
100
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
101
|
+
`The browser ${role} ${operation} context could not be read.`,
|
|
102
|
+
error,
|
|
103
|
+
`${role}-provider-${operation}-context-read-rejected`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveReason(reason, fallback = UNMAPPED_PROVIDER_REASON) {
|
|
108
|
+
const resolved = typeof reason === "function" ? reason() : reason;
|
|
109
|
+
return typeof resolved === "string" && resolved ? resolved : fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function abortError(signal, reason = UNMAPPED_PROVIDER_REASON) {
|
|
39
113
|
return providerError(
|
|
40
114
|
"ARCANE_AI_REQUEST_ABORTED",
|
|
41
115
|
"The browser speech operation was cancelled.",
|
|
42
116
|
signal?.reason,
|
|
117
|
+
resolveReason(reason),
|
|
43
118
|
);
|
|
44
119
|
}
|
|
45
120
|
|
|
46
|
-
function throwIfAborted(signal) {
|
|
47
|
-
if (signal?.aborted) throw abortError(signal);
|
|
121
|
+
function throwIfAborted(signal, reason) {
|
|
122
|
+
if (signal?.aborted) throw abortError(signal, reason);
|
|
48
123
|
}
|
|
49
124
|
|
|
50
125
|
function workerFailureCode(error) {
|
|
@@ -60,50 +135,234 @@ function requiredIdentifier(value, label) {
|
|
|
60
135
|
return value.trim();
|
|
61
136
|
}
|
|
62
137
|
|
|
138
|
+
function createProviderAuthority({
|
|
139
|
+
providerId,
|
|
140
|
+
role,
|
|
141
|
+
graph,
|
|
142
|
+
model,
|
|
143
|
+
runtime,
|
|
144
|
+
security,
|
|
145
|
+
}) {
|
|
146
|
+
if (graph !== undefined) {
|
|
147
|
+
if (model !== undefined || runtime !== undefined) {
|
|
148
|
+
throw new TypeError("Browser speech graph is mutually exclusive with legacy model and runtime descriptors.");
|
|
149
|
+
}
|
|
150
|
+
if (!isBrowserSpeechArtifactGraph(graph)) {
|
|
151
|
+
throw new TypeError("Browser speech graph must be created by createBrowserSpeechArtifactGraph().");
|
|
152
|
+
}
|
|
153
|
+
if (graph.role !== role) {
|
|
154
|
+
throw new TypeError(`Browser speech graph role must be ${role}.`);
|
|
155
|
+
}
|
|
156
|
+
if (graph.providerId !== null
|
|
157
|
+
&& graph.providerId !== undefined
|
|
158
|
+
&& graph.providerId !== providerId) {
|
|
159
|
+
throw new TypeError("Browser speech graph providerId must match the provider id.");
|
|
160
|
+
}
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
protocol: AI_MODEL_AUTHORITY_PROTOCOL,
|
|
163
|
+
admitted: true,
|
|
164
|
+
graph,
|
|
165
|
+
artifactGraphProtocol: graph.protocol,
|
|
166
|
+
providerId,
|
|
167
|
+
role,
|
|
168
|
+
modelId: graph.model.id,
|
|
169
|
+
repository: graph.model.repository,
|
|
170
|
+
revision: graph.model.revision,
|
|
171
|
+
dtype: graph.model.dtype,
|
|
172
|
+
defaultVoice: graph.model.defaultVoice,
|
|
173
|
+
voices: graph.model.voices ?? Object.freeze([]),
|
|
174
|
+
inputSampleRate: graph.model.inputSampleRate,
|
|
175
|
+
outputSampleRate: graph.model.outputSampleRate,
|
|
176
|
+
runtime: graph.runtime,
|
|
177
|
+
files: graph.files,
|
|
178
|
+
security,
|
|
179
|
+
artifactGraphId: graph.identitySha256,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const authority = createBrowserSpeechAuthority({
|
|
183
|
+
providerId,
|
|
184
|
+
role,
|
|
185
|
+
model,
|
|
186
|
+
runtime,
|
|
187
|
+
security,
|
|
188
|
+
});
|
|
189
|
+
if (!isBrowserSpeechAuthority(authority)) {
|
|
190
|
+
throw new TypeError("Browser speech authority construction did not return an SDK authority.");
|
|
191
|
+
}
|
|
192
|
+
return authority;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inputSampleRate(authority) {
|
|
196
|
+
return authority.graph ? authority.inputSampleRate : STT_SAMPLE_RATE;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function outputSampleRate(authority) {
|
|
200
|
+
return authority.graph ? authority.outputSampleRate : TTS_SAMPLE_RATE;
|
|
201
|
+
}
|
|
202
|
+
|
|
63
203
|
function linkSignal(signal) {
|
|
64
204
|
const controller = new AbortController();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
205
|
+
let internalReason = null;
|
|
206
|
+
let internalCode = null;
|
|
207
|
+
const forwardExternalAbort = () => controller.abort(signal?.reason);
|
|
208
|
+
if (signal?.aborted) forwardExternalAbort();
|
|
209
|
+
else signal?.addEventListener?.("abort", forwardExternalAbort, { once: true });
|
|
68
210
|
return Object.freeze({
|
|
69
211
|
controller,
|
|
70
|
-
|
|
212
|
+
abort(reason, code = "ARCANE_AI_REQUEST_ABORTED") {
|
|
213
|
+
if (controller.signal.aborted) return;
|
|
214
|
+
internalReason = resolveReason(reason);
|
|
215
|
+
internalCode = code;
|
|
216
|
+
controller.abort(internalReason);
|
|
217
|
+
},
|
|
218
|
+
code() {
|
|
219
|
+
return internalCode;
|
|
220
|
+
},
|
|
221
|
+
reason(fallback) {
|
|
222
|
+
return internalReason ?? resolveReason(fallback);
|
|
223
|
+
},
|
|
224
|
+
release: () => signal?.removeEventListener?.("abort", forwardExternalAbort),
|
|
71
225
|
});
|
|
72
226
|
}
|
|
73
227
|
|
|
74
|
-
function
|
|
228
|
+
function linkedAbortFailure(linked, fallbackReason) {
|
|
229
|
+
const reason = linked.reason(fallbackReason);
|
|
230
|
+
const code = linked.code();
|
|
231
|
+
if (code === "ARCANE_AI_OPERATION_SUPERSEDED") {
|
|
232
|
+
return providerError(
|
|
233
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
234
|
+
"The browser speech operation was superseded.",
|
|
235
|
+
linked.controller.signal.reason,
|
|
236
|
+
reason,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (code === "ARCANE_AI_LOAD_PROGRESS_CALLBACK_THREW") {
|
|
240
|
+
return providerError(
|
|
241
|
+
code,
|
|
242
|
+
"The browser speech load progress callback was rejected.",
|
|
243
|
+
linked.controller.signal.reason,
|
|
244
|
+
reason,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (code === "ARCANE_AI_PROVIDER_LOAD_FAILED") {
|
|
248
|
+
return providerError(
|
|
249
|
+
code,
|
|
250
|
+
"The browser speech provider load was rejected.",
|
|
251
|
+
linked.controller.signal.reason,
|
|
252
|
+
reason,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return abortError(linked.controller.signal, reason);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isAbortSignal(value) {
|
|
259
|
+
return value === null
|
|
260
|
+
|| value === undefined
|
|
261
|
+
|| (typeof value === "object"
|
|
262
|
+
&& typeof value.aborted === "boolean"
|
|
263
|
+
&& typeof value.addEventListener === "function"
|
|
264
|
+
&& typeof value.removeEventListener === "function");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function providerContext(context, role, operation) {
|
|
268
|
+
if (context === undefined) return Object.freeze({ signal: null });
|
|
269
|
+
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
270
|
+
throw providerError(
|
|
271
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
272
|
+
`Browser ${role} ${operation} context must be an object.`,
|
|
273
|
+
undefined,
|
|
274
|
+
`${role}-provider-${operation}-context-not-object`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (Object.hasOwn(context, "role") && context.role !== role) {
|
|
278
|
+
throw providerError(
|
|
279
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
280
|
+
`Browser ${role} ${operation} context role does not match the provider.`,
|
|
281
|
+
undefined,
|
|
282
|
+
`${role}-provider-${operation}-role-mismatch`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const signal = context.signal ?? null;
|
|
286
|
+
if (!isAbortSignal(signal)) {
|
|
287
|
+
throw providerError(
|
|
288
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
289
|
+
`Browser ${role} ${operation} signal must be an AbortSignal.`,
|
|
290
|
+
undefined,
|
|
291
|
+
`${role}-provider-${operation}-signal-not-abort-signal`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return Object.freeze({ context, signal });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function graphSecurity(scope, label) {
|
|
298
|
+
return normalizeModelSecurity(scope, label);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function publicStatus({
|
|
302
|
+
role,
|
|
303
|
+
id,
|
|
304
|
+
authority,
|
|
305
|
+
state,
|
|
306
|
+
busy,
|
|
307
|
+
generation,
|
|
308
|
+
errorCode,
|
|
309
|
+
cache,
|
|
310
|
+
lifecycleReason,
|
|
311
|
+
activeOperation,
|
|
312
|
+
artifactGraphAdmission,
|
|
313
|
+
security,
|
|
314
|
+
}) {
|
|
75
315
|
return Object.freeze({
|
|
76
316
|
role,
|
|
77
317
|
providerId: id,
|
|
78
318
|
modelId: authority.modelId,
|
|
79
319
|
state,
|
|
320
|
+
lifecycleStatus: `${role}-provider-${state}`,
|
|
321
|
+
lifecycleReason,
|
|
322
|
+
activeOperation,
|
|
80
323
|
loaded: state === "ready",
|
|
81
324
|
busy,
|
|
82
325
|
generation,
|
|
83
326
|
errorCode,
|
|
84
327
|
cache,
|
|
328
|
+
security,
|
|
329
|
+
artifactGraphId: authority.artifactGraphId ?? null,
|
|
330
|
+
artifactGraphAdmission,
|
|
85
331
|
});
|
|
86
332
|
}
|
|
87
333
|
|
|
88
|
-
function assertRequestAuthority(
|
|
334
|
+
function assertRequestAuthority(
|
|
335
|
+
context,
|
|
336
|
+
authority,
|
|
337
|
+
providerId,
|
|
338
|
+
reason = `${authority.role}-provider-request-selection-authority-mismatch`,
|
|
339
|
+
) {
|
|
89
340
|
if (!Object.hasOwn(context, "selection")) return;
|
|
90
341
|
const selection = context.selection;
|
|
91
342
|
if (selection?.providerId !== providerId
|
|
92
343
|
|| selection?.modelId !== authority.modelId
|
|
344
|
+
|| (Object.hasOwn(selection ?? {}, "role") && selection.role !== authority.role)
|
|
93
345
|
|| selection?.localOnly === false) {
|
|
94
346
|
throw providerError(
|
|
95
347
|
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
96
348
|
"The browser speech request selection changed from its immutable provider authority.",
|
|
349
|
+
undefined,
|
|
350
|
+
reason,
|
|
97
351
|
);
|
|
98
352
|
}
|
|
99
353
|
}
|
|
100
354
|
|
|
101
|
-
function assertPayloadModel(payload, authority, {
|
|
355
|
+
function assertPayloadModel(payload, authority, {
|
|
356
|
+
required = false,
|
|
357
|
+
operationSubject = authority.role === "stt" ? "stt-transcription" : "tts-synthesis",
|
|
358
|
+
} = {}) {
|
|
102
359
|
if (!Object.hasOwn(payload, "model")) {
|
|
103
360
|
if (!required) return;
|
|
104
361
|
throw providerError(
|
|
105
362
|
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
106
363
|
"The shared speech request must identify the provider's immutable model authority.",
|
|
364
|
+
undefined,
|
|
365
|
+
`${operationSubject}-model-authority-missing`,
|
|
107
366
|
);
|
|
108
367
|
}
|
|
109
368
|
const descriptor = Object.getOwnPropertyDescriptor(payload, "model");
|
|
@@ -111,51 +370,89 @@ function assertPayloadModel(payload, authority, { required = false } = {}) {
|
|
|
111
370
|
throw providerError(
|
|
112
371
|
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
113
372
|
"The shared speech request model does not match this provider authority.",
|
|
373
|
+
undefined,
|
|
374
|
+
`${operationSubject}-model-authority-mismatch`,
|
|
114
375
|
);
|
|
115
376
|
}
|
|
116
377
|
}
|
|
117
378
|
|
|
118
|
-
function genericPayloadDescriptors(
|
|
379
|
+
function genericPayloadDescriptors(
|
|
380
|
+
payload,
|
|
381
|
+
allowedKeys,
|
|
382
|
+
requiredKeys,
|
|
383
|
+
label,
|
|
384
|
+
operationSubject,
|
|
385
|
+
) {
|
|
119
386
|
const prototype = Object.getPrototypeOf(payload);
|
|
120
387
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
121
|
-
throw providerError(
|
|
388
|
+
throw providerError(
|
|
389
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
390
|
+
`${label} must be a plain object.`,
|
|
391
|
+
undefined,
|
|
392
|
+
`${operationSubject}-payload-not-plain-object`,
|
|
393
|
+
);
|
|
122
394
|
}
|
|
123
395
|
const descriptors = Object.getOwnPropertyDescriptors(payload);
|
|
124
396
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
125
397
|
if (typeof key === "symbol" || !allowedKeys.includes(key)) {
|
|
126
|
-
throw providerError(
|
|
398
|
+
throw providerError(
|
|
399
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
400
|
+
`${label} contains an unknown field.`,
|
|
401
|
+
undefined,
|
|
402
|
+
`${operationSubject}-payload-field-unknown`,
|
|
403
|
+
);
|
|
127
404
|
}
|
|
128
405
|
if (!Object.hasOwn(descriptors[key], "value")) {
|
|
129
|
-
throw providerError(
|
|
406
|
+
throw providerError(
|
|
407
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
408
|
+
`${label}.${key} must be a data property.`,
|
|
409
|
+
undefined,
|
|
410
|
+
`${operationSubject}-payload-accessor-rejected`,
|
|
411
|
+
);
|
|
130
412
|
}
|
|
131
413
|
}
|
|
132
414
|
for (const key of requiredKeys) {
|
|
133
415
|
if (!Object.hasOwn(descriptors, key)) {
|
|
134
|
-
throw providerError(
|
|
416
|
+
throw providerError(
|
|
417
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
418
|
+
`${label}.${key} is required.`,
|
|
419
|
+
undefined,
|
|
420
|
+
`${operationSubject}-payload-required-field-missing`,
|
|
421
|
+
);
|
|
135
422
|
}
|
|
136
423
|
}
|
|
137
424
|
return descriptors;
|
|
138
425
|
}
|
|
139
426
|
|
|
140
|
-
function audioMimeEssence(value, label) {
|
|
427
|
+
function audioMimeEssence(value, label, reasonPrefix) {
|
|
141
428
|
if (typeof value !== "string") {
|
|
142
|
-
throw providerError(
|
|
429
|
+
throw providerError(
|
|
430
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
431
|
+
`${label} must be an audio MIME type.`,
|
|
432
|
+
undefined,
|
|
433
|
+
`${reasonPrefix}-not-string`,
|
|
434
|
+
);
|
|
143
435
|
}
|
|
144
436
|
const essence = value.split(";", 1)[0].trim().toLowerCase();
|
|
145
437
|
if (!/^audio\/[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(essence)) {
|
|
146
|
-
throw providerError(
|
|
438
|
+
throw providerError(
|
|
439
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
440
|
+
`${label} must be an audio MIME type.`,
|
|
441
|
+
undefined,
|
|
442
|
+
`${reasonPrefix}-malformed`,
|
|
443
|
+
);
|
|
147
444
|
}
|
|
148
445
|
return essence;
|
|
149
446
|
}
|
|
150
447
|
|
|
151
|
-
function awaitAbortableSpeechOperation(operation, signal) {
|
|
448
|
+
function awaitAbortableSpeechOperation(operation, signal, reason) {
|
|
152
449
|
const observed = Promise.resolve(operation);
|
|
153
450
|
if (!signal) return observed;
|
|
154
451
|
if (signal.aborted) {
|
|
155
452
|
// Blob and Web Audio operations cannot be preempted; retain their eventual
|
|
156
453
|
// rejection after cancellation instead of leaving an unobserved promise.
|
|
157
454
|
observed.catch(function retainCancelledSpeechOperation() {});
|
|
158
|
-
return Promise.reject(abortError(signal));
|
|
455
|
+
return Promise.reject(abortError(signal, reason));
|
|
159
456
|
}
|
|
160
457
|
return new Promise((resolve, reject) => {
|
|
161
458
|
let settled = false;
|
|
@@ -166,7 +463,7 @@ function awaitAbortableSpeechOperation(operation, signal) {
|
|
|
166
463
|
release();
|
|
167
464
|
callback(value);
|
|
168
465
|
};
|
|
169
|
-
const cancel = () => settle(reject, abortError(signal));
|
|
466
|
+
const cancel = () => settle(reject, abortError(signal, reason));
|
|
170
467
|
signal.addEventListener("abort", cancel, { once: true });
|
|
171
468
|
observed.then(
|
|
172
469
|
(value) => settle(resolve, value),
|
|
@@ -175,26 +472,139 @@ function awaitAbortableSpeechOperation(operation, signal) {
|
|
|
175
472
|
});
|
|
176
473
|
}
|
|
177
474
|
|
|
178
|
-
|
|
475
|
+
function observeLoadOperation(record, { signal, progress }, role) {
|
|
476
|
+
if (signal?.aborted) {
|
|
477
|
+
return Promise.reject(abortError(signal, `${role}-load-cancelled`));
|
|
478
|
+
}
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
let settled = false;
|
|
481
|
+
const release = () => {
|
|
482
|
+
try {
|
|
483
|
+
signal?.removeEventListener?.("abort", cancel);
|
|
484
|
+
return null;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return error;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const settle = (callback, value) => {
|
|
490
|
+
if (settled) return;
|
|
491
|
+
settled = true;
|
|
492
|
+
record.observers.delete(observer);
|
|
493
|
+
const releaseError = release();
|
|
494
|
+
if (releaseError) {
|
|
495
|
+
reject(trustedLoadFailure(releaseError, role));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
callback(value);
|
|
499
|
+
};
|
|
500
|
+
const abandonIfUnobserved = (
|
|
501
|
+
reason,
|
|
502
|
+
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
503
|
+
) => {
|
|
504
|
+
if (!record.settled && record.observers.size === 0) record.abort(reason, code);
|
|
505
|
+
};
|
|
506
|
+
const cancel = () => {
|
|
507
|
+
settle(reject, abortError(signal, `${role}-load-cancelled`));
|
|
508
|
+
abandonIfUnobserved(`${role}-load-cancelled`);
|
|
509
|
+
};
|
|
510
|
+
const observer = Object.freeze({
|
|
511
|
+
progress(value) {
|
|
512
|
+
if (settled) return;
|
|
513
|
+
try {
|
|
514
|
+
progress(value);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
settle(reject, providerError(
|
|
517
|
+
"ARCANE_AI_LOAD_PROGRESS_CALLBACK_THREW",
|
|
518
|
+
`Browser ${role} load progress callback threw.`,
|
|
519
|
+
error,
|
|
520
|
+
`${role}-load-progress-callback-threw`,
|
|
521
|
+
));
|
|
522
|
+
abandonIfUnobserved(
|
|
523
|
+
`${role}-load-progress-callback-threw`,
|
|
524
|
+
"ARCANE_AI_LOAD_PROGRESS_CALLBACK_THREW",
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
record.observers.add(observer);
|
|
530
|
+
record.promise.then(
|
|
531
|
+
(value) => settle(resolve, value),
|
|
532
|
+
(error) => settle(reject, error),
|
|
533
|
+
);
|
|
534
|
+
try {
|
|
535
|
+
signal?.addEventListener?.("abort", cancel, { once: true });
|
|
536
|
+
} catch (error) {
|
|
537
|
+
settled = true;
|
|
538
|
+
record.observers.delete(observer);
|
|
539
|
+
reject(trustedLoadFailure(error, role));
|
|
540
|
+
abandonIfUnobserved(
|
|
541
|
+
`${role}-provider-load-rejected`,
|
|
542
|
+
"ARCANE_AI_PROVIDER_LOAD_FAILED",
|
|
543
|
+
);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (record.hasProgress) observer.progress(record.latestProgress);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function decodeSharedTranscriptionPayload(
|
|
551
|
+
payload,
|
|
552
|
+
authority,
|
|
553
|
+
signal,
|
|
554
|
+
cancellationReason = "stt-transcription-cancelled",
|
|
555
|
+
) {
|
|
556
|
+
const sampleRate = inputSampleRate(authority);
|
|
179
557
|
const descriptors = genericPayloadDescriptors(
|
|
180
558
|
payload,
|
|
181
559
|
["audio", "mimeType", "model"],
|
|
182
560
|
["audio", "mimeType", "model"],
|
|
183
561
|
"Shared speech transcription payload",
|
|
562
|
+
"stt-transcription",
|
|
184
563
|
);
|
|
185
|
-
assertPayloadModel(payload, authority, {
|
|
564
|
+
assertPayloadModel(payload, authority, {
|
|
565
|
+
required: true,
|
|
566
|
+
operationSubject: "stt-transcription",
|
|
567
|
+
});
|
|
186
568
|
const audio = descriptors.audio.value;
|
|
187
|
-
if (typeof Blob !== "function"
|
|
569
|
+
if (typeof Blob !== "function") {
|
|
188
570
|
throw providerError(
|
|
189
571
|
"ARCANE_AI_INVALID_REQUEST",
|
|
190
|
-
"Shared speech transcription requires
|
|
572
|
+
"Shared speech transcription requires the browser Blob constructor.",
|
|
573
|
+
undefined,
|
|
574
|
+
"stt-transcription-blob-constructor-unavailable",
|
|
191
575
|
);
|
|
192
576
|
}
|
|
193
|
-
|
|
194
|
-
|
|
577
|
+
if (!(audio instanceof Blob)) {
|
|
578
|
+
throw providerError(
|
|
579
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
580
|
+
"Shared speech transcription audio must be a Blob or File.",
|
|
581
|
+
undefined,
|
|
582
|
+
"stt-transcription-audio-not-blob-or-file",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (audio.size < 1) {
|
|
586
|
+
throw providerError(
|
|
587
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
588
|
+
"Shared speech transcription audio Blob or File must be nonempty.",
|
|
589
|
+
undefined,
|
|
590
|
+
"stt-transcription-audio-blob-empty",
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const mimeType = audioMimeEssence(
|
|
594
|
+
descriptors.mimeType.value,
|
|
595
|
+
"Speech transcription mimeType",
|
|
596
|
+
"stt-transcription-mime-type",
|
|
597
|
+
);
|
|
598
|
+
if (audio.type && audioMimeEssence(
|
|
599
|
+
audio.type,
|
|
600
|
+
"Speech transcription Blob.type",
|
|
601
|
+
"stt-transcription-audio-blob-mime-type",
|
|
602
|
+
) !== mimeType) {
|
|
195
603
|
throw providerError(
|
|
196
604
|
"ARCANE_AI_INVALID_REQUEST",
|
|
197
605
|
"Speech transcription mimeType does not match the audio Blob type.",
|
|
606
|
+
undefined,
|
|
607
|
+
"stt-transcription-audio-blob-mime-type-mismatch",
|
|
198
608
|
);
|
|
199
609
|
}
|
|
200
610
|
const OfflineAudioContext = globalThis.OfflineAudioContext
|
|
@@ -202,57 +612,136 @@ async function decodeSharedTranscriptionPayload(payload, authority, signal) {
|
|
|
202
612
|
if (typeof OfflineAudioContext !== "function") {
|
|
203
613
|
throw providerError(
|
|
204
614
|
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
205
|
-
|
|
615
|
+
`Shared Blob transcription requires OfflineAudioContext decoding at ${sampleRate} Hz.`,
|
|
616
|
+
undefined,
|
|
617
|
+
"stt-browser-offline-audio-context-unavailable",
|
|
206
618
|
);
|
|
207
619
|
}
|
|
208
620
|
let decoder;
|
|
209
621
|
try {
|
|
210
|
-
decoder = new OfflineAudioContext(1, 1,
|
|
622
|
+
decoder = new OfflineAudioContext(1, 1, sampleRate);
|
|
211
623
|
} catch (error) {
|
|
212
624
|
throw providerError(
|
|
213
625
|
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
214
|
-
|
|
626
|
+
`Unable to create the required ${sampleRate} Hz speech decoder.`,
|
|
215
627
|
error,
|
|
628
|
+
"stt-browser-offline-audio-context-construction-rejected",
|
|
216
629
|
);
|
|
217
630
|
}
|
|
218
631
|
if (typeof decoder.decodeAudioData !== "function") {
|
|
219
632
|
throw providerError(
|
|
220
633
|
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
221
634
|
"Shared Blob transcription requires browser audio decoding.",
|
|
635
|
+
undefined,
|
|
636
|
+
"stt-browser-audio-decode-method-unavailable",
|
|
222
637
|
);
|
|
223
638
|
}
|
|
224
639
|
let decoded;
|
|
225
640
|
try {
|
|
226
|
-
const encoded = await awaitAbortableSpeechOperation(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
641
|
+
const encoded = await awaitAbortableSpeechOperation(
|
|
642
|
+
audio.arrayBuffer(),
|
|
643
|
+
signal,
|
|
644
|
+
cancellationReason,
|
|
645
|
+
);
|
|
646
|
+
throwIfAborted(signal, cancellationReason);
|
|
647
|
+
decoded = await awaitAbortableSpeechOperation(
|
|
648
|
+
decoder.decodeAudioData(encoded),
|
|
649
|
+
signal,
|
|
650
|
+
cancellationReason,
|
|
651
|
+
);
|
|
652
|
+
throwIfAborted(signal, cancellationReason);
|
|
230
653
|
} catch (error) {
|
|
231
654
|
if (signal?.aborted && error?.code === "ARCANE_AI_REQUEST_ABORTED") throw error;
|
|
232
655
|
throw providerError(
|
|
233
656
|
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
234
657
|
"The browser could not decode the supplied speech audio.",
|
|
235
658
|
error,
|
|
659
|
+
"stt-browser-audio-decode-operation-rejected",
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (!decoded || typeof decoded !== "object") {
|
|
663
|
+
throw providerError(
|
|
664
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
665
|
+
"Decoded speech audio must be an AudioBuffer-like object.",
|
|
666
|
+
undefined,
|
|
667
|
+
"stt-browser-decoded-audio-not-object",
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
if (decoded.sampleRate !== sampleRate) {
|
|
671
|
+
throw providerError(
|
|
672
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
673
|
+
`Decoded speech audio must be sampled at ${sampleRate} Hz.`,
|
|
674
|
+
undefined,
|
|
675
|
+
"stt-browser-decoded-audio-sample-rate-mismatch",
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (!Number.isSafeInteger(decoded.length)) {
|
|
679
|
+
throw providerError(
|
|
680
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
681
|
+
"Decoded speech audio frame length must be a safe integer.",
|
|
682
|
+
undefined,
|
|
683
|
+
"stt-browser-decoded-audio-frame-length-not-safe-integer",
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (decoded.length < 1) {
|
|
687
|
+
throw providerError(
|
|
688
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
689
|
+
"Decoded speech audio must contain at least one frame.",
|
|
690
|
+
undefined,
|
|
691
|
+
"stt-browser-decoded-audio-empty",
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (!Number.isSafeInteger(decoded.numberOfChannels)) {
|
|
695
|
+
throw providerError(
|
|
696
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
697
|
+
"Decoded speech audio channel count must be a safe integer.",
|
|
698
|
+
undefined,
|
|
699
|
+
"stt-browser-decoded-audio-channel-count-not-safe-integer",
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (decoded.numberOfChannels < 1) {
|
|
703
|
+
throw providerError(
|
|
704
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
705
|
+
"Decoded speech audio must contain at least one channel.",
|
|
706
|
+
undefined,
|
|
707
|
+
"stt-browser-decoded-audio-channel-count-zero",
|
|
236
708
|
);
|
|
237
709
|
}
|
|
238
|
-
if (decoded
|
|
239
|
-
|| !Number.isSafeInteger(decoded.length)
|
|
240
|
-
|| decoded.length < 1
|
|
241
|
-
|| !Number.isSafeInteger(decoded.numberOfChannels)
|
|
242
|
-
|| decoded.numberOfChannels < 1
|
|
243
|
-
|| typeof decoded.getChannelData !== "function") {
|
|
710
|
+
if (typeof decoded.getChannelData !== "function") {
|
|
244
711
|
throw providerError(
|
|
245
712
|
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
246
|
-
"Decoded speech audio must
|
|
713
|
+
"Decoded speech audio must expose getChannelData().",
|
|
714
|
+
undefined,
|
|
715
|
+
"stt-browser-decoded-audio-get-channel-data-not-function",
|
|
247
716
|
);
|
|
248
717
|
}
|
|
249
718
|
const channels = [];
|
|
250
719
|
for (let index = 0; index < decoded.numberOfChannels; index += 1) {
|
|
251
|
-
|
|
252
|
-
|
|
720
|
+
let channel;
|
|
721
|
+
try {
|
|
722
|
+
channel = decoded.getChannelData(index);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
throw providerError(
|
|
725
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
726
|
+
"Decoded speech audio channel access was rejected.",
|
|
727
|
+
error,
|
|
728
|
+
"stt-browser-decoded-audio-channel-read-rejected",
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (!(channel instanceof Float32Array)) {
|
|
732
|
+
throw providerError(
|
|
733
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
734
|
+
"Decoded speech audio channels must be Float32Array values.",
|
|
735
|
+
undefined,
|
|
736
|
+
"stt-browser-decoded-audio-channel-not-float32-array",
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (channel.length !== decoded.length) {
|
|
253
740
|
throw providerError(
|
|
254
741
|
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
255
|
-
"Decoded speech audio
|
|
742
|
+
"Decoded speech audio channel length must match the decoded frame length.",
|
|
743
|
+
undefined,
|
|
744
|
+
"stt-browser-decoded-audio-channel-length-mismatch",
|
|
256
745
|
);
|
|
257
746
|
}
|
|
258
747
|
channels.push(channel);
|
|
@@ -266,29 +755,67 @@ async function decodeSharedTranscriptionPayload(payload, authority, signal) {
|
|
|
266
755
|
throw providerError(
|
|
267
756
|
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
268
757
|
"Decoded speech audio contains a non-finite sample.",
|
|
758
|
+
undefined,
|
|
759
|
+
"stt-browser-decoded-audio-sample-non-finite",
|
|
269
760
|
);
|
|
270
761
|
}
|
|
271
762
|
mono[index] = sample;
|
|
272
763
|
}
|
|
273
|
-
throwIfAborted(signal);
|
|
764
|
+
throwIfAborted(signal, cancellationReason);
|
|
274
765
|
return Object.freeze({
|
|
275
|
-
payload: Object.freeze({ audio: mono, sampleRate
|
|
766
|
+
payload: Object.freeze({ audio: mono, sampleRate }),
|
|
276
767
|
shared: true,
|
|
277
768
|
});
|
|
278
769
|
}
|
|
279
770
|
|
|
280
771
|
function cloneNativeTranscriptionPayload(payload, authority) {
|
|
281
|
-
|
|
282
|
-
|
|
772
|
+
const descriptors = genericPayloadDescriptors(
|
|
773
|
+
payload,
|
|
774
|
+
["audio", "sampleRate", "model"],
|
|
775
|
+
["audio", "sampleRate"],
|
|
776
|
+
"Browser Whisper transcription payload",
|
|
777
|
+
"stt-transcription",
|
|
778
|
+
);
|
|
779
|
+
assertPayloadModel(payload, authority, { operationSubject: "stt-transcription" });
|
|
780
|
+
const sampleRate = inputSampleRate(authority);
|
|
781
|
+
if (!(descriptors.audio.value instanceof Float32Array)) {
|
|
283
782
|
throw providerError(
|
|
284
783
|
"ARCANE_AI_INVALID_REQUEST",
|
|
285
|
-
"Whisper requires Float32Array audio
|
|
784
|
+
"Whisper requires Float32Array audio.",
|
|
785
|
+
undefined,
|
|
786
|
+
"stt-transcription-audio-not-float32-array",
|
|
286
787
|
);
|
|
287
788
|
}
|
|
789
|
+
if (descriptors.audio.value.length < 1) {
|
|
790
|
+
throw providerError(
|
|
791
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
792
|
+
"Whisper requires nonempty Float32 PCM audio.",
|
|
793
|
+
undefined,
|
|
794
|
+
"stt-transcription-pcm-input-empty",
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (descriptors.sampleRate.value !== sampleRate) {
|
|
798
|
+
throw providerError(
|
|
799
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
800
|
+
`Whisper requires audio sampled at exactly ${sampleRate} Hz.`,
|
|
801
|
+
undefined,
|
|
802
|
+
"stt-transcription-sample-rate-mismatch",
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
for (const sample of descriptors.audio.value) {
|
|
806
|
+
if (!Number.isFinite(sample)) {
|
|
807
|
+
throw providerError(
|
|
808
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
809
|
+
"Whisper audio must contain only finite Float32 PCM samples.",
|
|
810
|
+
undefined,
|
|
811
|
+
"stt-transcription-pcm-sample-non-finite",
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
288
815
|
return Object.freeze({
|
|
289
816
|
payload: Object.freeze({
|
|
290
|
-
audio: new Float32Array(
|
|
291
|
-
sampleRate
|
|
817
|
+
audio: new Float32Array(descriptors.audio.value),
|
|
818
|
+
sampleRate,
|
|
292
819
|
}),
|
|
293
820
|
shared: false,
|
|
294
821
|
});
|
|
@@ -296,34 +823,79 @@ function cloneNativeTranscriptionPayload(payload, authority) {
|
|
|
296
823
|
|
|
297
824
|
function normalizeSynthesisPayload(payload, authority) {
|
|
298
825
|
const shared = Object.hasOwn(payload, "input");
|
|
299
|
-
let
|
|
826
|
+
let descriptors;
|
|
827
|
+
let textValue;
|
|
300
828
|
if (shared) {
|
|
301
|
-
|
|
829
|
+
descriptors = genericPayloadDescriptors(
|
|
302
830
|
payload,
|
|
303
831
|
["model", "voice", "input", "responseFormat", "speed"],
|
|
304
832
|
["model", "input", "responseFormat"],
|
|
305
833
|
"Shared speech synthesis payload",
|
|
834
|
+
"tts-synthesis",
|
|
306
835
|
);
|
|
307
|
-
assertPayloadModel(payload, authority, {
|
|
836
|
+
assertPayloadModel(payload, authority, {
|
|
837
|
+
required: true,
|
|
838
|
+
operationSubject: "tts-synthesis",
|
|
839
|
+
});
|
|
308
840
|
if (descriptors.responseFormat.value !== TTS_RESPONSE_FORMAT) {
|
|
309
841
|
throw providerError(
|
|
310
842
|
"ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT",
|
|
311
843
|
"Browser Kokoro supports only wav responses for shared speech requests.",
|
|
844
|
+
undefined,
|
|
845
|
+
"tts-synthesis-response-format-not-wav",
|
|
312
846
|
);
|
|
313
847
|
}
|
|
314
848
|
textValue = descriptors.input.value;
|
|
315
849
|
} else {
|
|
316
|
-
|
|
850
|
+
descriptors = genericPayloadDescriptors(
|
|
851
|
+
payload,
|
|
852
|
+
["model", "voice", "text", "speed"],
|
|
853
|
+
["text"],
|
|
854
|
+
"Browser Kokoro synthesis payload",
|
|
855
|
+
"tts-synthesis",
|
|
856
|
+
);
|
|
857
|
+
assertPayloadModel(payload, authority, { operationSubject: "tts-synthesis" });
|
|
858
|
+
textValue = descriptors.text.value;
|
|
317
859
|
}
|
|
318
860
|
const text = typeof textValue === "string" ? textValue.trim() : "";
|
|
319
|
-
const
|
|
320
|
-
?
|
|
861
|
+
const voiceValue = Object.hasOwn(descriptors, "voice")
|
|
862
|
+
? descriptors.voice.value
|
|
863
|
+
: authority.defaultVoice;
|
|
864
|
+
const voice = typeof voiceValue === "string"
|
|
865
|
+
? voiceValue.trim()
|
|
321
866
|
: "";
|
|
322
|
-
const speed =
|
|
323
|
-
if (!text
|
|
867
|
+
const speed = Object.hasOwn(descriptors, "speed") ? descriptors.speed.value : 1;
|
|
868
|
+
if (!text) {
|
|
324
869
|
throw providerError(
|
|
325
870
|
"ARCANE_AI_INVALID_REQUEST",
|
|
326
|
-
"Kokoro requires text
|
|
871
|
+
"Kokoro requires nonempty text.",
|
|
872
|
+
undefined,
|
|
873
|
+
"tts-synthesis-text-empty",
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (!voice) {
|
|
877
|
+
throw providerError(
|
|
878
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
879
|
+
"Kokoro requires a nonempty voice id.",
|
|
880
|
+
undefined,
|
|
881
|
+
"tts-synthesis-voice-empty",
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (!Number.isFinite(speed) || speed <= 0 || speed > 4) {
|
|
885
|
+
throw providerError(
|
|
886
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
887
|
+
"Kokoro speed must be greater than 0 and at most 4.",
|
|
888
|
+
undefined,
|
|
889
|
+
"tts-synthesis-speed-out-of-range",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (authority.graph
|
|
893
|
+
&& !authority.voices.some((candidate) => candidate.id === voice)) {
|
|
894
|
+
throw providerError(
|
|
895
|
+
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
896
|
+
"Kokoro voice must match a caller-declared authenticated voice artifact.",
|
|
897
|
+
undefined,
|
|
898
|
+
"tts-synthesis-voice-not-declared",
|
|
327
899
|
);
|
|
328
900
|
}
|
|
329
901
|
return Object.freeze({
|
|
@@ -332,30 +904,81 @@ function normalizeSynthesisPayload(payload, authority) {
|
|
|
332
904
|
});
|
|
333
905
|
}
|
|
334
906
|
|
|
335
|
-
async function normalizeRequestPayload(
|
|
907
|
+
async function normalizeRequestPayload(
|
|
908
|
+
role,
|
|
909
|
+
payload,
|
|
910
|
+
authority,
|
|
911
|
+
signal,
|
|
912
|
+
cancellationReason,
|
|
913
|
+
) {
|
|
336
914
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
337
|
-
throw providerError(
|
|
915
|
+
throw providerError(
|
|
916
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
917
|
+
"Speech request payload must be an object.",
|
|
918
|
+
undefined,
|
|
919
|
+
role === "stt"
|
|
920
|
+
? "stt-transcription-payload-not-object"
|
|
921
|
+
: "tts-synthesis-payload-not-object",
|
|
922
|
+
);
|
|
338
923
|
}
|
|
339
924
|
if (role === "stt") {
|
|
340
925
|
const audio = Object.getOwnPropertyDescriptor(payload, "audio");
|
|
341
|
-
if (Object.hasOwn(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
926
|
+
if (Object.hasOwn(payload, "mimeType")
|
|
927
|
+
|| (Object.hasOwn(audio ?? {}, "value")
|
|
928
|
+
&& typeof Blob === "function"
|
|
929
|
+
&& audio.value instanceof Blob)) {
|
|
930
|
+
return decodeSharedTranscriptionPayload(
|
|
931
|
+
payload,
|
|
932
|
+
authority,
|
|
933
|
+
signal,
|
|
934
|
+
cancellationReason,
|
|
935
|
+
);
|
|
345
936
|
}
|
|
346
937
|
return cloneNativeTranscriptionPayload(payload, authority);
|
|
347
938
|
}
|
|
348
939
|
return normalizeSynthesisPayload(payload, authority);
|
|
349
940
|
}
|
|
350
941
|
|
|
351
|
-
function encodeSharedSynthesisResult(result) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
942
|
+
function encodeSharedSynthesisResult(result, authority) {
|
|
943
|
+
const sampleRate = outputSampleRate(authority);
|
|
944
|
+
if (!result || typeof result !== "object") {
|
|
945
|
+
throw providerError(
|
|
946
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
947
|
+
"Browser Kokoro must return a synthesis result object.",
|
|
948
|
+
undefined,
|
|
949
|
+
"tts-synthesis-result-not-object",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (!(result.audio instanceof Float32Array)) {
|
|
953
|
+
throw providerError(
|
|
954
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
955
|
+
"Browser Kokoro must return Float32 PCM audio.",
|
|
956
|
+
undefined,
|
|
957
|
+
"tts-synthesis-result-audio-not-float32-array",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (result.sampleRate !== sampleRate) {
|
|
961
|
+
throw providerError(
|
|
962
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
963
|
+
`Browser Kokoro must return audio sampled at ${sampleRate} Hz.`,
|
|
964
|
+
undefined,
|
|
965
|
+
"tts-synthesis-result-sample-rate-mismatch",
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (result.audio.length < 1) {
|
|
356
969
|
throw providerError(
|
|
357
970
|
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
358
|
-
"Browser Kokoro must return nonempty
|
|
971
|
+
"Browser Kokoro must return nonempty PCM audio.",
|
|
972
|
+
undefined,
|
|
973
|
+
"tts-synthesis-result-audio-empty",
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
if (result.audio.length > (0xffffffff - 44) / 2) {
|
|
977
|
+
throw providerError(
|
|
978
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
979
|
+
"Browser Kokoro PCM audio exceeds the WAV byte-length boundary.",
|
|
980
|
+
undefined,
|
|
981
|
+
"tts-synthesis-result-wav-byte-length-overflow",
|
|
359
982
|
);
|
|
360
983
|
}
|
|
361
984
|
const buffer = new ArrayBuffer(44 + result.audio.length * 2);
|
|
@@ -373,8 +996,8 @@ function encodeSharedSynthesisResult(result) {
|
|
|
373
996
|
view.setUint32(16, 16, true);
|
|
374
997
|
view.setUint16(20, 1, true);
|
|
375
998
|
view.setUint16(22, 1, true);
|
|
376
|
-
view.setUint32(24,
|
|
377
|
-
view.setUint32(28,
|
|
999
|
+
view.setUint32(24, sampleRate, true);
|
|
1000
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
378
1001
|
view.setUint16(32, 2, true);
|
|
379
1002
|
view.setUint16(34, 16, true);
|
|
380
1003
|
writeText(36, "data");
|
|
@@ -385,6 +1008,8 @@ function encodeSharedSynthesisResult(result) {
|
|
|
385
1008
|
throw providerError(
|
|
386
1009
|
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
387
1010
|
"Browser Kokoro returned a non-finite PCM sample.",
|
|
1011
|
+
undefined,
|
|
1012
|
+
"tts-synthesis-pcm-sample-non-finite",
|
|
388
1013
|
);
|
|
389
1014
|
}
|
|
390
1015
|
const clamped = Math.max(-1, Math.min(1, sample));
|
|
@@ -401,6 +1026,7 @@ function createBrowserSpeechProvider({
|
|
|
401
1026
|
role,
|
|
402
1027
|
id,
|
|
403
1028
|
localOnly = true,
|
|
1029
|
+
graph,
|
|
404
1030
|
model,
|
|
405
1031
|
runtime,
|
|
406
1032
|
appSecurity,
|
|
@@ -426,21 +1052,23 @@ function createBrowserSpeechProvider({
|
|
|
426
1052
|
security,
|
|
427
1053
|
"Browser speech provider security",
|
|
428
1054
|
);
|
|
429
|
-
const authority =
|
|
1055
|
+
const authority = createProviderAuthority({
|
|
430
1056
|
providerId,
|
|
431
1057
|
role,
|
|
1058
|
+
graph,
|
|
432
1059
|
model,
|
|
433
1060
|
runtime,
|
|
434
1061
|
security: configuredProviderSecurity,
|
|
435
1062
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1063
|
+
const defaultSecurity = resolveModelSecurity({
|
|
1064
|
+
app: configuredAppSecurity,
|
|
1065
|
+
binding: configuredProviderSecurity,
|
|
1066
|
+
});
|
|
439
1067
|
const operation = ROLE_OPERATION[role];
|
|
440
1068
|
const speech = role === "stt"
|
|
441
|
-
? Object.freeze({ inputSampleRate:
|
|
1069
|
+
? Object.freeze({ inputSampleRate: inputSampleRate(authority) })
|
|
442
1070
|
: Object.freeze({
|
|
443
|
-
outputSampleRate:
|
|
1071
|
+
outputSampleRate: outputSampleRate(authority),
|
|
444
1072
|
responseFormats: Object.freeze([TTS_RESPONSE_FORMAT]),
|
|
445
1073
|
defaultResponseFormat: TTS_RESPONSE_FORMAT,
|
|
446
1074
|
});
|
|
@@ -452,13 +1080,21 @@ function createBrowserSpeechProvider({
|
|
|
452
1080
|
...(role === "tts" ? { defaultVoice: authority.defaultVoice } : {}),
|
|
453
1081
|
repository: authority.repository,
|
|
454
1082
|
revision: authority.revision,
|
|
1083
|
+
...(authority.dtype ? { dtype: authority.dtype } : {}),
|
|
455
1084
|
runtime: authority.runtime,
|
|
456
1085
|
files: authority.files,
|
|
1086
|
+
...(authority.graph ? {
|
|
1087
|
+
artifactGraphId: authority.artifactGraphId,
|
|
1088
|
+
voices: authority.voices,
|
|
1089
|
+
} : {}),
|
|
457
1090
|
speech,
|
|
458
1091
|
});
|
|
459
1092
|
let state = "unloaded";
|
|
460
1093
|
let errorCode = null;
|
|
461
1094
|
let cache = null;
|
|
1095
|
+
let artifactGraphAdmission = null;
|
|
1096
|
+
let lifecycleReason = `${role}-provider-created`;
|
|
1097
|
+
let activeOperation = null;
|
|
462
1098
|
let generation = 0;
|
|
463
1099
|
let active = null;
|
|
464
1100
|
let loadOperation = null;
|
|
@@ -476,13 +1112,17 @@ function createBrowserSpeechProvider({
|
|
|
476
1112
|
generation,
|
|
477
1113
|
errorCode,
|
|
478
1114
|
cache,
|
|
1115
|
+
lifecycleReason,
|
|
1116
|
+
activeOperation,
|
|
1117
|
+
artifactGraphAdmission,
|
|
1118
|
+
security: active?.security ?? loadOperation?.security ?? defaultSecurity,
|
|
479
1119
|
});
|
|
480
1120
|
}
|
|
481
1121
|
|
|
482
1122
|
function releaseSlot(slot) {
|
|
483
1123
|
if (!slot || slot.released) return;
|
|
484
|
-
slot.released = true;
|
|
485
1124
|
slot.prepared.release();
|
|
1125
|
+
slot.released = true;
|
|
486
1126
|
}
|
|
487
1127
|
|
|
488
1128
|
async function terminateSlot(slot, reason, { intentional = true } = {}) {
|
|
@@ -491,10 +1131,22 @@ function createBrowserSpeechProvider({
|
|
|
491
1131
|
throw providerError(
|
|
492
1132
|
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
493
1133
|
"The browser speech Worker client is not SDK-owned.",
|
|
1134
|
+
undefined,
|
|
1135
|
+
`${role}-worker-client-authority-mismatch`,
|
|
494
1136
|
);
|
|
495
1137
|
}
|
|
496
|
-
|
|
497
|
-
|
|
1138
|
+
const trustedReason = trustedWorkerFailure(reason, role);
|
|
1139
|
+
try {
|
|
1140
|
+
await slot.client.terminate(trustedReason, { intentional });
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
throw trustedWorkerFailure(error, role);
|
|
1143
|
+
} finally {
|
|
1144
|
+
try {
|
|
1145
|
+
releaseSlot(slot);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
throw trustedWorkerFailure(error, role);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
498
1150
|
}
|
|
499
1151
|
|
|
500
1152
|
const provider = {
|
|
@@ -507,16 +1159,20 @@ function createBrowserSpeechProvider({
|
|
|
507
1159
|
return Object.freeze([catalogEntry]);
|
|
508
1160
|
},
|
|
509
1161
|
|
|
510
|
-
inspect(selection,
|
|
511
|
-
|
|
1162
|
+
inspect(selection, context = {}) {
|
|
1163
|
+
const inspection = providerContext(context, role, "inspection");
|
|
1164
|
+
const { signal } = inspection;
|
|
1165
|
+
throwIfAborted(signal, `${role}-provider-inspection-cancelled`);
|
|
512
1166
|
const available = selection?.providerId === providerId
|
|
513
1167
|
&& selection?.modelId === authority.modelId
|
|
1168
|
+
&& (!Object.hasOwn(selection ?? {}, "role") || selection.role === role)
|
|
514
1169
|
&& selection?.localOnly !== false;
|
|
515
1170
|
if (!available) {
|
|
516
1171
|
return Object.freeze({
|
|
517
1172
|
available: false,
|
|
518
1173
|
code: "ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
519
1174
|
message: "The selected browser speech model does not match this provider authority.",
|
|
1175
|
+
reason: `${role}-provider-inspection-selection-authority-mismatch`,
|
|
520
1176
|
});
|
|
521
1177
|
}
|
|
522
1178
|
return Object.freeze({ available: true, authority });
|
|
@@ -525,66 +1181,182 @@ function createBrowserSpeechProvider({
|
|
|
525
1181
|
status,
|
|
526
1182
|
|
|
527
1183
|
load(context = {}) {
|
|
1184
|
+
let loadContext;
|
|
1185
|
+
try {
|
|
1186
|
+
loadContext = providerContext(context, role, "load");
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
return Promise.reject(trustedLoadFailure(error, role));
|
|
1189
|
+
}
|
|
1190
|
+
const loadSignal = loadContext.signal;
|
|
1191
|
+
let contextRole;
|
|
1192
|
+
try {
|
|
1193
|
+
contextRole = context.role;
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
return Promise.reject(trustedLoadFailure(error, role));
|
|
1196
|
+
}
|
|
1197
|
+
if (contextRole !== role) {
|
|
1198
|
+
return Promise.reject(providerError(
|
|
1199
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
1200
|
+
`Browser ${role} load role does not match the provider.`,
|
|
1201
|
+
undefined,
|
|
1202
|
+
`${role}-provider-load-role-mismatch`,
|
|
1203
|
+
));
|
|
1204
|
+
}
|
|
528
1205
|
if (state === "disposed" || disposeOperation) {
|
|
529
|
-
return Promise.reject(providerError(
|
|
1206
|
+
return Promise.reject(providerError(
|
|
1207
|
+
"ARCANE_AI_PROVIDER_DISPOSED",
|
|
1208
|
+
"The browser speech provider is disposed.",
|
|
1209
|
+
undefined,
|
|
1210
|
+
`${role}-provider-load-rejected-after-dispose`,
|
|
1211
|
+
));
|
|
530
1212
|
}
|
|
531
1213
|
if (unloadOperation || state === "unloading") {
|
|
532
1214
|
return Promise.reject(providerError(
|
|
533
1215
|
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
534
1216
|
"The browser speech provider is unloading.",
|
|
1217
|
+
undefined,
|
|
1218
|
+
`${role}-load-rejected-during-unload`,
|
|
535
1219
|
));
|
|
536
1220
|
}
|
|
537
|
-
|
|
538
|
-
|
|
1221
|
+
let loadProgress;
|
|
1222
|
+
let selectionProviderId;
|
|
1223
|
+
let selectionModelId;
|
|
1224
|
+
let selectionLocalOnly;
|
|
1225
|
+
let selectionRoleMismatch;
|
|
1226
|
+
try {
|
|
1227
|
+
loadProgress = context.progress;
|
|
1228
|
+
const selection = context.selection;
|
|
1229
|
+
selectionProviderId = selection?.providerId;
|
|
1230
|
+
selectionModelId = selection?.modelId;
|
|
1231
|
+
selectionLocalOnly = selection?.localOnly;
|
|
1232
|
+
selectionRoleMismatch = Object.hasOwn(selection ?? {}, "role")
|
|
1233
|
+
&& selection.role !== role;
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
return Promise.reject(trustedLoadFailure(error, role));
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof loadProgress !== "function") {
|
|
1238
|
+
return Promise.reject(providerError(
|
|
1239
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
1240
|
+
"Browser speech load progress must be a function.",
|
|
1241
|
+
undefined,
|
|
1242
|
+
`${role}-load-progress-callback-not-function`,
|
|
1243
|
+
));
|
|
539
1244
|
}
|
|
540
|
-
if (
|
|
541
|
-
||
|
|
542
|
-
||
|
|
543
|
-
||
|
|
544
|
-
return Promise.reject(providerError(
|
|
1245
|
+
if (selectionProviderId !== providerId
|
|
1246
|
+
|| selectionModelId !== authority.modelId
|
|
1247
|
+
|| selectionRoleMismatch
|
|
1248
|
+
|| selectionLocalOnly === false) {
|
|
1249
|
+
return Promise.reject(providerError(
|
|
1250
|
+
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
1251
|
+
"Browser speech load selection changed.",
|
|
1252
|
+
undefined,
|
|
1253
|
+
`${role}-load-selection-authority-mismatch`,
|
|
1254
|
+
));
|
|
545
1255
|
}
|
|
546
1256
|
let effectiveSecurity;
|
|
547
1257
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1258
|
+
if (authority.graph) {
|
|
1259
|
+
effectiveSecurity = resolveModelSecurity({
|
|
1260
|
+
app: graphSecurity(
|
|
1261
|
+
configuredAppSecurity,
|
|
1262
|
+
"Browser speech graph app security",
|
|
1263
|
+
),
|
|
1264
|
+
binding: graphSecurity(
|
|
1265
|
+
configuredProviderSecurity,
|
|
1266
|
+
"Browser speech graph provider security",
|
|
1267
|
+
),
|
|
1268
|
+
load: graphSecurity(
|
|
1269
|
+
context.security,
|
|
1270
|
+
"Browser speech graph load security",
|
|
1271
|
+
),
|
|
1272
|
+
});
|
|
1273
|
+
if (effectiveSecurity.secure !== true) {
|
|
1274
|
+
throw providerError(
|
|
1275
|
+
"ARCANE_AI_SECURE_MODE_REQUIRED",
|
|
1276
|
+
"Authenticated browser speech artifact graphs require explicit secure:true.",
|
|
1277
|
+
undefined,
|
|
1278
|
+
`${role}-artifact-graph-secure-mode-required`,
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
} else {
|
|
1282
|
+
effectiveSecurity = resolveModelSecurity({
|
|
1283
|
+
app: configuredAppSecurity,
|
|
1284
|
+
binding: configuredProviderSecurity,
|
|
1285
|
+
load: context.security,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
throwIfAborted(loadSignal, `${role}-load-cancelled`);
|
|
554
1289
|
} catch (error) {
|
|
555
|
-
return Promise.reject(error);
|
|
1290
|
+
return Promise.reject(trustedLoadFailure(error, role));
|
|
556
1291
|
}
|
|
557
1292
|
if (state === "ready" && active) {
|
|
558
1293
|
if (sameModelSecurity(active.security, effectiveSecurity)) {
|
|
559
1294
|
return Promise.resolve(status());
|
|
560
1295
|
}
|
|
1296
|
+
lifecycleReason = `${role}-load-superseded-by-security-change`;
|
|
561
1297
|
return provider.unload().then(() => provider.load(context));
|
|
562
1298
|
}
|
|
563
1299
|
if (loadOperation) {
|
|
564
1300
|
if (sameModelSecurity(loadOperation.security, effectiveSecurity)) {
|
|
565
|
-
return loadOperation
|
|
1301
|
+
return observeLoadOperation(loadOperation, {
|
|
1302
|
+
signal: loadSignal,
|
|
1303
|
+
progress: loadProgress,
|
|
1304
|
+
}, role);
|
|
566
1305
|
}
|
|
567
|
-
|
|
1306
|
+
lifecycleReason = `${role}-load-superseded-by-security-change`;
|
|
1307
|
+
loadOperation.abort(
|
|
1308
|
+
`${role}-load-superseded-by-security-change`,
|
|
1309
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1310
|
+
);
|
|
568
1311
|
return loadOperation.promise.catch(() => undefined).then(() => provider.load(context));
|
|
569
1312
|
}
|
|
570
1313
|
generation += 1;
|
|
571
1314
|
const operationGeneration = generation;
|
|
572
|
-
const linked = linkSignal(
|
|
1315
|
+
const linked = linkSignal(null);
|
|
573
1316
|
state = "loading";
|
|
1317
|
+
lifecycleReason = `${role}-load-started`;
|
|
1318
|
+
activeOperation = `${role}-provider-load`;
|
|
574
1319
|
errorCode = null;
|
|
575
|
-
const
|
|
1320
|
+
const record = {
|
|
1321
|
+
promise: null,
|
|
1322
|
+
security: effectiveSecurity,
|
|
1323
|
+
observers: new Set(),
|
|
1324
|
+
settled: false,
|
|
1325
|
+
hasProgress: false,
|
|
1326
|
+
latestProgress: null,
|
|
1327
|
+
abort: (
|
|
1328
|
+
reason = `${role}-load-cancelled`,
|
|
1329
|
+
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
1330
|
+
) => linked.abort(reason, code),
|
|
1331
|
+
publishProgress(progress) {
|
|
1332
|
+
record.latestProgress = progress;
|
|
1333
|
+
record.hasProgress = true;
|
|
1334
|
+
for (const observer of [...record.observers]) observer.progress(progress);
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
const promise = Promise.resolve().then(async () => {
|
|
576
1338
|
let prepared = null;
|
|
577
1339
|
let slot = null;
|
|
578
1340
|
try {
|
|
579
|
-
prepared = await store.prepare(authority, {
|
|
1341
|
+
prepared = await store.prepare(authority.graph ?? authority, {
|
|
580
1342
|
signal: linked.controller.signal,
|
|
581
|
-
onProgress:
|
|
1343
|
+
onProgress: record.publishProgress,
|
|
582
1344
|
offline,
|
|
583
1345
|
security: effectiveSecurity,
|
|
584
1346
|
});
|
|
585
|
-
throwIfAborted(
|
|
1347
|
+
throwIfAborted(
|
|
1348
|
+
linked.controller.signal,
|
|
1349
|
+
() => linked.reason(`${role}-load-cancelled`),
|
|
1350
|
+
);
|
|
586
1351
|
if (operationGeneration !== generation) {
|
|
587
|
-
throw providerError(
|
|
1352
|
+
throw providerError(
|
|
1353
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1354
|
+
"Browser speech loading was superseded.",
|
|
1355
|
+
undefined,
|
|
1356
|
+
lifecycleReason === `${role}-load-superseded-by-security-change`
|
|
1357
|
+
? lifecycleReason
|
|
1358
|
+
: `${role}-load-superseded-by-unload`,
|
|
1359
|
+
);
|
|
588
1360
|
}
|
|
589
1361
|
slot = {
|
|
590
1362
|
prepared,
|
|
@@ -595,13 +1367,36 @@ function createBrowserSpeechProvider({
|
|
|
595
1367
|
slot.client = createSpeechWorkerClient({
|
|
596
1368
|
role,
|
|
597
1369
|
onTermination({ reason, intentional }) {
|
|
1370
|
+
const trustedReason = trustedWorkerFailure(reason, role);
|
|
598
1371
|
if (active === slot) {
|
|
599
1372
|
active = null;
|
|
600
1373
|
if (state !== "disposed" && state !== "unloading") {
|
|
601
1374
|
state = intentional ? "unloaded" : "error";
|
|
602
|
-
errorCode = intentional ? null : workerFailureCode(
|
|
1375
|
+
errorCode = intentional ? null : workerFailureCode(trustedReason);
|
|
1376
|
+
lifecycleReason = intentional
|
|
1377
|
+
? `${role}-worker-terminated`
|
|
1378
|
+
: trustedReason.reason ?? (errorCode === "ARCANE_AI_WORKER_MESSAGE_ERROR"
|
|
1379
|
+
|| errorCode === "ARCANE_AI_WORKER_MESSAGE_REJECTED"
|
|
1380
|
+
? `${role}-worker-message-rejected`
|
|
1381
|
+
: errorCode === "ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH"
|
|
1382
|
+
? `${role}-worker-protocol-mismatch`
|
|
1383
|
+
: `${role}-worker-crashed`);
|
|
1384
|
+
activeOperation = null;
|
|
1385
|
+
artifactGraphAdmission = null;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
releaseSlot(slot);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
const releaseFailure = trustedWorkerFailure(error, role);
|
|
1391
|
+
if (state !== "disposed" && state !== "unloading") {
|
|
1392
|
+
state = "error";
|
|
1393
|
+
errorCode = releaseFailure.code;
|
|
1394
|
+
lifecycleReason = releaseFailure.reason;
|
|
1395
|
+
activeOperation = null;
|
|
1396
|
+
artifactGraphAdmission = null;
|
|
1397
|
+
}
|
|
1398
|
+
throw releaseFailure;
|
|
603
1399
|
}
|
|
604
|
-
releaseSlot(slot);
|
|
605
1400
|
}
|
|
606
1401
|
},
|
|
607
1402
|
});
|
|
@@ -609,146 +1404,331 @@ function createBrowserSpeechProvider({
|
|
|
609
1404
|
throw providerError(
|
|
610
1405
|
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
611
1406
|
"The browser speech Worker client is not SDK-owned.",
|
|
1407
|
+
undefined,
|
|
1408
|
+
`${role}-worker-client-authority-mismatch`,
|
|
612
1409
|
);
|
|
613
1410
|
}
|
|
614
1411
|
const configuration = Object.freeze({
|
|
615
1412
|
role,
|
|
616
1413
|
runtime: prepared.runtime,
|
|
617
1414
|
model: prepared.model,
|
|
1415
|
+
security: effectiveSecurity,
|
|
1416
|
+
...(authority.graph ? {
|
|
1417
|
+
artifactGraphId: authority.artifactGraphId,
|
|
1418
|
+
artifactGraphProtocol: authority.graph.protocol,
|
|
1419
|
+
} : {}),
|
|
618
1420
|
});
|
|
619
1421
|
await slot.client.request("load", { configuration }, {
|
|
620
1422
|
signal: linked.controller.signal,
|
|
621
|
-
progress:
|
|
1423
|
+
progress: record.publishProgress,
|
|
622
1424
|
});
|
|
623
|
-
throwIfAborted(
|
|
1425
|
+
throwIfAborted(
|
|
1426
|
+
linked.controller.signal,
|
|
1427
|
+
() => linked.reason(`${role}-load-cancelled`),
|
|
1428
|
+
);
|
|
624
1429
|
if (operationGeneration !== generation) {
|
|
625
|
-
throw providerError(
|
|
1430
|
+
throw providerError(
|
|
1431
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1432
|
+
"Browser speech loading was superseded.",
|
|
1433
|
+
undefined,
|
|
1434
|
+
lifecycleReason === `${role}-load-superseded-by-security-change`
|
|
1435
|
+
? lifecycleReason
|
|
1436
|
+
: `${role}-load-superseded-by-unload`,
|
|
1437
|
+
);
|
|
626
1438
|
}
|
|
627
1439
|
active = slot;
|
|
628
1440
|
cache = prepared.cache;
|
|
1441
|
+
artifactGraphAdmission = prepared.artifactGraphAdmission ?? null;
|
|
629
1442
|
state = "ready";
|
|
1443
|
+
lifecycleReason = `${role}-load-completed`;
|
|
1444
|
+
activeOperation = null;
|
|
630
1445
|
return status();
|
|
631
1446
|
} catch (error) {
|
|
632
|
-
|
|
633
|
-
|
|
1447
|
+
let failure = linked.controller.signal.aborted
|
|
1448
|
+
? linkedAbortFailure(linked, `${role}-load-cancelled`)
|
|
1449
|
+
: trustedLoadFailure(error, role);
|
|
1450
|
+
try {
|
|
1451
|
+
if (slot) await terminateSlot(slot, failure);
|
|
1452
|
+
else prepared?.release();
|
|
1453
|
+
} catch (cleanupError) {
|
|
1454
|
+
failure = slot
|
|
1455
|
+
? trustedWorkerFailure(cleanupError, role)
|
|
1456
|
+
: trustedLoadFailure(cleanupError, role);
|
|
1457
|
+
}
|
|
634
1458
|
if (operationGeneration === generation && state !== "unloading" && state !== "disposed") {
|
|
635
|
-
state =
|
|
636
|
-
||
|
|
1459
|
+
state = failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1460
|
+
|| failure?.code === "ARCANE_AI_OPERATION_SUPERSEDED"
|
|
637
1461
|
? "unloaded"
|
|
638
1462
|
: "error";
|
|
639
|
-
errorCode =
|
|
1463
|
+
errorCode = failure?.code ?? "ARCANE_AI_PROVIDER_LOAD_FAILED";
|
|
1464
|
+
lifecycleReason = failure?.reason
|
|
1465
|
+
?? (state === "unloaded" ? `${role}-load-cancelled` : `${role}-load-rejected`);
|
|
1466
|
+
activeOperation = null;
|
|
640
1467
|
}
|
|
641
|
-
throw
|
|
1468
|
+
throw failure;
|
|
642
1469
|
} finally {
|
|
1470
|
+
record.settled = true;
|
|
643
1471
|
linked.release();
|
|
644
|
-
if (loadOperation
|
|
1472
|
+
if (loadOperation === record) loadOperation = null;
|
|
645
1473
|
}
|
|
646
|
-
})();
|
|
647
|
-
loadOperation = Object.freeze({
|
|
648
|
-
promise,
|
|
649
|
-
security: effectiveSecurity,
|
|
650
|
-
abort: () => linked.controller.abort(),
|
|
651
1474
|
});
|
|
652
|
-
|
|
1475
|
+
record.promise = promise;
|
|
1476
|
+
loadOperation = record;
|
|
1477
|
+
return observeLoadOperation(record, {
|
|
1478
|
+
signal: loadSignal,
|
|
1479
|
+
progress: loadProgress,
|
|
1480
|
+
}, role);
|
|
653
1481
|
},
|
|
654
1482
|
|
|
655
1483
|
async request(context = {}) {
|
|
656
|
-
|
|
657
|
-
|
|
1484
|
+
let requestContext;
|
|
1485
|
+
try {
|
|
1486
|
+
requestContext = providerContext(context, role, "request");
|
|
1487
|
+
if (context.role !== role) {
|
|
1488
|
+
throw providerError(
|
|
1489
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
1490
|
+
`Browser ${role} request role does not match the provider.`,
|
|
1491
|
+
undefined,
|
|
1492
|
+
`${role}-provider-request-role-mismatch`,
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (context.operation !== operation) {
|
|
1496
|
+
throw providerError(
|
|
1497
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
1498
|
+
`Browser ${role} supports only ${operation}.`,
|
|
1499
|
+
undefined,
|
|
1500
|
+
`${role}-provider-operation-mismatch`,
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
assertRequestAuthority(
|
|
1504
|
+
context,
|
|
1505
|
+
authority,
|
|
1506
|
+
providerId,
|
|
1507
|
+
`${role}-provider-request-selection-authority-mismatch`,
|
|
1508
|
+
);
|
|
1509
|
+
throwIfAborted(requestContext.signal, ROLE_REQUEST_REASON[role]);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw trustedRequestFailure(error, role);
|
|
658
1512
|
}
|
|
659
|
-
|
|
660
|
-
const externalSignal = context.signal ?? null;
|
|
661
|
-
throwIfAborted(externalSignal);
|
|
1513
|
+
const externalSignal = requestContext.signal;
|
|
662
1514
|
if (state !== "ready" || !active) {
|
|
663
|
-
throw providerError(
|
|
1515
|
+
throw providerError(
|
|
1516
|
+
"ARCANE_AI_NOT_READY",
|
|
1517
|
+
"The browser speech provider is not ready.",
|
|
1518
|
+
undefined,
|
|
1519
|
+
`${role}-provider-request-not-ready`,
|
|
1520
|
+
);
|
|
664
1521
|
}
|
|
665
1522
|
if (requestOperation) {
|
|
666
|
-
throw providerError(
|
|
1523
|
+
throw providerError(
|
|
1524
|
+
"ARCANE_AI_PROVIDER_BUSY",
|
|
1525
|
+
"The browser speech provider is already processing a request.",
|
|
1526
|
+
undefined,
|
|
1527
|
+
`${role}-provider-request-already-active`,
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
let linked;
|
|
1531
|
+
try {
|
|
1532
|
+
linked = linkSignal(externalSignal);
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
throw trustedRequestFailure(error, role);
|
|
667
1535
|
}
|
|
668
|
-
const linked = linkSignal(externalSignal);
|
|
669
1536
|
if (linked.controller.signal.aborted) {
|
|
670
1537
|
linked.release();
|
|
671
|
-
throw
|
|
1538
|
+
throw linkedAbortFailure(linked, ROLE_REQUEST_REASON[role]);
|
|
672
1539
|
}
|
|
673
1540
|
const slot = active;
|
|
674
1541
|
const requestGeneration = generation;
|
|
675
1542
|
let workerRequestStarted = false;
|
|
1543
|
+
activeOperation = role === "stt"
|
|
1544
|
+
? "stt-provider-transcription"
|
|
1545
|
+
: "tts-provider-synthesis";
|
|
1546
|
+
lifecycleReason = role === "stt"
|
|
1547
|
+
? "stt-transcription-started"
|
|
1548
|
+
: "tts-synthesis-started";
|
|
676
1549
|
const promise = (async () => {
|
|
677
1550
|
const normalized = await normalizeRequestPayload(
|
|
678
1551
|
role,
|
|
679
1552
|
context.payload,
|
|
680
1553
|
authority,
|
|
681
1554
|
linked.controller.signal,
|
|
1555
|
+
() => linked.reason(ROLE_REQUEST_REASON[role]),
|
|
1556
|
+
);
|
|
1557
|
+
throwIfAborted(
|
|
1558
|
+
linked.controller.signal,
|
|
1559
|
+
() => linked.reason(ROLE_REQUEST_REASON[role]),
|
|
682
1560
|
);
|
|
683
|
-
throwIfAborted(linked.controller.signal);
|
|
684
1561
|
workerRequestStarted = true;
|
|
685
1562
|
const result = await slot.client.request("use", normalized.payload, {
|
|
686
1563
|
signal: linked.controller.signal,
|
|
687
1564
|
});
|
|
688
1565
|
if (requestGeneration !== generation || active !== slot) {
|
|
689
|
-
throw providerError(
|
|
1566
|
+
throw providerError(
|
|
1567
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1568
|
+
"The browser speech result was superseded.",
|
|
1569
|
+
undefined,
|
|
1570
|
+
role === "stt"
|
|
1571
|
+
? "stt-transcription-superseded-by-unload"
|
|
1572
|
+
: "tts-synthesis-superseded-by-unload",
|
|
1573
|
+
);
|
|
690
1574
|
}
|
|
691
1575
|
return role === "tts" && normalized.shared
|
|
692
|
-
? encodeSharedSynthesisResult(result)
|
|
1576
|
+
? encodeSharedSynthesisResult(result, authority)
|
|
693
1577
|
: result;
|
|
694
1578
|
})();
|
|
695
1579
|
requestOperation = Object.freeze({
|
|
696
1580
|
promise,
|
|
697
|
-
abort: (
|
|
1581
|
+
abort: (
|
|
1582
|
+
reason = ROLE_REQUEST_REASON[role],
|
|
1583
|
+
code = "ARCANE_AI_REQUEST_ABORTED",
|
|
1584
|
+
) => linked.abort(reason, code),
|
|
698
1585
|
});
|
|
699
1586
|
try {
|
|
700
|
-
|
|
1587
|
+
const result = await promise;
|
|
1588
|
+
lifecycleReason = role === "stt"
|
|
1589
|
+
? "stt-transcription-completed"
|
|
1590
|
+
: "tts-synthesis-completed";
|
|
1591
|
+
return result;
|
|
701
1592
|
} catch (error) {
|
|
702
|
-
|
|
1593
|
+
let failure = linked.controller.signal.aborted
|
|
1594
|
+
&& (error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1595
|
+
|| linked.code() === "ARCANE_AI_OPERATION_SUPERSEDED")
|
|
1596
|
+
? linkedAbortFailure(linked, ROLE_REQUEST_REASON[role])
|
|
1597
|
+
: trustedRequestFailure(error, role);
|
|
1598
|
+
if (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
703
1599
|
&& workerRequestStarted
|
|
704
1600
|
&& active === slot) {
|
|
705
1601
|
active = null;
|
|
706
|
-
|
|
1602
|
+
try {
|
|
1603
|
+
releaseSlot(slot);
|
|
1604
|
+
} catch (cleanupError) {
|
|
1605
|
+
failure = trustedWorkerFailure(cleanupError, role);
|
|
1606
|
+
}
|
|
707
1607
|
state = "unloaded";
|
|
1608
|
+
artifactGraphAdmission = null;
|
|
708
1609
|
}
|
|
709
|
-
|
|
1610
|
+
lifecycleReason = failure?.reason
|
|
1611
|
+
?? (failure?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1612
|
+
? ROLE_REQUEST_REASON[role]
|
|
1613
|
+
: role === "stt"
|
|
1614
|
+
? "stt-transcription-engine-operation-rejected"
|
|
1615
|
+
: "tts-synthesis-engine-operation-rejected");
|
|
1616
|
+
throw failure;
|
|
710
1617
|
} finally {
|
|
711
1618
|
linked.release();
|
|
712
1619
|
if (requestOperation?.promise === promise) requestOperation = null;
|
|
1620
|
+
activeOperation = null;
|
|
713
1621
|
}
|
|
714
1622
|
},
|
|
715
1623
|
|
|
716
|
-
unload() {
|
|
1624
|
+
unload(context = {}) {
|
|
1625
|
+
let unloadContext;
|
|
1626
|
+
try {
|
|
1627
|
+
unloadContext = providerContext(context, role, "unload");
|
|
1628
|
+
assertRequestAuthority(
|
|
1629
|
+
context,
|
|
1630
|
+
authority,
|
|
1631
|
+
providerId,
|
|
1632
|
+
`${role}-provider-unload-selection-authority-mismatch`,
|
|
1633
|
+
);
|
|
1634
|
+
throwIfAborted(unloadContext.signal, `${role}-unload-cancelled`);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
return Promise.reject(trustedLifecycleFailure(error, role, "unload"));
|
|
1637
|
+
}
|
|
1638
|
+
const unloadSignal = unloadContext.signal;
|
|
717
1639
|
if (state === "disposed") return Promise.resolve(status());
|
|
718
|
-
if (unloadOperation)
|
|
1640
|
+
if (unloadOperation) {
|
|
1641
|
+
return awaitAbortableSpeechOperation(
|
|
1642
|
+
unloadOperation,
|
|
1643
|
+
unloadSignal,
|
|
1644
|
+
`${role}-unload-cancelled`,
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
719
1647
|
generation += 1;
|
|
720
1648
|
state = "unloading";
|
|
721
1649
|
errorCode = null;
|
|
722
|
-
|
|
723
|
-
|
|
1650
|
+
activeOperation = `${role}-provider-unload`;
|
|
1651
|
+
const capturedLoad = loadOperation?.promise ?? null;
|
|
1652
|
+
const capturedRequest = requestOperation?.promise ?? null;
|
|
1653
|
+
if (loadOperation) {
|
|
1654
|
+
lifecycleReason = `${role}-load-superseded-by-unload`;
|
|
1655
|
+
loadOperation.abort(
|
|
1656
|
+
`${role}-load-superseded-by-unload`,
|
|
1657
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1658
|
+
);
|
|
1659
|
+
} else if (requestOperation) {
|
|
1660
|
+
lifecycleReason = role === "stt"
|
|
1661
|
+
? "stt-transcription-superseded-by-unload"
|
|
1662
|
+
: "tts-synthesis-superseded-by-unload";
|
|
1663
|
+
requestOperation.abort(lifecycleReason, "ARCANE_AI_OPERATION_SUPERSEDED");
|
|
1664
|
+
} else {
|
|
1665
|
+
lifecycleReason = `${role}-unload-started`;
|
|
1666
|
+
}
|
|
724
1667
|
const promise = (async () => {
|
|
725
1668
|
await Promise.allSettled([
|
|
726
|
-
|
|
727
|
-
|
|
1669
|
+
capturedLoad,
|
|
1670
|
+
capturedRequest,
|
|
728
1671
|
].filter(Boolean));
|
|
729
1672
|
const slot = active;
|
|
730
1673
|
active = null;
|
|
731
1674
|
await terminateSlot(slot, providerError(
|
|
732
1675
|
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
733
1676
|
"The browser speech Worker was terminated by unload().",
|
|
1677
|
+
undefined,
|
|
1678
|
+
`${role}-worker-terminated-by-unload`,
|
|
734
1679
|
));
|
|
735
1680
|
cache = null;
|
|
1681
|
+
artifactGraphAdmission = null;
|
|
736
1682
|
state = "unloaded";
|
|
1683
|
+
lifecycleReason = `${role}-unload-completed`;
|
|
1684
|
+
activeOperation = null;
|
|
737
1685
|
return status();
|
|
738
1686
|
})();
|
|
739
|
-
|
|
740
|
-
if (unloadOperation ===
|
|
1687
|
+
const tracked = promise.then((value) => {
|
|
1688
|
+
if (unloadOperation === tracked) unloadOperation = null;
|
|
1689
|
+
return value;
|
|
1690
|
+
}, (error) => {
|
|
1691
|
+
if (unloadOperation === tracked) unloadOperation = null;
|
|
1692
|
+
throw error;
|
|
741
1693
|
});
|
|
742
|
-
|
|
743
|
-
return
|
|
1694
|
+
unloadOperation = tracked;
|
|
1695
|
+
return awaitAbortableSpeechOperation(
|
|
1696
|
+
tracked,
|
|
1697
|
+
unloadSignal,
|
|
1698
|
+
`${role}-unload-cancelled`,
|
|
1699
|
+
);
|
|
744
1700
|
},
|
|
745
1701
|
|
|
746
|
-
dispose() {
|
|
1702
|
+
dispose(context = {}) {
|
|
1703
|
+
let disposalContext;
|
|
1704
|
+
try {
|
|
1705
|
+
disposalContext = providerContext(context, role, "dispose");
|
|
1706
|
+
assertRequestAuthority(
|
|
1707
|
+
context,
|
|
1708
|
+
authority,
|
|
1709
|
+
providerId,
|
|
1710
|
+
`${role}-provider-dispose-selection-authority-mismatch`,
|
|
1711
|
+
);
|
|
1712
|
+
throwIfAborted(disposalContext.signal, `${role}-dispose-cancelled`);
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
return Promise.reject(trustedLifecycleFailure(error, role, "dispose"));
|
|
1715
|
+
}
|
|
1716
|
+
const disposeSignal = disposalContext.signal;
|
|
747
1717
|
if (state === "disposed") return Promise.resolve(status());
|
|
748
|
-
if (disposeOperation)
|
|
1718
|
+
if (disposeOperation) {
|
|
1719
|
+
return awaitAbortableSpeechOperation(
|
|
1720
|
+
disposeOperation,
|
|
1721
|
+
disposeSignal,
|
|
1722
|
+
`${role}-dispose-cancelled`,
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
749
1725
|
const promise = (async () => {
|
|
1726
|
+
activeOperation = `${role}-provider-dispose`;
|
|
1727
|
+
lifecycleReason = `${role}-dispose-started`;
|
|
750
1728
|
await provider.unload();
|
|
751
1729
|
state = "disposed";
|
|
1730
|
+
lifecycleReason = `${role}-dispose-completed`;
|
|
1731
|
+
activeOperation = null;
|
|
752
1732
|
return status();
|
|
753
1733
|
})();
|
|
754
1734
|
disposeOperation = promise.then((value) => {
|
|
@@ -758,7 +1738,11 @@ function createBrowserSpeechProvider({
|
|
|
758
1738
|
disposeOperation = null;
|
|
759
1739
|
throw error;
|
|
760
1740
|
});
|
|
761
|
-
return
|
|
1741
|
+
return awaitAbortableSpeechOperation(
|
|
1742
|
+
disposeOperation,
|
|
1743
|
+
disposeSignal,
|
|
1744
|
+
`${role}-dispose-cancelled`,
|
|
1745
|
+
);
|
|
762
1746
|
},
|
|
763
1747
|
};
|
|
764
1748
|
return Object.freeze(provider);
|