arcane-os 0.2.0 → 0.2.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 +18 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +5 -5
- package/browser-runtime/ai/browser-speech-providers.mjs +330 -25
- package/docs/architecture.md +2 -2
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +11 -11
- package/runtime/arcane/components/chat.html +272 -1
- package/runtime/arcane/modules/AI.js +698 -17
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +93 -8
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +448 -24
- package/schemas/arcane-lock.schema.json +6 -4
- package/src/dev-server.mjs +29 -13
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +134 -83
- package/src/packager/core.mjs +311 -39
- package/src/scaffold.mjs +45 -17
- package/src/templates/workspace-template.mjs +23 -4
- package/src/toolchain.mjs +10 -2
- package/src/workspace.mjs +177 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- Added explicit selected-unloaded chat activation, truthful legacy Cloud/Core
|
|
6
|
+
route readiness, and provider-native chat-response normalization without
|
|
7
|
+
startup downloads, hidden provider selection, or application policy.
|
|
8
|
+
- Normalized shared STT/TTS requests at the browser-speech provider boundary
|
|
9
|
+
while retaining caller-supplied immutable runtime, model, and voice
|
|
10
|
+
authorities and fail-closed format, lifecycle, error, and cancellation rules.
|
|
11
|
+
- Applied authenticated dependency import maps to every packaged browser
|
|
12
|
+
document and published a deterministic browser-readable runtime projection
|
|
13
|
+
inventory derived from the verified package receipt.
|
|
14
|
+
- Added caller-budgeted complete DBOPFS source evaluation with deterministic
|
|
15
|
+
zero-score fallback, independent scoring/excerpt/output bounds, and opt-in
|
|
16
|
+
preserve-readable bootstrap/evaluation read coverage.
|
|
17
|
+
- Bound external workspace validation, packaging, scaffolding, doctor,
|
|
18
|
+
toolchain, and development serving to one authenticated installed package,
|
|
19
|
+
including the exact `arcane-sdk@npm:arcane-os@0.2.1` alias form.
|
|
20
|
+
|
|
3
21
|
## 0.2.0
|
|
4
22
|
|
|
5
23
|
- Made the SDK the canonical owner of the portable Arcane runtime and retired
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.2.
|
|
22
|
+
This checkout defines the `0.2.1` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into immutable
|
|
24
24
|
application artifacts.
|
|
25
25
|
|
|
@@ -132,7 +132,7 @@ uses the same controller for automatic memory extraction.
|
|
|
132
132
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
133
133
|
|
|
134
134
|
```bash
|
|
135
|
-
npx arcane-os@0.2.
|
|
135
|
+
npx arcane-os@0.2.1 new my-app --path ./my-app --target portable --git
|
|
136
136
|
cd my-app
|
|
137
137
|
npm install
|
|
138
138
|
npm run check
|
|
@@ -143,7 +143,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
143
143
|
missing Arcane files:
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
|
-
npm install --save-dev --save-exact arcane-os@0.2.
|
|
146
|
+
npm install --save-dev --save-exact arcane-os@0.2.1
|
|
147
147
|
npm exec -- arcane init my-app --target portable
|
|
148
148
|
```
|
|
149
149
|
|
|
@@ -159,7 +159,7 @@ npm exec -- arcane-os targets
|
|
|
159
159
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
160
160
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
161
161
|
|
|
162
|
-
Use `npx arcane-os@0.2.
|
|
162
|
+
Use `npx arcane-os@0.2.1` for the initial bootstrap because it names this npm
|
|
163
163
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
164
164
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
165
165
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -180,7 +180,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
180
180
|
|
|
181
181
|
# From the generated app repository
|
|
182
182
|
cd ../local-app
|
|
183
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.2.
|
|
183
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.2.1.tgz
|
|
184
184
|
npm run check
|
|
185
185
|
npm ci
|
|
186
186
|
```
|
|
@@ -191,7 +191,7 @@ still verifies the installed package name and exact version, the locked runtime
|
|
|
191
191
|
identity, and the runtime bytes. Local directory `file:` dependencies are not
|
|
192
192
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
193
193
|
runner also needs that tarball at the locked path. After publication, replace
|
|
194
|
-
the local declaration with the exact `arcane-os@0.2.
|
|
194
|
+
the local declaration with the exact `arcane-os@0.2.1` registry package and
|
|
195
195
|
commit the regenerated lock.
|
|
196
196
|
|
|
197
197
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -304,7 +304,7 @@ recorded length through a final identity check; an appended byte, concurrent
|
|
|
304
304
|
growth, path replacement, or hard link fails closed. NFC paths use defined
|
|
305
305
|
UTF-8 byte ordering, covered by one pinned golden bundle digest on every
|
|
306
306
|
supported Node/runner combination. This SDK accepts only the explicitly listed
|
|
307
|
-
`0.2.
|
|
307
|
+
`0.2.1` bundle generation; structural validity does not imply cross-SDK
|
|
308
308
|
compatibility, and a release with zero payload bytes cannot be created. Portable
|
|
309
309
|
path validation rejects file/directory prefix conflicts, case-colliding prefix
|
|
310
310
|
spellings, and Windows device aliases including superscript COM/LPT digits.
|
|
@@ -350,7 +350,7 @@ package installation, or assertions.
|
|
|
350
350
|
|
|
351
351
|
## Current target support
|
|
352
352
|
|
|
353
|
-
Version `0.2.
|
|
353
|
+
Version `0.2.1` exposes one browser target and five explicitly paired
|
|
354
354
|
native development targets: a verified non-runnable portable directory, a
|
|
355
355
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
356
356
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"builder": "arcane-sdk-browser-runtime-v1",
|
|
4
|
-
"sdkVersion": "0.2.
|
|
4
|
+
"sdkVersion": "0.2.1",
|
|
5
5
|
"source": {
|
|
6
6
|
"authority": "arcane-os-sdk",
|
|
7
7
|
"repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
"fileCount": 25,
|
|
32
|
-
"totalBytes":
|
|
33
|
-
"contentSha256": "
|
|
32
|
+
"totalBytes": 9279974,
|
|
33
|
+
"contentSha256": "a9715c4b3aef70ec4042e4738089568d6588877b29582e0f758ed83897b6814f",
|
|
34
34
|
"files": [
|
|
35
35
|
{
|
|
36
36
|
"path": "ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"path": "ai/browser-speech-providers.mjs",
|
|
58
58
|
"sourcePath": "browser-runtime/ai/browser-speech-providers.mjs",
|
|
59
59
|
"provenance": "sdk-source-identity",
|
|
60
|
-
"bytes":
|
|
61
|
-
"sha256": "
|
|
60
|
+
"bytes": 25982,
|
|
61
|
+
"sha256": "65e0a8da2bcd46b11c3ea2ca496105875977decc53d4e4c94ca56949dbb569b1"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "ai/browser-speech.mjs",
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const AI_PROVIDER_PROTOCOL = "arcane-ai-provider/2";
|
|
17
17
|
const ROLE_OPERATION = Object.freeze({ stt: "transcribe", tts: "synthesize" });
|
|
18
|
+
const STT_SAMPLE_RATE = 16_000;
|
|
19
|
+
const TTS_SAMPLE_RATE = 24_000;
|
|
20
|
+
const TTS_RESPONSE_FORMAT = "wav";
|
|
18
21
|
const WORKER_FAILURE_CODES = new Set([
|
|
19
22
|
"ARCANE_AI_WORKER_CRASHED",
|
|
20
23
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
@@ -82,23 +85,237 @@ function publicStatus({ role, id, authority, state, busy, generation, errorCode,
|
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
function
|
|
86
|
-
if (!
|
|
87
|
-
|
|
88
|
+
function assertRequestAuthority(context, authority, providerId) {
|
|
89
|
+
if (!Object.hasOwn(context, "selection")) return;
|
|
90
|
+
const selection = context.selection;
|
|
91
|
+
if (selection?.providerId !== providerId
|
|
92
|
+
|| selection?.modelId !== authority.modelId
|
|
93
|
+
|| selection?.localOnly === false) {
|
|
94
|
+
throw providerError(
|
|
95
|
+
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
96
|
+
"The browser speech request selection changed from its immutable provider authority.",
|
|
97
|
+
);
|
|
88
98
|
}
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertPayloadModel(payload, authority, { required = false } = {}) {
|
|
102
|
+
if (!Object.hasOwn(payload, "model")) {
|
|
103
|
+
if (!required) return;
|
|
104
|
+
throw providerError(
|
|
105
|
+
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
106
|
+
"The shared speech request must identify the provider's immutable model authority.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const descriptor = Object.getOwnPropertyDescriptor(payload, "model");
|
|
110
|
+
if (!Object.hasOwn(descriptor ?? {}, "value") || descriptor.value !== authority.modelId) {
|
|
111
|
+
throw providerError(
|
|
112
|
+
"ARCANE_AI_MODEL_AUTHORITY_REQUIRED",
|
|
113
|
+
"The shared speech request model does not match this provider authority.",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function genericPayloadDescriptors(payload, allowedKeys, requiredKeys, label) {
|
|
119
|
+
const prototype = Object.getPrototypeOf(payload);
|
|
120
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
121
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be a plain object.`);
|
|
122
|
+
}
|
|
123
|
+
const descriptors = Object.getOwnPropertyDescriptors(payload);
|
|
124
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
125
|
+
if (typeof key === "symbol" || !allowedKeys.includes(key)) {
|
|
126
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} contains an unknown field.`);
|
|
127
|
+
}
|
|
128
|
+
if (!Object.hasOwn(descriptors[key], "value")) {
|
|
129
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label}.${key} must be a data property.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const key of requiredKeys) {
|
|
133
|
+
if (!Object.hasOwn(descriptors, key)) {
|
|
134
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label}.${key} is required.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return descriptors;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function audioMimeEssence(value, label) {
|
|
141
|
+
if (typeof value !== "string") {
|
|
142
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be an audio MIME type.`);
|
|
143
|
+
}
|
|
144
|
+
const essence = value.split(";", 1)[0].trim().toLowerCase();
|
|
145
|
+
if (!/^audio\/[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(essence)) {
|
|
146
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", `${label} must be an audio MIME type.`);
|
|
147
|
+
}
|
|
148
|
+
return essence;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function awaitAbortableSpeechOperation(operation, signal) {
|
|
152
|
+
const observed = Promise.resolve(operation);
|
|
153
|
+
if (!signal) return observed;
|
|
154
|
+
if (signal.aborted) {
|
|
155
|
+
// Blob and Web Audio operations cannot be preempted; retain their eventual
|
|
156
|
+
// rejection after cancellation instead of leaving an unobserved promise.
|
|
157
|
+
observed.catch(function retainCancelledSpeechOperation() {});
|
|
158
|
+
return Promise.reject(abortError(signal));
|
|
159
|
+
}
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
let settled = false;
|
|
162
|
+
const release = () => signal.removeEventListener("abort", cancel);
|
|
163
|
+
const settle = (callback, value) => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
settled = true;
|
|
166
|
+
release();
|
|
167
|
+
callback(value);
|
|
168
|
+
};
|
|
169
|
+
const cancel = () => settle(reject, abortError(signal));
|
|
170
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
171
|
+
observed.then(
|
|
172
|
+
(value) => settle(resolve, value),
|
|
173
|
+
(error) => settle(reject, error),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function decodeSharedTranscriptionPayload(payload, authority, signal) {
|
|
179
|
+
const descriptors = genericPayloadDescriptors(
|
|
180
|
+
payload,
|
|
181
|
+
["audio", "mimeType", "model"],
|
|
182
|
+
["audio", "mimeType", "model"],
|
|
183
|
+
"Shared speech transcription payload",
|
|
184
|
+
);
|
|
185
|
+
assertPayloadModel(payload, authority, { required: true });
|
|
186
|
+
const audio = descriptors.audio.value;
|
|
187
|
+
if (typeof Blob !== "function" || !(audio instanceof Blob) || audio.size < 1) {
|
|
188
|
+
throw providerError(
|
|
189
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
190
|
+
"Shared speech transcription requires a nonempty audio Blob or File.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const mimeType = audioMimeEssence(descriptors.mimeType.value, "Speech transcription mimeType");
|
|
194
|
+
if (audio.type && audioMimeEssence(audio.type, "Speech transcription Blob.type") !== mimeType) {
|
|
195
|
+
throw providerError(
|
|
196
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
197
|
+
"Speech transcription mimeType does not match the audio Blob type.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const OfflineAudioContext = globalThis.OfflineAudioContext
|
|
201
|
+
?? globalThis.webkitOfflineAudioContext;
|
|
202
|
+
if (typeof OfflineAudioContext !== "function") {
|
|
203
|
+
throw providerError(
|
|
204
|
+
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
205
|
+
"Shared Blob transcription requires OfflineAudioContext decoding at 16000 Hz.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
let decoder;
|
|
209
|
+
try {
|
|
210
|
+
decoder = new OfflineAudioContext(1, 1, STT_SAMPLE_RATE);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw providerError(
|
|
213
|
+
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
214
|
+
"Unable to create the required 16000 Hz speech decoder.",
|
|
215
|
+
error,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (typeof decoder.decodeAudioData !== "function") {
|
|
219
|
+
throw providerError(
|
|
220
|
+
"ARCANE_AI_AUDIO_DECODE_UNAVAILABLE",
|
|
221
|
+
"Shared Blob transcription requires browser audio decoding.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
let decoded;
|
|
225
|
+
try {
|
|
226
|
+
const encoded = await awaitAbortableSpeechOperation(audio.arrayBuffer(), signal);
|
|
227
|
+
throwIfAborted(signal);
|
|
228
|
+
decoded = await awaitAbortableSpeechOperation(decoder.decodeAudioData(encoded), signal);
|
|
229
|
+
throwIfAborted(signal);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (signal?.aborted && error?.code === "ARCANE_AI_REQUEST_ABORTED") throw error;
|
|
232
|
+
throw providerError(
|
|
233
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
234
|
+
"The browser could not decode the supplied speech audio.",
|
|
235
|
+
error,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (decoded?.sampleRate !== STT_SAMPLE_RATE
|
|
239
|
+
|| !Number.isSafeInteger(decoded.length)
|
|
240
|
+
|| decoded.length < 1
|
|
241
|
+
|| !Number.isSafeInteger(decoded.numberOfChannels)
|
|
242
|
+
|| decoded.numberOfChannels < 1
|
|
243
|
+
|| typeof decoded.getChannelData !== "function") {
|
|
244
|
+
throw providerError(
|
|
245
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
246
|
+
"Decoded speech audio must provide nonempty 16000 Hz channel data.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const channels = [];
|
|
250
|
+
for (let index = 0; index < decoded.numberOfChannels; index += 1) {
|
|
251
|
+
const channel = decoded.getChannelData(index);
|
|
252
|
+
if (!(channel instanceof Float32Array) || channel.length !== decoded.length) {
|
|
91
253
|
throw providerError(
|
|
92
|
-
"
|
|
93
|
-
"
|
|
254
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
255
|
+
"Decoded speech audio returned invalid channel data.",
|
|
94
256
|
);
|
|
95
257
|
}
|
|
96
|
-
|
|
258
|
+
channels.push(channel);
|
|
259
|
+
}
|
|
260
|
+
const mono = new Float32Array(decoded.length);
|
|
261
|
+
for (let index = 0; index < mono.length; index += 1) {
|
|
262
|
+
let sample = 0;
|
|
263
|
+
for (const channel of channels) sample += channel[index];
|
|
264
|
+
sample /= channels.length;
|
|
265
|
+
if (!Number.isFinite(sample)) {
|
|
266
|
+
throw providerError(
|
|
267
|
+
"ARCANE_AI_AUDIO_DECODE_FAILED",
|
|
268
|
+
"Decoded speech audio contains a non-finite sample.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
mono[index] = sample;
|
|
272
|
+
}
|
|
273
|
+
throwIfAborted(signal);
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
payload: Object.freeze({ audio: mono, sampleRate: STT_SAMPLE_RATE }),
|
|
276
|
+
shared: true,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function cloneNativeTranscriptionPayload(payload, authority) {
|
|
281
|
+
assertPayloadModel(payload, authority);
|
|
282
|
+
if (!(payload.audio instanceof Float32Array) || payload.sampleRate !== STT_SAMPLE_RATE) {
|
|
283
|
+
throw providerError(
|
|
284
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
285
|
+
"Whisper requires Float32Array audio sampled at exactly 16000 Hz.",
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
payload: Object.freeze({
|
|
97
290
|
audio: new Float32Array(payload.audio),
|
|
98
|
-
sampleRate:
|
|
99
|
-
})
|
|
291
|
+
sampleRate: STT_SAMPLE_RATE,
|
|
292
|
+
}),
|
|
293
|
+
shared: false,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeSynthesisPayload(payload, authority) {
|
|
298
|
+
const shared = Object.hasOwn(payload, "input");
|
|
299
|
+
let textValue = payload.text;
|
|
300
|
+
if (shared) {
|
|
301
|
+
const descriptors = genericPayloadDescriptors(
|
|
302
|
+
payload,
|
|
303
|
+
["model", "voice", "input", "responseFormat", "speed"],
|
|
304
|
+
["model", "input", "responseFormat"],
|
|
305
|
+
"Shared speech synthesis payload",
|
|
306
|
+
);
|
|
307
|
+
assertPayloadModel(payload, authority, { required: true });
|
|
308
|
+
if (descriptors.responseFormat.value !== TTS_RESPONSE_FORMAT) {
|
|
309
|
+
throw providerError(
|
|
310
|
+
"ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT",
|
|
311
|
+
"Browser Kokoro supports only wav responses for shared speech requests.",
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
textValue = descriptors.input.value;
|
|
315
|
+
} else {
|
|
316
|
+
assertPayloadModel(payload, authority);
|
|
100
317
|
}
|
|
101
|
-
const text = typeof
|
|
318
|
+
const text = typeof textValue === "string" ? textValue.trim() : "";
|
|
102
319
|
const voice = typeof (payload.voice ?? authority.defaultVoice) === "string"
|
|
103
320
|
? (payload.voice ?? authority.defaultVoice).trim()
|
|
104
321
|
: "";
|
|
@@ -109,7 +326,75 @@ function cloneRequestPayload(role, payload, authority) {
|
|
|
109
326
|
"Kokoro requires text, a voice, and speed greater than 0 and at most 4.",
|
|
110
327
|
);
|
|
111
328
|
}
|
|
112
|
-
return Object.freeze({
|
|
329
|
+
return Object.freeze({
|
|
330
|
+
payload: Object.freeze({ text, voice, speed }),
|
|
331
|
+
shared,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function normalizeRequestPayload(role, payload, authority, signal) {
|
|
336
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
337
|
+
throw providerError("ARCANE_AI_INVALID_REQUEST", "Speech request payload must be an object.");
|
|
338
|
+
}
|
|
339
|
+
if (role === "stt") {
|
|
340
|
+
const audio = Object.getOwnPropertyDescriptor(payload, "audio");
|
|
341
|
+
if (Object.hasOwn(audio ?? {}, "value")
|
|
342
|
+
&& typeof Blob === "function"
|
|
343
|
+
&& audio.value instanceof Blob) {
|
|
344
|
+
return decodeSharedTranscriptionPayload(payload, authority, signal);
|
|
345
|
+
}
|
|
346
|
+
return cloneNativeTranscriptionPayload(payload, authority);
|
|
347
|
+
}
|
|
348
|
+
return normalizeSynthesisPayload(payload, authority);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function encodeSharedSynthesisResult(result) {
|
|
352
|
+
if (!(result?.audio instanceof Float32Array)
|
|
353
|
+
|| result.sampleRate !== TTS_SAMPLE_RATE
|
|
354
|
+
|| result.audio.length < 1
|
|
355
|
+
|| result.audio.length > (0xffffffff - 44) / 2) {
|
|
356
|
+
throw providerError(
|
|
357
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
358
|
+
"Browser Kokoro must return nonempty 24000 Hz Float32 PCM.",
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const buffer = new ArrayBuffer(44 + result.audio.length * 2);
|
|
362
|
+
const bytes = new Uint8Array(buffer);
|
|
363
|
+
const view = new DataView(buffer);
|
|
364
|
+
const writeText = (offset, value) => {
|
|
365
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
366
|
+
view.setUint8(offset + index, value.charCodeAt(index));
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
writeText(0, "RIFF");
|
|
370
|
+
view.setUint32(4, buffer.byteLength - 8, true);
|
|
371
|
+
writeText(8, "WAVE");
|
|
372
|
+
writeText(12, "fmt ");
|
|
373
|
+
view.setUint32(16, 16, true);
|
|
374
|
+
view.setUint16(20, 1, true);
|
|
375
|
+
view.setUint16(22, 1, true);
|
|
376
|
+
view.setUint32(24, TTS_SAMPLE_RATE, true);
|
|
377
|
+
view.setUint32(28, TTS_SAMPLE_RATE * 2, true);
|
|
378
|
+
view.setUint16(32, 2, true);
|
|
379
|
+
view.setUint16(34, 16, true);
|
|
380
|
+
writeText(36, "data");
|
|
381
|
+
view.setUint32(40, result.audio.length * 2, true);
|
|
382
|
+
for (let index = 0; index < result.audio.length; index += 1) {
|
|
383
|
+
const sample = result.audio[index];
|
|
384
|
+
if (!Number.isFinite(sample)) {
|
|
385
|
+
throw providerError(
|
|
386
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
387
|
+
"Browser Kokoro returned a non-finite PCM sample.",
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const clamped = Math.max(-1, Math.min(1, sample));
|
|
391
|
+
view.setInt16(
|
|
392
|
+
44 + index * 2,
|
|
393
|
+
Math.round(clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff),
|
|
394
|
+
true,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return Object.freeze({ audio: bytes, contentType: "audio/wav" });
|
|
113
398
|
}
|
|
114
399
|
|
|
115
400
|
function createBrowserSpeechProvider({
|
|
@@ -152,6 +437,13 @@ function createBrowserSpeechProvider({
|
|
|
152
437
|
throw new TypeError("Browser speech authority construction failed.");
|
|
153
438
|
}
|
|
154
439
|
const operation = ROLE_OPERATION[role];
|
|
440
|
+
const speech = role === "stt"
|
|
441
|
+
? Object.freeze({ inputSampleRate: STT_SAMPLE_RATE })
|
|
442
|
+
: Object.freeze({
|
|
443
|
+
outputSampleRate: TTS_SAMPLE_RATE,
|
|
444
|
+
responseFormats: Object.freeze([TTS_RESPONSE_FORMAT]),
|
|
445
|
+
defaultResponseFormat: TTS_RESPONSE_FORMAT,
|
|
446
|
+
});
|
|
155
447
|
const catalogEntry = Object.freeze({
|
|
156
448
|
id: authority.modelId,
|
|
157
449
|
providerId,
|
|
@@ -161,6 +453,7 @@ function createBrowserSpeechProvider({
|
|
|
161
453
|
revision: authority.revision,
|
|
162
454
|
runtime: authority.runtime,
|
|
163
455
|
files: authority.files,
|
|
456
|
+
speech,
|
|
164
457
|
});
|
|
165
458
|
let state = "unloaded";
|
|
166
459
|
let errorCode = null;
|
|
@@ -362,6 +655,7 @@ function createBrowserSpeechProvider({
|
|
|
362
655
|
if (context.role !== role || context.operation !== operation) {
|
|
363
656
|
throw providerError("ARCANE_AI_INVALID_REQUEST", `Browser ${role} supports only ${operation}.`);
|
|
364
657
|
}
|
|
658
|
+
assertRequestAuthority(context, authority, providerId);
|
|
365
659
|
const externalSignal = context.signal ?? null;
|
|
366
660
|
throwIfAborted(externalSignal);
|
|
367
661
|
if (state !== "ready" || !active) {
|
|
@@ -370,10 +664,6 @@ function createBrowserSpeechProvider({
|
|
|
370
664
|
if (requestOperation) {
|
|
371
665
|
throw providerError("ARCANE_AI_PROVIDER_BUSY", "The browser speech provider is already processing a request.");
|
|
372
666
|
}
|
|
373
|
-
const payload = cloneRequestPayload(role, context.payload, {
|
|
374
|
-
defaultVoice: authority.defaultVoice,
|
|
375
|
-
});
|
|
376
|
-
throwIfAborted(externalSignal);
|
|
377
667
|
const linked = linkSignal(externalSignal);
|
|
378
668
|
if (linked.controller.signal.aborted) {
|
|
379
669
|
linked.release();
|
|
@@ -381,21 +671,36 @@ function createBrowserSpeechProvider({
|
|
|
381
671
|
}
|
|
382
672
|
const slot = active;
|
|
383
673
|
const requestGeneration = generation;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
674
|
+
let workerRequestStarted = false;
|
|
675
|
+
const promise = (async () => {
|
|
676
|
+
const normalized = await normalizeRequestPayload(
|
|
677
|
+
role,
|
|
678
|
+
context.payload,
|
|
679
|
+
authority,
|
|
680
|
+
linked.controller.signal,
|
|
681
|
+
);
|
|
682
|
+
throwIfAborted(linked.controller.signal);
|
|
683
|
+
workerRequestStarted = true;
|
|
684
|
+
const result = await slot.client.request("use", normalized.payload, {
|
|
685
|
+
signal: linked.controller.signal,
|
|
686
|
+
});
|
|
687
|
+
if (requestGeneration !== generation || active !== slot) {
|
|
688
|
+
throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "The browser speech result was superseded.");
|
|
689
|
+
}
|
|
690
|
+
return role === "tts" && normalized.shared
|
|
691
|
+
? encodeSharedSynthesisResult(result)
|
|
692
|
+
: result;
|
|
693
|
+
})();
|
|
387
694
|
requestOperation = Object.freeze({
|
|
388
695
|
promise,
|
|
389
696
|
abort: () => linked.controller.abort(),
|
|
390
697
|
});
|
|
391
698
|
try {
|
|
392
|
-
|
|
393
|
-
if (requestGeneration !== generation || active !== slot) {
|
|
394
|
-
throw providerError("ARCANE_AI_OPERATION_SUPERSEDED", "The browser speech result was superseded.");
|
|
395
|
-
}
|
|
396
|
-
return result;
|
|
699
|
+
return await promise;
|
|
397
700
|
} catch (error) {
|
|
398
|
-
if (error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
701
|
+
if (error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
702
|
+
&& workerRequestStarted
|
|
703
|
+
&& active === slot) {
|
|
399
704
|
active = null;
|
|
400
705
|
releaseSlot(slot);
|
|
401
706
|
state = "unloaded";
|
package/docs/architecture.md
CHANGED
|
@@ -200,7 +200,7 @@ length with an EOF growth probe, and rechecked by handle and pathname identity.
|
|
|
200
200
|
Every cumulative path prefix has one case-folded spelling and one file/directory
|
|
201
201
|
kind; prefix topology conflicts and the complete portable Windows device-name
|
|
202
202
|
set fail before creation or admission.
|
|
203
|
-
The current SDK admits only the explicitly compatible `0.2.
|
|
203
|
+
The current SDK admits only the explicitly compatible `0.2.1` bundle
|
|
204
204
|
generation and rejects zero-byte payload releases.
|
|
205
205
|
|
|
206
206
|
Promotion retains any prior output as an identity-bound backup until the new
|
|
@@ -320,7 +320,7 @@ through SDK-bound verified readers rather than accepting a mutable source path
|
|
|
320
320
|
as authority. Build completion requires provider verification, and later
|
|
321
321
|
verify/run calls receive the exact artifact receipt.
|
|
322
322
|
|
|
323
|
-
The SDK `0.2.
|
|
323
|
+
The SDK `0.2.1` runtime requires Arcane `0.8.12` or newer. Compatibility
|
|
324
324
|
is contractual rather than exact-version pinning: the prepared Core must meet
|
|
325
325
|
the highest minimum declared by the runtime, selected app, and bundled app
|
|
326
326
|
dependencies; keep each app's Arcane protocol generation; and provide every
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"builder": "arcane-sdk-runtime-v1",
|
|
4
|
-
"sdkVersion": "0.2.
|
|
4
|
+
"sdkVersion": "0.2.1",
|
|
5
5
|
"source": {
|
|
6
6
|
"authority": "sdk-canonical",
|
|
7
7
|
"repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"fileCount": 160,
|
|
17
|
-
"totalBytes":
|
|
18
|
-
"contentSha256": "
|
|
17
|
+
"totalBytes": 3605154,
|
|
18
|
+
"contentSha256": "5b921d50b6a0cf36a13f7a7dedf96cd3a68104a322e5139136fd4197aa1ca7cb",
|
|
19
19
|
"files": [
|
|
20
20
|
{
|
|
21
21
|
"path": "arcane/components/app-bar.html",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"path": "arcane/components/chat.html",
|
|
42
|
-
"bytes":
|
|
43
|
-
"sha256": "
|
|
42
|
+
"bytes": 61650,
|
|
43
|
+
"sha256": "d644848fbc3ccc85d491db7ada75231f393a371dd3a0ccd92f223c172571e47b"
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"path": "arcane/components/conversation-view.html",
|
|
@@ -374,8 +374,8 @@
|
|
|
374
374
|
},
|
|
375
375
|
{
|
|
376
376
|
"path": "arcane/modules/AI.js",
|
|
377
|
-
"bytes":
|
|
378
|
-
"sha256": "
|
|
377
|
+
"bytes": 103733,
|
|
378
|
+
"sha256": "3a051d67b1e895b7c6f14af36a34daf1d92fe70e02c95c1c4ffd80cc7c622561"
|
|
379
379
|
},
|
|
380
380
|
{
|
|
381
381
|
"path": "arcane/modules/AIPreferenceRuntime.js",
|
|
@@ -499,8 +499,8 @@
|
|
|
499
499
|
},
|
|
500
500
|
{
|
|
501
501
|
"path": "arcane/modules/ConfiguredAIChatSession.js",
|
|
502
|
-
"bytes":
|
|
503
|
-
"sha256": "
|
|
502
|
+
"bytes": 24501,
|
|
503
|
+
"sha256": "f95c3421bf07397e987f77fc3d02166cf780059dd3d6de90ceaded990752f51b"
|
|
504
504
|
},
|
|
505
505
|
{
|
|
506
506
|
"path": "arcane/modules/ConversationActionItems.js",
|
|
@@ -534,8 +534,8 @@
|
|
|
534
534
|
},
|
|
535
535
|
{
|
|
536
536
|
"path": "arcane/modules/DBOPFSDocumentLibrary.js",
|
|
537
|
-
"bytes":
|
|
538
|
-
"sha256": "
|
|
537
|
+
"bytes": 50695,
|
|
538
|
+
"sha256": "2831470ce54c3393595bf068d60c2e774804cbd51dba7491ad734bbfbea7f32f"
|
|
539
539
|
},
|
|
540
540
|
{
|
|
541
541
|
"path": "arcane/modules/DBOPFSWorker.js",
|