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