arcane-os 0.5.3 → 0.5.5
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 +21 -0
- package/README.md +37 -16
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +200 -26
- package/package.json +1 -1
- package/runtime/arcane/modules/AI.js +178 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.5
|
|
4
|
+
|
|
5
|
+
- Added configurable complete text-to-speech stream segmentation through
|
|
6
|
+
`AI.configureTTSSegmentation()`. Applications can speak on Unicode punctuation
|
|
7
|
+
or a whole-word cadence without discarding any text, while apostrophes,
|
|
8
|
+
commas, and hyphens that join Unicode letters or numbers remain intact.
|
|
9
|
+
Synthesis and playback stay sequential, and mute or cancellation still stops
|
|
10
|
+
the complete queue.
|
|
11
|
+
|
|
12
|
+
## 0.5.4
|
|
13
|
+
|
|
14
|
+
- Reduced Browser-WASM HTTP Range parts from roughly 128 MB to roughly 4 MB
|
|
15
|
+
and raised the deterministic part ceiling, so a refresh re-fetches only the
|
|
16
|
+
small in-flight parts while every completed part remains reusable. An
|
|
17
|
+
incomplete 0.5.3 cache keeps its completed legacy parts and subdivides only
|
|
18
|
+
the missing legacy intervals, preserving existing progress during adoption.
|
|
19
|
+
- Kept all built-in audio processing on device: Whisper owns transcription and
|
|
20
|
+
Kokoro owns speech synthesis. Legacy OpenAI audio selections migrate to those
|
|
21
|
+
local routes, non-local speech configurations are rejected, and the retired
|
|
22
|
+
OpenAI audio credential and endpoints are no longer used.
|
|
23
|
+
|
|
3
24
|
## 0.5.3
|
|
4
25
|
|
|
5
26
|
- Preserved every independently completed Browser-WASM HTTP Range part and
|
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.5.
|
|
22
|
+
This checkout defines the `0.5.5` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -88,10 +88,10 @@ OpenAI-compatible chat-completion requests to
|
|
|
88
88
|
`globalThis.arcane.config.twinCloud.accessKey`. The established `ai.license`
|
|
89
89
|
property and internal `OPENAI` route identifier remain compatibility aliases;
|
|
90
90
|
applications should present the service and credential as **TWiN Cloud** and
|
|
91
|
-
**TWiN access key**. The TWiN key is used only for remote LLM chat.
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
91
|
+
**TWiN access key**. The TWiN key is used only for remote LLM chat. Audio stays
|
|
92
|
+
on device: Whisper (`LOCAL_SPEACH` / `whisper-small`) owns transcription and
|
|
93
|
+
Kokoro (`LOCAL_SPEACH` / `kokoro`) owns speech synthesis. Neither audio route
|
|
94
|
+
uses the TWiN key, and no OpenAI audio key is required.
|
|
95
95
|
|
|
96
96
|
## Browser-local AI
|
|
97
97
|
|
|
@@ -124,11 +124,14 @@ Optional hardening is inactive unless the caller explicitly selects
|
|
|
124
124
|
`secure:true`. The DBOPFS store downloads ordered members with one bounded
|
|
125
125
|
parallel transfer pool and returns them in descriptor order. Completed shards
|
|
126
126
|
survive an interrupted attempt and only missing shard or Range-part work is
|
|
127
|
-
fetched on retry. A
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
fetched on retry. A monolithic file uses up to `downloadConcurrency` active
|
|
128
|
+
workers (four by default) over deterministic, resumable HTTP range parts of
|
|
129
|
+
roughly 4 MB each, up to 4,096 parts, when the server confirms `206` responses
|
|
130
|
+
and the total comes from `Content-Range` or, when that header is not exposed,
|
|
131
|
+
optional declared `bytes`.
|
|
132
|
+
An incomplete cache written by 0.5.3 keeps its completed coarse parts and
|
|
133
|
+
subdivides only the missing intervals into the smaller current parts, so an SDK
|
|
134
|
+
update preserves existing progress while reducing later refresh loss.
|
|
132
135
|
When a followed redirect turns the first Range probe into `200`, the SDK probes
|
|
133
136
|
the final URL directly before reusing that original response as the single-fetch
|
|
134
137
|
fallback. Completed range parts survive restart and are presented to Wllama as
|
|
@@ -150,6 +153,24 @@ must supply each runtime/model selection explicitly. Speech roles
|
|
|
150
153
|
load, cancel, unload, fail, and recover independently, so speech failure never
|
|
151
154
|
silently falls back or prevents text chat.
|
|
152
155
|
|
|
156
|
+
Applications that need faster spoken-response onset can configure the shared
|
|
157
|
+
TTS stream without taking over synthesis or playback:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
ai.configureTTSSegmentation({
|
|
161
|
+
punctuation:'any',
|
|
162
|
+
wordCadence:4
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The compatibility default remains sentence punctuation with no word cadence.
|
|
167
|
+
The configured stream preserves every character and punctuation mark, chooses
|
|
168
|
+
the earliest complete boundary, and keeps synthesis and playback sequential.
|
|
169
|
+
Any-punctuation mode recognizes boundaries without requiring whitespace while
|
|
170
|
+
keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
|
|
171
|
+
inside the same segment.
|
|
172
|
+
Mute, stop, provider transition, and cancellation still govern the whole queue.
|
|
173
|
+
|
|
153
174
|
The SDK runtime also owns `DBOPFSDocumentLibrary`,
|
|
154
175
|
`DocumentLexicalSearch`, and `PersistentAIChatSession`. Document bootstrap is
|
|
155
176
|
schema-driven and explicit; chat never searches a corpus unless the app wires
|
|
@@ -165,7 +186,7 @@ uses the same controller for automatic memory extraction.
|
|
|
165
186
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
166
187
|
|
|
167
188
|
```bash
|
|
168
|
-
npx arcane-os@0.5.
|
|
189
|
+
npx arcane-os@0.5.5 new my-app --path ./my-app --target portable --git
|
|
169
190
|
cd my-app
|
|
170
191
|
npm install
|
|
171
192
|
npm run check
|
|
@@ -176,7 +197,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
176
197
|
missing Arcane files:
|
|
177
198
|
|
|
178
199
|
```bash
|
|
179
|
-
npm install --save-dev --save-exact arcane-os@0.5.
|
|
200
|
+
npm install --save-dev --save-exact arcane-os@0.5.5
|
|
180
201
|
npm exec -- arcane init my-app --target portable
|
|
181
202
|
```
|
|
182
203
|
|
|
@@ -192,7 +213,7 @@ npm exec -- arcane-os targets
|
|
|
192
213
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
193
214
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
194
215
|
|
|
195
|
-
Use `npx arcane-os@0.5.
|
|
216
|
+
Use `npx arcane-os@0.5.5` for the initial bootstrap because it names this npm
|
|
196
217
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
197
218
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
198
219
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -213,7 +234,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
213
234
|
|
|
214
235
|
# From the generated app repository
|
|
215
236
|
cd ../local-app
|
|
216
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.
|
|
237
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.5.tgz
|
|
217
238
|
npm run check
|
|
218
239
|
npm ci
|
|
219
240
|
```
|
|
@@ -223,7 +244,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
223
244
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
224
245
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
225
246
|
runner also needs that tarball at the locked path. After publication, replace
|
|
226
|
-
the local declaration with the exact `arcane-os@0.5.
|
|
247
|
+
the local declaration with the exact `arcane-os@0.5.5` registry package and
|
|
227
248
|
commit the regenerated lock.
|
|
228
249
|
|
|
229
250
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -361,7 +382,7 @@ package installation, or assertions.
|
|
|
361
382
|
|
|
362
383
|
## Current target support
|
|
363
384
|
|
|
364
|
-
Version `0.5.
|
|
385
|
+
Version `0.5.5` exposes one browser target and five explicitly paired
|
|
365
386
|
native development targets: a non-runnable portable directory, a
|
|
366
387
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
367
388
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -29,8 +29,10 @@ const MODEL_LOAD_HEARTBEAT_MS = 5_000;
|
|
|
29
29
|
const DEFAULT_MODEL_DOWNLOAD_CONCURRENCY = 4;
|
|
30
30
|
const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
|
|
31
31
|
const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
|
|
32
|
-
const MODEL_DOWNLOAD_MAX_RANGE_PARTS =
|
|
33
|
-
const MODEL_DOWNLOAD_TARGET_RANGE_BYTES =
|
|
32
|
+
const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 4_096;
|
|
33
|
+
const MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 4_000_000;
|
|
34
|
+
const LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS = 16;
|
|
35
|
+
const LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 128_000_000;
|
|
34
36
|
const INTEL_VENDOR_ID = 0x8086;
|
|
35
37
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
36
38
|
let highPerformanceGpuNoticeShown = false;
|
|
@@ -103,10 +105,10 @@ function downloadConcurrencyValue(value) {
|
|
|
103
105
|
return value;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function
|
|
108
|
+
function modelHttpRangesFor(total, maximumParts, targetPartBytes) {
|
|
107
109
|
const count = Math.min(
|
|
108
|
-
|
|
109
|
-
Math.ceil(total /
|
|
110
|
+
maximumParts,
|
|
111
|
+
Math.ceil(total / targetPartBytes),
|
|
110
112
|
total,
|
|
111
113
|
);
|
|
112
114
|
const width = Math.floor(total / count);
|
|
@@ -122,6 +124,47 @@ function modelHttpRanges(total) {
|
|
|
122
124
|
return completeValue(ranges);
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
function modelHttpRanges(total) {
|
|
128
|
+
return modelHttpRangesFor(
|
|
129
|
+
total,
|
|
130
|
+
MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
131
|
+
MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function modelHttpSubranges(range) {
|
|
136
|
+
const length = range.end - range.start + 1;
|
|
137
|
+
const maximumParts = Math.floor(
|
|
138
|
+
MODEL_DOWNLOAD_MAX_RANGE_PARTS / LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
139
|
+
);
|
|
140
|
+
return completeValue(modelHttpRangesFor(
|
|
141
|
+
length,
|
|
142
|
+
maximumParts,
|
|
143
|
+
MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
144
|
+
).map((part) => completeValue({
|
|
145
|
+
start: range.start + part.start,
|
|
146
|
+
end: range.start + part.end,
|
|
147
|
+
total: range.total,
|
|
148
|
+
})));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function modelHttpRangePlans(total) {
|
|
152
|
+
const current = modelHttpRanges(total);
|
|
153
|
+
const legacy = modelHttpRangesFor(
|
|
154
|
+
total,
|
|
155
|
+
LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
156
|
+
LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
157
|
+
);
|
|
158
|
+
if (
|
|
159
|
+
current.length === legacy.length
|
|
160
|
+
&& current.every((range, index) => (
|
|
161
|
+
range.start === legacy[index].start
|
|
162
|
+
&& range.end === legacy[index].end
|
|
163
|
+
))
|
|
164
|
+
) return completeValue([current]);
|
|
165
|
+
return completeValue([current, legacy]);
|
|
166
|
+
}
|
|
167
|
+
|
|
125
168
|
function progressClock() {
|
|
126
169
|
return typeof globalThis.performance?.now === "function"
|
|
127
170
|
? globalThis.performance.now()
|
|
@@ -930,7 +973,7 @@ export function createDbopfsModelStore({
|
|
|
930
973
|
} = {},
|
|
931
974
|
) {
|
|
932
975
|
const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
|
|
933
|
-
const ranges =
|
|
976
|
+
const ranges = await rangePlanForInstall(name, total, signal);
|
|
934
977
|
const partFiles = new Array(ranges.length);
|
|
935
978
|
const pendingRangeIndexes = [];
|
|
936
979
|
for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
|
|
@@ -1003,16 +1046,31 @@ export function createDbopfsModelStore({
|
|
|
1003
1046
|
writable = null;
|
|
1004
1047
|
partFiles[rangeIndex] = await storedModelFile(partName);
|
|
1005
1048
|
} catch (error) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1049
|
+
let transferError = error;
|
|
1050
|
+
if (downloadSignal.aborted
|
|
1051
|
+
&& error?.code !== "ARCANE_AI_REQUEST_ABORTED") {
|
|
1052
|
+
try {
|
|
1053
|
+
throwIfAborted(downloadSignal, "install");
|
|
1054
|
+
} catch (abortError) {
|
|
1055
|
+
transferError = abortError;
|
|
1056
|
+
}
|
|
1057
|
+
} else if (!error?.code) {
|
|
1058
|
+
transferError = fail(
|
|
1059
|
+
"ARCANE_AI_MODEL_DOWNLOAD_FAILED",
|
|
1060
|
+
"The model download failed while reading an HTTP byte range.",
|
|
1061
|
+
error,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
retainFailure(transferError);
|
|
1065
|
+
await cancelOpenedDownload(opened, transferError);
|
|
1008
1066
|
try {
|
|
1009
|
-
await writable?.abort?.(
|
|
1067
|
+
await writable?.abort?.(transferError);
|
|
1010
1068
|
} catch {
|
|
1011
1069
|
// The range part is removed below even if its writable already closed.
|
|
1012
1070
|
}
|
|
1013
1071
|
await removeEntry(partName).catch(() => undefined);
|
|
1014
1072
|
progress?.discardBytes(received);
|
|
1015
|
-
throw
|
|
1073
|
+
throw transferError;
|
|
1016
1074
|
} finally {
|
|
1017
1075
|
progress?.endTransfer({ publishProgress: failure === null });
|
|
1018
1076
|
}
|
|
@@ -1118,16 +1176,93 @@ export function createDbopfsModelStore({
|
|
|
1118
1176
|
return names;
|
|
1119
1177
|
}
|
|
1120
1178
|
|
|
1179
|
+
async function rangePlanForInstall(
|
|
1180
|
+
modelName,
|
|
1181
|
+
total,
|
|
1182
|
+
signal,
|
|
1183
|
+
knownNames = undefined,
|
|
1184
|
+
) {
|
|
1185
|
+
const plans = modelHttpRangePlans(total);
|
|
1186
|
+
const existingNames = knownNames === undefined
|
|
1187
|
+
? await memberRangePartNames(modelName)
|
|
1188
|
+
: knownNames;
|
|
1189
|
+
const available = existingNames === null
|
|
1190
|
+
? null
|
|
1191
|
+
: existingNames instanceof Set
|
|
1192
|
+
? existingNames
|
|
1193
|
+
: new Set(existingNames);
|
|
1194
|
+
if (available?.size === 0) return plans[0];
|
|
1195
|
+
if (plans.length === 1) return plans[0];
|
|
1196
|
+
const completedParts = new Map();
|
|
1197
|
+
|
|
1198
|
+
async function completedPart(range) {
|
|
1199
|
+
throwIfAborted(signal, "install");
|
|
1200
|
+
const partName = rangePartName(modelName, range);
|
|
1201
|
+
if (available && !available.has(partName)) return null;
|
|
1202
|
+
if (completedParts.has(partName)) return completedParts.get(partName);
|
|
1203
|
+
const partFile = await file(partName);
|
|
1204
|
+
const expected = range.end - range.start + 1;
|
|
1205
|
+
const completed = partFile?.size === expected ? partFile : null;
|
|
1206
|
+
completedParts.set(partName, completed);
|
|
1207
|
+
return completed;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const completedLegacyRanges = new Set();
|
|
1211
|
+
for (const range of plans[1]) {
|
|
1212
|
+
if (await completedPart(range)) completedLegacyRanges.add(range);
|
|
1213
|
+
}
|
|
1214
|
+
const hybrid = completeValue(plans[1].flatMap((range) => (
|
|
1215
|
+
completedLegacyRanges.has(range)
|
|
1216
|
+
? [range]
|
|
1217
|
+
: modelHttpSubranges(range)
|
|
1218
|
+
)));
|
|
1219
|
+
let selected = plans[0];
|
|
1220
|
+
let selectedBytes = 0;
|
|
1221
|
+
let selectedLastModified = 0;
|
|
1222
|
+
for (const ranges of [plans[0], hybrid]) {
|
|
1223
|
+
let completedBytes = 0;
|
|
1224
|
+
let lastModified = 0;
|
|
1225
|
+
for (const range of ranges) {
|
|
1226
|
+
const partFile = await completedPart(range);
|
|
1227
|
+
if (!partFile) continue;
|
|
1228
|
+
completedBytes += range.end - range.start + 1;
|
|
1229
|
+
if (Number.isFinite(partFile.lastModified)) {
|
|
1230
|
+
lastModified = Math.max(lastModified, partFile.lastModified);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (
|
|
1234
|
+
completedBytes > selectedBytes
|
|
1235
|
+
|| (
|
|
1236
|
+
completedBytes === selectedBytes
|
|
1237
|
+
&& completedBytes > 0
|
|
1238
|
+
&& lastModified > selectedLastModified
|
|
1239
|
+
)
|
|
1240
|
+
) {
|
|
1241
|
+
selected = ranges;
|
|
1242
|
+
selectedBytes = completedBytes;
|
|
1243
|
+
selectedLastModified = lastModified;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return selected;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1121
1249
|
async function removeMemberRangeParts(modelName, {
|
|
1122
1250
|
except = null,
|
|
1123
1251
|
total = null,
|
|
1124
1252
|
} = {}) {
|
|
1125
1253
|
const existingNames = await memberRangePartNames(modelName);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1254
|
+
let names = existingNames;
|
|
1255
|
+
if (names === null && Number.isSafeInteger(total) && total > 0) {
|
|
1256
|
+
const plans = modelHttpRangePlans(total);
|
|
1257
|
+
const hybridRanges = plans.length === 1
|
|
1258
|
+
? []
|
|
1259
|
+
: plans[1].flatMap((range) => modelHttpSubranges(range));
|
|
1260
|
+
names = [...new Set([
|
|
1261
|
+
...plans.flat(),
|
|
1262
|
+
...hybridRanges,
|
|
1263
|
+
].map((range) => rangePartName(modelName, range)))];
|
|
1264
|
+
}
|
|
1265
|
+
names ??= [];
|
|
1131
1266
|
const removed = [];
|
|
1132
1267
|
for (const name of names) {
|
|
1133
1268
|
if (except?.has(name)) continue;
|
|
@@ -1211,9 +1346,10 @@ export function createDbopfsModelStore({
|
|
|
1211
1346
|
for (let memberIndex = 0; memberIndex < members.length; memberIndex += 1) {
|
|
1212
1347
|
const total = members[memberIndex].bytes;
|
|
1213
1348
|
if (!Number.isSafeInteger(total) || total <= 0) continue;
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1349
|
+
removed.push(await removeMemberRangeParts(
|
|
1350
|
+
names.models[memberIndex].name,
|
|
1351
|
+
{ total },
|
|
1352
|
+
));
|
|
1217
1353
|
}
|
|
1218
1354
|
return removed.some(Boolean);
|
|
1219
1355
|
}
|
|
@@ -1364,12 +1500,18 @@ export function createDbopfsModelStore({
|
|
|
1364
1500
|
return modelFiles.every(Boolean) ? modelFiles : null;
|
|
1365
1501
|
}
|
|
1366
1502
|
|
|
1367
|
-
async function
|
|
1503
|
+
async function storedRangeCandidateForPlan(
|
|
1504
|
+
modelName,
|
|
1505
|
+
ranges,
|
|
1506
|
+
signal,
|
|
1507
|
+
availableNames = null,
|
|
1508
|
+
) {
|
|
1368
1509
|
const partFiles = [];
|
|
1369
1510
|
let lastModified = 0;
|
|
1370
|
-
for (const range of
|
|
1511
|
+
for (const range of ranges) {
|
|
1371
1512
|
throwIfAborted(signal, "install");
|
|
1372
1513
|
const partName = rangePartName(modelName, range);
|
|
1514
|
+
if (availableNames && !availableNames.has(partName)) return null;
|
|
1373
1515
|
const partFile = await file(partName);
|
|
1374
1516
|
if (!partFile) return null;
|
|
1375
1517
|
const expected = range.end - range.start + 1;
|
|
@@ -1385,26 +1527,53 @@ export function createDbopfsModelStore({
|
|
|
1385
1527
|
return completeValue({
|
|
1386
1528
|
file: new Blob(partFiles, { type: "application/octet-stream" }),
|
|
1387
1529
|
lastModified,
|
|
1388
|
-
|
|
1530
|
+
ranges,
|
|
1531
|
+
total: ranges[0].total,
|
|
1389
1532
|
});
|
|
1390
1533
|
}
|
|
1391
1534
|
|
|
1535
|
+
async function storedRangeCandidate(
|
|
1536
|
+
modelName,
|
|
1537
|
+
total,
|
|
1538
|
+
signal,
|
|
1539
|
+
availableNames = null,
|
|
1540
|
+
) {
|
|
1541
|
+
const ranges = await rangePlanForInstall(
|
|
1542
|
+
modelName,
|
|
1543
|
+
total,
|
|
1544
|
+
signal,
|
|
1545
|
+
availableNames,
|
|
1546
|
+
);
|
|
1547
|
+
return storedRangeCandidateForPlan(
|
|
1548
|
+
modelName,
|
|
1549
|
+
ranges,
|
|
1550
|
+
signal,
|
|
1551
|
+
availableNames,
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1392
1555
|
async function storedRangeMember(member, modelName, signal) {
|
|
1556
|
+
const names = await memberRangePartNames(modelName);
|
|
1557
|
+
const availableNames = names === null ? null : new Set(names);
|
|
1393
1558
|
const declaredTotal = Number.isSafeInteger(member.bytes) && member.bytes > 0
|
|
1394
1559
|
? member.bytes
|
|
1395
1560
|
: null;
|
|
1396
1561
|
if (declaredTotal !== null) {
|
|
1397
|
-
const declared = await storedRangeCandidate(
|
|
1562
|
+
const declared = await storedRangeCandidate(
|
|
1563
|
+
modelName,
|
|
1564
|
+
declaredTotal,
|
|
1565
|
+
signal,
|
|
1566
|
+
availableNames,
|
|
1567
|
+
);
|
|
1398
1568
|
if (declared) {
|
|
1399
1569
|
await removeStaleRangePartsAfterCompletion(
|
|
1400
1570
|
modelName,
|
|
1401
|
-
|
|
1571
|
+
declared.ranges,
|
|
1402
1572
|
);
|
|
1403
1573
|
return declared.file;
|
|
1404
1574
|
}
|
|
1405
1575
|
}
|
|
1406
1576
|
|
|
1407
|
-
const names = await memberRangePartNames(modelName);
|
|
1408
1577
|
if (names === null) return null;
|
|
1409
1578
|
const discoveredTotals = new Set();
|
|
1410
1579
|
for (const name of names) {
|
|
@@ -1414,7 +1583,12 @@ export function createDbopfsModelStore({
|
|
|
1414
1583
|
let selected = null;
|
|
1415
1584
|
const totals = [...discoveredTotals].sort((left, right) => right - left);
|
|
1416
1585
|
for (const total of totals) {
|
|
1417
|
-
const candidate = await storedRangeCandidate(
|
|
1586
|
+
const candidate = await storedRangeCandidate(
|
|
1587
|
+
modelName,
|
|
1588
|
+
total,
|
|
1589
|
+
signal,
|
|
1590
|
+
availableNames,
|
|
1591
|
+
);
|
|
1418
1592
|
if (
|
|
1419
1593
|
candidate
|
|
1420
1594
|
&& (
|
|
@@ -1426,7 +1600,7 @@ export function createDbopfsModelStore({
|
|
|
1426
1600
|
if (selected === null) return null;
|
|
1427
1601
|
await removeStaleRangePartsAfterCompletion(
|
|
1428
1602
|
modelName,
|
|
1429
|
-
|
|
1603
|
+
selected.ranges,
|
|
1430
1604
|
);
|
|
1431
1605
|
return selected.file;
|
|
1432
1606
|
}
|
package/package.json
CHANGED
|
@@ -17,6 +17,16 @@ const completeValue=(value)=>value;
|
|
|
17
17
|
|
|
18
18
|
let credentials='include';
|
|
19
19
|
const LEGACY_TTS_RESPONSE_FORMAT='opus';
|
|
20
|
+
const DEFAULT_TTS_SEGMENTATION={
|
|
21
|
+
punctuation:'sentence',
|
|
22
|
+
wordCadence:null
|
|
23
|
+
};
|
|
24
|
+
const TTS_PUNCTUATION_MODES=new Set(['sentence','any','none']);
|
|
25
|
+
// A complete punctuation run is a boundary unless the whole run consists of
|
|
26
|
+
// apostrophe, comma, or dash punctuation joining Unicode letters or numbers.
|
|
27
|
+
// The streaming expression leaves a trailing joining run for the next chunk.
|
|
28
|
+
const TTS_ANY_PUNCTUATION=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+(?!\p{P})(?=[\s\S])|\p{P}+(?!\p{P})(?=[^\p{L}\p{N}])|\p{P}*[^\P{P}'\u2019\uFF07,\u060C\u3001\uFF0C\p{Pd}]\p{P}*)(?!\p{P})/gu;
|
|
29
|
+
const TTS_ANY_PUNCTUATION_AT_END=/(?<!\p{P})(?:(?<![\p{L}\p{N}])\p{P}+|\p{P}+(?![\p{L}\p{N}])|\p{P}*[^\P{P}'\u2019\uFF07,\u060C\u3001\uFF0C\p{Pd}]\p{P}*)(?!\p{P})/gu;
|
|
20
30
|
credentials='omit';
|
|
21
31
|
|
|
22
32
|
const LEGACY_AI_SERVICES=new Set(['OPENAI','OLLAMA','LOCAL_SPEACH']);
|
|
@@ -142,6 +152,38 @@ function isPlainAIRecord(value){
|
|
|
142
152
|
return prototype===Object.prototype||prototype===null;
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
function normalizeTTSSegmentation(value,current=DEFAULT_TTS_SEGMENTATION){
|
|
156
|
+
if(value===undefined||value===null){
|
|
157
|
+
return {...current};
|
|
158
|
+
}
|
|
159
|
+
if(!isPlainAIRecord(value)){
|
|
160
|
+
throw new TypeError('TTS segmentation must be a plain object.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const punctuation=value.punctuation===undefined
|
|
164
|
+
?current.punctuation
|
|
165
|
+
:value.punctuation;
|
|
166
|
+
if(!TTS_PUNCTUATION_MODES.has(punctuation)){
|
|
167
|
+
throw new TypeError(
|
|
168
|
+
'TTS segmentation punctuation must be sentence, any, or none.'
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const wordCadence=value.wordCadence===undefined
|
|
173
|
+
?current.wordCadence
|
|
174
|
+
:value.wordCadence;
|
|
175
|
+
if(
|
|
176
|
+
wordCadence!==null
|
|
177
|
+
&&(!Number.isSafeInteger(wordCadence)||wordCadence<1)
|
|
178
|
+
){
|
|
179
|
+
throw new RangeError(
|
|
180
|
+
'TTS segmentation wordCadence must be null or a positive integer.'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {punctuation,wordCadence};
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
function requireAIToolMessageSchemas(value,label='AI tools'){
|
|
146
188
|
if(!Array.isArray(value)){
|
|
147
189
|
throw aiStructuralError(
|
|
@@ -922,12 +964,10 @@ class AI {
|
|
|
922
964
|
OPENAI: 'https://inference.do-ai.run/v1'
|
|
923
965
|
},
|
|
924
966
|
sttURL: {
|
|
925
|
-
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
926
|
-
OPENAI: 'https://api.openai.com/v1'
|
|
967
|
+
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
927
968
|
},
|
|
928
969
|
ttsURL: {
|
|
929
|
-
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
930
|
-
OPENAI: 'https://api.openai.com/v1'
|
|
970
|
+
LOCAL_SPEACH: 'http://127.0.0.1:8011/v1'
|
|
931
971
|
},
|
|
932
972
|
}
|
|
933
973
|
|
|
@@ -936,12 +976,10 @@ class AI {
|
|
|
936
976
|
OPENAI: '/chat/completions'
|
|
937
977
|
},
|
|
938
978
|
stt: {
|
|
939
|
-
LOCAL_SPEACH: '/audio/transcriptions'
|
|
940
|
-
OPENAI: '/audio/transcriptions'
|
|
979
|
+
LOCAL_SPEACH: '/audio/transcriptions'
|
|
941
980
|
},
|
|
942
981
|
tts: {
|
|
943
|
-
LOCAL_SPEACH: '/audio/speech'
|
|
944
|
-
OPENAI: '/audio/speech'
|
|
982
|
+
LOCAL_SPEACH: '/audio/speech'
|
|
945
983
|
}
|
|
946
984
|
}
|
|
947
985
|
|
|
@@ -950,12 +988,10 @@ class AI {
|
|
|
950
988
|
}
|
|
951
989
|
|
|
952
990
|
#sttModels = {
|
|
953
|
-
OPENAI: 'whisper-1',
|
|
954
991
|
LOCAL_SPEACH: 'whisper-small'
|
|
955
992
|
}
|
|
956
993
|
|
|
957
994
|
#ttsModels = {
|
|
958
|
-
OPENAI: 'gpt-4o-mini-tts',
|
|
959
995
|
LOCAL_SPEACH: 'kokoro'
|
|
960
996
|
}
|
|
961
997
|
|
|
@@ -974,17 +1010,8 @@ class AI {
|
|
|
974
1010
|
};
|
|
975
1011
|
}
|
|
976
1012
|
|
|
977
|
-
get #legacyOpenAISpeechKey(){
|
|
978
|
-
const value=globalThis.arcane?.config?.openAI?.apiKey;
|
|
979
|
-
return typeof value==='string'?value:'';
|
|
980
|
-
}
|
|
981
|
-
|
|
982
1013
|
get #ttsHeaders(){
|
|
983
1014
|
return {
|
|
984
|
-
OPENAI: {
|
|
985
|
-
'Content-Type': 'application/json',
|
|
986
|
-
'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`
|
|
987
|
-
},
|
|
988
1015
|
LOCAL_SPEACH: {
|
|
989
1016
|
'Content-Type': 'application/json',
|
|
990
1017
|
}
|
|
@@ -993,9 +1020,6 @@ class AI {
|
|
|
993
1020
|
|
|
994
1021
|
get #sttHeaders(){
|
|
995
1022
|
return {
|
|
996
|
-
OPENAI: {
|
|
997
|
-
'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`,
|
|
998
|
-
},
|
|
999
1023
|
LOCAL_SPEACH: {}
|
|
1000
1024
|
};
|
|
1001
1025
|
}
|
|
@@ -1048,11 +1072,11 @@ class AI {
|
|
|
1048
1072
|
|
|
1049
1073
|
const preferences=[
|
|
1050
1074
|
llmService||'OPENAI',
|
|
1051
|
-
sttService||'
|
|
1052
|
-
ttsService||'
|
|
1075
|
+
sttService||'LOCAL_SPEACH',
|
|
1076
|
+
ttsService||'LOCAL_SPEACH',
|
|
1053
1077
|
model||'OPENAI',
|
|
1054
|
-
modelTTS||'
|
|
1055
|
-
modelSTT||'
|
|
1078
|
+
modelTTS||'LOCAL_SPEACH',
|
|
1079
|
+
modelSTT||'LOCAL_SPEACH'
|
|
1056
1080
|
];
|
|
1057
1081
|
this.setAI(
|
|
1058
1082
|
...preferences
|
|
@@ -1087,13 +1111,14 @@ class AI {
|
|
|
1087
1111
|
#speechControlGeneration=0;
|
|
1088
1112
|
#speechFailureSequence=0;
|
|
1089
1113
|
#stopOllamaReady=null;
|
|
1114
|
+
#ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
|
|
1090
1115
|
#preferenceTuple=completeValue([
|
|
1091
1116
|
'OPENAI',
|
|
1117
|
+
'LOCAL_SPEACH',
|
|
1118
|
+
'LOCAL_SPEACH',
|
|
1092
1119
|
'OPENAI',
|
|
1093
|
-
'
|
|
1094
|
-
'
|
|
1095
|
-
'OPENAI',
|
|
1096
|
-
'OPENAI'
|
|
1120
|
+
'LOCAL_SPEACH',
|
|
1121
|
+
'LOCAL_SPEACH'
|
|
1097
1122
|
]);
|
|
1098
1123
|
|
|
1099
1124
|
get providerRuntime(){
|
|
@@ -1173,9 +1198,6 @@ class AI {
|
|
|
1173
1198
|
this.#retainLegacyLLMReadiness(
|
|
1174
1199
|
this.#reconcileLegacyLLMReadiness()
|
|
1175
1200
|
);
|
|
1176
|
-
this.#retainLegacySpeechReadiness(
|
|
1177
|
-
this.#reconcileLegacySpeechReadiness()
|
|
1178
|
-
);
|
|
1179
1201
|
return this.#license;
|
|
1180
1202
|
}
|
|
1181
1203
|
|
|
@@ -1427,12 +1449,6 @@ class AI {
|
|
|
1427
1449
|
if(role!=='tts'){
|
|
1428
1450
|
return null;
|
|
1429
1451
|
}
|
|
1430
|
-
if(providerId==='OPENAI'){
|
|
1431
|
-
const selected=typeof globalThis.window?.user?.AI_voice==='string'
|
|
1432
|
-
?globalThis.window.user.AI_voice.trim()
|
|
1433
|
-
:'';
|
|
1434
|
-
return selected||'alloy';
|
|
1435
|
-
}
|
|
1436
1452
|
if(providerId==='LOCAL_SPEACH'){
|
|
1437
1453
|
return 'af_heart';
|
|
1438
1454
|
}
|
|
@@ -1445,10 +1461,6 @@ class AI {
|
|
|
1445
1461
|
if(service!==providerId||!model){
|
|
1446
1462
|
return false;
|
|
1447
1463
|
}
|
|
1448
|
-
if(providerId==='OPENAI'){
|
|
1449
|
-
return Boolean(this.#legacyOpenAISpeechKey)
|
|
1450
|
-
&&typeof globalThis.fetch==='function';
|
|
1451
|
-
}
|
|
1452
1464
|
if(providerId==='LOCAL_SPEACH'){
|
|
1453
1465
|
return Boolean(this.#nativeSpeech(service,role));
|
|
1454
1466
|
}
|
|
@@ -1685,8 +1697,7 @@ class AI {
|
|
|
1685
1697
|
}
|
|
1686
1698
|
|
|
1687
1699
|
#ensureLegacySpeechProvider(role,providerId){
|
|
1688
|
-
if(!['stt','tts'].includes(role)
|
|
1689
|
-
||!['OPENAI','LOCAL_SPEACH'].includes(providerId)){
|
|
1700
|
+
if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
|
|
1690
1701
|
return false;
|
|
1691
1702
|
}
|
|
1692
1703
|
if(this.#providerRuntime.hasProvider(role,providerId)){
|
|
@@ -1875,11 +1886,7 @@ class AI {
|
|
|
1875
1886
|
throw error;
|
|
1876
1887
|
}
|
|
1877
1888
|
|
|
1878
|
-
if(service==='OPENAI'&&(
|
|
1879
|
-
role==='llm'
|
|
1880
|
-
?Boolean(this.twinKey)
|
|
1881
|
-
:Boolean(this.#legacyOpenAISpeechKey)
|
|
1882
|
-
)){
|
|
1889
|
+
if(service==='OPENAI'&&role==='llm'&&Boolean(this.twinKey)){
|
|
1883
1890
|
return true;
|
|
1884
1891
|
}
|
|
1885
1892
|
|
|
@@ -1947,6 +1954,10 @@ class AI {
|
|
|
1947
1954
|
}
|
|
1948
1955
|
return value;
|
|
1949
1956
|
});
|
|
1957
|
+
next[1]='LOCAL_SPEACH';
|
|
1958
|
+
next[2]='LOCAL_SPEACH';
|
|
1959
|
+
next[4]='LOCAL_SPEACH';
|
|
1960
|
+
next[5]='LOCAL_SPEACH';
|
|
1950
1961
|
return completeValue(next);
|
|
1951
1962
|
}
|
|
1952
1963
|
|
|
@@ -1961,6 +1972,26 @@ class AI {
|
|
|
1961
1972
|
}
|
|
1962
1973
|
}
|
|
1963
1974
|
|
|
1975
|
+
#assertDeviceSpeechConfiguration(configuration){
|
|
1976
|
+
for(const role of ['stt','tts']){
|
|
1977
|
+
for(const routeName of ['default','localOnly']){
|
|
1978
|
+
const selection=configuration?.[role]?.[routeName];
|
|
1979
|
+
if(selection&&selection.localOnly!==true){
|
|
1980
|
+
const operation=role==='stt'
|
|
1981
|
+
?'Speech recognition'
|
|
1982
|
+
:'Speech synthesis';
|
|
1983
|
+
const error=new TypeError(
|
|
1984
|
+
`${operation} supports on-device providers only.`
|
|
1985
|
+
);
|
|
1986
|
+
error.code=role==='stt'
|
|
1987
|
+
?'AI_STT_DEVICE_ONLY'
|
|
1988
|
+
:'AI_TTS_DEVICE_ONLY';
|
|
1989
|
+
throw error;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1964
1995
|
#normalizedLLMModel(service,model){
|
|
1965
1996
|
if(service==='OLLAMA'){
|
|
1966
1997
|
const mappedModel=model==='OPENAI'?null:this.#models[model];
|
|
@@ -2187,6 +2218,7 @@ class AI {
|
|
|
2187
2218
|
|
|
2188
2219
|
configureProviders(selections){
|
|
2189
2220
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2221
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2190
2222
|
this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
|
|
2191
2223
|
this.#ensureLegacyLLMProvider(
|
|
2192
2224
|
prepared.llm.default?.providerId
|
|
@@ -2221,6 +2253,7 @@ class AI {
|
|
|
2221
2253
|
|
|
2222
2254
|
configureSpeechProviders(selections){
|
|
2223
2255
|
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
2256
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2224
2257
|
this.#assertSynchronousBrowserSpeechSupersession(
|
|
2225
2258
|
'AI.configureSpeechProviders'
|
|
2226
2259
|
);
|
|
@@ -2287,6 +2320,7 @@ class AI {
|
|
|
2287
2320
|
|
|
2288
2321
|
async transitionProviders(selections){
|
|
2289
2322
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2323
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2290
2324
|
await this.#supersedeBrowserSpeechForRouteChange();
|
|
2291
2325
|
this.#ensureLegacyLLMProvider(
|
|
2292
2326
|
prepared.llm.default?.providerId
|
|
@@ -2318,6 +2352,7 @@ class AI {
|
|
|
2318
2352
|
|
|
2319
2353
|
async transitionSpeechProviders(selections){
|
|
2320
2354
|
const prepared=this.#providerRuntime.validateSpeechConfiguration(selections);
|
|
2355
|
+
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2321
2356
|
await this.#supersedeBrowserSpeechForRouteChange();
|
|
2322
2357
|
this.#invalidateSpeechControl();
|
|
2323
2358
|
await this.#unloadSpeechProviderRolesForTransition();
|
|
@@ -5520,6 +5555,18 @@ class AI {
|
|
|
5520
5555
|
return responseJSON;
|
|
5521
5556
|
}
|
|
5522
5557
|
|
|
5558
|
+
get ttsSegmentation(){
|
|
5559
|
+
return {...this.#ttsSegmentation};
|
|
5560
|
+
}
|
|
5561
|
+
|
|
5562
|
+
configureTTSSegmentation(options={}){
|
|
5563
|
+
this.#ttsSegmentation=normalizeTTSSegmentation(
|
|
5564
|
+
options,
|
|
5565
|
+
this.#ttsSegmentation
|
|
5566
|
+
);
|
|
5567
|
+
return this.ttsSegmentation;
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5523
5570
|
streamTTS(
|
|
5524
5571
|
text='',
|
|
5525
5572
|
end=false
|
|
@@ -5571,10 +5618,16 @@ class AI {
|
|
|
5571
5618
|
let remainder=this.audioMessageChunks;
|
|
5572
5619
|
|
|
5573
5620
|
while(remainder.length>0){
|
|
5574
|
-
const
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5621
|
+
const punctuationBoundary=this.#findSpeechPunctuationBoundary(
|
|
5622
|
+
remainder,
|
|
5623
|
+
end
|
|
5624
|
+
);
|
|
5625
|
+
const cadenceBoundary=this.#findSpeechCadenceBoundary(remainder,end);
|
|
5626
|
+
let boundary=punctuationBoundary;
|
|
5627
|
+
|
|
5628
|
+
if(boundary<0||(cadenceBoundary>0&&cadenceBoundary<boundary)){
|
|
5629
|
+
boundary=cadenceBoundary;
|
|
5630
|
+
}
|
|
5578
5631
|
|
|
5579
5632
|
if(boundary<0&&end){
|
|
5580
5633
|
boundary=remainder.length;
|
|
@@ -5596,7 +5649,14 @@ class AI {
|
|
|
5596
5649
|
return segments;
|
|
5597
5650
|
}
|
|
5598
5651
|
|
|
5599
|
-
#
|
|
5652
|
+
#findSpeechPunctuationBoundary(text,end=false){
|
|
5653
|
+
if(this.#ttsSegmentation.punctuation==='none'){
|
|
5654
|
+
return -1;
|
|
5655
|
+
}
|
|
5656
|
+
if(this.#ttsSegmentation.punctuation==='any'){
|
|
5657
|
+
return this.#findAnySpeechPunctuationBoundary(text,end);
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5600
5660
|
const pattern=end
|
|
5601
5661
|
?/(?:[\u3002\uFF01\uFF1F]|[.!?](?=\s|$))/g
|
|
5602
5662
|
:/(?:[\u3002\uFF01\uFF1F]|[.!?](?=\s+\S))/g;
|
|
@@ -5610,10 +5670,71 @@ class AI {
|
|
|
5610
5670
|
continue;
|
|
5611
5671
|
}
|
|
5612
5672
|
|
|
5613
|
-
return
|
|
5673
|
+
return this.#includeSpeechBoundaryWhitespace(
|
|
5674
|
+
text,
|
|
5675
|
+
terminator.index+terminator[0].length
|
|
5676
|
+
);
|
|
5614
5677
|
}
|
|
5615
5678
|
|
|
5616
|
-
return
|
|
5679
|
+
return -1;
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
#findAnySpeechPunctuationBoundary(text,end=false){
|
|
5683
|
+
const pattern=end
|
|
5684
|
+
?TTS_ANY_PUNCTUATION_AT_END
|
|
5685
|
+
:TTS_ANY_PUNCTUATION;
|
|
5686
|
+
pattern.lastIndex=0;
|
|
5687
|
+
const firstContent=text.search(/\S/u);
|
|
5688
|
+
let punctuation;
|
|
5689
|
+
|
|
5690
|
+
while((punctuation=pattern.exec(text))){
|
|
5691
|
+
if(firstContent<0||punctuation.index<=firstContent){
|
|
5692
|
+
continue;
|
|
5693
|
+
}
|
|
5694
|
+
const boundary=punctuation.index+punctuation[0].length;
|
|
5695
|
+
return this.#includeSpeechBoundaryWhitespace(text,boundary);
|
|
5696
|
+
}
|
|
5697
|
+
|
|
5698
|
+
return -1;
|
|
5699
|
+
}
|
|
5700
|
+
|
|
5701
|
+
#findSpeechCadenceBoundary(text,end=false){
|
|
5702
|
+
const wordCadence=this.#ttsSegmentation.wordCadence;
|
|
5703
|
+
if(wordCadence===null){
|
|
5704
|
+
return -1;
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
const words=/\S+/gu;
|
|
5708
|
+
let word;
|
|
5709
|
+
let wordCount=0;
|
|
5710
|
+
|
|
5711
|
+
while((word=words.exec(text))){
|
|
5712
|
+
wordCount+=1;
|
|
5713
|
+
if(wordCount!==wordCadence){
|
|
5714
|
+
continue;
|
|
5715
|
+
}
|
|
5716
|
+
const boundary=word.index+word[0].length;
|
|
5717
|
+
if(boundary===text.length&&!end){
|
|
5718
|
+
return -1;
|
|
5719
|
+
}
|
|
5720
|
+
if(boundary<text.length&&!/\s/u.test(text[boundary])){
|
|
5721
|
+
return -1;
|
|
5722
|
+
}
|
|
5723
|
+
return this.#includeSpeechBoundaryWhitespace(text,boundary);
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
return -1;
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
#includeSpeechBoundaryWhitespace(text,boundary){
|
|
5730
|
+
let completeBoundary=boundary;
|
|
5731
|
+
while(
|
|
5732
|
+
completeBoundary<text.length
|
|
5733
|
+
&&/\s/u.test(text[completeBoundary])
|
|
5734
|
+
){
|
|
5735
|
+
completeBoundary+=1;
|
|
5736
|
+
}
|
|
5737
|
+
return completeBoundary;
|
|
5617
5738
|
}
|
|
5618
5739
|
|
|
5619
5740
|
#isSpeechAbbreviation(text,periodIndex){
|