@pie-players/pie-assessment-toolkit 0.3.50 → 0.3.52
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/README.md +25 -0
- package/dist/components/ItemToolBar.custom-element.js +98 -14
- package/dist/components/PieAssessmentToolkit.custom-element.js +293 -48
- package/dist/components/SectionToolBar.custom-element.js +98 -14
- package/dist/context/assessment-toolkit-context.d.ts +2 -0
- package/dist/context/assessment-toolkit-context.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/services/HighlightCoordinator.d.ts +6 -0
- package/dist/services/HighlightCoordinator.js +20 -5
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/TTSService.d.ts +19 -0
- package/dist/services/TTSService.js +238 -35
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +3 -2
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/createDefaultToolRegistry.d.ts +2 -1
- package/dist/services/createDefaultToolRegistry.js +2 -1
- package/dist/services/createDefaultToolRegistry.js.map +1 -1
- package/dist/services/interfaces.d.ts +15 -0
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/tool-config-defaults.d.ts +11 -0
- package/dist/services/tool-config-defaults.js +18 -0
- package/dist/services/tool-config-defaults.js.map +1 -1
- package/dist/services/tts/highlight-target-resolver.d.ts +17 -0
- package/dist/services/tts/highlight-target-resolver.js +2 -0
- package/dist/services/tts/highlight-target-resolver.js.map +1 -0
- package/dist/services/tts-runtime-config.d.ts +15 -1
- package/dist/services/tts-runtime-config.js +76 -10
- package/dist/services/tts-runtime-config.js.map +1 -1
- package/dist/tools/registrations/tts.js +6 -8
- package/dist/tools/registrations/tts.js.map +1 -1
- package/package.json +7 -7
|
@@ -74,6 +74,7 @@ export class TTSService {
|
|
|
74
74
|
currentProvider = null;
|
|
75
75
|
provider = null;
|
|
76
76
|
highlightCoordinator = null;
|
|
77
|
+
highlightTargetResolverProvider = null;
|
|
77
78
|
catalogResolver = null;
|
|
78
79
|
state = PlaybackState.IDLE;
|
|
79
80
|
ttsConfig = {};
|
|
@@ -84,6 +85,7 @@ export class TTSService {
|
|
|
84
85
|
lastError = null;
|
|
85
86
|
speakRunId = 0;
|
|
86
87
|
currentBoundaryOffset = 0;
|
|
88
|
+
activeWordBoundaryOffset = 0;
|
|
87
89
|
seekSegments = [];
|
|
88
90
|
sentenceHighlightSegments = [];
|
|
89
91
|
currentSeekSegmentIndex = 0;
|
|
@@ -209,6 +211,17 @@ export class TTSService {
|
|
|
209
211
|
setHighlightCoordinator(coordinator) {
|
|
210
212
|
this.highlightCoordinator = coordinator;
|
|
211
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Set a late-bound provider for optional host TTS highlight target remapping.
|
|
216
|
+
*/
|
|
217
|
+
setHighlightTargetResolverProvider(provider) {
|
|
218
|
+
this.highlightTargetResolverProvider = provider;
|
|
219
|
+
return () => {
|
|
220
|
+
if (this.highlightTargetResolverProvider === provider) {
|
|
221
|
+
this.highlightTargetResolverProvider = null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
212
225
|
/**
|
|
213
226
|
* Set accessibility catalog resolver for spoken content
|
|
214
227
|
*
|
|
@@ -220,6 +233,145 @@ export class TTSService {
|
|
|
220
233
|
setCatalogResolver(resolver) {
|
|
221
234
|
this.catalogResolver = resolver;
|
|
222
235
|
}
|
|
236
|
+
getHighlightResolverRuntime() {
|
|
237
|
+
let provided = null;
|
|
238
|
+
try {
|
|
239
|
+
provided = this.highlightTargetResolverProvider?.() ?? null;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
provided = null;
|
|
243
|
+
}
|
|
244
|
+
const providedContext = provided?.context ?? {};
|
|
245
|
+
const fallbackScope = typeof HTMLElement !== "undefined" &&
|
|
246
|
+
this.currentContentElement instanceof HTMLElement
|
|
247
|
+
? this.currentContentElement
|
|
248
|
+
: null;
|
|
249
|
+
return {
|
|
250
|
+
context: {
|
|
251
|
+
...providedContext,
|
|
252
|
+
scopeElement: providedContext.scopeElement ?? fallbackScope,
|
|
253
|
+
},
|
|
254
|
+
resolver: provided?.resolver ?? null,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
getNodeElement(node) {
|
|
258
|
+
if (!node)
|
|
259
|
+
return null;
|
|
260
|
+
return node.nodeType === Node.ELEMENT_NODE
|
|
261
|
+
? node
|
|
262
|
+
: node.parentElement;
|
|
263
|
+
}
|
|
264
|
+
isElementWithinScope(element, scope) {
|
|
265
|
+
if (!scope)
|
|
266
|
+
return true;
|
|
267
|
+
return element === scope || scope.contains(element);
|
|
268
|
+
}
|
|
269
|
+
isRangeWithinScope(range, scope) {
|
|
270
|
+
const startElement = this.getNodeElement(range.startContainer);
|
|
271
|
+
const endElement = this.getNodeElement(range.endContainer);
|
|
272
|
+
if (!startElement || !endElement)
|
|
273
|
+
return false;
|
|
274
|
+
return (this.isElementWithinScope(startElement, scope) &&
|
|
275
|
+
this.isElementWithinScope(endElement, scope));
|
|
276
|
+
}
|
|
277
|
+
resolveWordHighlightRange(nativeRange) {
|
|
278
|
+
const { context, resolver } = this.getHighlightResolverRuntime();
|
|
279
|
+
const scope = context.scopeElement ?? this.currentContentElement;
|
|
280
|
+
try {
|
|
281
|
+
const resolved = resolver?.resolveWordRange?.(nativeRange, context);
|
|
282
|
+
if (resolved && this.isRangeWithinScope(resolved, scope)) {
|
|
283
|
+
return { range: resolved, remapped: !sameRange(nativeRange, resolved) };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Fail open: custom resolver failures must not interrupt TTS playback.
|
|
288
|
+
}
|
|
289
|
+
return { range: nativeRange, remapped: false };
|
|
290
|
+
}
|
|
291
|
+
resolveSentenceHighlightTargets(nativeRanges) {
|
|
292
|
+
const { context, resolver } = this.getHighlightResolverRuntime();
|
|
293
|
+
const scope = context.scopeElement ?? this.currentContentElement;
|
|
294
|
+
try {
|
|
295
|
+
const resolved = resolver?.resolveSentenceRanges?.(nativeRanges, context);
|
|
296
|
+
if (!resolved)
|
|
297
|
+
return { ranges: nativeRanges, elements: [] };
|
|
298
|
+
const ranges = [];
|
|
299
|
+
const elements = [];
|
|
300
|
+
for (const target of resolved) {
|
|
301
|
+
if (typeof Range !== "undefined" && target instanceof Range) {
|
|
302
|
+
if (!this.isRangeWithinScope(target, scope)) {
|
|
303
|
+
return { ranges: nativeRanges, elements: [] };
|
|
304
|
+
}
|
|
305
|
+
ranges.push(target);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (typeof Element !== "undefined" && target instanceof Element) {
|
|
309
|
+
if (!this.isElementWithinScope(target, scope)) {
|
|
310
|
+
return { ranges: nativeRanges, elements: [] };
|
|
311
|
+
}
|
|
312
|
+
elements.push(target);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
return { ranges: nativeRanges, elements: [] };
|
|
316
|
+
}
|
|
317
|
+
return { ranges, elements };
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Fail open: custom resolver failures must not interrupt TTS playback.
|
|
321
|
+
return { ranges: nativeRanges, elements: [] };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
createTextRange(node, startOffset, endOffset) {
|
|
325
|
+
if (typeof document === "undefined")
|
|
326
|
+
return null;
|
|
327
|
+
if (typeof document.createRange !== "function")
|
|
328
|
+
return null;
|
|
329
|
+
try {
|
|
330
|
+
const range = document.createRange();
|
|
331
|
+
range.setStart(node, startOffset);
|
|
332
|
+
range.setEnd(node, endOffset);
|
|
333
|
+
if (range.startContainer !== node || range.endContainer !== node) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
return range;
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
paintTTSWordRange(range) {
|
|
343
|
+
if (!this.highlightCoordinator)
|
|
344
|
+
return;
|
|
345
|
+
try {
|
|
346
|
+
if (range.startContainer === range.endContainer &&
|
|
347
|
+
range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
348
|
+
this.highlightCoordinator.highlightTTSWord(range.startContainer, range.startOffset, range.endOffset);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Fall through to generic range painting for lightweight test doubles.
|
|
354
|
+
}
|
|
355
|
+
this.highlightCoordinator.highlightRange(range, HighlightType.TTS_WORD, HighlightColor.YELLOW);
|
|
356
|
+
}
|
|
357
|
+
paintTTSSentenceRanges(nativeRanges) {
|
|
358
|
+
if (!this.highlightCoordinator)
|
|
359
|
+
return;
|
|
360
|
+
const { ranges, elements } = this.resolveSentenceHighlightTargets(nativeRanges);
|
|
361
|
+
if (elements.length > 0 &&
|
|
362
|
+
ranges.length === 0 &&
|
|
363
|
+
this.highlightCoordinator.highlightTTSSentenceElements) {
|
|
364
|
+
this.highlightCoordinator.highlightTTSSentenceElements(elements);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const paintedRanges = [...ranges];
|
|
368
|
+
for (const element of elements) {
|
|
369
|
+
const elementRange = document.createRange();
|
|
370
|
+
elementRange.selectNodeContents(element);
|
|
371
|
+
paintedRanges.push(elementRange);
|
|
372
|
+
}
|
|
373
|
+
this.highlightCoordinator.highlightTTSSentence(paintedRanges);
|
|
374
|
+
}
|
|
223
375
|
/**
|
|
224
376
|
* Get provider capabilities
|
|
225
377
|
*/
|
|
@@ -577,11 +729,11 @@ export class TTSService {
|
|
|
577
729
|
this.currentBoundaryOffset = 0;
|
|
578
730
|
const originalOnWordBoundary = this.provider.onWordBoundary;
|
|
579
731
|
this.provider.onWordBoundary = (word, position, length) => {
|
|
732
|
+
originalOnWordBoundary?.(word, position, length);
|
|
580
733
|
if (Number.isFinite(position)) {
|
|
581
734
|
this.currentBoundaryOffset = position;
|
|
582
735
|
this.currentSeekSegmentIndex = this.getCurrentSeekSegmentIndex();
|
|
583
736
|
}
|
|
584
|
-
originalOnWordBoundary?.(word, position, length);
|
|
585
737
|
};
|
|
586
738
|
try {
|
|
587
739
|
await providerWithPlan.speakSegments(segments);
|
|
@@ -593,6 +745,10 @@ export class TTSService {
|
|
|
593
745
|
}
|
|
594
746
|
return;
|
|
595
747
|
}
|
|
748
|
+
this.configureWordBoundaryHighlighting({
|
|
749
|
+
highlightMode: options?.highlightMode || "word",
|
|
750
|
+
wordBoundaryOffset: this.activeWordBoundaryOffset,
|
|
751
|
+
});
|
|
596
752
|
for (const segment of segments) {
|
|
597
753
|
if (runId !== this.speakRunId)
|
|
598
754
|
return;
|
|
@@ -624,7 +780,7 @@ export class TTSService {
|
|
|
624
780
|
const range = document.createRange();
|
|
625
781
|
range.setStart(start.node, start.offset);
|
|
626
782
|
range.setEnd(end.node, end.offset + 1);
|
|
627
|
-
this.
|
|
783
|
+
this.paintTTSSentenceRanges([range]);
|
|
628
784
|
this.activeSentenceStartOffset = startOffset;
|
|
629
785
|
}
|
|
630
786
|
getSegmentIndexForOffset(segments, offset) {
|
|
@@ -732,6 +888,7 @@ export class TTSService {
|
|
|
732
888
|
const highlightMode = options?.highlightModeOverride ||
|
|
733
889
|
(speechMatchesVisibleText ? this.resolveHighlightMode() : "sentence");
|
|
734
890
|
this.activeHighlightMode = highlightMode;
|
|
891
|
+
this.activeWordBoundaryOffset = options?.wordBoundaryOffset || 0;
|
|
735
892
|
const hasExplicitBreaks = this.hasExplicitBreakSemantics(contentToSpeak);
|
|
736
893
|
const shouldUsePlan = !!this.currentContentElement &&
|
|
737
894
|
!usedCatalogSpoken &&
|
|
@@ -1168,7 +1325,7 @@ export class TTSService {
|
|
|
1168
1325
|
try {
|
|
1169
1326
|
const range = document.createRange();
|
|
1170
1327
|
range.selectNodeContents(this.currentContentElement);
|
|
1171
|
-
this.
|
|
1328
|
+
this.paintTTSSentenceRanges([range]);
|
|
1172
1329
|
}
|
|
1173
1330
|
catch {
|
|
1174
1331
|
// No-op fallback when DOM range creation fails.
|
|
@@ -1192,7 +1349,13 @@ export class TTSService {
|
|
|
1192
1349
|
if (highlightRange && this.highlightCoordinator) {
|
|
1193
1350
|
const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
|
|
1194
1351
|
this.debugLog(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${globalIndex}`);
|
|
1195
|
-
this.
|
|
1352
|
+
const nativeRange = this.createTextRange(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
1353
|
+
if (nativeRange) {
|
|
1354
|
+
this.paintTTSWordRange(this.resolveWordHighlightRange(nativeRange).range);
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
|
|
1358
|
+
}
|
|
1196
1359
|
}
|
|
1197
1360
|
else {
|
|
1198
1361
|
console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
|
|
@@ -1212,7 +1375,7 @@ export class TTSService {
|
|
|
1212
1375
|
this.lastRenderedRegionTarget = null;
|
|
1213
1376
|
if (chunk.regionRange) {
|
|
1214
1377
|
this.highlightCoordinator.clearHighlights?.(HighlightType.TTS_WORD);
|
|
1215
|
-
this.
|
|
1378
|
+
this.paintTTSSentenceRanges([chunk.regionRange]);
|
|
1216
1379
|
return;
|
|
1217
1380
|
}
|
|
1218
1381
|
if (!element)
|
|
@@ -1220,7 +1383,7 @@ export class TTSService {
|
|
|
1220
1383
|
const range = document.createRange();
|
|
1221
1384
|
range.selectNodeContents(element);
|
|
1222
1385
|
this.highlightCoordinator.clearHighlights?.(HighlightType.TTS_WORD);
|
|
1223
|
-
this.
|
|
1386
|
+
this.paintTTSSentenceRanges([range]);
|
|
1224
1387
|
}
|
|
1225
1388
|
catch {
|
|
1226
1389
|
// Continue playback even when a detached node cannot be highlighted.
|
|
@@ -1229,12 +1392,7 @@ export class TTSService {
|
|
|
1229
1392
|
highlightCatalogActiveRange(range) {
|
|
1230
1393
|
if (!this.highlightCoordinator)
|
|
1231
1394
|
return;
|
|
1232
|
-
|
|
1233
|
-
range.startContainer.nodeType === Node.TEXT_NODE) {
|
|
1234
|
-
this.highlightCoordinator.highlightTTSWord(range.startContainer, range.startOffset, range.endOffset);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
this.highlightCoordinator.highlightRange(range, HighlightType.TTS_WORD, HighlightColor.YELLOW);
|
|
1395
|
+
this.paintTTSWordRange(this.resolveWordHighlightRange(range).range);
|
|
1238
1396
|
}
|
|
1239
1397
|
highlightRenderableRegionTarget(target) {
|
|
1240
1398
|
if (!this.highlightCoordinator ||
|
|
@@ -1242,7 +1400,8 @@ export class TTSService {
|
|
|
1242
1400
|
typeof document === "undefined") {
|
|
1243
1401
|
return;
|
|
1244
1402
|
}
|
|
1245
|
-
if (
|
|
1403
|
+
if (!this.highlightTargetResolverProvider &&
|
|
1404
|
+
sameRenderableHighlightTarget(this.lastRenderedRegionTarget, target)) {
|
|
1246
1405
|
return;
|
|
1247
1406
|
}
|
|
1248
1407
|
try {
|
|
@@ -1251,7 +1410,7 @@ export class TTSService {
|
|
|
1251
1410
|
range.selectNodeContents(target.element);
|
|
1252
1411
|
}
|
|
1253
1412
|
else if (target.type === "range") {
|
|
1254
|
-
this.
|
|
1413
|
+
this.paintTTSSentenceRanges([target.range]);
|
|
1255
1414
|
this.lastRenderedRegionTarget = target;
|
|
1256
1415
|
return;
|
|
1257
1416
|
}
|
|
@@ -1259,7 +1418,7 @@ export class TTSService {
|
|
|
1259
1418
|
range.setStart(target.node, target.startOffset);
|
|
1260
1419
|
range.setEnd(target.node, target.endOffset);
|
|
1261
1420
|
}
|
|
1262
|
-
this.
|
|
1421
|
+
this.paintTTSSentenceRanges([range]);
|
|
1263
1422
|
this.lastRenderedRegionTarget = target;
|
|
1264
1423
|
}
|
|
1265
1424
|
catch {
|
|
@@ -1280,13 +1439,26 @@ export class TTSService {
|
|
|
1280
1439
|
return;
|
|
1281
1440
|
}
|
|
1282
1441
|
if (target.type === "text-range") {
|
|
1283
|
-
this.
|
|
1442
|
+
const nativeRange = this.createTextRange(target.node, target.startOffset, target.endOffset);
|
|
1443
|
+
if (nativeRange) {
|
|
1444
|
+
this.paintTTSWordRange(this.resolveWordHighlightRange(nativeRange).range);
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
this.highlightCoordinator.highlightTTSWord(target.node, target.startOffset, target.endOffset);
|
|
1448
|
+
}
|
|
1284
1449
|
return;
|
|
1285
1450
|
}
|
|
1286
1451
|
if (target.type === "range") {
|
|
1287
1452
|
this.highlightCatalogActiveRange(target.range);
|
|
1288
1453
|
return;
|
|
1289
1454
|
}
|
|
1455
|
+
const elementNativeRange = document.createRange();
|
|
1456
|
+
elementNativeRange.selectNodeContents(target.element);
|
|
1457
|
+
const resolvedElementRange = this.resolveWordHighlightRange(elementNativeRange);
|
|
1458
|
+
if (resolvedElementRange.remapped) {
|
|
1459
|
+
this.paintTTSWordRange(resolvedElementRange.range);
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1290
1462
|
// Element targets are atomic: a resolved math token, a whole-expression
|
|
1291
1463
|
// fallback, or a replaced element (image/svg). Paint exactly the resolved
|
|
1292
1464
|
// element. Native MathML / HTML tokens that expose a single text node keep
|
|
@@ -1438,6 +1610,7 @@ export class TTSService {
|
|
|
1438
1610
|
this.currentContentElement = null;
|
|
1439
1611
|
this.normalizedToDOM.clear();
|
|
1440
1612
|
this.currentBoundaryOffset = 0;
|
|
1613
|
+
this.activeWordBoundaryOffset = 0;
|
|
1441
1614
|
this.seekSegments = [];
|
|
1442
1615
|
this.sentenceHighlightSegments = [];
|
|
1443
1616
|
this.currentSeekSegmentIndex = 0;
|
|
@@ -1514,32 +1687,27 @@ export class TTSService {
|
|
|
1514
1687
|
this.setState(PlaybackState.PLAYING);
|
|
1515
1688
|
}
|
|
1516
1689
|
}
|
|
1517
|
-
|
|
1518
|
-
if (!
|
|
1519
|
-
return;
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (this.hasExplicitBreakSemantics(this.currentText))
|
|
1525
|
-
return;
|
|
1526
|
-
if (this.seekSegments.length === 0)
|
|
1527
|
-
return;
|
|
1528
|
-
const delta = Number.isFinite(units) ? Math.trunc(units) : 0;
|
|
1529
|
-
if (delta === 0)
|
|
1530
|
-
return;
|
|
1531
|
-
const currentIndex = this.getCurrentSeekSegmentIndex();
|
|
1532
|
-
const targetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, currentIndex + delta));
|
|
1533
|
-
if (targetIndex === currentIndex)
|
|
1690
|
+
normalizePlaybackRate(rate) {
|
|
1691
|
+
if (!Number.isFinite(rate))
|
|
1692
|
+
return 1;
|
|
1693
|
+
return Math.max(0.25, Math.min(4, rate));
|
|
1694
|
+
}
|
|
1695
|
+
async restartFromSeekIndex(targetIndex) {
|
|
1696
|
+
if (!this.provider || this.seekSegments.length === 0)
|
|
1534
1697
|
return;
|
|
1698
|
+
const safeTargetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, targetIndex));
|
|
1535
1699
|
this.speakRunId += 1;
|
|
1536
1700
|
this.provider.onWordBoundary = undefined;
|
|
1537
1701
|
this.provider.stop();
|
|
1538
|
-
this.currentSeekSegmentIndex =
|
|
1702
|
+
this.currentSeekSegmentIndex = safeTargetIndex;
|
|
1539
1703
|
const runId = ++this.speakRunId;
|
|
1540
|
-
const restartSegments = this.seekSegments.slice(
|
|
1704
|
+
const restartSegments = this.seekSegments.slice(safeTargetIndex);
|
|
1541
1705
|
this.setState(PlaybackState.PLAYING);
|
|
1542
1706
|
try {
|
|
1707
|
+
this.configureWordBoundaryHighlighting({
|
|
1708
|
+
highlightMode: this.activeHighlightMode,
|
|
1709
|
+
wordBoundaryOffset: this.activeWordBoundaryOffset,
|
|
1710
|
+
});
|
|
1543
1711
|
await this.speakWithPlan(restartSegments, runId, {
|
|
1544
1712
|
highlightMode: this.activeHighlightMode,
|
|
1545
1713
|
});
|
|
@@ -1547,6 +1715,7 @@ export class TTSService {
|
|
|
1547
1715
|
return;
|
|
1548
1716
|
this.setState(PlaybackState.IDLE);
|
|
1549
1717
|
this.clearHighlightsAndTracking();
|
|
1718
|
+
this.highlightTargetResolverProvider = null;
|
|
1550
1719
|
}
|
|
1551
1720
|
catch (error) {
|
|
1552
1721
|
if (runId !== this.speakRunId)
|
|
@@ -1554,9 +1723,41 @@ export class TTSService {
|
|
|
1554
1723
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
1555
1724
|
this.setState(PlaybackState.ERROR);
|
|
1556
1725
|
this.clearHighlightsAndTracking();
|
|
1726
|
+
this.highlightTargetResolverProvider = null;
|
|
1557
1727
|
throw error;
|
|
1558
1728
|
}
|
|
1559
1729
|
}
|
|
1730
|
+
async setPlaybackRate(rate) {
|
|
1731
|
+
const nextRate = this.normalizePlaybackRate(rate);
|
|
1732
|
+
await this.updateSettings({ rate: nextRate });
|
|
1733
|
+
if (this.state !== PlaybackState.PLAYING ||
|
|
1734
|
+
!this.currentText ||
|
|
1735
|
+
this.seekSegments.length === 0 ||
|
|
1736
|
+
this.hasExplicitBreakSemantics(this.currentText)) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
await this.restartFromSeekIndex(this.getCurrentSeekSegmentIndex());
|
|
1740
|
+
}
|
|
1741
|
+
async seekBy(units) {
|
|
1742
|
+
if (!this.provider || !this.currentText)
|
|
1743
|
+
return;
|
|
1744
|
+
if (this.state !== PlaybackState.PLAYING &&
|
|
1745
|
+
this.state !== PlaybackState.PAUSED) {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (this.hasExplicitBreakSemantics(this.currentText))
|
|
1749
|
+
return;
|
|
1750
|
+
if (this.seekSegments.length === 0)
|
|
1751
|
+
return;
|
|
1752
|
+
const delta = Number.isFinite(units) ? Math.trunc(units) : 0;
|
|
1753
|
+
if (delta === 0)
|
|
1754
|
+
return;
|
|
1755
|
+
const currentIndex = this.getCurrentSeekSegmentIndex();
|
|
1756
|
+
const targetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, currentIndex + delta));
|
|
1757
|
+
if (targetIndex === currentIndex)
|
|
1758
|
+
return;
|
|
1759
|
+
await this.restartFromSeekIndex(targetIndex);
|
|
1760
|
+
}
|
|
1560
1761
|
async seekForward(units = 1) {
|
|
1561
1762
|
const step = Number.isFinite(units) ? Math.max(1, Math.trunc(units)) : 1;
|
|
1562
1763
|
await this.seekBy(step);
|
|
@@ -1574,6 +1775,7 @@ export class TTSService {
|
|
|
1574
1775
|
this.speakRunId += 1;
|
|
1575
1776
|
this.provider.onWordBoundary = undefined;
|
|
1576
1777
|
this.provider.stop();
|
|
1778
|
+
this.highlightTargetResolverProvider = null;
|
|
1577
1779
|
this.setState(PlaybackState.IDLE);
|
|
1578
1780
|
this.currentText = null;
|
|
1579
1781
|
// Clear highlights
|
|
@@ -1585,6 +1787,7 @@ export class TTSService {
|
|
|
1585
1787
|
this.currentContentElement = null;
|
|
1586
1788
|
this.normalizedToDOM.clear();
|
|
1587
1789
|
this.currentBoundaryOffset = 0;
|
|
1790
|
+
this.activeWordBoundaryOffset = 0;
|
|
1588
1791
|
this.seekSegments = [];
|
|
1589
1792
|
this.sentenceHighlightSegments = [];
|
|
1590
1793
|
this.currentSeekSegmentIndex = 0;
|