arcane-os 0.5.10 → 0.5.12
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 +41 -0
- package/README.md +117 -26
- package/browser-runtime/ai/browser-speech-providers.mjs +1 -1
- package/docs/architecture.md +303 -0
- package/docs/compatibility.md +38 -0
- package/docs/event-manager.md +263 -0
- package/docs/platform-targets.md +104 -0
- package/docs/publishing.md +126 -0
- package/docs/reference/README.md +206 -0
- package/docs/reference/ai/browser-speech.md +879 -0
- package/docs/reference/ai/browser-wasm.md +637 -0
- package/docs/reference/ai/twin-cloud.md +156 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +224 -0
- package/docs/reference/behavioral-testing.md +129 -0
- package/docs/reference/cli.md +820 -0
- package/docs/reference/core/README.md +61 -0
- package/docs/reference/core/arcane-ai-contracts.md +906 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +59 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +1409 -0
- package/docs/reference/inventory/package-api.json +3194 -0
- package/docs/reference/inventory/runtime-components.json +1015 -0
- package/docs/reference/inventory/runtime-entities.json +25 -0
- package/docs/reference/inventory/runtime-modules.json +1367 -0
- package/docs/reference/mail.md +309 -0
- package/docs/reference/protocols.md +749 -0
- package/docs/reference/runtime-components.md +1532 -0
- package/docs/reference/runtime-entities.md +305 -0
- package/docs/reference/runtime-modules.md +3310 -0
- package/docs/reference/sdk-api.md +6733 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +66 -0
- package/examples/wasm-ai-demo/README.md +80 -0
- package/examples/wasm-ai-demo/app.js +787 -0
- package/examples/wasm-ai-demo/index.html +343 -0
- package/examples/wasm-ai-demo/profile-tools.js +217 -0
- package/examples/wasm-ai-demo/profiles/BOSS.Modelfile +106 -0
- package/examples/wasm-ai-demo/profiles/PreCrisis.Modelfile +693 -0
- package/examples/wasm-ai-demo/rag/boss-library.json +3006 -0
- package/examples/wasm-ai-demo/rag.js +295 -0
- package/examples/wasm-ai-demo/server.mjs +71 -0
- package/package.json +10 -1
- package/runtime/arcane/modules/AI.js +60 -11
- package/runtime/arcane/modules/AIProviderRuntime.js +26 -5
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import DBOPFSDocumentLibrary from "arcane/DBOPFSDocumentLibrary";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BOSS_BUNDLE_URL = "./rag/boss-library.json";
|
|
4
|
+
const PROFILE_IDS = new Set(["general", "precrisis", "boss"]);
|
|
5
|
+
const libraryEntries = new WeakMap();
|
|
6
|
+
|
|
7
|
+
function normalizeProfileId(profileId) {
|
|
8
|
+
const normalized = String(profileId || "general").trim().toLowerCase();
|
|
9
|
+
if (!PROFILE_IDS.has(normalized)) {
|
|
10
|
+
throw new TypeError(`Unsupported example profile: ${normalized || "empty"}.`);
|
|
11
|
+
}
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function reportStatus(onStatus, message) {
|
|
16
|
+
if (typeof onStatus === "function") onStatus(message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function entryMap(dbopfs) {
|
|
20
|
+
let entries = libraryEntries.get(dbopfs);
|
|
21
|
+
if (!entries) {
|
|
22
|
+
entries = new Map();
|
|
23
|
+
libraryEntries.set(dbopfs, entries);
|
|
24
|
+
}
|
|
25
|
+
return entries;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function schemaFor(profileId, version = "1") {
|
|
29
|
+
return {
|
|
30
|
+
id: `wasm-ai-demo-${profileId}`,
|
|
31
|
+
table: `wasm_ai_demo_${profileId}_documents`,
|
|
32
|
+
version: String(version),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createLibrary(dbopfs, profileId, version) {
|
|
37
|
+
return new DBOPFSDocumentLibrary({
|
|
38
|
+
db: dbopfs,
|
|
39
|
+
schema: schemaFor(profileId, version),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitTerms(value) {
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
return String(value || "")
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((entry) => entry.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bossRecord(document, index, sourcePath) {
|
|
54
|
+
const record = document && typeof document === "object" && !Array.isArray(document)
|
|
55
|
+
? document
|
|
56
|
+
: {};
|
|
57
|
+
const id = String(record.id || `boss-library-${index + 1}`).trim();
|
|
58
|
+
const title = String(record.title || id).trim();
|
|
59
|
+
const body = String(record.text ?? record.body ?? "");
|
|
60
|
+
const terms = splitTerms(record.topics);
|
|
61
|
+
return {
|
|
62
|
+
body,
|
|
63
|
+
id,
|
|
64
|
+
kind: "boss-library",
|
|
65
|
+
mediaType: "text/markdown",
|
|
66
|
+
path: id,
|
|
67
|
+
searchTerms: terms,
|
|
68
|
+
sourcePath,
|
|
69
|
+
tags: ["boss", ...terms],
|
|
70
|
+
title,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchBossBundle(bundleUrl) {
|
|
75
|
+
const response = await fetch(bundleUrl, { cache: "no-store" });
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`Unable to load the BOSS library: HTTP ${response.status}.`);
|
|
78
|
+
}
|
|
79
|
+
const bundle = await response.json();
|
|
80
|
+
if (!bundle || typeof bundle !== "object" || !Array.isArray(bundle.documents)) {
|
|
81
|
+
throw new TypeError("The BOSS library response must contain a documents array.");
|
|
82
|
+
}
|
|
83
|
+
return bundle;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readCorpus(library, signal) {
|
|
87
|
+
try {
|
|
88
|
+
return { ...(await library.search("", { signal })), bootstrapped: true };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error?.code === "DBOPFS_DOCUMENT_NOT_BOOTSTRAPPED") {
|
|
91
|
+
return { bootstrapped: false, failures: [], matches: [], total: 0 };
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function prepareLibrary({ dbopfs, bundleUrl, initialLibrary, onStatus, profileId, signal }) {
|
|
98
|
+
let version = "1";
|
|
99
|
+
let bossDocuments = [];
|
|
100
|
+
if (profileId === "boss") {
|
|
101
|
+
reportStatus(onStatus, "Loading the BOSS document catalog…");
|
|
102
|
+
const bundle = await fetchBossBundle(bundleUrl);
|
|
103
|
+
version = String(bundle.schema_version ?? "1");
|
|
104
|
+
bossDocuments = bundle.documents.map((document, index) => (
|
|
105
|
+
bossRecord(document, index, bundleUrl)
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const library = profileId === "boss"
|
|
110
|
+
? createLibrary(dbopfs, profileId, version)
|
|
111
|
+
: initialLibrary;
|
|
112
|
+
const existing = await readCorpus(library, signal);
|
|
113
|
+
const userDocuments = existing.matches.filter((document) => document.kind === "user-import");
|
|
114
|
+
const hasBossDocuments = existing.matches.some((document) => document.kind === "boss-library");
|
|
115
|
+
const needsBootstrap = !existing.bootstrapped || (profileId === "boss" && !hasBossDocuments);
|
|
116
|
+
|
|
117
|
+
if (needsBootstrap) {
|
|
118
|
+
const documents = profileId === "boss"
|
|
119
|
+
? [...bossDocuments, ...userDocuments]
|
|
120
|
+
: userDocuments;
|
|
121
|
+
reportStatus(
|
|
122
|
+
onStatus,
|
|
123
|
+
profileId === "boss"
|
|
124
|
+
? "Storing the BOSS library through the Arcane document library…"
|
|
125
|
+
: "Opening the Arcane document library…",
|
|
126
|
+
);
|
|
127
|
+
await library.bootstrap({
|
|
128
|
+
files: documents,
|
|
129
|
+
signal,
|
|
130
|
+
onProgress(progress) {
|
|
131
|
+
if (progress?.phase !== "writing") return;
|
|
132
|
+
reportStatus(
|
|
133
|
+
onStatus,
|
|
134
|
+
`Storing local documents ${progress.completed} of ${progress.total}…`,
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return library;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function libraryFor(profileId, { dbopfs, bundleUrl, onStatus, signal } = {}) {
|
|
144
|
+
const profile = normalizeProfileId(profileId);
|
|
145
|
+
const db = dbopfs ?? globalThis.dbopfs;
|
|
146
|
+
const initialLibrary = createLibrary(db, profile, "1");
|
|
147
|
+
if (db.readyPromise) await db.readyPromise;
|
|
148
|
+
const entries = entryMap(db);
|
|
149
|
+
let entry = entries.get(profile);
|
|
150
|
+
if (!entry) {
|
|
151
|
+
entry = {
|
|
152
|
+
library: null,
|
|
153
|
+
ready: prepareLibrary({
|
|
154
|
+
dbopfs: db,
|
|
155
|
+
bundleUrl: bundleUrl || DEFAULT_BOSS_BUNDLE_URL,
|
|
156
|
+
initialLibrary,
|
|
157
|
+
onStatus,
|
|
158
|
+
profileId: profile,
|
|
159
|
+
signal,
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
entries.set(profile, entry);
|
|
163
|
+
try {
|
|
164
|
+
entry.library = await entry.ready;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
entries.delete(profile);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
} else if (!entry.library) {
|
|
170
|
+
entry.library = await entry.ready;
|
|
171
|
+
}
|
|
172
|
+
return entry.library;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function corpusStats(corpus) {
|
|
176
|
+
const documents = corpus.matches || [];
|
|
177
|
+
const bossCount = documents.filter((document) => document.kind === "boss-library").length;
|
|
178
|
+
const userCount = documents.filter((document) => document.kind === "user-import").length;
|
|
179
|
+
return {
|
|
180
|
+
boss: bossCount,
|
|
181
|
+
bossCount,
|
|
182
|
+
bossDocuments: bossCount,
|
|
183
|
+
failures: corpus.failures || [],
|
|
184
|
+
imported: userCount,
|
|
185
|
+
total: documents.length,
|
|
186
|
+
userCount,
|
|
187
|
+
userDocuments: userCount,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function initializeRag({
|
|
192
|
+
dbopfs = globalThis.dbopfs,
|
|
193
|
+
bundleUrl = DEFAULT_BOSS_BUNDLE_URL,
|
|
194
|
+
onStatus,
|
|
195
|
+
profileId = "general",
|
|
196
|
+
signal,
|
|
197
|
+
} = {}) {
|
|
198
|
+
const library = await libraryFor(profileId, {
|
|
199
|
+
dbopfs,
|
|
200
|
+
bundleUrl,
|
|
201
|
+
onStatus,
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
204
|
+
const corpus = await library.search("", { signal });
|
|
205
|
+
reportStatus(onStatus, "Local Arcane document library ready.");
|
|
206
|
+
return corpusStats(corpus);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function getRagStats(profileId = "general", {
|
|
210
|
+
dbopfs = globalThis.dbopfs,
|
|
211
|
+
signal,
|
|
212
|
+
} = {}) {
|
|
213
|
+
const library = await libraryFor(profileId, { dbopfs, signal });
|
|
214
|
+
return corpusStats(await library.search("", { signal }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function importedDocument(file, profileId) {
|
|
218
|
+
const fallbackId = `${Date.now()}-${Math.random()}`;
|
|
219
|
+
const id = `user-${profileId}-${globalThis.crypto?.randomUUID?.() || fallbackId}`;
|
|
220
|
+
const body = await file.text();
|
|
221
|
+
return {
|
|
222
|
+
body,
|
|
223
|
+
id,
|
|
224
|
+
kind: "user-import",
|
|
225
|
+
mediaType: file.type === "text/markdown" ? "text/markdown" : "text/plain",
|
|
226
|
+
path: file.name || id,
|
|
227
|
+
searchTerms: splitTerms(file.name),
|
|
228
|
+
sourcePath: file.name || id,
|
|
229
|
+
tags: [profileId, "user-import"],
|
|
230
|
+
title: file.name || "Imported document",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function importRagFiles(files, {
|
|
235
|
+
dbopfs = globalThis.dbopfs,
|
|
236
|
+
onStatus,
|
|
237
|
+
profileId = "general",
|
|
238
|
+
signal,
|
|
239
|
+
} = {}) {
|
|
240
|
+
if (!Array.isArray(files)) throw new TypeError("RAG files must be an array.");
|
|
241
|
+
const profile = normalizeProfileId(profileId);
|
|
242
|
+
const library = await libraryFor(profile, { dbopfs, onStatus, signal });
|
|
243
|
+
const corpus = await library.search("", { signal });
|
|
244
|
+
const imported = [];
|
|
245
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
246
|
+
if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
|
|
247
|
+
const file = files[index];
|
|
248
|
+
if (!file || typeof file.text !== "function") {
|
|
249
|
+
throw new TypeError(`RAG file ${index + 1} is not a browser File.`);
|
|
250
|
+
}
|
|
251
|
+
reportStatus(onStatus, `Reading ${file.name || `document ${index + 1}`}…`);
|
|
252
|
+
imported.push(await importedDocument(file, profile));
|
|
253
|
+
}
|
|
254
|
+
await library.bootstrap({
|
|
255
|
+
files: [...corpus.matches, ...imported],
|
|
256
|
+
signal,
|
|
257
|
+
onProgress(progress) {
|
|
258
|
+
if (progress?.phase === "writing") {
|
|
259
|
+
reportStatus(
|
|
260
|
+
onStatus,
|
|
261
|
+
`Storing local documents ${progress.completed} of ${progress.total}…`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
imported: imported.length,
|
|
268
|
+
stats: corpusStats(await library.search("", { signal })),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function retrieveRagContext(query, {
|
|
273
|
+
dbopfs = globalThis.dbopfs,
|
|
274
|
+
profileId = "general",
|
|
275
|
+
signal,
|
|
276
|
+
} = {}) {
|
|
277
|
+
const profile = normalizeProfileId(profileId);
|
|
278
|
+
const library = await libraryFor(profile, { dbopfs, signal });
|
|
279
|
+
const result = await library.buildContext(String(query || ""), { signal });
|
|
280
|
+
const text = result.text && profile === "boss"
|
|
281
|
+
? ["<boss_library_context>", result.text, "</boss_library_context>"].join("\n\n")
|
|
282
|
+
: result.text;
|
|
283
|
+
return {
|
|
284
|
+
failures: result.failures,
|
|
285
|
+
matches: result.documents,
|
|
286
|
+
text,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export {
|
|
291
|
+
getRagStats,
|
|
292
|
+
importRagFiles,
|
|
293
|
+
initializeRag,
|
|
294
|
+
retrieveRagContext,
|
|
295
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { startSourceExampleServer } from "arcane-os";
|
|
5
|
+
|
|
6
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const sdkRoot = path.resolve(root, "..", "..");
|
|
8
|
+
const examplePath = "/examples/wasm-ai-demo";
|
|
9
|
+
const requestedPort = Number.parseInt(process.argv[2] || "4173", 10);
|
|
10
|
+
const port = Number.isFinite(requestedPort) ? requestedPort : 4173;
|
|
11
|
+
const host = process.argv[3] || "0.0.0.0";
|
|
12
|
+
const modelDirectory = process.env.ARCANE_WASM_MODEL_ROOT
|
|
13
|
+
? path.resolve(process.env.ARCANE_WASM_MODEL_ROOT)
|
|
14
|
+
: path.join(root, "models");
|
|
15
|
+
const tlsDirectory = process.env.ARCANE_WASM_TLS_ROOT
|
|
16
|
+
? path.resolve(process.env.ARCANE_WASM_TLS_ROOT)
|
|
17
|
+
: path.join(root, "tls");
|
|
18
|
+
const tlsKeyPath = path.join(tlsDirectory, "server-key.pem");
|
|
19
|
+
const tlsCertificatePath = path.join(tlsDirectory, "server.pem");
|
|
20
|
+
const tls = existsSync(tlsKeyPath) && existsSync(tlsCertificatePath)
|
|
21
|
+
? {
|
|
22
|
+
key: readFileSync(tlsKeyPath),
|
|
23
|
+
cert: readFileSync(tlsCertificatePath),
|
|
24
|
+
}
|
|
25
|
+
: undefined;
|
|
26
|
+
|
|
27
|
+
const running = await startSourceExampleServer({
|
|
28
|
+
crossOriginIsolated: true,
|
|
29
|
+
host,
|
|
30
|
+
mounts: [
|
|
31
|
+
{
|
|
32
|
+
include: [
|
|
33
|
+
"index.html",
|
|
34
|
+
"app.js",
|
|
35
|
+
"rag.js",
|
|
36
|
+
"profile-tools.js",
|
|
37
|
+
"profiles/",
|
|
38
|
+
"rag/",
|
|
39
|
+
],
|
|
40
|
+
index: "index.html",
|
|
41
|
+
root,
|
|
42
|
+
urlPath: examplePath,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
root: modelDirectory,
|
|
46
|
+
urlPath: `${examplePath}/models`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
root: path.join(sdkRoot, "src"),
|
|
50
|
+
urlPath: "/src",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
root: path.join(sdkRoot, "browser-runtime"),
|
|
54
|
+
urlPath: "/browser-runtime",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
include: ["arcane/", "strong-type/"],
|
|
58
|
+
root: path.join(sdkRoot, "runtime"),
|
|
59
|
+
urlPath: "/runtime",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
port,
|
|
63
|
+
startPath: `${examplePath}/`,
|
|
64
|
+
tls,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`Arcane WASM Voice Chat listening on ${running.origin}`);
|
|
68
|
+
console.log(`Example URL: ${running.url}`);
|
|
69
|
+
console.log(`Live Arcane SDK source: ${sdkRoot}`);
|
|
70
|
+
|
|
71
|
+
await running.closed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -44,6 +44,15 @@
|
|
|
44
44
|
"src/",
|
|
45
45
|
"runtime/",
|
|
46
46
|
"schemas/",
|
|
47
|
+
"docs/",
|
|
48
|
+
"examples/wasm-ai-demo/README.md",
|
|
49
|
+
"examples/wasm-ai-demo/index.html",
|
|
50
|
+
"examples/wasm-ai-demo/app.js",
|
|
51
|
+
"examples/wasm-ai-demo/server.mjs",
|
|
52
|
+
"examples/wasm-ai-demo/profile-tools.js",
|
|
53
|
+
"examples/wasm-ai-demo/rag.js",
|
|
54
|
+
"examples/wasm-ai-demo/profiles/",
|
|
55
|
+
"examples/wasm-ai-demo/rag/",
|
|
47
56
|
"README.md",
|
|
48
57
|
"CHANGELOG.md",
|
|
49
58
|
"LICENSE",
|
|
@@ -23,7 +23,7 @@ const DEFAULT_TTS_SEGMENTATION={
|
|
|
23
23
|
};
|
|
24
24
|
const DEFAULT_BROWSER_TTS_EXECUTION={
|
|
25
25
|
device:'auto',
|
|
26
|
-
maxConcurrentRequests:
|
|
26
|
+
maxConcurrentRequests:4
|
|
27
27
|
};
|
|
28
28
|
const BROWSER_TTS_EXECUTION_DEVICES=new Set(['auto','webgpu','wasm']);
|
|
29
29
|
const MAX_BROWSER_TTS_CONCURRENT_REQUESTS=4;
|
|
@@ -5673,7 +5673,8 @@ class AI {
|
|
|
5673
5673
|
|
|
5674
5674
|
streamTTS(
|
|
5675
5675
|
text='',
|
|
5676
|
-
end=false
|
|
5676
|
+
end=false,
|
|
5677
|
+
options={}
|
|
5677
5678
|
){
|
|
5678
5679
|
if(this.muted){
|
|
5679
5680
|
if(end){
|
|
@@ -5682,6 +5683,12 @@ class AI {
|
|
|
5682
5683
|
return Promise.resolve(false);
|
|
5683
5684
|
}
|
|
5684
5685
|
|
|
5686
|
+
const {voice,speed,waitForPlayback=false}=options;
|
|
5687
|
+
const pauseAfterMs=Number(options.pauseAfterMs??0);
|
|
5688
|
+
if(!Number.isFinite(pauseAfterMs)||pauseAfterMs<0){
|
|
5689
|
+
throw new RangeError('Speech pauses must be a nonnegative number of milliseconds.');
|
|
5690
|
+
}
|
|
5691
|
+
|
|
5685
5692
|
this.audioMessageChunks+=String(text||'');
|
|
5686
5693
|
const outputs=this.#extractSpeechSegments(end);
|
|
5687
5694
|
|
|
@@ -5702,8 +5709,13 @@ class AI {
|
|
|
5702
5709
|
const generation=this.speechGeneration;
|
|
5703
5710
|
const jobs=[];
|
|
5704
5711
|
|
|
5705
|
-
for(const output of outputs){
|
|
5706
|
-
jobs.push(this.#queueSpeechJob(output,generation
|
|
5712
|
+
for(const [index,output] of outputs.entries()){
|
|
5713
|
+
jobs.push(this.#queueSpeechJob(output,generation,{
|
|
5714
|
+
voice,
|
|
5715
|
+
speed,
|
|
5716
|
+
waitForPlayback,
|
|
5717
|
+
pauseAfterMs:index===outputs.length-1?pauseAfterMs:0
|
|
5718
|
+
}));
|
|
5707
5719
|
}
|
|
5708
5720
|
|
|
5709
5721
|
return Promise.all(jobs).then(
|
|
@@ -5857,7 +5869,7 @@ class AI {
|
|
|
5857
5869
|
return this.#speechAbbreviations.has(token);
|
|
5858
5870
|
}
|
|
5859
5871
|
|
|
5860
|
-
#queueSpeechJob(text,generation){
|
|
5872
|
+
#queueSpeechJob(text,generation,options={}){
|
|
5861
5873
|
const job={
|
|
5862
5874
|
abortController:null,
|
|
5863
5875
|
audioBuffer:null,
|
|
@@ -5867,13 +5879,22 @@ class AI {
|
|
|
5867
5879
|
scheduledStart:null,
|
|
5868
5880
|
sourceNode:null,
|
|
5869
5881
|
state:'queued',
|
|
5870
|
-
text
|
|
5882
|
+
text,
|
|
5883
|
+
voice:options.voice,
|
|
5884
|
+
speed:options.speed,
|
|
5885
|
+
pauseAfterMs:options.pauseAfterMs??0,
|
|
5886
|
+
resolvePlayback:null
|
|
5871
5887
|
};
|
|
5872
5888
|
const runtime=this;
|
|
5889
|
+
const playback=options.waitForPlayback===true
|
|
5890
|
+
?new Promise(function captureSpeechPlaybackCompletion(resolve){
|
|
5891
|
+
job.resolvePlayback=resolve;
|
|
5892
|
+
})
|
|
5893
|
+
:null;
|
|
5873
5894
|
|
|
5874
5895
|
this.speechJobs.push(job);
|
|
5875
5896
|
|
|
5876
|
-
|
|
5897
|
+
const preparation=Promise.resolve().then(
|
|
5877
5898
|
function synthesizeAvailableSpeech(){
|
|
5878
5899
|
return runtime.#prepareSpeechJob(job);
|
|
5879
5900
|
}
|
|
@@ -5886,6 +5907,7 @@ class AI {
|
|
|
5886
5907
|
);
|
|
5887
5908
|
}
|
|
5888
5909
|
);
|
|
5910
|
+
return playback||preparation;
|
|
5889
5911
|
}
|
|
5890
5912
|
|
|
5891
5913
|
async #prepareSpeechJob(job){
|
|
@@ -5914,16 +5936,18 @@ class AI {
|
|
|
5914
5936
|
async #requestSpeechAudio(job){
|
|
5915
5937
|
job.abortController=new AbortController();
|
|
5916
5938
|
const selection=this.#providerRuntime.selection('tts');
|
|
5917
|
-
const voice=
|
|
5939
|
+
const voice=job.voice===undefined
|
|
5940
|
+
?(selection?this.#providerSpeechVoice():null)
|
|
5941
|
+
:job.voice;
|
|
5918
5942
|
const response=await this.fetchTTS(
|
|
5919
5943
|
{
|
|
5920
5944
|
model:selection?.modelId||this.modelTTS,
|
|
5921
5945
|
input:job.text,
|
|
5922
|
-
...(voice?{voice}:{}),
|
|
5946
|
+
...(job.voice!==undefined||voice?{voice}:{}),
|
|
5923
5947
|
responseFormat:selection
|
|
5924
5948
|
?this.#providerSpeechResponseFormat()
|
|
5925
5949
|
:this.audioFormat,
|
|
5926
|
-
speed:this.voiceSpeed
|
|
5950
|
+
speed:job.speed===undefined?this.voiceSpeed:job.speed
|
|
5927
5951
|
},
|
|
5928
5952
|
job.abortController.signal
|
|
5929
5953
|
);
|
|
@@ -6294,6 +6318,8 @@ class AI {
|
|
|
6294
6318
|
for(const job of this.speechJobs){
|
|
6295
6319
|
job.abortController?.abort();
|
|
6296
6320
|
job.state='cancelled';
|
|
6321
|
+
job.resolvePlayback?.(false);
|
|
6322
|
+
job.resolvePlayback=null;
|
|
6297
6323
|
|
|
6298
6324
|
if(job.sourceNode){
|
|
6299
6325
|
job.sourceNode.onended=null;
|
|
@@ -6370,6 +6396,22 @@ class AI {
|
|
|
6370
6396
|
if(attempt===this.speechResumeAttempt){
|
|
6371
6397
|
this.speechResumePending=false;
|
|
6372
6398
|
}
|
|
6399
|
+
if(context?.state==='closed'){
|
|
6400
|
+
this.#clearSpeechUnlock();
|
|
6401
|
+
const jobs=this.speechJobs.filter(function usesClosedSpeechContext(job){
|
|
6402
|
+
return job.audioContext===context;
|
|
6403
|
+
});
|
|
6404
|
+
if(!jobs.length){
|
|
6405
|
+
this.#publishTTSFailure(error,{
|
|
6406
|
+
boundary:'playback-resume',
|
|
6407
|
+
generation:this.speechGeneration
|
|
6408
|
+
});
|
|
6409
|
+
}
|
|
6410
|
+
for(const job of jobs){
|
|
6411
|
+
this.#failSpeechJob(job,error,'playback-resume');
|
|
6412
|
+
}
|
|
6413
|
+
return false;
|
|
6414
|
+
}
|
|
6373
6415
|
this.#waitForSpeechGesture(error,context);
|
|
6374
6416
|
if(error?.name!=='NotAllowedError'){
|
|
6375
6417
|
this.#publishTTSFailure(error,{
|
|
@@ -6543,7 +6585,7 @@ class AI {
|
|
|
6543
6585
|
job.state='scheduled';
|
|
6544
6586
|
job.scheduledStart=scheduledStart;
|
|
6545
6587
|
job.scheduledEnd=hasKnownDuration
|
|
6546
|
-
?scheduledStart+duration
|
|
6588
|
+
?scheduledStart+duration+(job.pauseAfterMs??0)/1000
|
|
6547
6589
|
:null;
|
|
6548
6590
|
job.sourceNode.__arcaneStarted=true;
|
|
6549
6591
|
if(!this.currentSpeechJob){
|
|
@@ -6606,6 +6648,11 @@ class AI {
|
|
|
6606
6648
|
job.sourceNode.onended=null;
|
|
6607
6649
|
}
|
|
6608
6650
|
|
|
6651
|
+
if(job.scheduledEnd===null&&job.pauseAfterMs>0){
|
|
6652
|
+
this.speechScheduleContext=job.audioContext;
|
|
6653
|
+
this.speechScheduleTime=
|
|
6654
|
+
(Number(job.audioContext?.currentTime)||0)+job.pauseAfterMs/1000;
|
|
6655
|
+
}
|
|
6609
6656
|
job.state='complete';
|
|
6610
6657
|
this.#removeSpeechJob(job);
|
|
6611
6658
|
|
|
@@ -6727,6 +6774,8 @@ class AI {
|
|
|
6727
6774
|
}
|
|
6728
6775
|
|
|
6729
6776
|
#removeSpeechJob(job){
|
|
6777
|
+
job.resolvePlayback?.(job.state==='complete');
|
|
6778
|
+
job.resolvePlayback=null;
|
|
6730
6779
|
const jobIndex=this.speechJobs.indexOf(job);
|
|
6731
6780
|
|
|
6732
6781
|
if(jobIndex>=0){
|
|
@@ -2415,12 +2415,33 @@ export class AIProviderRuntime {
|
|
|
2415
2415
|
return this.configure(selections);
|
|
2416
2416
|
}
|
|
2417
2417
|
|
|
2418
|
-
status(role = null) {
|
|
2419
|
-
if (role
|
|
2420
|
-
|
|
2418
|
+
status(role = null, options = {}) {
|
|
2419
|
+
if (role !== null) {
|
|
2420
|
+
assertRole(role);
|
|
2421
2421
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2422
|
+
const snapshot = getAIRuntimeState();
|
|
2423
|
+
if (options?.execution !== true) {
|
|
2424
|
+
return role === null ? snapshot : snapshot.roles[role];
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// Live provider inspection is explicit so ordinary lifecycle reads
|
|
2428
|
+
// retain their sticky snapshot identity and never call a provider.
|
|
2429
|
+
const roles = {...snapshot.roles};
|
|
2430
|
+
for (const selectedRole of role === null ? AI_RUNTIME_ROLES : [role]) {
|
|
2431
|
+
const provider = this.#providerFor(this.#slots[selectedRole]);
|
|
2432
|
+
if (!provider) {
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
const providerStatus = validateProviderStatus(provider.status());
|
|
2436
|
+
if (providerStatus.execution == null) {
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
roles[selectedRole] = {
|
|
2440
|
+
...roles[selectedRole],
|
|
2441
|
+
execution: {...providerStatus.execution}
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
return role === null ? {...snapshot, roles} : roles[role];
|
|
2424
2445
|
}
|
|
2425
2446
|
|
|
2426
2447
|
catalog(role) {
|