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,11 +1,457 @@
|
|
|
1
1
|
export const SPEECH_WORKER_PROTOCOL = "arcane-ai-speech-worker/1";
|
|
2
2
|
|
|
3
|
+
const SPEECH_WORKER_ERROR_PROTOCOL = "arcane-ai-speech-worker-error/1";
|
|
4
|
+
|
|
5
|
+
const ARTIFACT_GRAPH_MODULE_GRAPH =
|
|
6
|
+
"browser-speech-authenticated-artifact-graph";
|
|
7
|
+
const GRAPH_GUARD_NAME = "__arcaneBrowserSpeechArtifactGraphGuardsV1";
|
|
8
|
+
const NESTED_WORKER_PROTOCOL =
|
|
9
|
+
"arcane-ai-browser-speech-artifact-module-worker/1";
|
|
10
|
+
const STRICT_GRAPH_ADMISSIONS = new Set([
|
|
11
|
+
"artifact-graph-network-dbopfs-verified",
|
|
12
|
+
"artifact-graph-dbopfs-cache-verified",
|
|
13
|
+
"artifact-graph-offline-dbopfs-cache-verified",
|
|
14
|
+
"artifact-graph-network-dbopfs-partially-checked",
|
|
15
|
+
"artifact-graph-dbopfs-cache-partially-checked",
|
|
16
|
+
"artifact-graph-offline-dbopfs-cache-partially-checked",
|
|
17
|
+
"artifact-graph-network-dbopfs-unchecked",
|
|
18
|
+
"artifact-graph-dbopfs-cache-unchecked",
|
|
19
|
+
"artifact-graph-offline-dbopfs-cache-unchecked",
|
|
20
|
+
]);
|
|
3
21
|
const ADAPTERS = Object.freeze({
|
|
4
22
|
stt: "transformers-whisper",
|
|
5
23
|
tts: "kokoro-js",
|
|
6
24
|
});
|
|
25
|
+
const ONNX_NAMESPACES = Object.freeze({
|
|
26
|
+
stt: "transformers-env-backends-onnx-wasm",
|
|
27
|
+
tts: "kokoro-env-wasm-paths",
|
|
28
|
+
});
|
|
29
|
+
const ARTIFACT_GRAPH_TRANSFORM_KINDS = new Set([
|
|
30
|
+
"function-return-this-to-global-this",
|
|
31
|
+
"typed-array-constructor",
|
|
32
|
+
]);
|
|
33
|
+
const PUBLIC_WORKER_OPERATIONS = Object.freeze([
|
|
34
|
+
"load",
|
|
35
|
+
"use",
|
|
36
|
+
"status",
|
|
37
|
+
"unload",
|
|
38
|
+
"dispose",
|
|
39
|
+
]);
|
|
40
|
+
const TRANSPORT_WORKER_OPERATION_SET = new Set([
|
|
41
|
+
...PUBLIC_WORKER_OPERATIONS,
|
|
42
|
+
"cancel",
|
|
43
|
+
]);
|
|
44
|
+
const BOTH_WORKER_ROLES = Object.freeze(["stt", "tts"]);
|
|
45
|
+
const LOAD_OPERATION = Object.freeze(["load"]);
|
|
46
|
+
const USE_OPERATION = Object.freeze(["use"]);
|
|
47
|
+
const STATUS_OPERATION = Object.freeze(["status"]);
|
|
48
|
+
const UNLOAD_OPERATION = Object.freeze(["unload"]);
|
|
49
|
+
const DISPOSE_OPERATION = Object.freeze(["dispose"]);
|
|
50
|
+
const UNLOAD_OR_DISPOSE_OPERATIONS = Object.freeze(["unload", "dispose"]);
|
|
51
|
+
const LOAD_OR_USE_OPERATIONS = Object.freeze(["load", "use"]);
|
|
52
|
+
const SDK_WORKER_ERRORS = new WeakSet();
|
|
53
|
+
const WORKER_ERROR_MESSAGES = Object.freeze({
|
|
54
|
+
ARCANE_AI_REQUEST_ABORTED: "The speech worker operation was cancelled.",
|
|
55
|
+
ARCANE_AI_NOT_READY: "The speech worker is not loaded.",
|
|
56
|
+
ARCANE_AI_INVALID_REQUEST: "The speech worker request was rejected.",
|
|
57
|
+
ARCANE_AI_INVALID_PROVIDER_RESULT: "The speech engine result was rejected.",
|
|
58
|
+
ARCANE_AI_UNDECLARED_ARTIFACT: "The speech engine requested an undeclared artifact.",
|
|
59
|
+
ARCANE_AI_PROVIDER_UNAVAILABLE: "The selected speech engine is unavailable.",
|
|
60
|
+
ARCANE_AI_PROVIDER_DISPOSED: "The speech worker is disposed.",
|
|
61
|
+
ARCANE_AI_PROVIDER_REQUEST_FAILED: "The speech engine operation was rejected.",
|
|
62
|
+
ARCANE_AI_WORKER_CRASHED: "The speech Worker stopped unexpectedly.",
|
|
63
|
+
ARCANE_AI_WORKER_MESSAGE_ERROR: "The speech Worker message was rejected.",
|
|
64
|
+
ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID:
|
|
65
|
+
"The authenticated artifact graph Worker configuration was rejected.",
|
|
66
|
+
ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED:
|
|
67
|
+
"The runtime used an undeclared artifact graph fetch edge.",
|
|
68
|
+
ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED:
|
|
69
|
+
"The runtime used an undeclared artifact graph import edge.",
|
|
70
|
+
ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED:
|
|
71
|
+
"The runtime used an undeclared artifact graph cache edge.",
|
|
72
|
+
ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED:
|
|
73
|
+
"The runtime used an undeclared artifact graph module Worker edge.",
|
|
74
|
+
ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE:
|
|
75
|
+
"The Worker cannot install the authenticated artifact graph isolation boundary.",
|
|
76
|
+
});
|
|
77
|
+
const WORKER_ERROR_REASON_ADMISSIONS = new Map();
|
|
78
|
+
|
|
79
|
+
function registerWorkerErrorReasons(code, roles, operations, reasons) {
|
|
80
|
+
const admission = Object.freeze({
|
|
81
|
+
code,
|
|
82
|
+
roles: Object.freeze([...roles]),
|
|
83
|
+
operations: Object.freeze([...operations]),
|
|
84
|
+
});
|
|
85
|
+
for (const reason of reasons) WORKER_ERROR_REASON_ADMISSIONS.set(reason, admission);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], LOAD_OPERATION, [
|
|
89
|
+
"stt-load-cancelled",
|
|
90
|
+
]);
|
|
91
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], STATUS_OPERATION, [
|
|
92
|
+
"stt-status-cancelled",
|
|
93
|
+
]);
|
|
94
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], UNLOAD_OPERATION, [
|
|
95
|
+
"stt-unload-cancelled",
|
|
96
|
+
]);
|
|
97
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], DISPOSE_OPERATION, [
|
|
98
|
+
"stt-dispose-cancelled",
|
|
99
|
+
]);
|
|
100
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["stt"], USE_OPERATION, [
|
|
101
|
+
"stt-transcription-cancelled",
|
|
102
|
+
]);
|
|
103
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], LOAD_OPERATION, [
|
|
104
|
+
"tts-load-cancelled",
|
|
105
|
+
]);
|
|
106
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], STATUS_OPERATION, [
|
|
107
|
+
"tts-status-cancelled",
|
|
108
|
+
]);
|
|
109
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], UNLOAD_OPERATION, [
|
|
110
|
+
"tts-unload-cancelled",
|
|
111
|
+
]);
|
|
112
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], DISPOSE_OPERATION, [
|
|
113
|
+
"tts-dispose-cancelled",
|
|
114
|
+
]);
|
|
115
|
+
registerWorkerErrorReasons("ARCANE_AI_REQUEST_ABORTED", ["tts"], USE_OPERATION, [
|
|
116
|
+
"tts-synthesis-cancelled",
|
|
117
|
+
]);
|
|
118
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], LOAD_OPERATION, [
|
|
119
|
+
"stt-worker-runtime-configuration-rejected",
|
|
120
|
+
"stt-worker-runtime-import-rejected",
|
|
121
|
+
"stt-worker-model-load-rejected",
|
|
122
|
+
]);
|
|
123
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], LOAD_OPERATION, [
|
|
124
|
+
"tts-worker-runtime-configuration-rejected",
|
|
125
|
+
"tts-worker-runtime-import-rejected",
|
|
126
|
+
"tts-worker-model-load-rejected",
|
|
127
|
+
]);
|
|
128
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], USE_OPERATION, [
|
|
129
|
+
"stt-transcription-engine-operation-rejected",
|
|
130
|
+
]);
|
|
131
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], USE_OPERATION, [
|
|
132
|
+
"tts-synthesis-engine-operation-rejected",
|
|
133
|
+
]);
|
|
134
|
+
registerWorkerErrorReasons(
|
|
135
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
136
|
+
["stt"],
|
|
137
|
+
UNLOAD_OR_DISPOSE_OPERATIONS,
|
|
138
|
+
["stt-worker-engine-dispose-rejected"],
|
|
139
|
+
);
|
|
140
|
+
registerWorkerErrorReasons(
|
|
141
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
142
|
+
["tts"],
|
|
143
|
+
UNLOAD_OR_DISPOSE_OPERATIONS,
|
|
144
|
+
["tts-worker-engine-dispose-rejected"],
|
|
145
|
+
);
|
|
146
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], DISPOSE_OPERATION, [
|
|
147
|
+
"stt-worker-dispose-rejected",
|
|
148
|
+
]);
|
|
149
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], DISPOSE_OPERATION, [
|
|
150
|
+
"tts-worker-dispose-rejected",
|
|
151
|
+
]);
|
|
152
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["stt"], STATUS_OPERATION, [
|
|
153
|
+
"stt-worker-status-rejected",
|
|
154
|
+
]);
|
|
155
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_REQUEST_FAILED", ["tts"], STATUS_OPERATION, [
|
|
156
|
+
"tts-worker-status-rejected",
|
|
157
|
+
]);
|
|
158
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", BOTH_WORKER_ROLES, LOAD_OPERATION, [
|
|
159
|
+
"artifact-graph-entrypoint-not-materialized",
|
|
160
|
+
"artifact-graph-materialized-file-not-object",
|
|
161
|
+
"artifact-graph-materialized-media-type-empty",
|
|
162
|
+
"artifact-graph-materialized-module-url-empty",
|
|
163
|
+
"artifact-graph-materialized-path-ambiguous",
|
|
164
|
+
"artifact-graph-materialized-path-empty",
|
|
165
|
+
"artifact-graph-materialized-source-url-empty",
|
|
166
|
+
"artifact-graph-runtime-request-routes-not-array",
|
|
167
|
+
"speech-model-sample-rate-not-positive-safe-integer",
|
|
168
|
+
"speech-runtime-entrypoint-not-materialized",
|
|
169
|
+
"speech-worker-configuration-missing",
|
|
170
|
+
"speech-worker-materialized-files-missing",
|
|
171
|
+
"speech-worker-model-dtype-empty",
|
|
172
|
+
"speech-worker-model-id-empty",
|
|
173
|
+
"speech-worker-model-repository-empty",
|
|
174
|
+
"speech-worker-model-revision-empty",
|
|
175
|
+
"speech-worker-runtime-entry-empty",
|
|
176
|
+
"speech-worker-runtime-selection-mismatch",
|
|
177
|
+
]);
|
|
178
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["stt"], USE_OPERATION, [
|
|
179
|
+
"stt-transcription-audio-not-float32array",
|
|
180
|
+
"stt-transcription-sample-rate-mismatch",
|
|
181
|
+
]);
|
|
182
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], LOAD_OPERATION, [
|
|
183
|
+
"tts-default-voice-empty",
|
|
184
|
+
]);
|
|
185
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_REQUEST", ["tts"], USE_OPERATION, [
|
|
186
|
+
"tts-synthesis-speed-out-of-range",
|
|
187
|
+
"tts-synthesis-text-empty",
|
|
188
|
+
"tts-synthesis-voice-empty",
|
|
189
|
+
"tts-synthesis-voice-not-declared",
|
|
190
|
+
]);
|
|
191
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["stt"], USE_OPERATION, [
|
|
192
|
+
"stt-transcription-result-text-not-string",
|
|
193
|
+
]);
|
|
194
|
+
registerWorkerErrorReasons("ARCANE_AI_INVALID_PROVIDER_RESULT", ["tts"], USE_OPERATION, [
|
|
195
|
+
"tts-synthesis-pcm-result-not-float32array",
|
|
196
|
+
"tts-synthesis-pcm-sample-non-finite",
|
|
197
|
+
"tts-synthesis-result-sample-rate-mismatch",
|
|
198
|
+
]);
|
|
199
|
+
registerWorkerErrorReasons(
|
|
200
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
201
|
+
BOTH_WORKER_ROLES,
|
|
202
|
+
LOAD_OR_USE_OPERATIONS,
|
|
203
|
+
[
|
|
204
|
+
"artifact-graph-cache-write-rejected",
|
|
205
|
+
"artifact-graph-runtime-request-url-malformed",
|
|
206
|
+
"speech-worker-artifact-request-method-rejected",
|
|
207
|
+
"speech-worker-artifact-request-undeclared",
|
|
208
|
+
"speech-worker-cache-match-rejected",
|
|
209
|
+
"speech-worker-cache-open-rejected",
|
|
210
|
+
],
|
|
211
|
+
);
|
|
212
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["stt"], LOAD_OPERATION, [
|
|
213
|
+
"stt-load-rejected-after-dispose",
|
|
214
|
+
]);
|
|
215
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_DISPOSED", ["tts"], LOAD_OPERATION, [
|
|
216
|
+
"tts-load-rejected-after-dispose",
|
|
217
|
+
]);
|
|
218
|
+
registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["stt"], USE_OPERATION, [
|
|
219
|
+
"stt-transcription-rejected-before-load",
|
|
220
|
+
]);
|
|
221
|
+
registerWorkerErrorReasons("ARCANE_AI_NOT_READY", ["tts"], USE_OPERATION, [
|
|
222
|
+
"tts-synthesis-rejected-before-load",
|
|
223
|
+
]);
|
|
224
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", BOTH_WORKER_ROLES, LOAD_OPERATION, [
|
|
225
|
+
"artifact-graph-fetch-constructor-unavailable",
|
|
226
|
+
"artifact-graph-onnx-wasm-pair-not-materialized",
|
|
227
|
+
"speech-worker-fetch-unavailable",
|
|
228
|
+
]);
|
|
229
|
+
registerWorkerErrorReasons(
|
|
230
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
231
|
+
BOTH_WORKER_ROLES,
|
|
232
|
+
LOAD_OR_USE_OPERATIONS,
|
|
233
|
+
[
|
|
234
|
+
"artifact-graph-module-worker-constructor-unavailable",
|
|
235
|
+
"artifact-graph-negative-response-constructor-unavailable",
|
|
236
|
+
],
|
|
237
|
+
);
|
|
238
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], LOAD_OPERATION, [
|
|
239
|
+
"kokoro-env-wasm-paths-assignment-rejected",
|
|
240
|
+
"kokoro-env-wasm-paths-unavailable",
|
|
241
|
+
"kokoro-tts-constructor-export-missing",
|
|
242
|
+
]);
|
|
243
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], LOAD_OPERATION, [
|
|
244
|
+
"transformers-env-allow-local-models-assignment-rejected",
|
|
245
|
+
"transformers-env-allow-local-models-unavailable",
|
|
246
|
+
"transformers-env-allow-remote-models-assignment-rejected",
|
|
247
|
+
"transformers-env-allow-remote-models-unavailable",
|
|
248
|
+
"transformers-env-backends-onnx-wasm-unavailable",
|
|
249
|
+
"transformers-env-browser-cache-assignment-rejected",
|
|
250
|
+
"transformers-env-browser-cache-unavailable",
|
|
251
|
+
"transformers-env-custom-cache-assignment-rejected",
|
|
252
|
+
"transformers-env-custom-cache-toggle-unavailable",
|
|
253
|
+
"transformers-env-custom-cache-toggle-assignment-rejected",
|
|
254
|
+
"transformers-env-custom-cache-unavailable",
|
|
255
|
+
"transformers-env-fs-cache-assignment-rejected",
|
|
256
|
+
"transformers-env-fs-cache-unavailable",
|
|
257
|
+
"transformers-env-num-threads-assignment-rejected",
|
|
258
|
+
"transformers-env-num-threads-unavailable",
|
|
259
|
+
"transformers-env-wasm-paths-assignment-rejected",
|
|
260
|
+
"transformers-env-wasm-paths-unavailable",
|
|
261
|
+
"transformers-whisper-pipeline-export-missing",
|
|
262
|
+
]);
|
|
263
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["stt"], USE_OPERATION, [
|
|
264
|
+
"stt-transcription-method-unavailable",
|
|
265
|
+
]);
|
|
266
|
+
registerWorkerErrorReasons("ARCANE_AI_PROVIDER_UNAVAILABLE", ["tts"], USE_OPERATION, [
|
|
267
|
+
"tts-synthesis-method-unavailable",
|
|
268
|
+
]);
|
|
269
|
+
registerWorkerErrorReasons(
|
|
270
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
271
|
+
BOTH_WORKER_ROLES,
|
|
272
|
+
LOAD_OPERATION,
|
|
273
|
+
[
|
|
274
|
+
"artifact-graph-kokoro-voice-inventory-missing",
|
|
275
|
+
"artifact-graph-module-worker-target-not-materialized",
|
|
276
|
+
"artifact-graph-negative-request-routes-not-array",
|
|
277
|
+
"artifact-graph-onnx-wasm-configuration-mismatch",
|
|
278
|
+
"artifact-graph-runtime-request-route-ambiguous",
|
|
279
|
+
"artifact-graph-transform-identity-ambiguous",
|
|
280
|
+
"artifact-graph-transform-kind-not-admitted",
|
|
281
|
+
"artifact-graph-transform-module-path-empty",
|
|
282
|
+
"artifact-graph-transform-occurrence-not-positive-safe-integer",
|
|
283
|
+
"artifact-graph-transformers-cache-edge-ambiguous",
|
|
284
|
+
"artifact-graph-worker-configuration-incomplete",
|
|
285
|
+
],
|
|
286
|
+
);
|
|
287
|
+
registerWorkerErrorReasons(
|
|
288
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
289
|
+
BOTH_WORKER_ROLES,
|
|
290
|
+
LOAD_OR_USE_OPERATIONS,
|
|
291
|
+
["artifact-graph-typed-array-constructor-transform-undeclared"],
|
|
292
|
+
);
|
|
293
|
+
registerWorkerErrorReasons(
|
|
294
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
295
|
+
["tts"],
|
|
296
|
+
LOAD_OPERATION,
|
|
297
|
+
[
|
|
298
|
+
"kokoro-env-num-threads-field-not-exposed",
|
|
299
|
+
],
|
|
300
|
+
);
|
|
301
|
+
registerWorkerErrorReasons(
|
|
302
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
303
|
+
["stt"],
|
|
304
|
+
LOAD_OPERATION,
|
|
305
|
+
[
|
|
306
|
+
"transformers-env-num-threads-not-positive-safe-integer",
|
|
307
|
+
],
|
|
308
|
+
);
|
|
309
|
+
for (const label of ["cache-open", "dynamic-import", "fetch", "module-worker"]) {
|
|
310
|
+
registerWorkerErrorReasons(
|
|
311
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
312
|
+
BOTH_WORKER_ROLES,
|
|
313
|
+
LOAD_OPERATION,
|
|
314
|
+
[
|
|
315
|
+
`artifact-graph-${label}-edges-not-array`,
|
|
316
|
+
`artifact-graph-${label}-edge-not-object`,
|
|
317
|
+
`artifact-graph-${label}-edge-module-path-empty`,
|
|
318
|
+
`artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
|
|
319
|
+
`artifact-graph-${label}-edge-policy-not-admitted`,
|
|
320
|
+
`artifact-graph-${label}-edge-identity-ambiguous`,
|
|
321
|
+
],
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
registerWorkerErrorReasons(
|
|
325
|
+
"ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
|
|
326
|
+
BOTH_WORKER_ROLES,
|
|
327
|
+
LOAD_OR_USE_OPERATIONS,
|
|
328
|
+
[
|
|
329
|
+
"artifact-graph-dynamic-import-edge-undeclared",
|
|
330
|
+
"artifact-graph-dynamic-import-edge-undeclared-inactive-runtime-branch-entered",
|
|
331
|
+
"artifact-graph-dynamic-import-target-undeclared",
|
|
332
|
+
],
|
|
333
|
+
);
|
|
334
|
+
registerWorkerErrorReasons(
|
|
335
|
+
"ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
|
|
336
|
+
BOTH_WORKER_ROLES,
|
|
337
|
+
LOAD_OR_USE_OPERATIONS,
|
|
338
|
+
[
|
|
339
|
+
"artifact-graph-fetch-edge-undeclared",
|
|
340
|
+
"artifact-graph-fetch-edge-undeclared-inactive-runtime-branch-entered",
|
|
341
|
+
"artifact-graph-fetch-guard-bypassed",
|
|
342
|
+
"artifact-graph-fetch-method-undeclared",
|
|
343
|
+
"artifact-graph-fetch-target-undeclared",
|
|
344
|
+
],
|
|
345
|
+
);
|
|
346
|
+
registerWorkerErrorReasons(
|
|
347
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
348
|
+
BOTH_WORKER_ROLES,
|
|
349
|
+
LOAD_OR_USE_OPERATIONS,
|
|
350
|
+
[
|
|
351
|
+
"artifact-graph-cache-match-guard-bypassed",
|
|
352
|
+
"artifact-graph-cache-name-mismatch",
|
|
353
|
+
"artifact-graph-cache-open-edge-undeclared",
|
|
354
|
+
"artifact-graph-cache-open-edge-undeclared-inactive-runtime-branch-entered",
|
|
355
|
+
"artifact-graph-cache-open-guard-bypassed",
|
|
356
|
+
"artifact-graph-cache-read-target-undeclared",
|
|
357
|
+
],
|
|
358
|
+
);
|
|
359
|
+
registerWorkerErrorReasons(
|
|
360
|
+
"ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
|
|
361
|
+
BOTH_WORKER_ROLES,
|
|
362
|
+
LOAD_OR_USE_OPERATIONS,
|
|
363
|
+
[
|
|
364
|
+
"artifact-graph-module-worker-edge-undeclared",
|
|
365
|
+
"artifact-graph-module-worker-edge-undeclared-inactive-runtime-branch-entered",
|
|
366
|
+
"artifact-graph-module-worker-target-undeclared",
|
|
367
|
+
"artifact-graph-module-worker-type-mismatch",
|
|
368
|
+
],
|
|
369
|
+
);
|
|
370
|
+
registerWorkerErrorReasons(
|
|
371
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
372
|
+
BOTH_WORKER_ROLES,
|
|
373
|
+
LOAD_OPERATION,
|
|
374
|
+
[
|
|
375
|
+
"artifact-graph-cache-isolation-unavailable",
|
|
376
|
+
"artifact-graph-dynamic-code-constructor-isolation-unavailable",
|
|
377
|
+
"artifact-graph-fetch-isolation-unavailable",
|
|
378
|
+
"artifact-graph-guard-global-collision",
|
|
379
|
+
"artifact-graph-guard-global-definition-rejected",
|
|
380
|
+
"artifact-graph-indexeddb-isolation-unavailable",
|
|
381
|
+
"artifact-graph-opfs-isolation-unavailable",
|
|
382
|
+
"artifact-graph-private-message-port-missing",
|
|
383
|
+
"artifact-graph-setinterval-isolation-unavailable",
|
|
384
|
+
"artifact-graph-settimeout-isolation-unavailable",
|
|
385
|
+
"artifact-graph-typed-array-validation-unavailable",
|
|
386
|
+
"speech-worker-cache-isolation-unavailable",
|
|
387
|
+
"speech-worker-fetch-isolation-unavailable",
|
|
388
|
+
],
|
|
389
|
+
);
|
|
390
|
+
registerWorkerErrorReasons(
|
|
391
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
392
|
+
BOTH_WORKER_ROLES,
|
|
393
|
+
LOAD_OR_USE_OPERATIONS,
|
|
394
|
+
[
|
|
395
|
+
"artifact-graph-dynamic-code-constructor-rejected",
|
|
396
|
+
"artifact-graph-guard-capability-mismatch",
|
|
397
|
+
"artifact-graph-setinterval-string-callback-rejected",
|
|
398
|
+
"artifact-graph-settimeout-string-callback-rejected",
|
|
399
|
+
"artifact-graph-typed-array-constructor-intrinsic-mismatch",
|
|
400
|
+
"artifact-graph-typed-array-constructor-receiver-not-typed-array",
|
|
401
|
+
],
|
|
402
|
+
);
|
|
403
|
+
for (const name of [
|
|
404
|
+
"broadcastchannel",
|
|
405
|
+
"eventsource",
|
|
406
|
+
"function",
|
|
407
|
+
"rtcpeerconnection",
|
|
408
|
+
"shadowrealm",
|
|
409
|
+
"sharedworker",
|
|
410
|
+
"websocket",
|
|
411
|
+
"websocketstream",
|
|
412
|
+
"webtransport",
|
|
413
|
+
"worker",
|
|
414
|
+
"xmlhttprequest",
|
|
415
|
+
"eval",
|
|
416
|
+
"importscripts",
|
|
417
|
+
]) {
|
|
418
|
+
registerWorkerErrorReasons(
|
|
419
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
420
|
+
BOTH_WORKER_ROLES,
|
|
421
|
+
LOAD_OR_USE_OPERATIONS,
|
|
422
|
+
[`artifact-graph-${name}-capability-undeclared`],
|
|
423
|
+
);
|
|
424
|
+
registerWorkerErrorReasons(
|
|
425
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
426
|
+
BOTH_WORKER_ROLES,
|
|
427
|
+
LOAD_OPERATION,
|
|
428
|
+
[`artifact-graph-${name}-isolation-unavailable`],
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
registerWorkerErrorReasons(
|
|
432
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
433
|
+
BOTH_WORKER_ROLES,
|
|
434
|
+
LOAD_OR_USE_OPERATIONS,
|
|
435
|
+
[
|
|
436
|
+
"artifact-graph-module-worker-error-envelope-rejected",
|
|
437
|
+
"artifact-graph-module-worker-initialization-message-rejected",
|
|
438
|
+
],
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
function operationFailureReason(role, op) {
|
|
442
|
+
if (op === "load") return `${role}-worker-model-load-rejected`;
|
|
443
|
+
if (op === "use") {
|
|
444
|
+
return role === "stt"
|
|
445
|
+
? "stt-transcription-engine-operation-rejected"
|
|
446
|
+
: "tts-synthesis-engine-operation-rejected";
|
|
447
|
+
}
|
|
448
|
+
if (op === "unload") return `${role}-worker-engine-dispose-rejected`;
|
|
449
|
+
if (op === "dispose") return `${role}-worker-dispose-rejected`;
|
|
450
|
+
if (op === "status") return `${role}-worker-status-rejected`;
|
|
451
|
+
return `${role}-worker-operation-unknown`;
|
|
452
|
+
}
|
|
7
453
|
|
|
8
|
-
function workerError(code, message, cause) {
|
|
454
|
+
function workerError(code, message, cause, reason) {
|
|
9
455
|
const error = cause === undefined
|
|
10
456
|
? new Error(message)
|
|
11
457
|
: new Error(message, { cause });
|
|
@@ -13,15 +459,35 @@ function workerError(code, message, cause) {
|
|
|
13
459
|
? "AbortError"
|
|
14
460
|
: "ArcaneSpeechWorkerError";
|
|
15
461
|
error.code = code;
|
|
462
|
+
if (typeof reason === "string" && reason) error.reason = reason;
|
|
463
|
+
SDK_WORKER_ERRORS.add(error);
|
|
16
464
|
return error;
|
|
17
465
|
}
|
|
18
466
|
|
|
19
|
-
function
|
|
467
|
+
function isSdkWorkerError(value) {
|
|
468
|
+
return value instanceof Error && SDK_WORKER_ERRORS.has(value);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function admitWorkerFailure(error, code, message, reason) {
|
|
472
|
+
return isSdkWorkerError(error)
|
|
473
|
+
? error
|
|
474
|
+
: workerError(code, message, error, reason);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function operationReason(role, op, suffix) {
|
|
478
|
+
const subject = op === "use"
|
|
479
|
+
? role === "stt" ? "stt-transcription" : "tts-synthesis"
|
|
480
|
+
: `${role}-${op}`;
|
|
481
|
+
return `${subject}-${suffix}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function throwIfAborted(signal, reason) {
|
|
20
485
|
if (!signal?.aborted) return;
|
|
21
486
|
throw workerError(
|
|
22
487
|
"ARCANE_AI_REQUEST_ABORTED",
|
|
23
488
|
"The speech worker operation was cancelled.",
|
|
24
489
|
signal.reason,
|
|
490
|
+
reason,
|
|
25
491
|
);
|
|
26
492
|
}
|
|
27
493
|
|
|
@@ -51,119 +517,559 @@ export function collectSpeechTransferables(value) {
|
|
|
51
517
|
return transfers;
|
|
52
518
|
}
|
|
53
519
|
|
|
54
|
-
function serializedError(error) {
|
|
55
|
-
const
|
|
56
|
-
? error.
|
|
57
|
-
:
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
520
|
+
function serializedError(error, role, op) {
|
|
521
|
+
const admission = isSdkWorkerError(error)
|
|
522
|
+
? workerErrorReasonAdmission(error.reason, role, op)
|
|
523
|
+
: null;
|
|
524
|
+
const admittedCode = isSdkWorkerError(error)
|
|
525
|
+
&& Object.hasOwn(WORKER_ERROR_MESSAGES, error.code)
|
|
526
|
+
&& admission?.code === error.code;
|
|
527
|
+
const code = admittedCode ? error.code : "ARCANE_AI_PROVIDER_REQUEST_FAILED";
|
|
528
|
+
const reason = admittedCode ? error.reason : operationFailureReason(role, op);
|
|
529
|
+
return Object.freeze({
|
|
530
|
+
protocol: SPEECH_WORKER_ERROR_PROTOCOL,
|
|
531
|
+
code,
|
|
532
|
+
message: WORKER_ERROR_MESSAGES[code],
|
|
533
|
+
reason,
|
|
65
534
|
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function workerErrorReasonAdmission(reason, role, op) {
|
|
538
|
+
const admission = WORKER_ERROR_REASON_ADMISSIONS.get(reason);
|
|
539
|
+
return admission
|
|
540
|
+
&& admission.roles.includes(role)
|
|
541
|
+
&& admission.operations.includes(op)
|
|
542
|
+
? admission
|
|
543
|
+
: null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function normalizeSpeechWorkerErrorEnvelope(value, role, op) {
|
|
547
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
548
|
+
let descriptors;
|
|
549
|
+
try {
|
|
550
|
+
const keys = Reflect.ownKeys(value);
|
|
551
|
+
if (keys.some((key) => typeof key !== "string")
|
|
552
|
+
|| keys.sort().join(",") !== "code,message,protocol,reason") return null;
|
|
553
|
+
descriptors = Object.getOwnPropertyDescriptors(value);
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
for (const key of ["code", "message", "protocol", "reason"]) {
|
|
558
|
+
if (!Object.hasOwn(descriptors[key], "value")) return null;
|
|
559
|
+
}
|
|
560
|
+
const protocol = descriptors.protocol.value;
|
|
561
|
+
const code = descriptors.code.value;
|
|
562
|
+
const message = descriptors.message.value;
|
|
563
|
+
const reason = descriptors.reason.value;
|
|
564
|
+
if (protocol !== SPEECH_WORKER_ERROR_PROTOCOL) return null;
|
|
565
|
+
if (!Object.hasOwn(WORKER_ERROR_MESSAGES, code)) return null;
|
|
566
|
+
if (message !== WORKER_ERROR_MESSAGES[code]) return null;
|
|
567
|
+
const admission = workerErrorReasonAdmission(reason, role, op);
|
|
568
|
+
if (admission?.code !== code) return null;
|
|
66
569
|
return Object.freeze({
|
|
67
570
|
code,
|
|
68
|
-
message
|
|
571
|
+
message,
|
|
572
|
+
reason,
|
|
69
573
|
});
|
|
70
574
|
}
|
|
71
575
|
|
|
72
|
-
function requiredText(
|
|
576
|
+
function requiredText(
|
|
577
|
+
value,
|
|
578
|
+
label,
|
|
579
|
+
reason,
|
|
580
|
+
) {
|
|
73
581
|
if (typeof value !== "string" || !value.trim()) {
|
|
74
|
-
throw workerError(
|
|
582
|
+
throw workerError(
|
|
583
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
584
|
+
`${label} is required.`,
|
|
585
|
+
undefined,
|
|
586
|
+
reason,
|
|
587
|
+
);
|
|
75
588
|
}
|
|
76
589
|
return value.trim();
|
|
77
590
|
}
|
|
78
591
|
|
|
592
|
+
function requiredSampleRate(value, label, fallback) {
|
|
593
|
+
const candidate = value ?? fallback;
|
|
594
|
+
if (!Number.isSafeInteger(candidate) || candidate < 1) {
|
|
595
|
+
throw workerError(
|
|
596
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
597
|
+
`${label} must be a positive safe integer.`,
|
|
598
|
+
undefined,
|
|
599
|
+
"speech-model-sample-rate-not-positive-safe-integer",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
return candidate;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function graphConfiguration(configuration) {
|
|
606
|
+
return configuration?.runtime?.moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function validateMaterializedFile(file, label) {
|
|
610
|
+
if (!file || typeof file !== "object" || Array.isArray(file)) {
|
|
611
|
+
throw workerError(
|
|
612
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
613
|
+
`${label} must be an object.`,
|
|
614
|
+
undefined,
|
|
615
|
+
"artifact-graph-materialized-file-not-object",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
requiredText(file.path, `${label} path`, "artifact-graph-materialized-path-empty");
|
|
619
|
+
requiredText(file.sourceUrl, `${label} sourceUrl`, "artifact-graph-materialized-source-url-empty");
|
|
620
|
+
requiredText(file.moduleUrl, `${label} moduleUrl`, "artifact-graph-materialized-module-url-empty");
|
|
621
|
+
requiredText(file.mediaType, `${label} mediaType`, "artifact-graph-materialized-media-type-empty");
|
|
622
|
+
if (file.runtimeRequestUrls !== undefined && !Array.isArray(file.runtimeRequestUrls)) {
|
|
623
|
+
throw workerError(
|
|
624
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
625
|
+
`${label} runtimeRequestUrls must be an array.`,
|
|
626
|
+
undefined,
|
|
627
|
+
"artifact-graph-runtime-request-routes-not-array",
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return file;
|
|
631
|
+
}
|
|
632
|
+
|
|
79
633
|
function validateConfiguration(configuration, role) {
|
|
80
634
|
if (!configuration || typeof configuration !== "object" || Array.isArray(configuration)) {
|
|
81
|
-
throw workerError(
|
|
635
|
+
throw workerError(
|
|
636
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
637
|
+
"Speech worker configuration is required.",
|
|
638
|
+
undefined,
|
|
639
|
+
"speech-worker-configuration-missing",
|
|
640
|
+
);
|
|
82
641
|
}
|
|
642
|
+
const moduleGraph = configuration.runtime?.moduleGraph;
|
|
83
643
|
if (
|
|
84
644
|
configuration.role !== role
|
|
85
645
|
|| configuration.runtime?.adapter !== ADAPTERS[role]
|
|
86
|
-
||
|
|
646
|
+
|| (moduleGraph !== "self-contained" && moduleGraph !== ARTIFACT_GRAPH_MODULE_GRAPH)
|
|
87
647
|
) {
|
|
88
|
-
throw workerError(
|
|
648
|
+
throw workerError(
|
|
649
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
650
|
+
"Speech worker role, runtime adapter, or module graph does not match.",
|
|
651
|
+
undefined,
|
|
652
|
+
"speech-worker-runtime-selection-mismatch",
|
|
653
|
+
);
|
|
89
654
|
}
|
|
90
|
-
requiredText(configuration.model?.id, "Speech model id");
|
|
91
|
-
requiredText(configuration.model?.repository, "Speech model repository");
|
|
92
|
-
requiredText(configuration.model?.revision, "Speech model revision");
|
|
93
|
-
requiredText(configuration.runtime?.entry, "Speech runtime entry");
|
|
655
|
+
requiredText(configuration.model?.id, "Speech model id", "speech-worker-model-id-empty");
|
|
656
|
+
requiredText(configuration.model?.repository, "Speech model repository", "speech-worker-model-repository-empty");
|
|
657
|
+
requiredText(configuration.model?.revision, "Speech model revision", "speech-worker-model-revision-empty");
|
|
658
|
+
requiredText(configuration.runtime?.entry, "Speech runtime entry", "speech-worker-runtime-entry-empty");
|
|
94
659
|
if (!Array.isArray(configuration.runtime?.files) || !Array.isArray(configuration.model?.files)) {
|
|
95
|
-
throw workerError(
|
|
660
|
+
throw workerError(
|
|
661
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
662
|
+
"Speech runtime and model files are required.",
|
|
663
|
+
undefined,
|
|
664
|
+
"speech-worker-materialized-files-missing",
|
|
665
|
+
);
|
|
96
666
|
}
|
|
97
667
|
const entry = configuration.runtime.files.find((file) =>
|
|
98
668
|
file.path === configuration.runtime.entry);
|
|
99
669
|
if (!entry?.moduleUrl) {
|
|
100
|
-
throw workerError(
|
|
670
|
+
throw workerError(
|
|
671
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
672
|
+
"Speech runtime entry was not materialized.",
|
|
673
|
+
undefined,
|
|
674
|
+
moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH
|
|
675
|
+
? "artifact-graph-entrypoint-not-materialized"
|
|
676
|
+
: "speech-runtime-entrypoint-not-materialized",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH) {
|
|
680
|
+
const allFiles = [
|
|
681
|
+
...configuration.runtime.files,
|
|
682
|
+
...configuration.model.files,
|
|
683
|
+
];
|
|
684
|
+
const paths = new Set();
|
|
685
|
+
for (let index = 0; index < allFiles.length; index += 1) {
|
|
686
|
+
const file = validateMaterializedFile(allFiles[index], `Speech file ${String(index)}`);
|
|
687
|
+
if (paths.has(file.path)) {
|
|
688
|
+
throw workerError(
|
|
689
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
690
|
+
"Speech materialized file paths must be unique.",
|
|
691
|
+
undefined,
|
|
692
|
+
"artifact-graph-materialized-path-ambiguous",
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
paths.add(file.path);
|
|
696
|
+
}
|
|
697
|
+
const runtimePaths = new Set(configuration.runtime.files.map((file) => file.path));
|
|
698
|
+
if (
|
|
699
|
+
configuration.security?.secure !== true
|
|
700
|
+
|| typeof configuration.runtime.artifactGraphId !== "string"
|
|
701
|
+
|| !/^[a-f0-9]{64}$/u.test(configuration.runtime.artifactGraphId)
|
|
702
|
+
|| (
|
|
703
|
+
typeof configuration.runtime.guardCapability !== "string"
|
|
704
|
+
|| !/^[a-f0-9]{64}$/u.test(configuration.runtime.guardCapability)
|
|
705
|
+
|| !STRICT_GRAPH_ADMISSIONS.has(configuration.runtime.artifactGraphAdmission)
|
|
706
|
+
|| !configuration.runtime.edges
|
|
707
|
+
|| typeof configuration.runtime.edges !== "object"
|
|
708
|
+
|| !Array.isArray(configuration.runtime.transforms)
|
|
709
|
+
)
|
|
710
|
+
) {
|
|
711
|
+
throw workerError(
|
|
712
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
713
|
+
"Explicit secure:true plus authenticated artifact graph identity, admission, edges, and transforms are required.",
|
|
714
|
+
undefined,
|
|
715
|
+
"artifact-graph-worker-configuration-incomplete",
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const wasm = configuration.runtime.onnxWasm;
|
|
719
|
+
if (
|
|
720
|
+
!wasm
|
|
721
|
+
|| wasm.namespace !== ONNX_NAMESPACES[role]
|
|
722
|
+
|| !runtimePaths.has(wasm.mjsPath)
|
|
723
|
+
|| !runtimePaths.has(wasm.wasmPath)
|
|
724
|
+
) {
|
|
725
|
+
throw workerError(
|
|
726
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
727
|
+
"The authenticated ONNX Runtime Web MJS/WASM pair is incomplete or uses the wrong namespace.",
|
|
728
|
+
undefined,
|
|
729
|
+
"artifact-graph-onnx-wasm-configuration-mismatch",
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (role === "tts" && wasm.numThreads !== undefined) {
|
|
733
|
+
throw workerError(
|
|
734
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
735
|
+
"Kokoro does not expose a verified numThreads configuration field.",
|
|
736
|
+
undefined,
|
|
737
|
+
"kokoro-env-num-threads-field-not-exposed",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
if (
|
|
741
|
+
wasm.numThreads !== undefined
|
|
742
|
+
&& (!Number.isSafeInteger(wasm.numThreads) || wasm.numThreads < 1)
|
|
743
|
+
) {
|
|
744
|
+
throw workerError(
|
|
745
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
746
|
+
"Transformers numThreads must be a positive safe integer.",
|
|
747
|
+
undefined,
|
|
748
|
+
"transformers-env-num-threads-not-positive-safe-integer",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
requiredText(configuration.model?.dtype, "Speech model dtype", "speech-worker-model-dtype-empty");
|
|
752
|
+
if (role === "stt") {
|
|
753
|
+
requiredSampleRate(configuration.model.inputSampleRate, "Whisper inputSampleRate");
|
|
754
|
+
} else {
|
|
755
|
+
requiredSampleRate(configuration.model.outputSampleRate, "Kokoro outputSampleRate");
|
|
756
|
+
requiredText(configuration.model.defaultVoice, "Kokoro defaultVoice", "tts-default-voice-empty");
|
|
757
|
+
if (!Array.isArray(configuration.model.voices) || configuration.model.voices.length < 1) {
|
|
758
|
+
throw workerError(
|
|
759
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
760
|
+
"Kokoro graph configuration requires the caller-owned voice inventory.",
|
|
761
|
+
undefined,
|
|
762
|
+
"artifact-graph-kokoro-voice-inventory-missing",
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (
|
|
767
|
+
configuration.runtime.negativeRuntimeRequestUrls !== undefined
|
|
768
|
+
&& !Array.isArray(configuration.runtime.negativeRuntimeRequestUrls)
|
|
769
|
+
) {
|
|
770
|
+
throw workerError(
|
|
771
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
772
|
+
"Artifact graph negative runtime request routes must be an array.",
|
|
773
|
+
undefined,
|
|
774
|
+
"artifact-graph-negative-request-routes-not-array",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
101
777
|
}
|
|
102
778
|
return configuration;
|
|
103
779
|
}
|
|
104
780
|
|
|
105
|
-
function
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
781
|
+
function absoluteRequestUrl(value, scope) {
|
|
782
|
+
const input = typeof value === "string" || value instanceof URL
|
|
783
|
+
? String(value)
|
|
784
|
+
: value?.url;
|
|
785
|
+
try {
|
|
786
|
+
return new URL(input, scope.location?.href).href;
|
|
787
|
+
} catch {
|
|
788
|
+
throw workerError(
|
|
789
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
790
|
+
"The speech engine requested a malformed artifact URL.",
|
|
791
|
+
undefined,
|
|
792
|
+
"artifact-graph-runtime-request-url-malformed",
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function createArtifactRoutes(scope, configuration) {
|
|
798
|
+
const positive = new Map();
|
|
799
|
+
const negative = new Set();
|
|
800
|
+
const files = [
|
|
109
801
|
...configuration.runtime.files,
|
|
110
802
|
...configuration.model.files,
|
|
111
|
-
]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
function addPositive(route, file) {
|
|
806
|
+
const absolute = absoluteRequestUrl(route, scope);
|
|
807
|
+
const existing = positive.get(absolute);
|
|
808
|
+
if (existing && existing.path !== file.path) {
|
|
809
|
+
throw workerError(
|
|
810
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
811
|
+
"An artifact graph runtime request route names more than one file.",
|
|
812
|
+
undefined,
|
|
813
|
+
"artifact-graph-runtime-request-route-ambiguous",
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
positive.set(absolute, file);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
for (const file of files) {
|
|
820
|
+
addPositive(file.sourceUrl, file);
|
|
821
|
+
addPositive(file.moduleUrl, file);
|
|
822
|
+
for (const route of file.runtimeRequestUrls ?? []) addPositive(route, file);
|
|
823
|
+
}
|
|
824
|
+
for (const route of configuration.runtime.negativeRuntimeRequestUrls ?? []) {
|
|
825
|
+
const absolute = absoluteRequestUrl(route, scope);
|
|
826
|
+
if (positive.has(absolute) || negative.has(absolute)) {
|
|
827
|
+
throw workerError(
|
|
828
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
829
|
+
"Artifact graph positive and negative runtime request routes overlap.",
|
|
830
|
+
undefined,
|
|
831
|
+
"artifact-graph-runtime-request-route-ambiguous",
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
negative.add(absolute);
|
|
115
835
|
}
|
|
116
|
-
return Object.freeze({
|
|
836
|
+
return Object.freeze({ files, positive, negative });
|
|
117
837
|
}
|
|
118
838
|
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
839
|
+
function requestMethod(input, init, scope) {
|
|
840
|
+
const RequestConstructor = scope.Request ?? globalThis.Request;
|
|
841
|
+
const inherited = typeof RequestConstructor === "function" && input instanceof RequestConstructor
|
|
842
|
+
? input.method
|
|
843
|
+
: "GET";
|
|
844
|
+
return String(init?.method ?? inherited ?? "GET").toUpperCase();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function installScopeValue(scope, name, value, reason) {
|
|
848
|
+
const descriptor = Object.getOwnPropertyDescriptor(scope, name);
|
|
849
|
+
if (descriptor?.configurable === false && descriptor.writable !== true) {
|
|
850
|
+
throw workerError(
|
|
851
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
852
|
+
`The speech Worker cannot isolate globalThis.${name}.`,
|
|
853
|
+
undefined,
|
|
854
|
+
reason,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
if (descriptor?.configurable === false) {
|
|
859
|
+
scope[name] = value;
|
|
860
|
+
} else {
|
|
861
|
+
Object.defineProperty(scope, name, {
|
|
862
|
+
configurable: true,
|
|
863
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
864
|
+
writable: false,
|
|
865
|
+
value,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw workerError(
|
|
870
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
871
|
+
`The speech Worker could not isolate globalThis.${name}.`,
|
|
872
|
+
error,
|
|
873
|
+
reason,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return function restoreScopeValue() {
|
|
877
|
+
if (descriptor) Object.defineProperty(scope, name, descriptor);
|
|
878
|
+
else delete scope[name];
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function restoreScopeValues(restores) {
|
|
883
|
+
for (const restore of restores.splice(0).reverse()) {
|
|
130
884
|
try {
|
|
131
|
-
|
|
885
|
+
restore();
|
|
132
886
|
} catch {
|
|
133
|
-
|
|
887
|
+
// Terminating the owning Worker remains the final isolation boundary.
|
|
134
888
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function installObjectValue(object, name, value, label, reason) {
|
|
893
|
+
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
|
894
|
+
if (descriptor?.configurable === false && descriptor.writable !== true) {
|
|
138
895
|
throw workerError(
|
|
139
|
-
"
|
|
140
|
-
|
|
896
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
897
|
+
`The speech Worker cannot isolate ${label}.`,
|
|
898
|
+
undefined,
|
|
899
|
+
reason,
|
|
141
900
|
);
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
Object.defineProperty(object, name, {
|
|
904
|
+
configurable: true,
|
|
905
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
906
|
+
writable: false,
|
|
907
|
+
value,
|
|
908
|
+
});
|
|
909
|
+
} catch (error) {
|
|
910
|
+
throw workerError(
|
|
911
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
912
|
+
`The speech Worker could not isolate ${label}.`,
|
|
913
|
+
error,
|
|
914
|
+
reason,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
return function restoreObjectValue() {
|
|
918
|
+
if (descriptor) Object.defineProperty(object, name, descriptor);
|
|
919
|
+
else delete object[name];
|
|
142
920
|
};
|
|
143
|
-
return function restoreSpeechWorkerFetch() {
|
|
144
|
-
scope.fetch = original;
|
|
145
|
-
};
|
|
146
921
|
}
|
|
147
922
|
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
923
|
+
function createRouteReader(scope, configuration, originalFetch) {
|
|
924
|
+
const routes = createArtifactRoutes(scope, configuration);
|
|
925
|
+
const ResponseConstructor = scope.Response ?? globalThis.Response;
|
|
926
|
+
|
|
927
|
+
async function responseFor(resolution, signal) {
|
|
928
|
+
if (resolution.kind === "negative") {
|
|
929
|
+
if (typeof ResponseConstructor !== "function") {
|
|
930
|
+
throw workerError(
|
|
931
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
932
|
+
"Browser Response is unavailable in the speech Worker.",
|
|
933
|
+
undefined,
|
|
934
|
+
"artifact-graph-negative-response-constructor-unavailable",
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
return new ResponseConstructor(null, {
|
|
938
|
+
status: 404,
|
|
939
|
+
statusText: "Not Found",
|
|
940
|
+
headers: { "cache-control": "no-store" },
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
return originalFetch(resolution.file.moduleUrl, {
|
|
944
|
+
method: "GET",
|
|
945
|
+
credentials: "omit",
|
|
946
|
+
redirect: "error",
|
|
947
|
+
signal,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function resolve(input) {
|
|
952
|
+
const absolute = absoluteRequestUrl(input, scope);
|
|
953
|
+
const file = routes.positive.get(absolute);
|
|
954
|
+
if (file) return Object.freeze({ kind: "file", absolute, file });
|
|
955
|
+
if (routes.negative.has(absolute)) {
|
|
956
|
+
return Object.freeze({ kind: "negative", absolute, file: null });
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function cacheForPaths(targetPaths) {
|
|
962
|
+
const admittedPaths = new Set(targetPaths);
|
|
963
|
+
return Object.freeze({
|
|
964
|
+
async match(input) {
|
|
965
|
+
const resolution = resolve(input);
|
|
966
|
+
if (
|
|
967
|
+
!resolution
|
|
968
|
+
|| resolution.kind !== "file"
|
|
969
|
+
|| !admittedPaths.has(resolution.file.path)
|
|
970
|
+
) {
|
|
971
|
+
throw workerError(
|
|
972
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
973
|
+
"The runtime cache read target is outside its admitted artifact graph edge.",
|
|
974
|
+
undefined,
|
|
975
|
+
"artifact-graph-cache-read-target-undeclared",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return responseFor(resolution);
|
|
979
|
+
},
|
|
980
|
+
async put() {
|
|
981
|
+
throw workerError(
|
|
982
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
983
|
+
"Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
|
|
984
|
+
undefined,
|
|
985
|
+
"artifact-graph-cache-write-rejected",
|
|
986
|
+
);
|
|
987
|
+
},
|
|
988
|
+
async add() {
|
|
989
|
+
throw workerError(
|
|
990
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
991
|
+
"Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
|
|
992
|
+
undefined,
|
|
993
|
+
"artifact-graph-cache-write-rejected",
|
|
994
|
+
);
|
|
995
|
+
},
|
|
996
|
+
async addAll() {
|
|
997
|
+
throw workerError(
|
|
998
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
999
|
+
"Speech runtime cache writes are disabled; DBOPFS is the sole durable artifact store.",
|
|
1000
|
+
undefined,
|
|
1001
|
+
"artifact-graph-cache-write-rejected",
|
|
1002
|
+
);
|
|
1003
|
+
},
|
|
1004
|
+
async delete() {
|
|
1005
|
+
return false;
|
|
1006
|
+
},
|
|
1007
|
+
async keys() {
|
|
1008
|
+
return Object.freeze([]);
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const cache = cacheForPaths(routes.files.map((file) => file.path));
|
|
1014
|
+
return Object.freeze({ cache, cacheForPaths, resolve, responseFor, routes });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function installLegacyAuthorizedFetch(scope, configuration) {
|
|
1018
|
+
const original = scope.fetch?.bind(scope);
|
|
1019
|
+
if (typeof original !== "function") {
|
|
151
1020
|
throw workerError(
|
|
152
1021
|
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
153
|
-
"
|
|
1022
|
+
"Browser fetch is unavailable in the speech Worker.",
|
|
1023
|
+
undefined,
|
|
1024
|
+
"speech-worker-fetch-unavailable",
|
|
154
1025
|
);
|
|
155
1026
|
}
|
|
1027
|
+
const reader = createRouteReader(scope, configuration, original);
|
|
1028
|
+
const authorized = async function fetchAuthorizedSpeechArtifact(input, init) {
|
|
1029
|
+
if (requestMethod(input, init, scope) !== "GET") {
|
|
1030
|
+
throw workerError(
|
|
1031
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
1032
|
+
"Legacy speech artifacts admit only GET requests.",
|
|
1033
|
+
undefined,
|
|
1034
|
+
"speech-worker-artifact-request-method-rejected",
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
const resolution = reader.resolve(input);
|
|
1038
|
+
if (!resolution) {
|
|
1039
|
+
throw workerError(
|
|
1040
|
+
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
1041
|
+
"The speech engine requested an artifact outside its admitted file map.",
|
|
1042
|
+
undefined,
|
|
1043
|
+
"speech-worker-artifact-request-undeclared",
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return reader.responseFor(resolution, init?.signal);
|
|
1047
|
+
};
|
|
1048
|
+
const restore = installScopeValue(
|
|
1049
|
+
scope,
|
|
1050
|
+
"fetch",
|
|
1051
|
+
authorized,
|
|
1052
|
+
"speech-worker-fetch-isolation-unavailable",
|
|
1053
|
+
);
|
|
1054
|
+
return Object.freeze({ cache: null, cleanup: restore });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function installDeniedCacheIsolation(scope) {
|
|
156
1058
|
const denied = Object.freeze({
|
|
157
1059
|
async open() {
|
|
158
1060
|
throw workerError(
|
|
159
1061
|
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
160
1062
|
"Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
|
|
1063
|
+
undefined,
|
|
1064
|
+
"speech-worker-cache-open-rejected",
|
|
161
1065
|
);
|
|
162
1066
|
},
|
|
163
1067
|
async match() {
|
|
164
1068
|
throw workerError(
|
|
165
1069
|
"ARCANE_AI_UNDECLARED_ARTIFACT",
|
|
166
1070
|
"Speech runtime cache access is disabled; DBOPFS admission is the sole artifact source.",
|
|
1071
|
+
undefined,
|
|
1072
|
+
"speech-worker-cache-match-rejected",
|
|
167
1073
|
);
|
|
168
1074
|
},
|
|
169
1075
|
async has() {
|
|
@@ -176,54 +1082,915 @@ function installCacheIsolation(scope) {
|
|
|
176
1082
|
return false;
|
|
177
1083
|
},
|
|
178
1084
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
1085
|
+
return installScopeValue(
|
|
1086
|
+
scope,
|
|
1087
|
+
"caches",
|
|
1088
|
+
denied,
|
|
1089
|
+
"speech-worker-cache-isolation-unavailable",
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function edgeKey(modulePath, occurrence) {
|
|
1094
|
+
return `${modulePath}\u0000${String(occurrence)}`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function edgeMap(edges, label) {
|
|
1098
|
+
if (!Array.isArray(edges)) {
|
|
1099
|
+
throw workerError(
|
|
1100
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1101
|
+
`Artifact graph ${label} edges must be an array.`,
|
|
1102
|
+
undefined,
|
|
1103
|
+
`artifact-graph-${label}-edges-not-array`,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const map = new Map();
|
|
1107
|
+
for (const edge of edges) {
|
|
1108
|
+
if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
|
|
1109
|
+
throw workerError(
|
|
1110
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1111
|
+
`Artifact graph ${label} edge must be an object.`,
|
|
1112
|
+
undefined,
|
|
1113
|
+
`artifact-graph-${label}-edge-not-object`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof edge.modulePath !== "string" || !edge.modulePath) {
|
|
1117
|
+
throw workerError(
|
|
1118
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1119
|
+
`Artifact graph ${label} edge modulePath must be nonempty.`,
|
|
1120
|
+
undefined,
|
|
1121
|
+
`artifact-graph-${label}-edge-module-path-empty`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (!Number.isSafeInteger(edge.occurrence) || edge.occurrence < 1) {
|
|
1125
|
+
throw workerError(
|
|
1126
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1127
|
+
`Artifact graph ${label} edge occurrence must be a positive safe integer.`,
|
|
1128
|
+
undefined,
|
|
1129
|
+
`artifact-graph-${label}-edge-occurrence-not-positive-safe-integer`,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (edge.edgePolicy !== "artifact-targets-admitted"
|
|
1133
|
+
&& edge.edgePolicy !== "inactive-runtime-branch-rejected") {
|
|
1134
|
+
throw workerError(
|
|
1135
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1136
|
+
`Artifact graph ${label} edge policy is not admitted.`,
|
|
1137
|
+
undefined,
|
|
1138
|
+
`artifact-graph-${label}-edge-policy-not-admitted`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const key = edgeKey(edge.modulePath, edge.occurrence);
|
|
1142
|
+
if (map.has(key)) {
|
|
1143
|
+
throw workerError(
|
|
1144
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1145
|
+
`Artifact graph ${label} edge identity is ambiguous.`,
|
|
1146
|
+
undefined,
|
|
1147
|
+
`artifact-graph-${label}-edge-identity-ambiguous`,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
map.set(key, edge);
|
|
1151
|
+
}
|
|
1152
|
+
return map;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function transformSet(transforms, kind) {
|
|
1156
|
+
const result = new Set();
|
|
1157
|
+
for (const transform of transforms) {
|
|
1158
|
+
if (!ARTIFACT_GRAPH_TRANSFORM_KINDS.has(transform?.kind)) {
|
|
1159
|
+
throw workerError(
|
|
1160
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1161
|
+
"Artifact graph Worker configuration contains a transform kind that is not admitted.",
|
|
1162
|
+
undefined,
|
|
1163
|
+
"artifact-graph-transform-kind-not-admitted",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
if (transform?.kind !== kind) continue;
|
|
1167
|
+
if (typeof transform.modulePath !== "string" || !transform.modulePath) {
|
|
1168
|
+
throw workerError(
|
|
1169
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1170
|
+
`Artifact graph ${kind} transform modulePath must be nonempty.`,
|
|
1171
|
+
undefined,
|
|
1172
|
+
"artifact-graph-transform-module-path-empty",
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (!Number.isSafeInteger(transform.occurrence) || transform.occurrence < 1) {
|
|
1176
|
+
throw workerError(
|
|
1177
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1178
|
+
`Artifact graph ${kind} transform occurrence must be a positive safe integer.`,
|
|
1179
|
+
undefined,
|
|
1180
|
+
"artifact-graph-transform-occurrence-not-positive-safe-integer",
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const key = edgeKey(transform.modulePath, transform.occurrence);
|
|
1184
|
+
if (result.has(key)) {
|
|
1185
|
+
throw workerError(
|
|
1186
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1187
|
+
`Artifact graph ${kind} transform identity is ambiguous.`,
|
|
1188
|
+
undefined,
|
|
1189
|
+
"artifact-graph-transform-identity-ambiguous",
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
result.add(key);
|
|
1193
|
+
}
|
|
1194
|
+
return result;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function typedArrayIntrinsics(scope) {
|
|
1198
|
+
const constructors = new Map();
|
|
1199
|
+
for (const name of [
|
|
1200
|
+
"BigInt64Array",
|
|
1201
|
+
"BigUint64Array",
|
|
1202
|
+
"Float32Array",
|
|
1203
|
+
"Float64Array",
|
|
1204
|
+
"Int8Array",
|
|
1205
|
+
"Int16Array",
|
|
1206
|
+
"Int32Array",
|
|
1207
|
+
"Uint8Array",
|
|
1208
|
+
"Uint8ClampedArray",
|
|
1209
|
+
"Uint16Array",
|
|
1210
|
+
"Uint32Array",
|
|
1211
|
+
]) {
|
|
1212
|
+
const Constructor = scope[name] ?? globalThis[name];
|
|
1213
|
+
if (typeof Constructor === "function" && Constructor.prototype) {
|
|
1214
|
+
constructors.set(Constructor.prototype, Constructor);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
const ArrayBufferConstructor = scope.ArrayBuffer ?? globalThis.ArrayBuffer;
|
|
1218
|
+
const DataViewConstructor = scope.DataView ?? globalThis.DataView;
|
|
1219
|
+
if (typeof ArrayBufferConstructor?.isView !== "function") {
|
|
1220
|
+
throw workerError(
|
|
1221
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1222
|
+
"Authenticated artifact graph typed-array validation is unavailable.",
|
|
1223
|
+
undefined,
|
|
1224
|
+
"artifact-graph-typed-array-validation-unavailable",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
return Object.freeze({
|
|
1228
|
+
constructors,
|
|
1229
|
+
dataViewPrototype: DataViewConstructor?.prototype ?? null,
|
|
1230
|
+
getPrototypeOf: Object.getPrototypeOf,
|
|
1231
|
+
hasOwn: Object.hasOwn,
|
|
1232
|
+
isView: ArrayBufferConstructor.isView.bind(ArrayBufferConstructor),
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function runtimeFileMap(configuration) {
|
|
1237
|
+
return new Map(configuration.runtime.files.map((file) => [file.path, file]));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function targetRecords(edge) {
|
|
1241
|
+
if (Array.isArray(edge.targets)) return edge.targets;
|
|
1242
|
+
if (Array.isArray(edge.targetPaths)) {
|
|
1243
|
+
return edge.targetPaths.map((targetPath) => ({ targetPath }));
|
|
1244
|
+
}
|
|
1245
|
+
return [];
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function targetForRequest(edge, requested, files, sourceFile) {
|
|
1249
|
+
const value = String(requested);
|
|
1250
|
+
for (const target of targetRecords(edge)) {
|
|
1251
|
+
const file = files.get(target?.targetPath);
|
|
1252
|
+
if (!file) continue;
|
|
1253
|
+
if (target.match === "exact-runtime-specifier") {
|
|
1254
|
+
if (value === target.exactSpecifier) return file;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (target.match === "self-module-url") {
|
|
1258
|
+
if (file.path === sourceFile?.path && value === sourceFile.moduleUrl) return file;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (target.match === "materialized-module-url" && value === file.moduleUrl) {
|
|
1262
|
+
return file;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function nestedWorkerUrl(role) {
|
|
1269
|
+
const url = new URL(
|
|
1270
|
+
role === "stt"
|
|
1271
|
+
? "./browser-whisper-worker.mjs"
|
|
1272
|
+
: "./browser-kokoro-worker.mjs",
|
|
1273
|
+
import.meta.url,
|
|
1274
|
+
);
|
|
1275
|
+
url.searchParams.set("arcaneSpeechWorkerMode", "artifact-module-worker");
|
|
1276
|
+
return url;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function nestedWorkerFailureEvent(scope, error) {
|
|
1280
|
+
const ErrorEventConstructor = scope.ErrorEvent ?? globalThis.ErrorEvent;
|
|
1281
|
+
if (typeof ErrorEventConstructor === "function") {
|
|
1282
|
+
return new ErrorEventConstructor("error", {
|
|
1283
|
+
error,
|
|
1284
|
+
message: error.message,
|
|
1285
|
+
cancelable: true,
|
|
185
1286
|
});
|
|
1287
|
+
}
|
|
1288
|
+
const EventConstructor = scope.Event ?? globalThis.Event;
|
|
1289
|
+
const event = new EventConstructor("error", { cancelable: true });
|
|
1290
|
+
Object.defineProperties(event, {
|
|
1291
|
+
error: { value: error },
|
|
1292
|
+
message: { value: error.message },
|
|
1293
|
+
});
|
|
1294
|
+
return event;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function createArtifactGraphGuard(scope, configuration, role, reader, originalWorker) {
|
|
1298
|
+
const files = runtimeFileMap(configuration);
|
|
1299
|
+
const edges = configuration.runtime.edges;
|
|
1300
|
+
const imports = edgeMap(edges.dynamicImports ?? [], "dynamic-import");
|
|
1301
|
+
const fetches = edgeMap(edges.fetches ?? [], "fetch");
|
|
1302
|
+
const workers = edgeMap(edges.moduleWorkers ?? [], "module-worker");
|
|
1303
|
+
const cacheOpens = edgeMap(edges.cacheOpens ?? [], "cache-open");
|
|
1304
|
+
const typedArrayConstructors = transformSet(
|
|
1305
|
+
configuration.runtime.transforms,
|
|
1306
|
+
"typed-array-constructor",
|
|
1307
|
+
);
|
|
1308
|
+
transformSet(configuration.runtime.transforms, "function-return-this-to-global-this");
|
|
1309
|
+
const typedArrays = typedArrayIntrinsics(scope);
|
|
1310
|
+
const guardCapability = configuration.runtime.guardCapability;
|
|
1311
|
+
const nestedWorkers = new Set();
|
|
1312
|
+
|
|
1313
|
+
function assertGuardCapability(candidate) {
|
|
1314
|
+
if (candidate === guardCapability) return;
|
|
1315
|
+
throw workerError(
|
|
1316
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1317
|
+
"Authenticated artifact graph runtime guard capability did not match.",
|
|
1318
|
+
undefined,
|
|
1319
|
+
"artifact-graph-guard-capability-mismatch",
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function declaredEdge(map, modulePath, occurrence, code, reason) {
|
|
1324
|
+
const edge = map.get(edgeKey(modulePath, occurrence));
|
|
1325
|
+
if (edge?.edgePolicy === "artifact-targets-admitted") return edge;
|
|
1326
|
+
if (edge?.edgePolicy === "inactive-runtime-branch-rejected") {
|
|
1327
|
+
throw workerError(
|
|
1328
|
+
code,
|
|
1329
|
+
"The runtime entered an artifact graph branch declared inactive.",
|
|
1330
|
+
undefined,
|
|
1331
|
+
`${reason}-inactive-runtime-branch-entered`,
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
throw workerError(
|
|
1335
|
+
code,
|
|
1336
|
+
"The runtime used an undeclared authenticated artifact graph edge.",
|
|
1337
|
+
undefined,
|
|
1338
|
+
reason,
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function fetchEdgeAllows(edge, resolution) {
|
|
1343
|
+
if (resolution.kind === "negative") {
|
|
1344
|
+
return (edge.negativeRuntimeRequestUrls ?? [])
|
|
1345
|
+
.map((route) => absoluteRequestUrl(route, scope))
|
|
1346
|
+
.includes(resolution.absolute);
|
|
1347
|
+
}
|
|
1348
|
+
const paths = new Set([
|
|
1349
|
+
...(edge.targetPaths ?? []),
|
|
1350
|
+
...targetRecords(edge).map((target) => target?.targetPath),
|
|
1351
|
+
]);
|
|
1352
|
+
if (!paths.has(resolution.file.path)) return false;
|
|
1353
|
+
if (resolution.absolute === absoluteRequestUrl(resolution.file.moduleUrl, scope)) {
|
|
1354
|
+
return edge.allowMaterializedUrls === true;
|
|
1355
|
+
}
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const guard = Object.freeze({
|
|
1360
|
+
protocol: "arcane-ai-browser-speech-artifact-graph-runtime/1",
|
|
1361
|
+
|
|
1362
|
+
async dynamicImport(capability, modulePath, occurrence, specifier) {
|
|
1363
|
+
assertGuardCapability(capability);
|
|
1364
|
+
const edge = declaredEdge(
|
|
1365
|
+
imports,
|
|
1366
|
+
modulePath,
|
|
1367
|
+
occurrence,
|
|
1368
|
+
"ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
|
|
1369
|
+
"artifact-graph-dynamic-import-edge-undeclared",
|
|
1370
|
+
);
|
|
1371
|
+
const sourceFile = files.get(modulePath);
|
|
1372
|
+
const target = targetForRequest(edge, specifier, files, sourceFile);
|
|
1373
|
+
if (!target) {
|
|
1374
|
+
throw workerError(
|
|
1375
|
+
"ARCANE_AI_ARTIFACT_GRAPH_IMPORT_EDGE_UNDECLARED",
|
|
1376
|
+
"The runtime dynamic import target is outside its admitted artifact graph edge.",
|
|
1377
|
+
undefined,
|
|
1378
|
+
"artifact-graph-dynamic-import-target-undeclared",
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
return import(target.moduleUrl);
|
|
1382
|
+
},
|
|
1383
|
+
|
|
1384
|
+
async fetch(capability, modulePath, occurrence, input, init) {
|
|
1385
|
+
assertGuardCapability(capability);
|
|
1386
|
+
const edge = declaredEdge(
|
|
1387
|
+
fetches,
|
|
1388
|
+
modulePath,
|
|
1389
|
+
occurrence,
|
|
1390
|
+
"ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
|
|
1391
|
+
"artifact-graph-fetch-edge-undeclared",
|
|
1392
|
+
);
|
|
1393
|
+
const method = requestMethod(input, init, scope);
|
|
1394
|
+
if (!(edge.methods ?? ["GET"]).includes(method)) {
|
|
1395
|
+
throw workerError(
|
|
1396
|
+
"ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
|
|
1397
|
+
"The runtime fetch method is outside its admitted artifact graph edge.",
|
|
1398
|
+
undefined,
|
|
1399
|
+
"artifact-graph-fetch-method-undeclared",
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const resolution = reader.resolve(input);
|
|
1403
|
+
if (!resolution || !fetchEdgeAllows(edge, resolution)) {
|
|
1404
|
+
throw workerError(
|
|
1405
|
+
"ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
|
|
1406
|
+
"The runtime fetch target is outside its admitted artifact graph edge.",
|
|
1407
|
+
undefined,
|
|
1408
|
+
"artifact-graph-fetch-target-undeclared",
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return reader.responseFor(resolution, init?.signal);
|
|
1412
|
+
},
|
|
1413
|
+
|
|
1414
|
+
async openCache(capability, modulePath, occurrence, cacheName) {
|
|
1415
|
+
assertGuardCapability(capability);
|
|
1416
|
+
const edge = declaredEdge(
|
|
1417
|
+
cacheOpens,
|
|
1418
|
+
modulePath,
|
|
1419
|
+
occurrence,
|
|
1420
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
1421
|
+
"artifact-graph-cache-open-edge-undeclared",
|
|
1422
|
+
);
|
|
1423
|
+
if (cacheName !== edge.cacheName) {
|
|
1424
|
+
throw workerError(
|
|
1425
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
1426
|
+
"The runtime cache name is outside its admitted artifact graph edge.",
|
|
1427
|
+
undefined,
|
|
1428
|
+
"artifact-graph-cache-name-mismatch",
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
return reader.cacheForPaths(edge.targetPaths);
|
|
1432
|
+
},
|
|
1433
|
+
|
|
1434
|
+
createWorker(capability, modulePath, occurrence, specifier, options = {}) {
|
|
1435
|
+
assertGuardCapability(capability);
|
|
1436
|
+
const edge = declaredEdge(
|
|
1437
|
+
workers,
|
|
1438
|
+
modulePath,
|
|
1439
|
+
occurrence,
|
|
1440
|
+
"ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
|
|
1441
|
+
"artifact-graph-module-worker-edge-undeclared",
|
|
1442
|
+
);
|
|
1443
|
+
const sourceFile = files.get(modulePath);
|
|
1444
|
+
const target = targetForRequest(edge, specifier, files, sourceFile);
|
|
1445
|
+
if (!target) {
|
|
1446
|
+
throw workerError(
|
|
1447
|
+
"ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
|
|
1448
|
+
"The runtime module Worker target is outside its admitted artifact graph edge.",
|
|
1449
|
+
undefined,
|
|
1450
|
+
"artifact-graph-module-worker-target-undeclared",
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
if (options?.type !== "module") {
|
|
1454
|
+
throw workerError(
|
|
1455
|
+
"ARCANE_AI_ARTIFACT_GRAPH_WORKER_EDGE_UNDECLARED",
|
|
1456
|
+
"Authenticated artifact graph child Workers must be module Workers.",
|
|
1457
|
+
undefined,
|
|
1458
|
+
"artifact-graph-module-worker-type-mismatch",
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
if (typeof originalWorker !== "function") {
|
|
1462
|
+
throw workerError(
|
|
1463
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1464
|
+
"Nested browser module Workers are unavailable.",
|
|
1465
|
+
undefined,
|
|
1466
|
+
"artifact-graph-module-worker-constructor-unavailable",
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
const worker = new originalWorker(nestedWorkerUrl(role), {
|
|
1470
|
+
type: "module",
|
|
1471
|
+
name: typeof options.name === "string" ? options.name : "arcane-speech-artifact-module",
|
|
1472
|
+
});
|
|
1473
|
+
nestedWorkers.add(worker);
|
|
1474
|
+
const onBootstrapMessage = (event) => {
|
|
1475
|
+
if (event.data?.protocol !== NESTED_WORKER_PROTOCOL
|
|
1476
|
+
|| event.data?.event !== "artifact-module-worker-bootstrap-rejected") return;
|
|
1477
|
+
event.stopImmediatePropagation?.();
|
|
1478
|
+
const admitted = normalizeSpeechWorkerErrorEnvelope(event.data.error, role, "load");
|
|
1479
|
+
const failure = admitted
|
|
1480
|
+
? workerError(admitted.code, admitted.message, undefined, admitted.reason)
|
|
1481
|
+
: workerError(
|
|
1482
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
1483
|
+
"The authenticated artifact module Worker error envelope was rejected.",
|
|
1484
|
+
undefined,
|
|
1485
|
+
"artifact-graph-module-worker-error-envelope-rejected",
|
|
1486
|
+
);
|
|
1487
|
+
worker.dispatchEvent?.(nestedWorkerFailureEvent(scope, failure));
|
|
1488
|
+
};
|
|
1489
|
+
worker.addEventListener?.("message", onBootstrapMessage);
|
|
1490
|
+
try {
|
|
1491
|
+
worker.postMessage({
|
|
1492
|
+
protocol: NESTED_WORKER_PROTOCOL,
|
|
1493
|
+
op: "initialize-authenticated-artifact-module-worker",
|
|
1494
|
+
role,
|
|
1495
|
+
targetPath: target.path,
|
|
1496
|
+
configuration,
|
|
1497
|
+
});
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
nestedWorkers.delete(worker);
|
|
1500
|
+
worker.terminate();
|
|
1501
|
+
throw workerError(
|
|
1502
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
1503
|
+
"The authenticated artifact module Worker initialization message was rejected.",
|
|
1504
|
+
error,
|
|
1505
|
+
"artifact-graph-module-worker-initialization-message-rejected",
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
return worker;
|
|
1509
|
+
},
|
|
1510
|
+
|
|
1511
|
+
typedArrayConstructor(capability, modulePath, occurrence, receiver) {
|
|
1512
|
+
assertGuardCapability(capability);
|
|
1513
|
+
if (!typedArrayConstructors.has(edgeKey(modulePath, occurrence))) {
|
|
1514
|
+
throw workerError(
|
|
1515
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1516
|
+
"The runtime used an undeclared typed-array constructor transform.",
|
|
1517
|
+
undefined,
|
|
1518
|
+
"artifact-graph-typed-array-constructor-transform-undeclared",
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
if (
|
|
1522
|
+
!typedArrays.isView(receiver)
|
|
1523
|
+
|| typedArrays.getPrototypeOf(receiver) === typedArrays.dataViewPrototype
|
|
1524
|
+
|| typedArrays.hasOwn(receiver, "constructor")
|
|
1525
|
+
) {
|
|
1526
|
+
throw workerError(
|
|
1527
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1528
|
+
"The typed-array constructor transform received an unauthenticated receiver.",
|
|
1529
|
+
undefined,
|
|
1530
|
+
"artifact-graph-typed-array-constructor-receiver-not-typed-array",
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const Constructor = typedArrays.constructors.get(
|
|
1534
|
+
typedArrays.getPrototypeOf(receiver),
|
|
1535
|
+
);
|
|
1536
|
+
if (typeof Constructor !== "function") {
|
|
1537
|
+
throw workerError(
|
|
1538
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1539
|
+
"The typed-array constructor transform receiver does not use an intrinsic prototype.",
|
|
1540
|
+
undefined,
|
|
1541
|
+
"artifact-graph-typed-array-constructor-intrinsic-mismatch",
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
return Constructor;
|
|
1545
|
+
},
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
return Object.freeze({
|
|
1549
|
+
guard,
|
|
1550
|
+
transformersCache: (() => {
|
|
1551
|
+
const edgesForTransformers = [...cacheOpens.values()].filter((edge) =>
|
|
1552
|
+
edge.edgePolicy === "artifact-targets-admitted"
|
|
1553
|
+
&& edge.cacheName === "transformers-cache");
|
|
1554
|
+
if (edgesForTransformers.length === 0) return null;
|
|
1555
|
+
if (edgesForTransformers.length !== 1) {
|
|
1556
|
+
throw workerError(
|
|
1557
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
1558
|
+
"The Transformers custom cache edge is ambiguous.",
|
|
1559
|
+
undefined,
|
|
1560
|
+
"artifact-graph-transformers-cache-edge-ambiguous",
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
return reader.cacheForPaths(edgesForTransformers[0].targetPaths);
|
|
1564
|
+
})(),
|
|
1565
|
+
cleanup() {
|
|
1566
|
+
for (const worker of nestedWorkers) {
|
|
1567
|
+
try {
|
|
1568
|
+
worker.terminate();
|
|
1569
|
+
} catch {
|
|
1570
|
+
// The owning speech Worker is already being torn down.
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
nestedWorkers.clear();
|
|
1574
|
+
},
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function installDynamicCodeConstructorIsolation() {
|
|
1579
|
+
const rejectingConstructor = function rejectArtifactGraphDynamicCodeConstructor() {
|
|
1580
|
+
throw workerError(
|
|
1581
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1582
|
+
"Authenticated artifact graph runtime code cannot construct executable strings.",
|
|
1583
|
+
undefined,
|
|
1584
|
+
"artifact-graph-dynamic-code-constructor-rejected",
|
|
1585
|
+
);
|
|
1586
|
+
};
|
|
1587
|
+
const prototypes = new Set([
|
|
1588
|
+
Object.getPrototypeOf(function artifactGraphFunctionProbe() {}),
|
|
1589
|
+
Object.getPrototypeOf(async function artifactGraphAsyncFunctionProbe() {}),
|
|
1590
|
+
Object.getPrototypeOf(function* artifactGraphGeneratorFunctionProbe() {}),
|
|
1591
|
+
Object.getPrototypeOf(async function* artifactGraphAsyncGeneratorFunctionProbe() {}),
|
|
1592
|
+
]);
|
|
1593
|
+
const restores = [];
|
|
1594
|
+
try {
|
|
1595
|
+
for (const prototype of prototypes) {
|
|
1596
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, "constructor");
|
|
1597
|
+
if (!descriptor || (descriptor.configurable === false && descriptor.writable !== true)) {
|
|
1598
|
+
throw workerError(
|
|
1599
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1600
|
+
"The Worker cannot isolate a dynamic-code constructor prototype.",
|
|
1601
|
+
undefined,
|
|
1602
|
+
"artifact-graph-dynamic-code-constructor-isolation-unavailable",
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
Object.defineProperty(prototype, "constructor", {
|
|
1606
|
+
...descriptor,
|
|
1607
|
+
value: rejectingConstructor,
|
|
1608
|
+
});
|
|
1609
|
+
restores.push(() => Object.defineProperty(prototype, "constructor", descriptor));
|
|
1610
|
+
}
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
restoreScopeValues(restores);
|
|
1613
|
+
throw error;
|
|
1614
|
+
}
|
|
1615
|
+
return () => restoreScopeValues(restores);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function installStringTimerIsolation(scope, name) {
|
|
1619
|
+
const original = scope[name];
|
|
1620
|
+
if (typeof original !== "function") return () => undefined;
|
|
1621
|
+
const invoke = original.bind(scope);
|
|
1622
|
+
return installScopeValue(
|
|
1623
|
+
scope,
|
|
1624
|
+
name,
|
|
1625
|
+
function authenticatedArtifactGraphTimer(callback, ...arguments_) {
|
|
1626
|
+
if (typeof callback !== "function") {
|
|
1627
|
+
throw workerError(
|
|
1628
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1629
|
+
`Authenticated artifact graph ${name} requires a function callback.`,
|
|
1630
|
+
undefined,
|
|
1631
|
+
`artifact-graph-${name.toLowerCase()}-string-callback-rejected`,
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
return invoke(callback, ...arguments_);
|
|
1635
|
+
},
|
|
1636
|
+
`artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function installArtifactGraphEnvironment(scope, configuration, role) {
|
|
1641
|
+
const originalFetch = scope.fetch?.bind(scope);
|
|
1642
|
+
const originalWorker = scope.Worker;
|
|
1643
|
+
const strictSecurity = configuration.security?.secure === true;
|
|
1644
|
+
if (typeof originalFetch !== "function") {
|
|
1645
|
+
throw workerError(
|
|
1646
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1647
|
+
"Browser fetch is unavailable in the speech Worker.",
|
|
1648
|
+
undefined,
|
|
1649
|
+
"artifact-graph-fetch-constructor-unavailable",
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
if (GRAPH_GUARD_NAME in scope) {
|
|
1653
|
+
throw workerError(
|
|
1654
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1655
|
+
"The authenticated artifact graph guard global already exists.",
|
|
1656
|
+
undefined,
|
|
1657
|
+
"artifact-graph-guard-global-collision",
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
const reader = createRouteReader(scope, configuration, originalFetch);
|
|
1661
|
+
const graphGuard = createArtifactGraphGuard(
|
|
1662
|
+
scope,
|
|
1663
|
+
configuration,
|
|
1664
|
+
role,
|
|
1665
|
+
reader,
|
|
1666
|
+
originalWorker,
|
|
1667
|
+
);
|
|
1668
|
+
const restores = [];
|
|
1669
|
+
try {
|
|
1670
|
+
if (strictSecurity) {
|
|
1671
|
+
restores.push(installDynamicCodeConstructorIsolation());
|
|
1672
|
+
restores.push(installStringTimerIsolation(scope, "setInterval"));
|
|
1673
|
+
restores.push(installStringTimerIsolation(scope, "setTimeout"));
|
|
1674
|
+
if ("indexedDB" in scope) {
|
|
1675
|
+
restores.push(installScopeValue(
|
|
1676
|
+
scope,
|
|
1677
|
+
"indexedDB",
|
|
1678
|
+
undefined,
|
|
1679
|
+
"artifact-graph-indexeddb-isolation-unavailable",
|
|
1680
|
+
));
|
|
1681
|
+
}
|
|
1682
|
+
if (scope.navigator && "storage" in scope.navigator) {
|
|
1683
|
+
restores.push(installObjectValue(
|
|
1684
|
+
scope.navigator,
|
|
1685
|
+
"storage",
|
|
1686
|
+
undefined,
|
|
1687
|
+
"WorkerNavigator.storage",
|
|
1688
|
+
"artifact-graph-opfs-isolation-unavailable",
|
|
1689
|
+
));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
restores.push(installScopeValue(
|
|
1693
|
+
scope,
|
|
1694
|
+
GRAPH_GUARD_NAME,
|
|
1695
|
+
graphGuard.guard,
|
|
1696
|
+
"artifact-graph-guard-global-definition-rejected",
|
|
1697
|
+
));
|
|
1698
|
+
if (strictSecurity) {
|
|
1699
|
+
restores.push(installScopeValue(
|
|
1700
|
+
scope,
|
|
1701
|
+
"fetch",
|
|
1702
|
+
async function rejectUntransformedArtifactGraphFetch() {
|
|
1703
|
+
throw workerError(
|
|
1704
|
+
"ARCANE_AI_ARTIFACT_GRAPH_FETCH_EDGE_UNDECLARED",
|
|
1705
|
+
"Artifact graph fetch must use its declared transformed edge.",
|
|
1706
|
+
undefined,
|
|
1707
|
+
"artifact-graph-fetch-guard-bypassed",
|
|
1708
|
+
);
|
|
1709
|
+
},
|
|
1710
|
+
"artifact-graph-fetch-isolation-unavailable",
|
|
1711
|
+
));
|
|
1712
|
+
const cacheStorage = Object.freeze({
|
|
1713
|
+
async open() {
|
|
1714
|
+
throw workerError(
|
|
1715
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
1716
|
+
"Artifact graph CacheStorage.open must use its declared transformed edge.",
|
|
1717
|
+
undefined,
|
|
1718
|
+
"artifact-graph-cache-open-guard-bypassed",
|
|
1719
|
+
);
|
|
1720
|
+
},
|
|
1721
|
+
async match() {
|
|
1722
|
+
throw workerError(
|
|
1723
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CACHE_EDGE_UNDECLARED",
|
|
1724
|
+
"Artifact graph CacheStorage.match requires a declared cache-open edge.",
|
|
1725
|
+
undefined,
|
|
1726
|
+
"artifact-graph-cache-match-guard-bypassed",
|
|
1727
|
+
);
|
|
1728
|
+
},
|
|
1729
|
+
async has() {
|
|
1730
|
+
return true;
|
|
1731
|
+
},
|
|
1732
|
+
async keys() {
|
|
1733
|
+
return Object.freeze([]);
|
|
1734
|
+
},
|
|
1735
|
+
async delete() {
|
|
1736
|
+
return false;
|
|
1737
|
+
},
|
|
1738
|
+
});
|
|
1739
|
+
restores.push(installScopeValue(
|
|
1740
|
+
scope,
|
|
1741
|
+
"caches",
|
|
1742
|
+
cacheStorage,
|
|
1743
|
+
"artifact-graph-cache-isolation-unavailable",
|
|
1744
|
+
));
|
|
1745
|
+
|
|
1746
|
+
const deniedCapabilities = [
|
|
1747
|
+
"BroadcastChannel",
|
|
1748
|
+
"EventSource",
|
|
1749
|
+
"Function",
|
|
1750
|
+
"RTCPeerConnection",
|
|
1751
|
+
"ShadowRealm",
|
|
1752
|
+
"SharedWorker",
|
|
1753
|
+
"WebSocket",
|
|
1754
|
+
"WebSocketStream",
|
|
1755
|
+
"WebTransport",
|
|
1756
|
+
"Worker",
|
|
1757
|
+
"XMLHttpRequest",
|
|
1758
|
+
"eval",
|
|
1759
|
+
"importScripts",
|
|
1760
|
+
];
|
|
1761
|
+
for (const name of deniedCapabilities) {
|
|
1762
|
+
if (!(name in scope)) continue;
|
|
1763
|
+
restores.push(installScopeValue(
|
|
1764
|
+
scope,
|
|
1765
|
+
name,
|
|
1766
|
+
function rejectUndeclaredArtifactGraphCapability() {
|
|
1767
|
+
throw workerError(
|
|
1768
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
1769
|
+
`The authenticated artifact graph denied raw ${name} access.`,
|
|
1770
|
+
undefined,
|
|
1771
|
+
`artifact-graph-${name.toLowerCase()}-capability-undeclared`,
|
|
1772
|
+
);
|
|
1773
|
+
},
|
|
1774
|
+
`artifact-graph-${name.toLowerCase()}-isolation-unavailable`,
|
|
1775
|
+
));
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
restoreScopeValues(restores);
|
|
1780
|
+
graphGuard.cleanup();
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1783
|
+
return Object.freeze({
|
|
1784
|
+
cache: graphGuard.transformersCache,
|
|
1785
|
+
cleanup() {
|
|
1786
|
+
graphGuard.cleanup();
|
|
1787
|
+
restoreScopeValues(restores);
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function assignSetting(object, name, value, cleanup, {
|
|
1793
|
+
allowCreate = false,
|
|
1794
|
+
assignmentRejectedReason,
|
|
1795
|
+
unavailableReason,
|
|
1796
|
+
} = {}) {
|
|
1797
|
+
if (!object) {
|
|
1798
|
+
throw workerError(
|
|
1799
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1800
|
+
`The selected runtime does not expose the verified ${name} setting.`,
|
|
1801
|
+
undefined,
|
|
1802
|
+
unavailableReason,
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
let hadOwn;
|
|
1806
|
+
let previous;
|
|
1807
|
+
let canRestore = false;
|
|
1808
|
+
try {
|
|
1809
|
+
if (!allowCreate && !(name in object)) {
|
|
1810
|
+
throw workerError(
|
|
1811
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1812
|
+
`The selected runtime does not expose the verified ${name} setting.`,
|
|
1813
|
+
undefined,
|
|
1814
|
+
unavailableReason,
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
hadOwn = Object.hasOwn(object, name);
|
|
1818
|
+
previous = object[name];
|
|
1819
|
+
canRestore = true;
|
|
1820
|
+
object[name] = value;
|
|
1821
|
+
if (object[name] !== value) {
|
|
1822
|
+
throw new TypeError(`The ${name} assignment was not retained.`);
|
|
1823
|
+
}
|
|
186
1824
|
} catch (error) {
|
|
1825
|
+
if (isSdkWorkerError(error)) throw error;
|
|
1826
|
+
if (canRestore) {
|
|
1827
|
+
try {
|
|
1828
|
+
if (hadOwn) object[name] = previous;
|
|
1829
|
+
else delete object[name];
|
|
1830
|
+
} catch {
|
|
1831
|
+
// Preserve the exact configuration failure as the public boundary.
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
187
1834
|
throw workerError(
|
|
188
1835
|
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
189
|
-
|
|
1836
|
+
`The selected runtime rejected the verified ${name} setting.`,
|
|
190
1837
|
error,
|
|
1838
|
+
assignmentRejectedReason,
|
|
191
1839
|
);
|
|
192
1840
|
}
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
else delete
|
|
196
|
-
};
|
|
1841
|
+
cleanup.push(() => {
|
|
1842
|
+
if (hadOwn) object[name] = previous;
|
|
1843
|
+
else delete object[name];
|
|
1844
|
+
});
|
|
197
1845
|
}
|
|
198
1846
|
|
|
199
|
-
function
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const wasm = namespace?.env?.backends?.onnx?.wasm;
|
|
209
|
-
if (!wasm) return;
|
|
210
|
-
const paths = {};
|
|
211
|
-
for (const file of configuration.runtime.files) {
|
|
212
|
-
if (/\.(?:m?js|wasm)$/iu.test(file.path) && file.path !== configuration.runtime.entry) {
|
|
213
|
-
paths[file.path.split("/").pop()] = file.moduleUrl;
|
|
1847
|
+
function configuredWasmPaths(configuration) {
|
|
1848
|
+
if (configuration.runtime.wasmPaths !== undefined) {
|
|
1849
|
+
if (configuration.security?.secure === true) {
|
|
1850
|
+
throw workerError(
|
|
1851
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
1852
|
+
"Secure browser speech cannot use remote wasmPaths.",
|
|
1853
|
+
undefined,
|
|
1854
|
+
"speech-worker-secure-remote-wasm-paths-rejected",
|
|
1855
|
+
);
|
|
214
1856
|
}
|
|
1857
|
+
return configuration.runtime.wasmPaths;
|
|
1858
|
+
}
|
|
1859
|
+
const byPath = new Map(configuration.runtime.files.map((file) => [file.path, file]));
|
|
1860
|
+
const declared = configuration.runtime.onnxWasm;
|
|
1861
|
+
let mjs = declared ? byPath.get(declared.mjsPath) : null;
|
|
1862
|
+
let wasm = declared ? byPath.get(declared.wasmPath) : null;
|
|
1863
|
+
if (!declared) {
|
|
1864
|
+
mjs = configuration.runtime.files.find((file) =>
|
|
1865
|
+
file.path !== configuration.runtime.entry && /\.mjs$/iu.test(file.path));
|
|
1866
|
+
wasm = configuration.runtime.files.find((file) => /\.wasm$/iu.test(file.path));
|
|
215
1867
|
}
|
|
216
|
-
if (
|
|
217
|
-
if (
|
|
218
|
-
|
|
1868
|
+
if (!mjs && !wasm) return null;
|
|
1869
|
+
if (!mjs?.moduleUrl || !wasm?.moduleUrl) {
|
|
1870
|
+
throw workerError(
|
|
1871
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1872
|
+
"The selected runtime did not materialize its complete ONNX MJS/WASM pair.",
|
|
1873
|
+
undefined,
|
|
1874
|
+
"artifact-graph-onnx-wasm-pair-not-materialized",
|
|
1875
|
+
);
|
|
219
1876
|
}
|
|
1877
|
+
return Object.freeze({ mjs: mjs.moduleUrl, wasm: wasm.moduleUrl });
|
|
220
1878
|
}
|
|
221
1879
|
|
|
222
|
-
function
|
|
223
|
-
|
|
1880
|
+
function configureRuntimeNamespace(namespace, configuration, role, cache) {
|
|
1881
|
+
const cleanup = [];
|
|
1882
|
+
const strictSecurity = configuration.security?.secure === true;
|
|
1883
|
+
const restoreSettings = () => {
|
|
1884
|
+
for (const restore of cleanup.splice(0).reverse()) {
|
|
1885
|
+
try {
|
|
1886
|
+
restore();
|
|
1887
|
+
} catch {
|
|
1888
|
+
// Worker isolation is still released by terminating the owning Worker.
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const paths = configuredWasmPaths(configuration);
|
|
1893
|
+
if (role === "tts") {
|
|
1894
|
+
const env = namespace?.env;
|
|
1895
|
+
if (!env || !("wasmPaths" in env)) {
|
|
1896
|
+
throw workerError(
|
|
1897
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1898
|
+
"Kokoro does not expose the verified env.wasmPaths configuration field.",
|
|
1899
|
+
undefined,
|
|
1900
|
+
"kokoro-env-wasm-paths-unavailable",
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
try {
|
|
1904
|
+
if (paths) {
|
|
1905
|
+
assignSetting(
|
|
1906
|
+
env,
|
|
1907
|
+
"wasmPaths",
|
|
1908
|
+
paths,
|
|
1909
|
+
cleanup,
|
|
1910
|
+
{
|
|
1911
|
+
assignmentRejectedReason: "kokoro-env-wasm-paths-assignment-rejected",
|
|
1912
|
+
unavailableReason: "kokoro-env-wasm-paths-unavailable",
|
|
1913
|
+
},
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
restoreSettings();
|
|
1918
|
+
throw error;
|
|
1919
|
+
}
|
|
1920
|
+
return restoreSettings;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const env = namespace?.env;
|
|
1924
|
+
const wasm = env?.backends?.onnx?.wasm;
|
|
1925
|
+
if (!env || !wasm) {
|
|
1926
|
+
throw workerError(
|
|
1927
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
1928
|
+
"Transformers does not expose env.backends.onnx.wasm.",
|
|
1929
|
+
undefined,
|
|
1930
|
+
"transformers-env-backends-onnx-wasm-unavailable",
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
try {
|
|
1934
|
+
assignSetting(env, "allowLocalModels", false, cleanup, {
|
|
1935
|
+
assignmentRejectedReason: "transformers-env-allow-local-models-assignment-rejected",
|
|
1936
|
+
unavailableReason: "transformers-env-allow-local-models-unavailable",
|
|
1937
|
+
});
|
|
1938
|
+
assignSetting(env, "allowRemoteModels", true, cleanup, {
|
|
1939
|
+
assignmentRejectedReason: "transformers-env-allow-remote-models-assignment-rejected",
|
|
1940
|
+
unavailableReason: "transformers-env-allow-remote-models-unavailable",
|
|
1941
|
+
});
|
|
1942
|
+
if (strictSecurity) {
|
|
1943
|
+
assignSetting(env, "useBrowserCache", false, cleanup, {
|
|
1944
|
+
assignmentRejectedReason: "transformers-env-browser-cache-assignment-rejected",
|
|
1945
|
+
unavailableReason: "transformers-env-browser-cache-unavailable",
|
|
1946
|
+
});
|
|
1947
|
+
assignSetting(env, "useFSCache", false, cleanup, {
|
|
1948
|
+
assignmentRejectedReason: "transformers-env-fs-cache-assignment-rejected",
|
|
1949
|
+
unavailableReason: "transformers-env-fs-cache-unavailable",
|
|
1950
|
+
});
|
|
1951
|
+
assignSetting(env, "useCustomCache", cache !== null, cleanup, {
|
|
1952
|
+
assignmentRejectedReason: "transformers-env-custom-cache-toggle-assignment-rejected",
|
|
1953
|
+
unavailableReason: "transformers-env-custom-cache-toggle-unavailable",
|
|
1954
|
+
});
|
|
1955
|
+
assignSetting(env, "customCache", cache, cleanup, {
|
|
1956
|
+
assignmentRejectedReason: "transformers-env-custom-cache-assignment-rejected",
|
|
1957
|
+
unavailableReason: "transformers-env-custom-cache-unavailable",
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
if (paths) {
|
|
1961
|
+
assignSetting(
|
|
1962
|
+
wasm,
|
|
1963
|
+
"wasmPaths",
|
|
1964
|
+
paths,
|
|
1965
|
+
cleanup,
|
|
1966
|
+
{
|
|
1967
|
+
allowCreate: true,
|
|
1968
|
+
assignmentRejectedReason: "transformers-env-wasm-paths-assignment-rejected",
|
|
1969
|
+
unavailableReason: "transformers-env-wasm-paths-unavailable",
|
|
1970
|
+
},
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
if (configuration.runtime.onnxWasm?.numThreads !== undefined) {
|
|
1974
|
+
assignSetting(
|
|
1975
|
+
wasm,
|
|
1976
|
+
"numThreads",
|
|
1977
|
+
configuration.runtime.onnxWasm.numThreads,
|
|
1978
|
+
cleanup,
|
|
1979
|
+
{
|
|
1980
|
+
allowCreate: true,
|
|
1981
|
+
assignmentRejectedReason: "transformers-env-num-threads-assignment-rejected",
|
|
1982
|
+
unavailableReason: "transformers-env-num-threads-unavailable",
|
|
1983
|
+
},
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
restoreSettings();
|
|
1988
|
+
throw error;
|
|
1989
|
+
}
|
|
1990
|
+
return restoreSettings;
|
|
224
1991
|
}
|
|
225
1992
|
|
|
226
|
-
function workerProgress(send, requestId, phase, completed = 0, total = null, unit = "items") {
|
|
1993
|
+
function workerProgress(send, role, requestId, phase, completed = 0, total = null, unit = "items") {
|
|
227
1994
|
send({
|
|
228
1995
|
protocol: SPEECH_WORKER_PROTOCOL,
|
|
229
1996
|
event: "progress",
|
|
@@ -238,6 +2005,24 @@ function workerProgress(send, requestId, phase, completed = 0, total = null, uni
|
|
|
238
2005
|
}, []);
|
|
239
2006
|
}
|
|
240
2007
|
|
|
2008
|
+
function upstreamProgress(send, role, requestId) {
|
|
2009
|
+
return function reportSpeechModelProgress(update = {}) {
|
|
2010
|
+
const completed = Number.isFinite(update.loaded)
|
|
2011
|
+
? update.loaded
|
|
2012
|
+
: Number.isFinite(update.progress) ? update.progress : 0;
|
|
2013
|
+
const total = Number.isFinite(update.total) ? update.total : null;
|
|
2014
|
+
workerProgress(
|
|
2015
|
+
send,
|
|
2016
|
+
role,
|
|
2017
|
+
requestId,
|
|
2018
|
+
`${role}-model-load-progress`,
|
|
2019
|
+
completed,
|
|
2020
|
+
total,
|
|
2021
|
+
Number.isFinite(update.loaded) ? "bytes" : "items",
|
|
2022
|
+
);
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
241
2026
|
async function disposeEngine(engine) {
|
|
242
2027
|
if (!engine) return;
|
|
243
2028
|
if (typeof engine.dispose === "function") {
|
|
@@ -254,26 +2039,39 @@ async function disposeEngine(engine) {
|
|
|
254
2039
|
|
|
255
2040
|
async function createWhisperEngine(namespace, configuration, signal, report) {
|
|
256
2041
|
if (typeof namespace?.pipeline !== "function") {
|
|
257
|
-
throw workerError(
|
|
2042
|
+
throw workerError(
|
|
2043
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
2044
|
+
"The Whisper runtime does not export pipeline().",
|
|
2045
|
+
undefined,
|
|
2046
|
+
"transformers-whisper-pipeline-export-missing",
|
|
2047
|
+
);
|
|
258
2048
|
}
|
|
259
|
-
|
|
260
|
-
throwIfAborted(signal);
|
|
2049
|
+
throwIfAborted(signal, "stt-load-cancelled");
|
|
261
2050
|
const transcriber = await namespace.pipeline(
|
|
262
2051
|
"automatic-speech-recognition",
|
|
263
2052
|
configuration.model.repository,
|
|
264
2053
|
{
|
|
265
2054
|
device: "wasm",
|
|
266
|
-
dtype: "fp32",
|
|
2055
|
+
dtype: configuration.model.dtype ?? "fp32",
|
|
267
2056
|
revision: configuration.model.revision,
|
|
268
2057
|
progress_callback: report,
|
|
269
2058
|
},
|
|
270
2059
|
);
|
|
271
|
-
|
|
2060
|
+
try {
|
|
2061
|
+
throwIfAborted(signal, "stt-load-cancelled");
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
try {
|
|
2064
|
+
await disposeEngine(transcriber);
|
|
2065
|
+
} catch {
|
|
2066
|
+
// Preserve the cancellation boundary while the owning Worker terminates.
|
|
2067
|
+
}
|
|
2068
|
+
throw error;
|
|
2069
|
+
}
|
|
272
2070
|
return Object.freeze({
|
|
273
2071
|
async transcribe(input, { signal: requestSignal } = {}) {
|
|
274
|
-
throwIfAborted(requestSignal);
|
|
2072
|
+
throwIfAborted(requestSignal, "stt-transcription-cancelled");
|
|
275
2073
|
const output = await transcriber(input.audio, { signal: requestSignal });
|
|
276
|
-
throwIfAborted(requestSignal);
|
|
2074
|
+
throwIfAborted(requestSignal, "stt-transcription-cancelled");
|
|
277
2075
|
return Object.freeze({ text: String(output?.text ?? "").trim() });
|
|
278
2076
|
},
|
|
279
2077
|
dispose: () => disposeEngine(transcriber),
|
|
@@ -282,29 +2080,42 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
|
|
|
282
2080
|
|
|
283
2081
|
async function createKokoroEngine(namespace, configuration, signal, report) {
|
|
284
2082
|
if (typeof namespace?.KokoroTTS?.from_pretrained !== "function") {
|
|
285
|
-
throw workerError(
|
|
2083
|
+
throw workerError(
|
|
2084
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
2085
|
+
"The Kokoro runtime does not export KokoroTTS.",
|
|
2086
|
+
undefined,
|
|
2087
|
+
"kokoro-tts-constructor-export-missing",
|
|
2088
|
+
);
|
|
286
2089
|
}
|
|
287
|
-
|
|
288
|
-
throwIfAborted(signal);
|
|
2090
|
+
throwIfAborted(signal, "tts-load-cancelled");
|
|
289
2091
|
const synthesizer = await namespace.KokoroTTS.from_pretrained(
|
|
290
2092
|
configuration.model.repository,
|
|
291
2093
|
{
|
|
292
2094
|
device: "wasm",
|
|
293
|
-
dtype: "q8",
|
|
2095
|
+
dtype: configuration.model.dtype ?? "q8",
|
|
294
2096
|
revision: configuration.model.revision,
|
|
295
2097
|
progress_callback: report,
|
|
296
2098
|
},
|
|
297
2099
|
);
|
|
298
|
-
|
|
2100
|
+
try {
|
|
2101
|
+
throwIfAborted(signal, "tts-load-cancelled");
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
try {
|
|
2104
|
+
await disposeEngine(synthesizer);
|
|
2105
|
+
} catch {
|
|
2106
|
+
// Preserve the cancellation boundary while the owning Worker terminates.
|
|
2107
|
+
}
|
|
2108
|
+
throw error;
|
|
2109
|
+
}
|
|
299
2110
|
return Object.freeze({
|
|
300
2111
|
async synthesize(input, { signal: requestSignal } = {}) {
|
|
301
|
-
throwIfAborted(requestSignal);
|
|
2112
|
+
throwIfAborted(requestSignal, "tts-synthesis-cancelled");
|
|
302
2113
|
const output = await synthesizer.generate(input.text, {
|
|
303
2114
|
voice: input.voice,
|
|
304
2115
|
speed: input.speed,
|
|
305
2116
|
signal: requestSignal,
|
|
306
2117
|
});
|
|
307
|
-
throwIfAborted(requestSignal);
|
|
2118
|
+
throwIfAborted(requestSignal, "tts-synthesis-cancelled");
|
|
308
2119
|
const audio = output?.audio instanceof Float32Array
|
|
309
2120
|
? output.audio
|
|
310
2121
|
: new Float32Array(output?.audio ?? []);
|
|
@@ -318,49 +2129,109 @@ async function createKokoroEngine(namespace, configuration, signal, report) {
|
|
|
318
2129
|
});
|
|
319
2130
|
}
|
|
320
2131
|
|
|
2132
|
+
function callerVoiceIds(configuration) {
|
|
2133
|
+
return new Set((configuration.model.voices ?? []).map((voice) =>
|
|
2134
|
+
typeof voice === "string" ? voice : voice?.id));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
321
2137
|
function validateInput(role, payload, configuration) {
|
|
322
2138
|
if (role === "stt") {
|
|
323
|
-
|
|
2139
|
+
const inputSampleRate = requiredSampleRate(
|
|
2140
|
+
configuration.model.inputSampleRate,
|
|
2141
|
+
"Whisper inputSampleRate",
|
|
2142
|
+
16_000,
|
|
2143
|
+
);
|
|
2144
|
+
if (!(payload?.audio instanceof Float32Array)) {
|
|
2145
|
+
throw workerError(
|
|
2146
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2147
|
+
"Whisper requires Float32Array audio.",
|
|
2148
|
+
undefined,
|
|
2149
|
+
"stt-transcription-audio-not-float32array",
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
if (payload.sampleRate !== inputSampleRate) {
|
|
324
2153
|
throw workerError(
|
|
325
2154
|
"ARCANE_AI_INVALID_REQUEST",
|
|
326
|
-
|
|
2155
|
+
`Whisper requires audio sampled at exactly ${String(inputSampleRate)} Hz.`,
|
|
2156
|
+
undefined,
|
|
2157
|
+
"stt-transcription-sample-rate-mismatch",
|
|
327
2158
|
);
|
|
328
2159
|
}
|
|
329
|
-
return Object.freeze({ audio: payload.audio, sampleRate:
|
|
2160
|
+
return Object.freeze({ audio: payload.audio, sampleRate: inputSampleRate });
|
|
330
2161
|
}
|
|
331
|
-
const text = requiredText(payload?.text, "Kokoro text");
|
|
2162
|
+
const text = requiredText(payload?.text, "Kokoro text", "tts-synthesis-text-empty");
|
|
332
2163
|
const voice = requiredText(
|
|
333
2164
|
payload?.voice ?? configuration.model.defaultVoice,
|
|
334
2165
|
"Kokoro voice",
|
|
2166
|
+
"tts-synthesis-voice-empty",
|
|
335
2167
|
);
|
|
2168
|
+
const voices = callerVoiceIds(configuration);
|
|
2169
|
+
if (voices.size > 0 && !voices.has(voice)) {
|
|
2170
|
+
throw workerError(
|
|
2171
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2172
|
+
"Kokoro voice is outside the caller-owned voice inventory.",
|
|
2173
|
+
undefined,
|
|
2174
|
+
"tts-synthesis-voice-not-declared",
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
336
2177
|
const speed = payload?.speed ?? 1;
|
|
337
2178
|
if (!Number.isFinite(speed) || speed <= 0 || speed > 4) {
|
|
338
|
-
throw workerError(
|
|
2179
|
+
throw workerError(
|
|
2180
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2181
|
+
"Kokoro speed must be greater than 0 and at most 4.",
|
|
2182
|
+
undefined,
|
|
2183
|
+
"tts-synthesis-speed-out-of-range",
|
|
2184
|
+
);
|
|
339
2185
|
}
|
|
340
2186
|
return Object.freeze({ text, voice, speed });
|
|
341
2187
|
}
|
|
342
2188
|
|
|
343
|
-
function validateResult(role, result) {
|
|
2189
|
+
function validateResult(role, result, configuration) {
|
|
344
2190
|
if (role === "stt") {
|
|
345
2191
|
if (!result || typeof result.text !== "string") {
|
|
346
|
-
throw workerError(
|
|
2192
|
+
throw workerError(
|
|
2193
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
2194
|
+
"Whisper did not return text.",
|
|
2195
|
+
undefined,
|
|
2196
|
+
"stt-transcription-result-text-not-string",
|
|
2197
|
+
);
|
|
347
2198
|
}
|
|
348
2199
|
return Object.freeze({ text: result.text.trim() });
|
|
349
2200
|
}
|
|
350
|
-
|
|
2201
|
+
const outputSampleRate = requiredSampleRate(
|
|
2202
|
+
configuration.model.outputSampleRate,
|
|
2203
|
+
"Kokoro outputSampleRate",
|
|
2204
|
+
24_000,
|
|
2205
|
+
);
|
|
2206
|
+
if (!(result?.audio instanceof Float32Array)) {
|
|
351
2207
|
throw workerError(
|
|
352
2208
|
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
353
|
-
"Kokoro must return
|
|
2209
|
+
"Kokoro must return Float32 PCM.",
|
|
2210
|
+
undefined,
|
|
2211
|
+
"tts-synthesis-pcm-result-not-float32array",
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
if (result.sampleRate !== outputSampleRate) {
|
|
2215
|
+
throw workerError(
|
|
2216
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
2217
|
+
`Kokoro must return ${String(outputSampleRate)} Hz PCM.`,
|
|
2218
|
+
undefined,
|
|
2219
|
+
"tts-synthesis-result-sample-rate-mismatch",
|
|
354
2220
|
);
|
|
355
2221
|
}
|
|
356
2222
|
for (const sample of result.audio) {
|
|
357
2223
|
if (!Number.isFinite(sample)) {
|
|
358
|
-
throw workerError(
|
|
2224
|
+
throw workerError(
|
|
2225
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
2226
|
+
"Kokoro returned non-finite PCM.",
|
|
2227
|
+
undefined,
|
|
2228
|
+
"tts-synthesis-pcm-sample-non-finite",
|
|
2229
|
+
);
|
|
359
2230
|
}
|
|
360
2231
|
}
|
|
361
2232
|
return Object.freeze({
|
|
362
2233
|
audio: result.audio,
|
|
363
|
-
sampleRate:
|
|
2234
|
+
sampleRate: outputSampleRate,
|
|
364
2235
|
voice: result.voice,
|
|
365
2236
|
});
|
|
366
2237
|
}
|
|
@@ -374,92 +2245,232 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
374
2245
|
}
|
|
375
2246
|
let configuration = null;
|
|
376
2247
|
let engine = null;
|
|
377
|
-
let
|
|
378
|
-
let
|
|
2248
|
+
let environment = null;
|
|
2249
|
+
let restoreNamespace = null;
|
|
379
2250
|
let disposed = false;
|
|
380
2251
|
let tail = Promise.resolve();
|
|
2252
|
+
let lifecycleReason = `${role}-worker-created`;
|
|
381
2253
|
const operations = new Map();
|
|
382
2254
|
|
|
383
2255
|
function status() {
|
|
2256
|
+
const state = disposed ? "disposed" : engine ? "ready" : "unloaded";
|
|
2257
|
+
const security = configuration?.security
|
|
2258
|
+
? Object.freeze({
|
|
2259
|
+
secure: configuration.security.secure === true,
|
|
2260
|
+
checks: Object.freeze({
|
|
2261
|
+
byteLength: configuration.security.checks?.byteLength === true,
|
|
2262
|
+
sha256: configuration.security.checks?.sha256 === true,
|
|
2263
|
+
}),
|
|
2264
|
+
})
|
|
2265
|
+
: null;
|
|
384
2266
|
return Object.freeze({
|
|
385
|
-
state
|
|
2267
|
+
state,
|
|
2268
|
+
lifecycleStatus: `${role}-worker-${state}`,
|
|
2269
|
+
lifecycleReason,
|
|
2270
|
+
role,
|
|
386
2271
|
loaded: engine !== null,
|
|
387
2272
|
busy: operations.size > 0,
|
|
2273
|
+
activeOperation: operations.values().next().value?.publicOperation ?? null,
|
|
2274
|
+
security,
|
|
2275
|
+
artifactGraphId: configuration?.runtime?.artifactGraphId ?? null,
|
|
2276
|
+
artifactGraphAdmission: configuration?.runtime?.artifactGraphAdmission ?? null,
|
|
388
2277
|
});
|
|
389
2278
|
}
|
|
390
2279
|
|
|
391
2280
|
async function load(request, signal) {
|
|
392
2281
|
if (disposed) {
|
|
393
|
-
throw workerError(
|
|
2282
|
+
throw workerError(
|
|
2283
|
+
"ARCANE_AI_PROVIDER_DISPOSED",
|
|
2284
|
+
"The speech worker is disposed.",
|
|
2285
|
+
undefined,
|
|
2286
|
+
`${role}-load-rejected-after-dispose`,
|
|
2287
|
+
);
|
|
394
2288
|
}
|
|
395
2289
|
if (engine) return status();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
file.path === configuration.runtime.entry);
|
|
399
|
-
restoreFetch = installAuthorizedFetch(scope, configuration);
|
|
2290
|
+
lifecycleReason = `${role}-load-started`;
|
|
2291
|
+
let loadFailureReason = `${role}-worker-runtime-configuration-rejected`;
|
|
400
2292
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
2293
|
+
configuration = validateConfiguration(request.payload?.configuration, role);
|
|
2294
|
+
const entry = configuration.runtime.files.find((file) =>
|
|
2295
|
+
file.path === configuration.runtime.entry);
|
|
2296
|
+
if (graphConfiguration(configuration) && configuration.security?.secure === true) {
|
|
2297
|
+
environment = installArtifactGraphEnvironment(scope, configuration, role);
|
|
2298
|
+
} else if (configuration.security?.secure === true) {
|
|
2299
|
+
const legacyFetch = installLegacyAuthorizedFetch(scope, configuration);
|
|
2300
|
+
try {
|
|
2301
|
+
const restoreCaches = installDeniedCacheIsolation(scope);
|
|
2302
|
+
environment = Object.freeze({
|
|
2303
|
+
cache: null,
|
|
2304
|
+
cleanup() {
|
|
2305
|
+
try {
|
|
2306
|
+
restoreCaches();
|
|
2307
|
+
} finally {
|
|
2308
|
+
legacyFetch.cleanup();
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
});
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
legacyFetch.cleanup();
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
} else {
|
|
2317
|
+
environment = Object.freeze({
|
|
2318
|
+
cache: null,
|
|
2319
|
+
cleanup() {},
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
workerProgress(send, role, request.id, `${role}-runtime-import-started`);
|
|
2323
|
+
loadFailureReason = `${role}-worker-runtime-import-rejected`;
|
|
403
2324
|
const namespace = await import(entry.moduleUrl);
|
|
404
|
-
throwIfAborted(signal);
|
|
405
|
-
|
|
2325
|
+
throwIfAborted(signal, `${role}-load-cancelled`);
|
|
2326
|
+
restoreNamespace = configureRuntimeNamespace(
|
|
2327
|
+
namespace,
|
|
2328
|
+
configuration,
|
|
2329
|
+
role,
|
|
2330
|
+
environment.cache,
|
|
2331
|
+
);
|
|
2332
|
+
workerProgress(send, role, request.id, `${role}-model-load-started`);
|
|
2333
|
+
loadFailureReason = `${role}-worker-model-load-rejected`;
|
|
2334
|
+
const report = upstreamProgress(send, role, request.id);
|
|
406
2335
|
engine = role === "stt"
|
|
407
2336
|
? await createWhisperEngine(namespace, configuration, signal, report)
|
|
408
2337
|
: await createKokoroEngine(namespace, configuration, signal, report);
|
|
409
|
-
|
|
2338
|
+
lifecycleReason = `${role}-load-completed`;
|
|
2339
|
+
workerProgress(send, role, request.id, `${role}-provider-ready`, 1, 1);
|
|
410
2340
|
return status();
|
|
411
2341
|
} catch (error) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
2342
|
+
const failure = admitWorkerFailure(
|
|
2343
|
+
error,
|
|
2344
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
2345
|
+
"The speech Worker load operation was rejected.",
|
|
2346
|
+
loadFailureReason,
|
|
2347
|
+
);
|
|
2348
|
+
const rejectedEngine = engine;
|
|
2349
|
+
engine = null;
|
|
2350
|
+
try {
|
|
2351
|
+
await disposeEngine(rejectedEngine);
|
|
2352
|
+
} catch {
|
|
2353
|
+
// Preserve the load boundary error that caused teardown.
|
|
2354
|
+
}
|
|
2355
|
+
restoreNamespace?.();
|
|
2356
|
+
restoreNamespace = null;
|
|
2357
|
+
try {
|
|
2358
|
+
environment?.cleanup();
|
|
2359
|
+
} catch {
|
|
2360
|
+
// A rejected load is followed by provider-owned Worker termination.
|
|
2361
|
+
}
|
|
2362
|
+
environment = null;
|
|
416
2363
|
configuration = null;
|
|
417
|
-
|
|
2364
|
+
lifecycleReason = failure.reason;
|
|
2365
|
+
throw failure;
|
|
418
2366
|
}
|
|
419
2367
|
}
|
|
420
2368
|
|
|
421
2369
|
async function use(request, signal) {
|
|
422
2370
|
if (!engine || !configuration) {
|
|
423
|
-
throw workerError(
|
|
2371
|
+
throw workerError(
|
|
2372
|
+
"ARCANE_AI_NOT_READY",
|
|
2373
|
+
"The speech worker is not loaded.",
|
|
2374
|
+
undefined,
|
|
2375
|
+
role === "stt"
|
|
2376
|
+
? "stt-transcription-rejected-before-load"
|
|
2377
|
+
: "tts-synthesis-rejected-before-load",
|
|
2378
|
+
);
|
|
424
2379
|
}
|
|
425
2380
|
const input = validateInput(role, request.payload, configuration);
|
|
426
2381
|
const method = role === "stt" ? engine.transcribe : engine.synthesize;
|
|
427
2382
|
if (typeof method !== "function") {
|
|
428
|
-
throw workerError(
|
|
2383
|
+
throw workerError(
|
|
2384
|
+
"ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
2385
|
+
"The speech engine operation is unavailable.",
|
|
2386
|
+
undefined,
|
|
2387
|
+
role === "stt"
|
|
2388
|
+
? "stt-transcription-method-unavailable"
|
|
2389
|
+
: "tts-synthesis-method-unavailable",
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
lifecycleReason = role === "stt"
|
|
2393
|
+
? "stt-transcription-started"
|
|
2394
|
+
: "tts-synthesis-started";
|
|
2395
|
+
try {
|
|
2396
|
+
const result = validateResult(
|
|
2397
|
+
role,
|
|
2398
|
+
await method(input, { signal }),
|
|
2399
|
+
configuration,
|
|
2400
|
+
);
|
|
2401
|
+
lifecycleReason = role === "stt"
|
|
2402
|
+
? "stt-transcription-completed"
|
|
2403
|
+
: "tts-synthesis-completed";
|
|
2404
|
+
return result;
|
|
2405
|
+
} catch (error) {
|
|
2406
|
+
const failure = admitWorkerFailure(
|
|
2407
|
+
error,
|
|
2408
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
2409
|
+
"The speech engine operation was rejected.",
|
|
2410
|
+
role === "stt"
|
|
2411
|
+
? "stt-transcription-engine-operation-rejected"
|
|
2412
|
+
: "tts-synthesis-engine-operation-rejected",
|
|
2413
|
+
);
|
|
2414
|
+
lifecycleReason = failure.reason;
|
|
2415
|
+
throw failure;
|
|
429
2416
|
}
|
|
430
|
-
return validateResult(role, await method(input, { signal }));
|
|
431
2417
|
}
|
|
432
2418
|
|
|
433
|
-
async function unload() {
|
|
434
|
-
for (const
|
|
2419
|
+
async function unload(reason = `${role}-unload-completed`) {
|
|
2420
|
+
for (const operation of operations.values()) {
|
|
2421
|
+
if (operation.op === "unload" || operation.op === "dispose") continue;
|
|
2422
|
+
operation.controller.abort(operationReason(role, operation.op, "superseded-by-unload"));
|
|
2423
|
+
}
|
|
435
2424
|
const current = engine;
|
|
436
2425
|
engine = null;
|
|
437
|
-
|
|
2426
|
+
let disposeFailure = null;
|
|
438
2427
|
try {
|
|
439
2428
|
await disposeEngine(current);
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
const failure = admitWorkerFailure(
|
|
2431
|
+
error,
|
|
2432
|
+
"ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
2433
|
+
"The speech engine dispose operation was rejected.",
|
|
2434
|
+
`${role}-worker-engine-dispose-rejected`,
|
|
2435
|
+
);
|
|
2436
|
+
disposeFailure = failure;
|
|
2437
|
+
lifecycleReason = failure.reason;
|
|
2438
|
+
throw failure;
|
|
440
2439
|
} finally {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
2440
|
+
restoreNamespace?.();
|
|
2441
|
+
restoreNamespace = null;
|
|
2442
|
+
try {
|
|
2443
|
+
environment?.cleanup();
|
|
2444
|
+
} catch {
|
|
2445
|
+
// Worker termination remains the final cleanup boundary.
|
|
2446
|
+
}
|
|
2447
|
+
environment = null;
|
|
2448
|
+
configuration = null;
|
|
2449
|
+
if (!disposeFailure) lifecycleReason = reason;
|
|
445
2450
|
}
|
|
446
2451
|
return status();
|
|
447
2452
|
}
|
|
448
2453
|
|
|
449
|
-
async function dispatch(request, signal) {
|
|
450
|
-
if (
|
|
451
|
-
if (
|
|
452
|
-
if (
|
|
453
|
-
if (
|
|
454
|
-
if (
|
|
455
|
-
await unload();
|
|
2454
|
+
async function dispatch(request, signal, op) {
|
|
2455
|
+
if (op === "load") return load(request, signal);
|
|
2456
|
+
if (op === "use") return use(request, signal);
|
|
2457
|
+
if (op === "status") return status();
|
|
2458
|
+
if (op === "unload") return unload(`${role}-unload-completed`);
|
|
2459
|
+
if (op === "dispose") {
|
|
2460
|
+
await unload(`${role}-dispose-unloaded-worker`);
|
|
456
2461
|
disposed = true;
|
|
2462
|
+
lifecycleReason = `${role}-dispose-completed`;
|
|
457
2463
|
return status();
|
|
458
2464
|
}
|
|
459
|
-
throw workerError(
|
|
2465
|
+
throw workerError(
|
|
2466
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2467
|
+
"The speech worker operation is not part of its protocol.",
|
|
2468
|
+
undefined,
|
|
2469
|
+
`${role}-worker-operation-unknown`,
|
|
2470
|
+
);
|
|
460
2471
|
}
|
|
461
2472
|
|
|
462
|
-
function respond(request, operation) {
|
|
2473
|
+
function respond(request, operation, op) {
|
|
463
2474
|
operation.then((result) => send({
|
|
464
2475
|
protocol: SPEECH_WORKER_PROTOCOL,
|
|
465
2476
|
id: request.id,
|
|
@@ -469,7 +2480,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
469
2480
|
protocol: SPEECH_WORKER_PROTOCOL,
|
|
470
2481
|
id: request.id,
|
|
471
2482
|
ok: false,
|
|
472
|
-
error: serializedError(error),
|
|
2483
|
+
error: serializedError(error, role, op),
|
|
473
2484
|
}, []));
|
|
474
2485
|
}
|
|
475
2486
|
|
|
@@ -477,40 +2488,172 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
|
|
|
477
2488
|
if (request?.protocol !== SPEECH_WORKER_PROTOCOL
|
|
478
2489
|
|| !Number.isSafeInteger(request.id)
|
|
479
2490
|
|| request.id < 1) {
|
|
480
|
-
return Promise.reject(workerError(
|
|
2491
|
+
return Promise.reject(workerError(
|
|
2492
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2493
|
+
"The speech worker envelope shape was rejected.",
|
|
2494
|
+
undefined,
|
|
2495
|
+
`${role}-worker-message-envelope-shape-rejected`,
|
|
2496
|
+
));
|
|
2497
|
+
}
|
|
2498
|
+
const op = request.op;
|
|
2499
|
+
if (!TRANSPORT_WORKER_OPERATION_SET.has(op)) {
|
|
2500
|
+
return Promise.reject(workerError(
|
|
2501
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
2502
|
+
"The speech worker operation is not part of its protocol.",
|
|
2503
|
+
undefined,
|
|
2504
|
+
`${role}-worker-operation-unknown`,
|
|
2505
|
+
));
|
|
481
2506
|
}
|
|
482
|
-
if (
|
|
2507
|
+
if (op === "cancel") {
|
|
483
2508
|
const target = operations.get(request.payload?.targetId);
|
|
484
|
-
target?.abort(
|
|
485
|
-
const result = Promise.resolve(Object.freeze({
|
|
486
|
-
|
|
2509
|
+
target?.controller.abort(operationReason(role, target.op, "cancelled"));
|
|
2510
|
+
const result = Promise.resolve(Object.freeze({
|
|
2511
|
+
cancelled: Boolean(target),
|
|
2512
|
+
reason: target
|
|
2513
|
+
? operationReason(role, target.op, "cancelled")
|
|
2514
|
+
: `${role}-cancel-target-not-active`,
|
|
2515
|
+
}));
|
|
2516
|
+
respond(request, result, op);
|
|
487
2517
|
return result;
|
|
488
2518
|
}
|
|
489
2519
|
const controller = new AbortController();
|
|
2520
|
+
const publicOperation = op === "use"
|
|
2521
|
+
? role === "stt" ? "stt-transcription" : "tts-synthesis"
|
|
2522
|
+
: `${role}-${op}`;
|
|
2523
|
+
const record = { controller, op, publicOperation };
|
|
490
2524
|
const execute = tail.catch(() => undefined).then(() => {
|
|
491
|
-
throwIfAborted(controller.signal);
|
|
492
|
-
return dispatch(request, controller.signal);
|
|
2525
|
+
throwIfAborted(controller.signal, operationReason(role, op, "cancelled"));
|
|
2526
|
+
return dispatch(request, controller.signal, op);
|
|
493
2527
|
});
|
|
494
|
-
operations.set(request.id,
|
|
2528
|
+
operations.set(request.id, record);
|
|
495
2529
|
const operation = execute.finally(() => operations.delete(request.id));
|
|
496
2530
|
tail = operation.catch(() => undefined);
|
|
497
|
-
respond(request, operation);
|
|
2531
|
+
respond(request, operation, op);
|
|
498
2532
|
return operation;
|
|
499
2533
|
}
|
|
500
2534
|
|
|
501
2535
|
return Object.freeze({ handleMessage, status });
|
|
502
2536
|
}
|
|
503
2537
|
|
|
2538
|
+
function privatePort(value) {
|
|
2539
|
+
return value
|
|
2540
|
+
&& typeof value.postMessage === "function"
|
|
2541
|
+
&& typeof value.addEventListener === "function"
|
|
2542
|
+
? value
|
|
2543
|
+
: null;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
504
2546
|
export function installBrowserSpeechWorker(role, scope = globalThis) {
|
|
2547
|
+
let transport = scope;
|
|
2548
|
+
let transportMode = null;
|
|
505
2549
|
const runtime = createSpeechWorkerRuntime({
|
|
506
2550
|
role,
|
|
507
2551
|
scope,
|
|
508
|
-
send: (message, transfers) =>
|
|
2552
|
+
send: (message, transfers) => transport.postMessage(message, transfers),
|
|
509
2553
|
});
|
|
2554
|
+
|
|
2555
|
+
function receive(request) {
|
|
2556
|
+
void runtime.handleMessage(request).catch(() => undefined);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
510
2559
|
scope.addEventListener("message", (event) => {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
2560
|
+
if (transportMode === "private-message-port") return;
|
|
2561
|
+
const requestedPort = privatePort(event.data?.privatePort);
|
|
2562
|
+
const isGraphLoad = event.data?.op === "load"
|
|
2563
|
+
&& graphConfiguration(event.data?.payload?.configuration);
|
|
2564
|
+
if (requestedPort) {
|
|
2565
|
+
if (!isGraphLoad || transportMode !== null) return;
|
|
2566
|
+
transportMode = "private-message-port";
|
|
2567
|
+
transport = requestedPort;
|
|
2568
|
+
requestedPort.addEventListener("message", (portEvent) => receive(portEvent.data));
|
|
2569
|
+
requestedPort.start?.();
|
|
2570
|
+
const { privatePort: ignored, ...request } = event.data;
|
|
2571
|
+
void ignored;
|
|
2572
|
+
receive(request);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (isGraphLoad) {
|
|
2576
|
+
scope.postMessage({
|
|
2577
|
+
protocol: SPEECH_WORKER_PROTOCOL,
|
|
2578
|
+
id: event.data.id,
|
|
2579
|
+
ok: false,
|
|
2580
|
+
error: serializedError(workerError(
|
|
2581
|
+
"ARCANE_AI_ARTIFACT_GRAPH_ISOLATION_UNAVAILABLE",
|
|
2582
|
+
"Authenticated artifact graph loading requires a private MessagePort.",
|
|
2583
|
+
undefined,
|
|
2584
|
+
"artifact-graph-private-message-port-missing",
|
|
2585
|
+
), role, "load"),
|
|
2586
|
+
});
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
transportMode ??= "worker-global-message";
|
|
2590
|
+
receive(event.data);
|
|
514
2591
|
});
|
|
515
2592
|
return runtime;
|
|
516
2593
|
}
|
|
2594
|
+
|
|
2595
|
+
function replayWorkerMessage(scope, event) {
|
|
2596
|
+
if (typeof scope.dispatchEvent === "function") {
|
|
2597
|
+
scope.dispatchEvent(event);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
scope.onmessage?.(event);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThis) {
|
|
2604
|
+
if (role !== "stt" && role !== "tts") {
|
|
2605
|
+
throw new TypeError('Speech artifact module Worker role must be "stt" or "tts".');
|
|
2606
|
+
}
|
|
2607
|
+
let initializing = false;
|
|
2608
|
+
const queued = [];
|
|
2609
|
+
const bootstrap = (event) => {
|
|
2610
|
+
if (initializing) {
|
|
2611
|
+
queued.push(event);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const request = event.data;
|
|
2615
|
+
if (request?.protocol !== NESTED_WORKER_PROTOCOL
|
|
2616
|
+
|| request.op !== "initialize-authenticated-artifact-module-worker"
|
|
2617
|
+
|| request.role !== role) return;
|
|
2618
|
+
initializing = true;
|
|
2619
|
+
void (async () => {
|
|
2620
|
+
let environment = null;
|
|
2621
|
+
try {
|
|
2622
|
+
const configuration = validateConfiguration(request.configuration, role);
|
|
2623
|
+
const target = configuration.runtime.files.find((file) =>
|
|
2624
|
+
file.path === request.targetPath);
|
|
2625
|
+
if (!target?.moduleUrl) {
|
|
2626
|
+
throw workerError(
|
|
2627
|
+
"ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
|
|
2628
|
+
"The nested module Worker target was not materialized.",
|
|
2629
|
+
undefined,
|
|
2630
|
+
"artifact-graph-module-worker-target-not-materialized",
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
environment = installArtifactGraphEnvironment(scope, configuration, role);
|
|
2634
|
+
await import(target.moduleUrl);
|
|
2635
|
+
scope.removeEventListener("message", bootstrap);
|
|
2636
|
+
await new Promise((resolve) => queueMicrotask(resolve));
|
|
2637
|
+
for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
try {
|
|
2640
|
+
environment?.cleanup();
|
|
2641
|
+
} catch {
|
|
2642
|
+
// Preserve the exact nested Worker bootstrap failure.
|
|
2643
|
+
}
|
|
2644
|
+
scope.postMessage({
|
|
2645
|
+
protocol: NESTED_WORKER_PROTOCOL,
|
|
2646
|
+
event: "artifact-module-worker-bootstrap-rejected",
|
|
2647
|
+
error: serializedError(error, role, "load"),
|
|
2648
|
+
});
|
|
2649
|
+
scope.close?.();
|
|
2650
|
+
}
|
|
2651
|
+
})();
|
|
2652
|
+
};
|
|
2653
|
+
scope.addEventListener("message", bootstrap);
|
|
2654
|
+
return Object.freeze({
|
|
2655
|
+
protocol: NESTED_WORKER_PROTOCOL,
|
|
2656
|
+
role,
|
|
2657
|
+
lifecycleStatus: `${role}-artifact-module-worker-awaiting-initialization`,
|
|
2658
|
+
});
|
|
2659
|
+
}
|