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