@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.
Files changed (34) hide show
  1. package/README.md +25 -0
  2. package/dist/components/ItemToolBar.custom-element.js +98 -14
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +293 -48
  4. package/dist/components/SectionToolBar.custom-element.js +98 -14
  5. package/dist/context/assessment-toolkit-context.d.ts +2 -0
  6. package/dist/context/assessment-toolkit-context.js.map +1 -1
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +2 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/HighlightCoordinator.d.ts +6 -0
  11. package/dist/services/HighlightCoordinator.js +20 -5
  12. package/dist/services/HighlightCoordinator.js.map +1 -1
  13. package/dist/services/TTSService.d.ts +19 -0
  14. package/dist/services/TTSService.js +238 -35
  15. package/dist/services/TTSService.js.map +1 -1
  16. package/dist/services/ToolkitCoordinator.d.ts +3 -2
  17. package/dist/services/ToolkitCoordinator.js.map +1 -1
  18. package/dist/services/createDefaultToolRegistry.d.ts +2 -1
  19. package/dist/services/createDefaultToolRegistry.js +2 -1
  20. package/dist/services/createDefaultToolRegistry.js.map +1 -1
  21. package/dist/services/interfaces.d.ts +15 -0
  22. package/dist/services/interfaces.js.map +1 -1
  23. package/dist/services/tool-config-defaults.d.ts +11 -0
  24. package/dist/services/tool-config-defaults.js +18 -0
  25. package/dist/services/tool-config-defaults.js.map +1 -1
  26. package/dist/services/tts/highlight-target-resolver.d.ts +17 -0
  27. package/dist/services/tts/highlight-target-resolver.js +2 -0
  28. package/dist/services/tts/highlight-target-resolver.js.map +1 -0
  29. package/dist/services/tts-runtime-config.d.ts +15 -1
  30. package/dist/services/tts-runtime-config.js +76 -10
  31. package/dist/services/tts-runtime-config.js.map +1 -1
  32. package/dist/tools/registrations/tts.js +6 -8
  33. package/dist/tools/registrations/tts.js.map +1 -1
  34. package/package.json +7 -7
@@ -18971,22 +18971,81 @@ var isServerBackend = (backend) => backend === "polly" || backend === "google" |
18971
18971
  var withDefault = (value, fallback2) => value === undefined ? fallback2 : value;
18972
18972
  var normalizeTTSLayoutMode = (value, fallback2 = "left-aligned") => typeof value === "string" && VALID_TTS_LAYOUT_MODES.has(value) ? value : fallback2;
18973
18973
  var DEFAULT_TTS_SPEED_OPTIONS = Object.freeze([0.8, 1.25]);
18974
- var normalizeTTSSpeedOptions = (value) => {
18974
+ var normalizeSpeedRate = (entry) => {
18975
+ if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0) {
18976
+ return;
18977
+ }
18978
+ const rounded = Math.round(entry * 100) / 100;
18979
+ return rounded === 1 ? undefined : rounded;
18980
+ };
18981
+ var trimOptionalText = (value) => {
18982
+ if (typeof value !== "string")
18983
+ return;
18984
+ const trimmed = value.trim();
18985
+ return trimmed.length ? trimmed : undefined;
18986
+ };
18987
+ var formatSpeedLabel = (rate) => `${rate}x`;
18988
+ var formatSpeedAriaLabel = (label, usedDefaultLabel) => usedDefaultLabel ? `Speed ${label}` : `${label} speed`;
18989
+ var normalizeSpeedAriaLabel = (label, ariaLabel, usedDefaultLabel) => {
18990
+ if (!ariaLabel)
18991
+ return formatSpeedAriaLabel(label, usedDefaultLabel);
18992
+ if (ariaLabel.toLowerCase().includes(label.toLowerCase()))
18993
+ return ariaLabel;
18994
+ return `${label} ${ariaLabel}`;
18995
+ };
18996
+ var normalizeTTSSpeedOptionConfig = (entry) => {
18997
+ if (typeof entry === "number")
18998
+ return normalizeSpeedRate(entry);
18999
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
19000
+ return;
19001
+ const record = entry;
19002
+ const rate = normalizeSpeedRate(record.rate);
19003
+ if (rate === undefined)
19004
+ return;
19005
+ const label = trimOptionalText(record.label);
19006
+ const ariaLabel = trimOptionalText(record.ariaLabel);
19007
+ return {
19008
+ rate,
19009
+ ...label ? { label } : {},
19010
+ ...ariaLabel ? { ariaLabel } : {}
19011
+ };
19012
+ };
19013
+ var normalizeTTSSpeedOptionConfigs = (value) => {
18975
19014
  if (!Array.isArray(value))
18976
19015
  return [...DEFAULT_TTS_SPEED_OPTIONS];
18977
19016
  if (value.length === 0)
18978
19017
  return [];
18979
- const deduped = new Set;
19018
+ const dedupedRates = new Set;
19019
+ const normalized = [];
18980
19020
  for (const entry of value) {
18981
- if (typeof entry !== "number" || !Number.isFinite(entry) || entry <= 0)
19021
+ const option = normalizeTTSSpeedOptionConfig(entry);
19022
+ if (option === undefined)
18982
19023
  continue;
18983
- const rounded = Math.round(entry * 100) / 100;
18984
- if (rounded === 1)
19024
+ const rate = typeof option === "number" ? option : option.rate;
19025
+ if (dedupedRates.has(rate))
18985
19026
  continue;
18986
- deduped.add(rounded);
19027
+ dedupedRates.add(rate);
19028
+ normalized.push(option);
18987
19029
  }
18988
- return deduped.size ? Array.from(deduped) : [...DEFAULT_TTS_SPEED_OPTIONS];
19030
+ return normalized.length ? normalized : [...DEFAULT_TTS_SPEED_OPTIONS];
18989
19031
  };
19032
+ var normalizeTTSSpeedControlOptions = (value) => normalizeTTSSpeedOptionConfigs(value).map((option) => {
19033
+ if (typeof option === "number") {
19034
+ const label2 = formatSpeedLabel(option);
19035
+ return {
19036
+ rate: option,
19037
+ label: label2,
19038
+ ariaLabel: formatSpeedAriaLabel(label2, true)
19039
+ };
19040
+ }
19041
+ const defaultLabel = formatSpeedLabel(option.rate);
19042
+ const label = option.label || defaultLabel;
19043
+ return {
19044
+ rate: option.rate,
19045
+ label,
19046
+ ariaLabel: normalizeSpeedAriaLabel(label, option.ariaLabel, label === defaultLabel)
19047
+ };
19048
+ });
18990
19049
  var applyRuntimeDefaults = (config) => {
18991
19050
  const withLayoutDefaults = {
18992
19051
  ...config,
@@ -19143,7 +19202,7 @@ var ttsToolRegistration = {
19143
19202
  settings.layoutMode = normalizeTTSLayoutMode(settings.layoutMode);
19144
19203
  }
19145
19204
  if (settings && "speedOptions" in settings) {
19146
- settings.speedOptions = normalizeTTSSpeedOptions(settings.speedOptions);
19205
+ settings.speedOptions = normalizeTTSSpeedOptionConfigs(settings.speedOptions);
19147
19206
  }
19148
19207
  const normalizedConfig = {
19149
19208
  ...config
@@ -19152,7 +19211,7 @@ var ttsToolRegistration = {
19152
19211
  normalizedConfig.layoutMode = normalizeTTSLayoutMode(normalizedConfig.layoutMode);
19153
19212
  }
19154
19213
  if ("speedOptions" in normalizedConfig) {
19155
- normalizedConfig.speedOptions = normalizeTTSSpeedOptions(normalizedConfig.speedOptions);
19214
+ normalizedConfig.speedOptions = normalizeTTSSpeedOptionConfigs(normalizedConfig.speedOptions);
19156
19215
  }
19157
19216
  if (settings) {
19158
19217
  normalizedConfig.settings = settings;
@@ -19175,7 +19234,7 @@ var ttsToolRegistration = {
19175
19234
  const resolveRuntimeSettings = () => resolveTTSRuntimeSettings(toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) || undefined);
19176
19235
  const resolveElementSpeedOptions = () => {
19177
19236
  const runtimeSettings = resolveRuntimeSettings();
19178
- return normalizeTTSSpeedOptions(runtimeSettings.speedOptions);
19237
+ return normalizeTTSSpeedControlOptions(runtimeSettings.speedOptions);
19179
19238
  };
19180
19239
  const resolveLayoutMode = () => resolveTTSLayoutMode(resolveRuntimeSettings());
19181
19240
  const resolveHostLayout = () => resolveTTSHostToolbarLayout(resolveRuntimeSettings());
@@ -21007,27 +21066,27 @@ class HighlightCoordinator {
21007
21066
 
21008
21067
  /* Annotation highlights - persistent */
21009
21068
  ::highlight(annotation-yellow) {
21010
- background-color: color-mix(in srgb, var(--pie-missing, #ffff00) 35%, transparent);
21069
+ background-color: var(--pie-annotation-yellow-highlight, rgba(253, 233, 149, 0.5));
21011
21070
  color: inherit;
21012
21071
  }
21013
21072
 
21014
21073
  ::highlight(annotation-green) {
21015
- background-color: color-mix(in srgb, var(--pie-correct, #90ee90) 35%, transparent);
21074
+ background-color: var(--pie-annotation-green-highlight, rgba(166, 225, 197, 0.5));
21016
21075
  color: inherit;
21017
21076
  }
21018
21077
 
21019
21078
  ::highlight(annotation-blue) {
21020
- background-color: color-mix(in srgb, var(--pie-tertiary, #add8e6) 35%, transparent);
21079
+ background-color: var(--pie-annotation-blue-highlight, rgba(167, 224, 246, 0.5));
21021
21080
  color: inherit;
21022
21081
  }
21023
21082
 
21024
21083
  ::highlight(annotation-pink) {
21025
- background-color: color-mix(in srgb, var(--pie-secondary-light, #ffb6c1) 35%, transparent);
21084
+ background-color: var(--pie-annotation-pink-highlight, rgba(255, 159, 174, 0.5));
21026
21085
  color: inherit;
21027
21086
  }
21028
21087
 
21029
21088
  ::highlight(annotation-orange) {
21030
- background-color: color-mix(in srgb, var(--pie-incorrect, #ffa500) 35%, transparent);
21089
+ background-color: var(--pie-annotation-orange-highlight, rgba(255, 165, 0, 0.5));
21031
21090
  color: inherit;
21032
21091
  }
21033
21092
 
@@ -21070,6 +21129,16 @@ class HighlightCoordinator {
21070
21129
  }
21071
21130
  this.highlightTTSElementFallbacks(ranges);
21072
21131
  }
21132
+ highlightTTSSentenceElements(elements) {
21133
+ if (!this.ttsSentenceHighlight)
21134
+ return;
21135
+ this.applyAdaptiveTTSStyle(elements[0] || null);
21136
+ this.clearTTSSentence();
21137
+ for (const element2 of elements) {
21138
+ element2.setAttribute("data-pie-tts-sentence-element", "true");
21139
+ this.ttsSentenceElementHighlights.add(element2);
21140
+ }
21141
+ }
21073
21142
  static SENTENCE_FALLBACK_SELECTOR = "math, mjx-container, svg, img, canvas, [role='img']";
21074
21143
  static WORD_FALLBACK_SELECTOR = "svg, img, canvas, [role='img']";
21075
21144
  highlightTTSElementFallbacks(ranges) {
@@ -23829,6 +23898,7 @@ class TTSService {
23829
23898
  currentProvider = null;
23830
23899
  provider = null;
23831
23900
  highlightCoordinator = null;
23901
+ highlightTargetResolverProvider = null;
23832
23902
  catalogResolver = null;
23833
23903
  state = PlaybackState.IDLE;
23834
23904
  ttsConfig = {};
@@ -23839,6 +23909,7 @@ class TTSService {
23839
23909
  lastError = null;
23840
23910
  speakRunId = 0;
23841
23911
  currentBoundaryOffset = 0;
23912
+ activeWordBoundaryOffset = 0;
23842
23913
  seekSegments = [];
23843
23914
  sentenceHighlightSegments = [];
23844
23915
  currentSeekSegmentIndex = 0;
@@ -23918,9 +23989,137 @@ class TTSService {
23918
23989
  setHighlightCoordinator(coordinator) {
23919
23990
  this.highlightCoordinator = coordinator;
23920
23991
  }
23992
+ setHighlightTargetResolverProvider(provider) {
23993
+ this.highlightTargetResolverProvider = provider;
23994
+ return () => {
23995
+ if (this.highlightTargetResolverProvider === provider) {
23996
+ this.highlightTargetResolverProvider = null;
23997
+ }
23998
+ };
23999
+ }
23921
24000
  setCatalogResolver(resolver) {
23922
24001
  this.catalogResolver = resolver;
23923
24002
  }
24003
+ getHighlightResolverRuntime() {
24004
+ let provided = null;
24005
+ try {
24006
+ provided = this.highlightTargetResolverProvider?.() ?? null;
24007
+ } catch {
24008
+ provided = null;
24009
+ }
24010
+ const providedContext = provided?.context ?? {};
24011
+ const fallbackScope = typeof HTMLElement !== "undefined" && this.currentContentElement instanceof HTMLElement ? this.currentContentElement : null;
24012
+ return {
24013
+ context: {
24014
+ ...providedContext,
24015
+ scopeElement: providedContext.scopeElement ?? fallbackScope
24016
+ },
24017
+ resolver: provided?.resolver ?? null
24018
+ };
24019
+ }
24020
+ getNodeElement(node) {
24021
+ if (!node)
24022
+ return null;
24023
+ return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
24024
+ }
24025
+ isElementWithinScope(element2, scope) {
24026
+ if (!scope)
24027
+ return true;
24028
+ return element2 === scope || scope.contains(element2);
24029
+ }
24030
+ isRangeWithinScope(range, scope) {
24031
+ const startElement = this.getNodeElement(range.startContainer);
24032
+ const endElement = this.getNodeElement(range.endContainer);
24033
+ if (!startElement || !endElement)
24034
+ return false;
24035
+ return this.isElementWithinScope(startElement, scope) && this.isElementWithinScope(endElement, scope);
24036
+ }
24037
+ resolveWordHighlightRange(nativeRange) {
24038
+ const { context, resolver } = this.getHighlightResolverRuntime();
24039
+ const scope = context.scopeElement ?? this.currentContentElement;
24040
+ try {
24041
+ const resolved = resolver?.resolveWordRange?.(nativeRange, context);
24042
+ if (resolved && this.isRangeWithinScope(resolved, scope)) {
24043
+ return { range: resolved, remapped: !sameRange(nativeRange, resolved) };
24044
+ }
24045
+ } catch {}
24046
+ return { range: nativeRange, remapped: false };
24047
+ }
24048
+ resolveSentenceHighlightTargets(nativeRanges) {
24049
+ const { context, resolver } = this.getHighlightResolverRuntime();
24050
+ const scope = context.scopeElement ?? this.currentContentElement;
24051
+ try {
24052
+ const resolved = resolver?.resolveSentenceRanges?.(nativeRanges, context);
24053
+ if (!resolved)
24054
+ return { ranges: nativeRanges, elements: [] };
24055
+ const ranges = [];
24056
+ const elements = [];
24057
+ for (const target of resolved) {
24058
+ if (typeof Range !== "undefined" && target instanceof Range) {
24059
+ if (!this.isRangeWithinScope(target, scope)) {
24060
+ return { ranges: nativeRanges, elements: [] };
24061
+ }
24062
+ ranges.push(target);
24063
+ continue;
24064
+ }
24065
+ if (typeof Element !== "undefined" && target instanceof Element) {
24066
+ if (!this.isElementWithinScope(target, scope)) {
24067
+ return { ranges: nativeRanges, elements: [] };
24068
+ }
24069
+ elements.push(target);
24070
+ continue;
24071
+ }
24072
+ return { ranges: nativeRanges, elements: [] };
24073
+ }
24074
+ return { ranges, elements };
24075
+ } catch {
24076
+ return { ranges: nativeRanges, elements: [] };
24077
+ }
24078
+ }
24079
+ createTextRange(node, startOffset, endOffset) {
24080
+ if (typeof document === "undefined")
24081
+ return null;
24082
+ if (typeof document.createRange !== "function")
24083
+ return null;
24084
+ try {
24085
+ const range = document.createRange();
24086
+ range.setStart(node, startOffset);
24087
+ range.setEnd(node, endOffset);
24088
+ if (range.startContainer !== node || range.endContainer !== node) {
24089
+ return null;
24090
+ }
24091
+ return range;
24092
+ } catch {
24093
+ return null;
24094
+ }
24095
+ }
24096
+ paintTTSWordRange(range) {
24097
+ if (!this.highlightCoordinator)
24098
+ return;
24099
+ try {
24100
+ if (range.startContainer === range.endContainer && range.startContainer.nodeType === Node.TEXT_NODE) {
24101
+ this.highlightCoordinator.highlightTTSWord(range.startContainer, range.startOffset, range.endOffset);
24102
+ return;
24103
+ }
24104
+ } catch {}
24105
+ this.highlightCoordinator.highlightRange(range, HighlightType.TTS_WORD, HighlightColor.YELLOW);
24106
+ }
24107
+ paintTTSSentenceRanges(nativeRanges) {
24108
+ if (!this.highlightCoordinator)
24109
+ return;
24110
+ const { ranges, elements } = this.resolveSentenceHighlightTargets(nativeRanges);
24111
+ if (elements.length > 0 && ranges.length === 0 && this.highlightCoordinator.highlightTTSSentenceElements) {
24112
+ this.highlightCoordinator.highlightTTSSentenceElements(elements);
24113
+ return;
24114
+ }
24115
+ const paintedRanges = [...ranges];
24116
+ for (const element2 of elements) {
24117
+ const elementRange = document.createRange();
24118
+ elementRange.selectNodeContents(element2);
24119
+ paintedRanges.push(elementRange);
24120
+ }
24121
+ this.highlightCoordinator.highlightTTSSentence(paintedRanges);
24122
+ }
23924
24123
  getCapabilities() {
23925
24124
  return this.currentProvider?.getCapabilities() || null;
23926
24125
  }
@@ -24232,11 +24431,11 @@ class TTSService {
24232
24431
  this.currentBoundaryOffset = 0;
24233
24432
  const originalOnWordBoundary = this.provider.onWordBoundary;
24234
24433
  this.provider.onWordBoundary = (word, position, length) => {
24434
+ originalOnWordBoundary?.(word, position, length);
24235
24435
  if (Number.isFinite(position)) {
24236
24436
  this.currentBoundaryOffset = position;
24237
24437
  this.currentSeekSegmentIndex = this.getCurrentSeekSegmentIndex();
24238
24438
  }
24239
- originalOnWordBoundary?.(word, position, length);
24240
24439
  };
24241
24440
  try {
24242
24441
  await providerWithPlan.speakSegments(segments);
@@ -24247,6 +24446,10 @@ class TTSService {
24247
24446
  }
24248
24447
  return;
24249
24448
  }
24449
+ this.configureWordBoundaryHighlighting({
24450
+ highlightMode: options?.highlightMode || "word",
24451
+ wordBoundaryOffset: this.activeWordBoundaryOffset
24452
+ });
24250
24453
  for (const segment of segments) {
24251
24454
  if (runId !== this.speakRunId)
24252
24455
  return;
@@ -24278,7 +24481,7 @@ class TTSService {
24278
24481
  const range = document.createRange();
24279
24482
  range.setStart(start.node, start.offset);
24280
24483
  range.setEnd(end.node, end.offset + 1);
24281
- this.highlightCoordinator.highlightTTSSentence([range]);
24484
+ this.paintTTSSentenceRanges([range]);
24282
24485
  this.activeSentenceStartOffset = startOffset;
24283
24486
  }
24284
24487
  getSegmentIndexForOffset(segments, offset) {
@@ -24366,6 +24569,7 @@ class TTSService {
24366
24569
  this.initializeSpeakTracking(highlightText, options);
24367
24570
  const highlightMode = options?.highlightModeOverride || (speechMatchesVisibleText ? this.resolveHighlightMode() : "sentence");
24368
24571
  this.activeHighlightMode = highlightMode;
24572
+ this.activeWordBoundaryOffset = options?.wordBoundaryOffset || 0;
24369
24573
  const hasExplicitBreaks = this.hasExplicitBreakSemantics(contentToSpeak);
24370
24574
  const shouldUsePlan = !!this.currentContentElement && !usedCatalogSpoken && !hasExplicitBreaks && speechMatchesVisibleText;
24371
24575
  this.seekSegments = hasExplicitBreaks || !speechMatchesVisibleText ? [] : shouldUsePlan && this.currentContentElement ? this.createSpeechPlan(this.currentContentElement, normalizedText) : this.createSeekSegmentsFromText(highlightText);
@@ -24743,7 +24947,7 @@ class TTSService {
24743
24947
  try {
24744
24948
  const range = document.createRange();
24745
24949
  range.selectNodeContents(this.currentContentElement);
24746
- this.highlightCoordinator.highlightTTSSentence([range]);
24950
+ this.paintTTSSentenceRanges([range]);
24747
24951
  } catch {}
24748
24952
  }
24749
24953
  }
@@ -24759,7 +24963,12 @@ class TTSService {
24759
24963
  if (highlightRange && this.highlightCoordinator) {
24760
24964
  const highlightText = highlightRange.node.textContent?.substring(highlightRange.start, highlightRange.end) || "";
24761
24965
  this.debugLog(`[TTSService] Highlighting "${highlightText}" (word: "${word}") at position ${globalIndex}`);
24762
- this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
24966
+ const nativeRange = this.createTextRange(highlightRange.node, highlightRange.start, highlightRange.end);
24967
+ if (nativeRange) {
24968
+ this.paintTTSWordRange(this.resolveWordHighlightRange(nativeRange).range);
24969
+ } else {
24970
+ this.highlightCoordinator.highlightTTSWord(highlightRange.node, highlightRange.start, highlightRange.end);
24971
+ }
24763
24972
  } else {
24764
24973
  console.warn(`[TTSService] Could not find highlight range for position ${globalIndex}, length ${wordLength}`);
24765
24974
  }
@@ -24778,7 +24987,7 @@ class TTSService {
24778
24987
  this.lastRenderedRegionTarget = null;
24779
24988
  if (chunk.regionRange) {
24780
24989
  this.highlightCoordinator.clearHighlights?.(HighlightType.TTS_WORD);
24781
- this.highlightCoordinator.highlightTTSSentence([chunk.regionRange]);
24990
+ this.paintTTSSentenceRanges([chunk.regionRange]);
24782
24991
  return;
24783
24992
  }
24784
24993
  if (!element2)
@@ -24786,23 +24995,19 @@ class TTSService {
24786
24995
  const range = document.createRange();
24787
24996
  range.selectNodeContents(element2);
24788
24997
  this.highlightCoordinator.clearHighlights?.(HighlightType.TTS_WORD);
24789
- this.highlightCoordinator.highlightTTSSentence([range]);
24998
+ this.paintTTSSentenceRanges([range]);
24790
24999
  } catch {}
24791
25000
  }
24792
25001
  highlightCatalogActiveRange(range) {
24793
25002
  if (!this.highlightCoordinator)
24794
25003
  return;
24795
- if (range.startContainer === range.endContainer && range.startContainer.nodeType === Node.TEXT_NODE) {
24796
- this.highlightCoordinator.highlightTTSWord(range.startContainer, range.startOffset, range.endOffset);
24797
- return;
24798
- }
24799
- this.highlightCoordinator.highlightRange(range, HighlightType.TTS_WORD, HighlightColor.YELLOW);
25004
+ this.paintTTSWordRange(this.resolveWordHighlightRange(range).range);
24800
25005
  }
24801
25006
  highlightRenderableRegionTarget(target) {
24802
25007
  if (!this.highlightCoordinator || !target || typeof document === "undefined") {
24803
25008
  return;
24804
25009
  }
24805
- if (sameRenderableHighlightTarget(this.lastRenderedRegionTarget, target)) {
25010
+ if (!this.highlightTargetResolverProvider && sameRenderableHighlightTarget(this.lastRenderedRegionTarget, target)) {
24806
25011
  return;
24807
25012
  }
24808
25013
  try {
@@ -24810,14 +25015,14 @@ class TTSService {
24810
25015
  if (target.type === "element") {
24811
25016
  range.selectNodeContents(target.element);
24812
25017
  } else if (target.type === "range") {
24813
- this.highlightCoordinator.highlightTTSSentence([target.range]);
25018
+ this.paintTTSSentenceRanges([target.range]);
24814
25019
  this.lastRenderedRegionTarget = target;
24815
25020
  return;
24816
25021
  } else {
24817
25022
  range.setStart(target.node, target.startOffset);
24818
25023
  range.setEnd(target.node, target.endOffset);
24819
25024
  }
24820
- this.highlightCoordinator.highlightTTSSentence([range]);
25025
+ this.paintTTSSentenceRanges([range]);
24821
25026
  this.lastRenderedRegionTarget = target;
24822
25027
  } catch {}
24823
25028
  }
@@ -24830,13 +25035,25 @@ class TTSService {
24830
25035
  return;
24831
25036
  }
24832
25037
  if (target.type === "text-range") {
24833
- this.highlightCoordinator.highlightTTSWord(target.node, target.startOffset, target.endOffset);
25038
+ const nativeRange = this.createTextRange(target.node, target.startOffset, target.endOffset);
25039
+ if (nativeRange) {
25040
+ this.paintTTSWordRange(this.resolveWordHighlightRange(nativeRange).range);
25041
+ } else {
25042
+ this.highlightCoordinator.highlightTTSWord(target.node, target.startOffset, target.endOffset);
25043
+ }
24834
25044
  return;
24835
25045
  }
24836
25046
  if (target.type === "range") {
24837
25047
  this.highlightCatalogActiveRange(target.range);
24838
25048
  return;
24839
25049
  }
25050
+ const elementNativeRange = document.createRange();
25051
+ elementNativeRange.selectNodeContents(target.element);
25052
+ const resolvedElementRange = this.resolveWordHighlightRange(elementNativeRange);
25053
+ if (resolvedElementRange.remapped) {
25054
+ this.paintTTSWordRange(resolvedElementRange.range);
25055
+ return;
25056
+ }
24840
25057
  const onlyChild = target.element.childNodes.length === 1 ? target.element.firstChild : null;
24841
25058
  if (target.quality === "semantic-token" && onlyChild?.nodeType === Node.TEXT_NODE) {
24842
25059
  this.highlightCoordinator.highlightTTSWord(onlyChild, 0, onlyChild.textContent?.length || 0);
@@ -24951,6 +25168,7 @@ class TTSService {
24951
25168
  this.currentContentElement = null;
24952
25169
  this.normalizedToDOM.clear();
24953
25170
  this.currentBoundaryOffset = 0;
25171
+ this.activeWordBoundaryOffset = 0;
24954
25172
  this.seekSegments = [];
24955
25173
  this.sentenceHighlightSegments = [];
24956
25174
  this.currentSeekSegmentIndex = 0;
@@ -25005,31 +25223,27 @@ class TTSService {
25005
25223
  this.setState(PlaybackState.PLAYING);
25006
25224
  }
25007
25225
  }
25008
- async seekBy(units) {
25009
- if (!this.provider || !this.currentText)
25010
- return;
25011
- if (this.state !== PlaybackState.PLAYING && this.state !== PlaybackState.PAUSED) {
25012
- return;
25013
- }
25014
- if (this.hasExplicitBreakSemantics(this.currentText))
25015
- return;
25016
- if (this.seekSegments.length === 0)
25017
- return;
25018
- const delta = Number.isFinite(units) ? Math.trunc(units) : 0;
25019
- if (delta === 0)
25020
- return;
25021
- const currentIndex = this.getCurrentSeekSegmentIndex();
25022
- const targetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, currentIndex + delta));
25023
- if (targetIndex === currentIndex)
25226
+ normalizePlaybackRate(rate) {
25227
+ if (!Number.isFinite(rate))
25228
+ return 1;
25229
+ return Math.max(0.25, Math.min(4, rate));
25230
+ }
25231
+ async restartFromSeekIndex(targetIndex) {
25232
+ if (!this.provider || this.seekSegments.length === 0)
25024
25233
  return;
25234
+ const safeTargetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, targetIndex));
25025
25235
  this.speakRunId += 1;
25026
25236
  this.provider.onWordBoundary = undefined;
25027
25237
  this.provider.stop();
25028
- this.currentSeekSegmentIndex = targetIndex;
25238
+ this.currentSeekSegmentIndex = safeTargetIndex;
25029
25239
  const runId = ++this.speakRunId;
25030
- const restartSegments = this.seekSegments.slice(targetIndex);
25240
+ const restartSegments = this.seekSegments.slice(safeTargetIndex);
25031
25241
  this.setState(PlaybackState.PLAYING);
25032
25242
  try {
25243
+ this.configureWordBoundaryHighlighting({
25244
+ highlightMode: this.activeHighlightMode,
25245
+ wordBoundaryOffset: this.activeWordBoundaryOffset
25246
+ });
25033
25247
  await this.speakWithPlan(restartSegments, runId, {
25034
25248
  highlightMode: this.activeHighlightMode
25035
25249
  });
@@ -25037,15 +25251,44 @@ class TTSService {
25037
25251
  return;
25038
25252
  this.setState(PlaybackState.IDLE);
25039
25253
  this.clearHighlightsAndTracking();
25254
+ this.highlightTargetResolverProvider = null;
25040
25255
  } catch (error) {
25041
25256
  if (runId !== this.speakRunId)
25042
25257
  return;
25043
25258
  this.lastError = error instanceof Error ? error.message : String(error);
25044
25259
  this.setState(PlaybackState.ERROR);
25045
25260
  this.clearHighlightsAndTracking();
25261
+ this.highlightTargetResolverProvider = null;
25046
25262
  throw error;
25047
25263
  }
25048
25264
  }
25265
+ async setPlaybackRate(rate) {
25266
+ const nextRate = this.normalizePlaybackRate(rate);
25267
+ await this.updateSettings({ rate: nextRate });
25268
+ if (this.state !== PlaybackState.PLAYING || !this.currentText || this.seekSegments.length === 0 || this.hasExplicitBreakSemantics(this.currentText)) {
25269
+ return;
25270
+ }
25271
+ await this.restartFromSeekIndex(this.getCurrentSeekSegmentIndex());
25272
+ }
25273
+ async seekBy(units) {
25274
+ if (!this.provider || !this.currentText)
25275
+ return;
25276
+ if (this.state !== PlaybackState.PLAYING && this.state !== PlaybackState.PAUSED) {
25277
+ return;
25278
+ }
25279
+ if (this.hasExplicitBreakSemantics(this.currentText))
25280
+ return;
25281
+ if (this.seekSegments.length === 0)
25282
+ return;
25283
+ const delta = Number.isFinite(units) ? Math.trunc(units) : 0;
25284
+ if (delta === 0)
25285
+ return;
25286
+ const currentIndex = this.getCurrentSeekSegmentIndex();
25287
+ const targetIndex = Math.max(0, Math.min(this.seekSegments.length - 1, currentIndex + delta));
25288
+ if (targetIndex === currentIndex)
25289
+ return;
25290
+ await this.restartFromSeekIndex(targetIndex);
25291
+ }
25049
25292
  async seekForward(units = 1) {
25050
25293
  const step = Number.isFinite(units) ? Math.max(1, Math.trunc(units)) : 1;
25051
25294
  await this.seekBy(step);
@@ -25060,6 +25303,7 @@ class TTSService {
25060
25303
  this.speakRunId += 1;
25061
25304
  this.provider.onWordBoundary = undefined;
25062
25305
  this.provider.stop();
25306
+ this.highlightTargetResolverProvider = null;
25063
25307
  this.setState(PlaybackState.IDLE);
25064
25308
  this.currentText = null;
25065
25309
  if (this.highlightCoordinator) {
@@ -25069,6 +25313,7 @@ class TTSService {
25069
25313
  this.currentContentElement = null;
25070
25314
  this.normalizedToDOM.clear();
25071
25315
  this.currentBoundaryOffset = 0;
25316
+ this.activeWordBoundaryOffset = 0;
25072
25317
  this.seekSegments = [];
25073
25318
  this.sentenceHighlightSegments = [];
25074
25319
  this.currentSeekSegmentIndex = 0;